Skip to content

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, update, publish or delete content through tool calls. The same server runs locally over stdio for an IDE assistant, or is published as an HTTP endpoint so external systems can drive the CMS over the network.

  • Module key mcp
  • Default on since 2.2.2
  • Switch EVOLVE_MODULE_MCP, see Modules
  • Role access the stdio transport trusts the local process; over HTTP a token unlocks the full tool list and each write tool checks the panel permissions of the token's user (2.18)
  • Panel no screen
  • Requires laravel/mcp in the project since 2.21, see below
  • Since 2.1

The panel has no screen for the MCP server. Role lists in evolve.roles.modules do not apply to it: the stdio transport trusts the local process, and the HTTP transport decides by token and, for write tools, by the panel permissions of the token's user, see Remote access.

Requirements 2.21 ​

The module needs laravel/mcp. Since 2.21 Evolve only suggests the package instead of requiring it, so a project that uses the MCP server requires it itself:

bash
composer require laravel/mcp

Before 2.21 the package came with Evolve. A project that updates to 2.21 without requiring it loses laravel/mcp on the next composer update, and the MCP server stops: no stdio server, no HTTP endpoint, no mcp:token command. Nothing else breaks. While the package is missing, the module stays off even with its switch on, the health endpoint reports it with active: false and missing_packages: ["laravel/mcp"], and the module switches check warns. A project that does not use the server switches it off with EVOLVE_MODULE_MCP=false, which also clears the warning.

Since 2.18 Evolve allows laravel/mcp 0.9.6 and 1.x, which fix the advisory GHSA-mx2h-h55v-pm44; projects pick the fix up with composer update laravel/mcp -W. Evolve does not use the affected OAuth routes, so the update is hygiene. Clients speaking the older protocol keep working on 1.x.

Configuration ​

VariableDefaultEffect
MCP_SERVER_NAMEEvolve CMSDisplay name reported to clients.
MCP_HTTP_ENABLEDfalseServe the server over HTTP as well.
MCP_HTTP_PATHmcp/evolvePath of the HTTP endpoint, POST only.
MCP_HTTP_AUTH_GUARDsanctumGuard that recognises a write-capable caller.

None of these are edited in the panel. Publish config/mcp.php with vendor:publish --tag=mcp.config to change them in code.

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.

The server announces itself with instructions that name every tool and its rules, so an agent that connects knows the surface without reading this page. Fleet, the agency's inventory, probes /mcp/evolve on every known site to record whether the remote transport is on.

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.
  • set-page-published-tool 2.9: publish or unpublish an existing page (page_id, is_published). Reversible, so no confirmation flag; authenticated callers only.

Delete tools (destructive, irreversible):

  • delete-block-tool: hard delete a block and all its nested children. Requires confirm: true.
  • delete-page-tool: hard delete a page along with its routes and complete block tree. Requires confirm: true. Refuses home pages.

Resources:

  • available-block-types-resource: every block type and its field schema, so a client knows what props each block accepts.
  • site-config-resource: 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 available-block-types-resource 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:

json
{
  "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:

bash
php artisan vendor:publish --tag=evolve.claude.starter

For existing projects whose .mcp.json predates v2.2.2, add the evolve entry manually:

json
{
    "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:

dotenv
MCP_HTTP_ENABLED=true
# Optional, these are the defaults:
# MCP_HTTP_PATH=mcp/evolve
# MCP_HTTP_AUTH_GUARD=sanctum

The equivalent block in the published config/mcp.php:

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:

CallerAccess
No tokenRead only, scoped to published, public content.
Valid tokenThe full tool list. Write tools run only when the token's user has the matching panel permission (2.18).
Local stdioUnchanged. Full access, trusted process.

Since 2.18 each write tool checks the user the token belongs to against the permission catalogue before it changes anything:

ToolPermission
create-page-toolpages.create
delete-page-toolpages.delete
set-page-published-toolpages.publish
update-block-content-tool, add-block-to-page-tool, add-child-block-tool, delete-block-toolpages.edit (also for blocks in sections or module records)

A denied call returns an MCP tool error that names the missing permission; the tools still appear in tools/list. By default developer, admin and content_manager have these permissions. Sanctum token abilities apply on top: a token issued with a narrower ability list must name the permission too. Tokens from mcp:token carry *, so only the user's permissions decide. Read tools for authenticated callers are not permission-checked. Before 2.18 any valid token had the full surface regardless of the user's role.

Issue tokens for a dedicated user whose role has exactly the permissions the external system needs, and treat them like a password.

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:

bash
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate

Add Sanctum's HasApiTokens trait to your user model (the one behind config('auth.providers.users.model'), usually App\Models\User):

php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;
    // ...
}

Then issue a token for a user:

bash
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:

json
{
    "mcpServers": {
        "evolve-remote": {
            "type": "http",
            "url": "https://your-site.test/mcp/evolve"
        }
    }
}

For full access, add the bearer token:

json
{
    "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:

bash
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"

Upgrading from 2.0 to 2.2.1 ​

Those versions shipped the module off. A project coming from them sets 'mcp' => true under modules in its published config/evolve.php (or EVOLVE_MODULE_MCP=true), runs composer update racerfish/evolve so that laravel/mcp arrives, and registers the server for its client as described above. Re-publishing the config with vendor:publish --tag=evolve.config --force overwrites the project file; back it up first.

Verifying activation ​

Run this checklist on any project that should have a working MCP server:

bash
# 1. Module enabled (project config or .env override)
grep "'mcp'" config/evolve.php; grep EVOLVE_MODULE_MCP .env
# expect: 'mcp' => true, and no EVOLVE_MODULE_MCP=false

# 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 errors

You can also list the exposed tools without an interactive client by piping a JSON-RPC request to the command:

bash
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 rest

If 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:

bash
composer update racerfish/evolve

Or install the dependency directly:

bash
composer require laravel/mcp

Tools 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 EVOLVE_MODULE_MCP=false in the .env or 'mcp' => false in config/evolve.php. The provider short circuits during boot: no stdio server, no HTTP endpoint, and 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, publish and delete tools (create-page-tool, update-block-content-tool, set-page-published-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.