MCP Server
Evolve ships an MCP (Model Context Protocol) server that exposes CMS read and write capabilities to compatible AI clients. Once configured, agents can list pages, inspect block structure, read media, and create or update content directly through tool calls. The same server can run locally over stdio for an IDE assistant, or be published as an HTTP endpoint so external systems can drive the CMS over the network.
Overview
The server is implemented on top of laravel/mcp. Out of the box it registers a single local stdio server (evolve) that AI clients connect to by spawning php artisan mcp:start evolve in the project root. No HTTP endpoint is exposed by default, so the server is only reachable from a local process invoked by you or your IDE. Remote access is opt in and covered under Remote access.
What the server exposes
Discovery tools:
list-pages-tool: list all pages, with optional locale and search filters.get-page-structure-tool: full block tree for a page.get-block-content-tool: props, media, and blueprint schema for a single block.get-seo-meta-tool: SEO metadata for a route or page.list-media-tool: media library entries.list-modules-tool: enabled modules.
Create tools:
create-page-tool: create a new page with a complete (optionally nested) block tree.add-block-to-page-tool: append a single block to an existing page. Use to fill empty pages without rebuilding them.add-child-block-tool: append a child block to an existing container (cms.cards,cms.tiles,cms.logoCloud, and similar). Use to fill empty container blocks.
Modify tools:
update-block-content-tool: update one block's fields by ID.
Delete tools (destructive, irreversible):
delete-block-tool: hard delete a block and all its nested children. Requiresconfirm: true.delete-page-tool: hard delete a page along with its routes and complete block tree. Requiresconfirm: true. Refuses home pages.
Resources:
AvailableBlockTypesResource: every block type and its field schema, so a client knows what props each block accepts.SiteConfigResource: site level configuration (locales, defaults).
These let an AI assistant work with content the same way the Evolve admin does.
Destructive operations
delete-block-tool and delete-page-tool are hard deletes. There is no soft delete or trash. Both refuse without an explicit confirm: true argument. This is intentional friction so an AI client cannot delete content by accident on a misread instruction. Recursive descent into morph children (block inside block nesting) is handled in the tool itself to avoid orphan rows.
What you can build
The tools compose into full content workflows. Below are the common patterns, framed as what you would ask an agent to do and which tools it reaches for. The agent always discovers the available block types from the AvailableBlockTypesResource first, so the props it sends are valid for your project.
Scaffold a page from a brief
"Create a German landing page titled 'High end CMS systems' with a hero, an intro paragraph, and a three card feature row."
The agent calls create-page-tool once with a nested block tree. A single call creates the page, its route, the SEO title, and the whole block hierarchy:
{
"title": "High end CMS systems",
"locale": "de",
"meta_description": "Discover what Evolve can do.",
"blocks": [
{ "type": "cms.hero", "props": { "de": { "title": "High end CMS systems" } } },
{ "type": "cms.simpleText1Col", "props": { "de": { "content": "<p>Intro paragraph.</p>" } } },
{
"type": "cms.cards",
"props": { "de": { "title": "Features" } },
"children": [
{ "type": "cms.card", "props": { "de": { "title": "Fast", "content": "<p>...</p>" } } },
{ "type": "cms.card", "props": { "de": { "title": "Flexible", "content": "<p>...</p>" } } },
{ "type": "cms.card", "props": { "de": { "title": "Friendly", "content": "<p>...</p>" } } }
]
}
]
}The response returns the new page_id, the route path, and an edit_url that opens the page in the Evolve admin.
Audit and edit existing content
"Find every page that still says 'beta' and update the wording."
The agent runs list-pages-tool with search: "beta", then get-page-structure-tool and get-block-content-tool to read the offending blocks, then update-block-content-tool per block. Each update targets one block by ID and patches only the fields you name, leaving the rest of the tree untouched.
Fill empty containers
"The features page has an empty card row. Add three cards."
The agent reads the page with get-page-structure-tool, finds the empty cms.cards block ID, and calls add-child-block-tool three times (or add-block-to-page-tool to append a fresh container). This is the targeted alternative to rebuilding the page.
Attach media
"Use logo IDs 5, 6, and 7 in the logo cloud."
The agent calls list-media-tool to resolve library entries, then passes media_ids on the relevant block when creating or updating it. Media is referenced by library ID, never re-uploaded.
Review SEO
"Check the meta descriptions on the top level pages."
list-pages-tool for the set, get-seo-meta-tool per route. Read only, safe to run unattended.
Connecting a local client
Any MCP compatible client that can spawn a local stdio process works (Claude Code, Cursor, and others). The client launches php artisan mcp:start evolve and speaks JSON-RPC over stdin and stdout.
Register the server in .mcp.json
Clients read .mcp.json in the project root to discover MCP servers. Fresh installs that publish the starter scaffolding get a working .mcp.json automatically:
php artisan vendor:publish --tag=evolve.claude.starterFor existing projects whose .mcp.json predates v2.2.2, add the evolve entry manually:
{
"mcpServers": {
"evolve": {
"type": "stdio",
"command": "php",
"args": ["artisan", "mcp:start", "evolve"],
"env": {}
}
}
}Restart your AI client after editing .mcp.json so it picks up the new server. Clients that do not read .mcp.json use their own config format, but the command stays the same: spawn php artisan mcp:start evolve in the project root.
Remote access (HTTP transport)
The stdio server only serves a local process. To let an external system drive the CMS (a remote agent, a SaaS, a backend service, a hosted Claude or ChatGPT connector), enable the HTTP transport. It is a first class feature of the module: you turn it on with config, there is no route code to write.
It is disabled by default, because putting the CMS on a network endpoint is a deliberate decision.
Enable it
Set the config keys (or the matching env vars) and you are done:
MCP_HTTP_ENABLED=true
# Optional, these are the defaults:
# MCP_HTTP_PATH=mcp/evolve
# MCP_HTTP_AUTH_GUARD=sanctumThe equivalent block in the published config/mcp.php:
'http' => [
'enabled' => env('MCP_HTTP_ENABLED', false),
'path' => env('MCP_HTTP_PATH', 'mcp/evolve'),
'auth_guard' => env('MCP_HTTP_AUTH_GUARD', 'sanctum'),
],This serves the endpoint at POST /mcp/evolve. The path string is the literal URL (there is no automatic prefix), so external clients connect to https://your-site.test/mcp/evolve.
Access is tiered automatically
There is no middleware to wire up. The server decides what a caller may do from the request itself:
| Caller | Access |
|---|---|
| No token | Read only, scoped to published, public content. |
| Valid token | Full access, including create, update, and delete. |
| Local stdio | Unchanged. Full access, trusted process. |
The anonymous, read only surface is deliberately small: list-pages-tool, get-page-structure-tool, and get-seo-meta-tool, plus the block type schema resource. All three are scoped to published content using the same visibility rules as the public frontend, so drafts, private pages, and unpublished blocks are never returned. Media enumeration, module and site config introspection, and every write or delete tool are withheld until the caller presents a token. They are not merely hidden, they are not registered for an anonymous request, so there is nothing to call.
In practice this means you can hand out the bare URL for read only access to published content, and only issue tokens to the systems that need to write.
Issue a write token
Write access uses a Laravel Sanctum personal access token. Install Sanctum once:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrateAdd Sanctum's HasApiTokens trait to your user model (the one behind config('auth.providers.users.model'), usually App\Models\User):
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ...
}Then issue a token for a user:
php artisan mcp:token admin@example.com
# or by id, with an optional label:
php artisan mcp:token 1 --name="content-bot"The command prints the plain text token once. Store it now, it is not shown again. The external system sends it as a bearer token.
TIP
If Sanctum is not installed, the endpoint still works, it just stays read only for everyone, and mcp:token tells you what to install. To authenticate with a different mechanism, point MCP_HTTP_AUTH_GUARD at any guard that resolves your callers (for example api).
Point an external client at it
A remote MCP client is configured with the HTTP URL instead of a spawn command. For read only access to published content, no header is needed:
{
"mcpServers": {
"evolve-remote": {
"type": "http",
"url": "https://your-site.test/mcp/evolve"
}
}
}For full access, add the bearer token:
{
"mcpServers": {
"evolve-remote": {
"type": "http",
"url": "https://your-site.test/mcp/evolve",
"headers": { "Authorization": "Bearer <token>" }
}
}
}The exact config key differs per client, but the inputs are always the same: transport type http, the URL, and (for write access) the auth header. Always serve the endpoint over HTTPS so tokens are not sent in clear text.
Verify the two tiers
A quick curl confirms the tiering. List the tools with no token, then with one:
RPC='{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# No token: public, read-only tools only
# (list-pages-tool, get-page-structure-tool, get-seo-meta-tool)
curl -s -X POST https://your-site.test/mcp/evolve \
-H 'Content-Type: application/json' -H 'Accept: application/json' -d "$RPC"
# With a token: the full tool set (create, update, delete, media, modules)
curl -s -X POST https://your-site.test/mcp/evolve \
-H "Authorization: Bearer <token>" \
-H 'Content-Type: application/json' -H 'Accept: application/json' -d "$RPC"Activation
The MCP module ships enabled by default as of v2.2.2. The laravel/mcp composer dependency is included in the package, so no extra composer require step is needed for fresh installs.
If you upgrade from an earlier version (v2.0 to v2.2.1) that had MCP disabled, you need to do two things in your project.
1. Enable the module in config/evolve.php
Open your published config/evolve.php and ensure the mcp entry under modules is true:
'modules' => [
// ...
'mcp' => true,
// ...
],If the entry is missing entirely, the easiest path is to re-publish the package config (this overwrites your file, so back it up first):
php artisan vendor:publish --tag=evolve --force2. Register the server for your client
For a local IDE assistant, follow Connecting a local client. For an external system, follow Remote access.
Verifying activation
Run this checklist on any project that should have a working MCP server:
# 1. Module enabled in config
grep "'mcp'" config/evolve.php
# expect: 'mcp' => true,
# 2. laravel/mcp installed (transitive dep of racerfish/evolve since v2.2.2)
composer show laravel/mcp 2>&1 | head -3
# expect: name : laravel/mcp
# 3. Artisan knows the command
php artisan list | grep mcp:start
# expect: mcp:start Start the MCP Server for a given handle
# 4. .mcp.json lists evolve (local clients only)
grep -A2 '"evolve"' .mcp.json
# expect: an entry running `mcp:start evolve`
# 5. The server actually starts (quick smoke test, Ctrl+C to exit)
php artisan mcp:start evolve
# expect: server listens on stdio without errorsYou can also list the exposed tools without an interactive client by piping a JSON-RPC request to the command:
php artisan mcp:start evolve <<< '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# expect: a JSON result listing list-pages-tool, get-page-structure-tool, and the restIf any of steps 1 to 4 fails, that is the activation gap.
Troubleshooting
"I don't find any Evolve MCP tools in my AI client"
Most common cause: .mcp.json does not list the evolve server. Restarting the AI client after editing .mcp.json is necessary. Claude Code in particular caches the server list.
"MCP module enabled but laravel/mcp is not installed" in the logs
Your project's composer.lock predates v2.2.2 and does not have laravel/mcp. Run:
composer update racerfish/evolveOr install the dependency directly:
composer require laravel/mcpTools appear, but every call fails
Confirm the server can be started manually with php artisan mcp:start evolve. If that fails, check the Laravel logs for a stack trace. Common causes: missing DB connection, unrunnable migrations, or a custom block composer throwing during context build.
Remote endpoint returns 405
The MCP HTTP endpoint only accepts POST. A GET or DELETE to the same path returns 405 by design. Make sure your client uses the streamable HTTP transport and posts JSON-RPC.
"I want to disable it"
Set 'mcp' => false in config/evolve.php. The provider short circuits during boot, and the artisan command stays available but the AI client gets no tools.
Security notes
The local stdio server runs as a process invoked by your AI client, with the same database permissions as your Laravel app. There is no network surface by default.
The HTTP transport (MCP_HTTP_ENABLED=true) is safe to expose because access is tiered (see Remote access):
- An anonymous caller only ever reads published, public content through a small discovery surface. The write and delete tools (
create-page-tool,update-block-content-tool,delete-block-tool,delete-page-tool), along with media and config introspection, are not registered for it, so they cannot be called. - Write access requires a valid token.
Still observe the basics:
- Serve the endpoint over HTTPS so bearer tokens are not sent in clear text.
- Issue tokens only to the systems that need write access, and rotate them. Sanctum tokens are revocable individually.
- If you do not need remote write access at all, hand out only the URL and issue no tokens. The endpoint stays read only.