sulu / mcp-bundle
Exposes Sulu content management as MCP tools for AI assistants
Package info
Type:symfony-bundle
pkg:composer/sulu/mcp-bundle
Requires
- php: ^8.2
- composer-runtime-api: ^2.0
- league/oauth2-server-bundle: ^1.2
- mcp/sdk: ^0.8.1
- nyholm/psr7: ^1.4
- sulu/sulu: ^3.0.9
- symfony/doctrine-bridge: ^7.3 || ^8.0
- symfony/framework-bundle: ^7.3 || ^8.0
- symfony/http-client: ^7.3 || ^8.0
- symfony/mcp-bundle: ^0.13
- symfony/mime: ^7.3 || ^8.0
- symfony/password-hasher: ^7.3 || ^8.0
- symfony/psr-http-message-bridge: ^7.3 || ^8.0
Requires (Dev)
- cmsig/seal-memory-adapter: ^0.12.2
- doctrine/data-fixtures: ^2.2
- friendsofsymfony/jsrouting-bundle: ^3.6
- php-cs-fixer/shim: ^3.15
- phpspec/prophecy-phpunit: ^2.0
- phpstan/extension-installer: ^1.2
- phpstan/phpstan: ^2.1
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^10.3
- rector/rector: ^2.1
- spaze/phpstan-disallowed-calls: ^4.7
- symfony/dotenv: ^7.3 || ^8.0
Suggests
- sulu/product-bundle: Enables the sulu_product_* MCP tools and type="product" on the unified content tools
Provides
None
Conflicts
- symfony/doctrine-bridge: <6.4.2
Replaces
None
This package is auto-updated.
Last update: 2026-09-19 19:12:34 UTC
README
The SuluMcpBundle turns a Sulu installation into a Model Context Protocol server. AI assistants connect over Streamable HTTP and create pages, edit articles, manage media and publish content through the same operations the administration interface uses. Every request runs as the authenticated Sulu user, so an operation that is denied in the administration interface is denied over MCP as well. There is no separate authentication layer and no privilege escalation.
🚀 Installation and Documentation
composer config extra.symfony.allow-contrib true
composer require sulu/mcp-bundle
The first command enables the contrib recipes, where this bundle's
Flex recipe lives. The recipe registers the
bundles, writes the routes and the configuration, including the allowed hosts of the transport and the default OAuth
scopes, and adds SULU_MCP_SERVER_URL to .env. The OAuth key pair, the league/oauth2-server-bundle grants, the
migration and the security setup have no Flex configurator and stay manual.
The steps below spell out everything, both for installations without Flex and for reading back what the recipe put
into your project. Start by registering the bundle in config/bundles.php, along with its two required
dependencies. league/oauth2-server-bundle registers itself through its own recipe, while symfony/mcp-bundle has
none and is registered by ours:
return [ // ... Symfony\AI\McpBundle\McpBundle::class => ['all' => true], League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], Sulu\Mcp\Infrastructure\Symfony\HttpKernel\SuluMcpBundle::class => ['all' => true], ];
Import the routes in config/routes.yaml. The mcp entry registers the MCP transport endpoint provided by
symfony/mcp-bundle. This bundle ships its OAuth endpoints in two files: the admin ones take the same prefix your
project already uses for the rest of the Sulu admin, and the RFC 8414/9728 discovery documents stay unprefixed in the
host's /.well-known/ namespace:
mcp: resource: . type: mcp sulu_mcp_admin: resource: '@SuluMcpBundle/config/routing_admin.yaml' prefix: /admin sulu_mcp_website: resource: '@SuluMcpBundle/config/routing_website.yaml'
Generate the RSA key pair that league/oauth2-server-bundle signs its tokens with. Skipping this step leaves every
MCP request failing with Invalid key supplied:
mkdir -p config/jwt openssl genrsa -aes128 -out config/jwt/private.pem 4096 openssl rsa -in config/jwt/private.pem -pubout -out config/jwt/public.pem
Both commands prompt for the passphrase, so it stays out of your shell history.
Keep both keys out of version control, for example by adding /config/jwt/*.pem to your .gitignore.
Set the public server URL and the OAuth secrets in your environment. The passphrase has to match the one used above, and the encryption key is any random string:
SULU_MCP_SERVER_URL=https://your-sulu-host.example.com OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem OAUTH_PASSPHRASE=<passphrase> OAUTH_ENCRYPTION_KEY=<random-string>
Configure league/oauth2-server-bundle in config/packages/league_oauth2_server.yaml. Its Flex recipe generates most
of the file; make sure the authorization-code and refresh-token grants MCP uses are enabled and the password and
implicit grants are explicitly off. scopes.default is required by league and comes from this bundle's recipe, which
appends the MCP scopes to the ones the league recipe wrote; set it yourself when you install without Flex:
league_oauth2_server: authorization_server: private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' private_key_passphrase: '%env(OAUTH_PASSPHRASE)%' encryption_key: '%env(OAUTH_ENCRYPTION_KEY)%' enable_auth_code_grant: true enable_refresh_token_grant: true enable_password_grant: false enable_implicit_grant: false resource_server: public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%' scopes: # `available` is contributed by SuluMcpBundle default: ['mcp:tools', 'mcp:resources'] persistence: doctrine: ~
Name the public host on the MCP transport. The transport ships with DNS rebinding protection that accepts only
localhost, so a server on its own domain rejects every client with Forbidden: Invalid Host header. once the OAuth
handshake is through. This bundle's recipe writes the setting into config/packages/sulu_mcp.yaml; without Flex, put
it in config/packages/mcp.yaml yourself:
mcp: http: allowed_hosts: - '%env(key:host:url:SULU_MCP_SERVER_URL)%' - localhost - 127.0.0.1 - '[::1]'
Create the database tables. league/oauth2-server-bundle persists clients, authorization codes, access tokens and
refresh tokens through Doctrine:
bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate
The MCP endpoint then answers at /admin/mcp. The docs/ directory documents the
configuration reference, the required security setup, and per-client connection guides for
Claude.ai, Claude Code,
Claude Cowork, ChatGPT and Codex.
💡 Key Concepts
Permissions
The bundle adds no permission model of its own. Every tool declares the Sulu security context and permission type it requires, a compile-time map is built from those declarations, and a central gate checks it before any tool runs. Tools the current role cannot use are hidden from the tool listing, and calling one anyway returns a permission denial rather than a missing-tool error.
Dangerous tools
Tools with hard-to-reverse effects are disabled by default and enabled per category through the dangerous_tools
configuration. When a category is disabled its tools are removed from the container at compile time, so they never
appear to a client at all.
# config/packages/sulu_mcp.yaml sulu_mcp: server_url: '%env(SULU_MCP_SERVER_URL)%' dangerous_tools: delete: false # sulu_content_delete, sulu_tag_delete, sulu_category_delete publish: false # sulu_content_publish, sulu_content_unpublish, sulu_preview_link_revoke, sulu_page_move, sulu_page_reorder block_remove: false # sulu_block_remove media_upload: false # sulu_media_upload
Available tools
40 tools spanning the core Sulu domains, plus 9 more when sulu/product-bundle is installed:
| Domain | Count | Examples |
|---|---|---|
| Pages | 7 | sulu_page_create, sulu_page_get, sulu_page_list, sulu_page_move, sulu_page_reorder, sulu_page_tree, sulu_page_update |
| Blocks | 5 | sulu_block_add, sulu_block_update, sulu_block_reorder, sulu_block_list, sulu_block_remove |
| Articles | 4 | sulu_article_create, sulu_article_update, sulu_article_get, sulu_article_list |
| Snippets | 4 | sulu_snippet_create, sulu_snippet_update, sulu_snippet_get, sulu_snippet_list |
| Unified content | 3 | sulu_content_delete, sulu_content_publish, sulu_content_unpublish |
| Media | 4 | sulu_media_list, sulu_media_get, sulu_media_update, sulu_media_upload |
| Taxonomy | 6 | sulu_tag_*, sulu_category_* |
| Preview | 2 | sulu_preview_link_generate, sulu_preview_link_revoke |
| Navigation | 1 | sulu_navigation_get |
| Contact | 1 | sulu_contact_list |
| Products (optional) | 9 | sulu_product_create, sulu_product_update, sulu_product_get, sulu_product_list, sulu_product_variant_*, sulu_product_family_list, sulu_attribute_list — require sulu/product-bundle |
| Misc | 3 | sulu_content_search, sulu_get_context, sulu_ping |
The block and unified content tools operate on pages, articles, snippets — and products, when
sulu/product-bundle is installed — alike through a type parameter.
Products (optional)
The product tools appear only when sulu/product-bundle
is installed and registered in bundles.php; without it they are hidden from tools/list. The
bundle has no 3.x release yet, so it installs from its branch:
composer require sulu/product-bundle:3.0.x-dev
Products are modelled with a product family that decides which attributes apply, and support one
level of variants: a product of type product_with_variants holds variant children. Variants
cannot be nested. Because a variant inherits its parent's family and only carries the attributes
the family marks variantSpecific, they are created with sulu_product_variant_create rather than
sulu_product_create. Publishing the parent through sulu_content_publish cascades to all of its
variants.
Authentication
Clients authenticate through OAuth 2.1 with Dynamic Client Registration, backed by league/oauth2-server-bundle.
Sulu opens the administration login when needed and then shows an explicit consent screen naming the client and the
requested scopes. Tokens are only issued once the user approves that screen.
For hosted clients, create an OAuth client up front:
bin/console sulu:mcp:create-client "Claude.ai Production"
❤️ Support and Contributions
The Sulu content management system is a community-driven open source project backed by various partner companies. We are committed to a fully transparent development process and highly appreciate any contributions.
Have a look at our contribution guidelines and the Sulu contribution documentation before opening a pull request. Security issues should be reported privately as described in SECURITY.md.
✅ Requirements
- PHP 8.2 or higher
- Symfony 7.3 or 8.x
- Sulu 3.0 or higher
Have a look at the require section in the composer.json for an up-to-date list.
📘 License
The Sulu content management system is released under the terms of the MIT License.