bahdan / bolt-mcp
Secure MCP endpoint and Bolt CMS tools for Bolt 5 and Bolt 6
Requires
- php: >=8.1
- bolt/core: ^5.0 || ^6.0
- mcp/sdk: ^0.8
- nyholm/psr7: ^1.8
- psr/http-message: ^1.1 || ^2.0
- symfony/psr-http-message-bridge: ^2.3 || ^6.4
- symfony/security-http: ^5.4 || ^6.4 || ^7.4
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5
- squizlabs/php_codesniffer: ^3.10
This package is auto-updated.
Last update: 2026-08-29 19:15:59 UTC
README
Secure Model Context Protocol integration for Bolt CMS 5 and 6.
Maintained by Bahdan Hal.
Status
Early development. The package provides a protected Streamable HTTP endpoint, OAuth/PKCE login, read-only discovery tools, and guarded content mutations behind an explicit capability gateway with Bolt permission checks and confirmation requirements.
Compatibility
The package targets both Bolt 5.x and Bolt 6.x in one Composer package:
"bolt/core": "^5.0 || ^6.0"
It uses the framework-agnostic official PHP MCP SDK rather than symfony/mcp-bundle. Symfony PSR bridge versions are selected per Bolt line: 2.3 for Bolt 5 and 6.4 for Bolt 6.
Installation
composer require bahdan/bolt-mcp
Set a long random token in the Bolt environment:
BOLT_MCP_TOKEN=replace-with-a-long-random-token
For a remote deployment, also allow the public MCP hostname used by the reverse proxy:
BOLT_MCP_ALLOWED_HOSTS=mcp.example.com
Use a comma-separated list when the same installation is intentionally served from more than one hostname. Keep the list narrow; it is used by the SDK's DNS rebinding protection.
For ChatGPT custom MCP apps, use the OAuth discovery URL instead of the static token. ChatGPT can register as a public PKCE client and send each signed-in Bolt user's access token. The OAuth authorization screen reuses Bolt's own login route, so no service account is required.
Set BOLT_MCP_PUBLIC_BASE_URL=https://mcp.example.com when the public URL is
different from the request host or when running behind multiple proxies. This
value is used for OAuth metadata and must be the canonical HTTPS origin.
The endpoint is then available at /mcp and accepts Streamable HTTP requests with:
Authorization: Bearer <token>
The token is deliberately mandatory. Empty configuration fails closed.
Run the Bolt compatibility matrix (Bolt 5 uses the official bolt/project 2.3 line; Bolt 6 uses 6.0):
docker compose --profile compatibility build bolt5-compat bolt6-compat docker compose --profile compatibility run --rm bolt5-compat docker compose --profile compatibility run --rm bolt6-compat
Included tools
bolt_get_site_info— safe installation summary.bolt_list_contenttypes— content type names and field names without secrets.bolt_list_content— published records from one content type, with pagination.bolt_get_content— one published record by numeric ID.
Content reads intentionally exclude drafts and unpublished records. Write tools are
disabled by default and become available only when BOLT_MCP_WRITE_ENABLED=true.
They always require the current Bolt user to pass the corresponding content voter.
The authenticated OAuth user is used for the permission check; no service account
is involved.
Write tools:
bolt_create_content(contentType, idempotencyKey, fields, status, locale, taxonomy)— create a record.bolt_update_content(id, idempotencyKey, fields, status, locale, taxonomy)— update fields and optionally status.localeis optional and applies to localized fields.bolt_change_content_status(id, status, idempotencyKey)— change only the record status.bolt_delete_content(id, confirm, idempotencyKey)— permanently delete a record; it rejects every request unlessconfirm=trueis supplied.
Every write requires a stable idempotencyKey (8-128 URL-safe characters).
Retries with the same key and the same request return the original result; reuse
of a key for different arguments is rejected. Results are retained for 24 hours.
fields accepts the normal Bolt field names. For a set, pass an object whose
keys are the child field names. For a collection, pass items such as
[{"name":"text","value":"First item"}]; this is converted into the child
Field entities Bolt expects. Media fields use Bolt's normal filename/media
reference and are rejected if the referenced media does not exist.
taxonomy is an object mapping taxonomy type to an array of slugs or objects:
{"categories":["news",{"slug":"featured","name":"Featured"}]}. Supplying a
taxonomy type replaces the record's existing terms for that type.
All writes use Bolt's content factory, lifecycle events, field definitions, content validation when configured, and the normal Doctrine persistence flow. Unknown fields and unsupported statuses are rejected before persistence. The write tools are also marked with MCP read-only/destructive annotations so clients can present an appropriate confirmation UI.
To enable writes deliberately:
BOLT_MCP_WRITE_ENABLED=true
Keep this disabled for read-only installations. Grant only the Bolt roles needed for the content types that the MCP user should manage.
OAuth endpoints
GET /mcp/oauth/.well-known/authorization-server— authorization server metadata.GET /mcp/.well-known/oauth-protected-resource— protected resource metadata.POST /mcp/oauth/register— public PKCE client registration.GET|POST /mcp/oauth/authorize— Bolt-session consent screen.POST /mcp/oauth/token— authorization-code and refresh-token exchange.
OAuth data is stored under var/bolt-mcp/oauth.json; mutation idempotency records
and an audit JSONL log are stored alongside it. Use a shared writable persistent
volume for multi-container deployments, and restrict that directory to the
web-process account with mode 0700/0600 permissions.
Development
Run the complete verification container:
docker compose run --rm test
Run style checks:
docker compose run --rm test vendor/bin/phpcs
Security model
The package keeps read and write capabilities separate, uses Bolt's own content voters, requires explicit write enablement, avoids arbitrary SQL/PHP/filesystem access, and records successful mutations through both the Bolt MCP logger and a local audit log without field values or bearer tokens. OAuth authorization codes and refresh tokens are single-use; the file store uses an exclusive lock for issuance and consumption. Delete is always a hard delete and requires an explicit confirmation argument.
License
MIT