Download the PHP package gesagtgetan/neos-mcp without Composer
On this page you can find all versions of the php package gesagtgetan/neos-mcp. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download gesagtgetan/neos-mcp
More information about gesagtgetan/neos-mcp
Files in gesagtgetan/neos-mcp
Package neos-mcp
Short Description MCP (Model Context Protocol) server for Neos Content Repository — gives LLMs structured access to content
License GPL-3.0-or-later
Informations about the package neos-mcp
GesagtGetan.NeosMcp
MCP (Model Context Protocol) server for the Neos 9 Content Repository. Gives LLMs structured access to content nodes, node types, and workspaces.
HTTP Transport (OAuth)
The package exposes the MCP server over HTTP at POST /api/mcp, secured with OAuth 2.0 (authorization code grant + PKCE). This is used by Claude.ai's remote MCP connector and ChatGPT.
Each authenticated user has their own personal workspace (the same one they use in the Neos UI). Changes are isolated per user and can be reviewed/published independently. The JWT sub claim contains the Neos UserId (UUID), not the username — no credentials are leaked in tokens.
Setup
-
Generate client credentials (or retrieve from password manager if they exist already)
- Enter them in the Claude.ai or ChatGPT connector's settings along with the MCP endpoint URL:
https://example.com/api/mcp. -
Configure
Configuration/Production/Settings.yaml: -
Run
./flow mcp:setupto create the stdio workspace, register the OAuth client, and generate RSA keys:⚠️ Re-run
mcp:setupwhenever you change client credentials, redirect URIs, or any otheroauth.client.*setting. The database is not updated automatically. -
Assign the
GesagtGetan.NeosMcp:McpUserrole to Neos accounts that should be able to authorize MCP access. -
(Only for custom backend logins) The OAuth consent screen identifies the logged-in backend user through the Flow security context. The default
Neos.Neos:Backendprovider is scoped viarequestPatternsto the Neos backend and service controllers, so it would be inactive on the authorize controller. This package ships a request pattern that also activates it there, so plain Neos installations work without extra configuration. If your backend login uses a custom authentication provider (e.g. a single sign-on package) instead of the default one, that provider is scoped to its own controllers and stays inactive on the consent screen, so the user appears logged out. In that case, add the same request pattern to your provider in your project'sSettings.yaml, for example: -
Ensure the following endpoints are publicly accessible (no basic auth, no firewall restrictions):
/.well-known/oauth-protected-resource/api/mcp,/.well-known/oauth-authorization-server,/oauth/authorize,/oauth/grant,/oauth/token,/api/mcp. If your server uses basic auth or IP restrictions, exempt these routes. The authorization endpoint (GET /oauth/authorize) and the consent form (POST /oauth/grant) are exempted too but require a Neos session, so there is no security gap.⚠️ Upgrading from a version that served the authorization endpoint at
GET /api/mcp: add the/oauth/authorizeand/oauth/grantpaths to your existing exemptions, otherwise the browser step of the OAuth flow runs into basic auth.How you exempt them depends on your server (for example a conditional in your virtual host or
Web/.htaccess). Match these paths precisely so you do not unintentionally widen public access beyond them. -
Apache with
mod_proxy_fcgistrips theAuthorizationheader before it reaches PHP, causing all bearer token requests to fail silently with401. Add the following lines toWeb/.htaccessinside the<IfModule mod_rewrite.c>block, right afterRewriteBase /:This copies the
Authorizationheader into theHTTP_AUTHORIZATIONenvironment variable so PHP can read it. TheRewriteCondensures it only fires when the header is present. - Connect the MCP server in Claude.ai or ChatGPT and test with a prompt like "List all node types for the connected Neos website" to verify the connection works end-to-end.
CLI Transport (stdio, optional)
Optional transport for local development. Useful for testing tools directly or connecting a local coding agent (e.g. Claude Code) without going through OAuth.
Setup
This creates the shared stdio workspace, generates OAuth RSA keys, and registers the OAuth client in the database. Run it during initial setup and after every configuration change (credentials, redirect URIs, etc.) to apply the new values.
Usage
Flow command that reads MCP requests from stdin and writes responses to stdout. Requires the stdio workspace to exist — run mcp:setup first.
Content writes from the stdio transport land in the shared stdio workspace (stdioWorkspaceName in Settings.yaml, default llm-review) and are not live until published. To review or publish them in the Neos backend, assign the GesagtGetan.NeosMcp:McpUser role to the relevant Neos accounts — otherwise the workspace exists but is invisible in the editor UI.
Claude Code Configuration
Add to .mcp.json in your project root:
Then run /mcp in Claude Code to connect the server.
Optional Workspace Configuration
In Settings.yaml:
Available Tools
Read
getContentRepositoryInfo— dimensions, workspaces, dimension space pointslistNodeTypes— list non-abstract node types (optional filter)getNodeTypeSchema— full schema for a node type including properties, child nodes, and references. Per-propertylabelanddescriptionare surfaced from each property'sui.labelandui.help.messagein NodeTypes.yaml — author once for the Neos editor tooltip, get the same hint for the LLM.findNodes— search by type and/or search termgetNode— get a single node with all propertiesgetChildren— list child nodes, optionally filtered by typegetNodeReferences— outgoing references (which nodes does this node point at?). References live outsidepropertiesin Neos 9, sogetNodedoes NOT return them.getNodeBackReferences— incoming references (which nodes point at this one?). Useful for impact analysis before deletes/moves.getWorkspaceStatus— workspace status including pending change count
Write (staged in workspace, requires human publishing)
createNode— create a node under a parent (ID auto-generated)setNodeProperties— partial property updatesetNodeReference— replace all references for one reference name (emptytargetsdeletes them)moveNode— move to new parenthideNode— hide a node from the public site (reversible)unhideNode— unhide a previously hidden nodefindAndReplace— batch find/replace across the content treeremoveNode— soft-delete a node (can be restored)
Workspace
discardWorkspaceChanges— discard all pending changes
Hints for the LLM (ui.help.message)
LLMs occasionally pick the wrong property when two look similar. Leave a hint on the property in NodeTypes.yaml:
getNodeTypeSchema returns ui.help.message as the per-property description field, so the LLM reads it whenever it inspects the schema before a write. The same string already renders as a tooltip in the Neos editor backend, so editors and LLM share one source of truth — authoring guidance once benefits both.
When to add a hint:
- Two properties have similar names but different purposes (
title/titleOverride,image/fallbackImage, etc.) - A property has a non-obvious format constraint or interaction with another property
- A property looks tempting but is actually deprecated or only used by one rendering path
When NOT to add a hint:
- The property name is self-explanatory and matches the editor field label
- Every property doesn't need a tooltip; only the genuinely confusing ones
Workspace Rebase
The Neos Content Repository does not automatically propagate changes from a base workspace (e.g. live) to derived workspaces. This means the MCP workspace can become stale: nodes deleted or modified in live remain visible in the workspace until an explicit rebase occurs.
To keep the LLM's view fresh, the MCP server rebases the workspace before every tool call. This ensures reads reflect the latest live state and writes don't target nodes that no longer exist.
Conflict handling
If unpublished changes in the MCP workspace conflict with live (e.g. a node was edited in the workspace but deleted in live), the rebase fails. When this happens, the tool call still executes against the stale workspace, but the response includes a conflict warning with details about which nodes are affected. The LLM can then decide to discard conflicting changes via discardWorkspaceChanges or inform the user.
Rebase performance (1000 live nodes, MariaDB, 10 runs)
| Scenario | Avg | Min | Max |
|---|---|---|---|
| No-op (workspace already up-to-date) | 4.6 ms | 3.6 ms | 5.8 ms |
| Empty workspace (outdated, no unpublished changes) | 41.2 ms | 36.5 ms | 51.8 ms |
| 10 unpublished changes | 285.7 ms | 264.1 ms | 366.1 ms |
| 50 unpublished changes | 1223.9 ms | 1158.9 ms | 1320.7 ms |
The common case (no-op) adds ~5 ms per tool call. The empty-but-outdated case (typical after publishing) adds ~40 ms. Workspaces with many unpublished changes are more expensive but uncommon in normal MCP usage.
Architecture
Production code uses a ContentRepositoryFacade interface (instead of the final ContentRepository class directly) to allow unit testing via mocks. DefaultContentRepositoryFacade wraps the real CR.
Tools are contributed through Tool\McpToolProvider implementations. The Tool\McpToolProviderRegistry Flow singleton uses Flow's ReflectionService to auto-discover every implementation in the application and dispatches each one per request — there is no Settings.yaml registry to maintain. Both controllers (Command\McpCommandController, Controller\McpHttpController) call registerAll() when building their server.
Extending with Custom Tools
Any Flow package can contribute MCP tools by implementing GesagtGetan\NeosMcp\Tool\McpToolProvider. The registry discovers implementations automatically through ReflectionService, so there is nothing to register in Settings.yaml.
A minimal stateless provider — for a tool that doesn't need workspace or CR access — keeps the #[McpTool] methods on itself:
If your tools need per-request workspace/CR state, build a small handler from McpRequestContext inside registerTools() (the way McpNodeToolProvider does) — singletons should not hold per-request state directly.
Companion Packages
gesagtgetan/neosmcpimages— contributesfindImagesByAltTextandsetImagePropertyso the LLM can pick existing media-library images by their alt-text description and assign them toImageInterface-typed node properties. Pairs withgesagtgetan/imagealttext, the Neos-side bridge that keeps the alt-text table populated from the external Altaca service.
Development
All commands run from the package directory (DistributionPackages/GesagtGetan.NeosMcp/). Requires Just >= 1.38.0.
Dev Dependencies
Dev tools are declared in require-dev in this package's composer.json.
Unit Tests
Unit tests run against vendor/autoload.php with no Flow bootstrap. They extend PHPUnit's TestCase directly, or Tests/Unit/AbstractUnitTest when a subject has #[Flow\Inject] properties that need to be populated by reflection. Prefer self::createStub() for collaborators; use $this->createMock() only when the test asserts how the collaborator is called (expects()).
Functional Tests
Functional tests need a booted Neos instance and a MariaDB database, so they run in Docker. just build-test-distribution assembles a throwaway Neos distribution in .test-distribution/ from Tests/TestDistribution/ and installs this package into it. just test-functional then runs PHPUnit inside the container using Tests/TestDistribution/phpunit-functional.xml, which bootstraps Flow's FunctionalTestBootstrap. The Content Repository's own tables (event store, projections) are created automatically by the test base class.
FAQ
Why two PHPUnit configs? The two suites have different runtime needs. phpunit.xml.dist covers the unit tests and only needs Composer's autoloader. The functional tests need Flow's test bootstrap and a database, so they get their own config in the test distribution rather than dragging Flow's build essentials into the package itself. Flow's own global FunctionalTests.xml is not used because it still targets the PHPUnit 9 schema.
Why not SQLite? The Neos Content Repository's DoctrineDbal adapter uses MySQL-specific SQL (e.g. INSERT IGNORE) that SQLite does not support. A real MySQL/MariaDB database is required.
Tip: Connect the MCP server while developing
When working on this package with an MCP-capable coding agent (e.g. Claude Code), connect it to a running instance of the MCP server — locally via stdio or remotely via the HTTP transport — so it can query actual nodes, inspect workspace state, and verify tool behavior against real data. (After local code changes, reconnect the MCP server — e.g. via /mcp in Claude Code — so the agent picks up the updated PHP files.)
For a faster feedback loop after tool changes, pipe JSON-RPC directly into ./flow mcp:server instead of reconnecting the MCP client — every invocation reads the current PHP files, so you can run a quick initialize → notifications/initialized → tools/call sequence to verify the tool's raw TextContent JSON output. Writes land in the same stdio workspace as the connected-client flow.
All versions of neos-mcp with dependencies
composer-runtime-api Version ^2.1
composer/semver Version ^3.0
guzzlehttp/guzzle Version ^7.4
league/oauth2-server Version ^8.5
neos/neos Version ^9.1
php-mcp/server Version ^3.3
neos/contentgraph-doctrinedbaladapter Version *