bahdan/bolt-mcp

Secure MCP endpoint and Bolt CMS tools for Bolt 5 and Bolt 6

Maintainers

Package info

github.com/bahdanhal/bolt-cms-mcp

Type:bolt-extension

pkg:composer/bahdan/bolt-mcp

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-29 18:59 UTC

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. locale is 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 unless confirm=true is 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