---
url: /modules/mcp-server.md
---
# 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.

::: glance

* **Module key** `mcp`
* **Default** on since 2.2.2
* **Switch** `EVOLVE_MODULE_MCP`, see [Modules](/modules/#switching-a-module-on-or-off)
* **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](#remote-access-http-transport).

## Requirements&#x20;

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](/site/health-checks) 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

| Variable | Default | Effect |
|---|---|---|
| `MCP_SERVER_NAME` | `Evolve CMS` | Display name reported to clients. |
| `MCP_HTTP_ENABLED` | `false` | Serve the server over HTTP as well. |
| `MCP_HTTP_PATH` | `mcp/evolve` | Path of the HTTP endpoint, POST only. |
| `MCP_HTTP_AUTH_GUARD` | `sanctum` | Guard 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`](https://github.com/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](#remote-access-http-transport).

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

| Caller | Access |
| --- | --- |
| No token | Read only, scoped to **published, public content**. |
| Valid token | The full tool list. Write tools run only when the token's user has the matching panel permission (2.18). |
| Local stdio | Unchanged. Full access, trusted process. |

Since 2.18 each write tool checks the user the token belongs to against the [permission catalogue](/getting-started/roles#permissions) before it changes anything:

| Tool | Permission |
| --- | --- |
| `create-page-tool` | `pages.create` |
| `delete-page-tool` | `pages.delete` |
| `set-page-published-tool` | `pages.publish` |
| `update-block-content-tool`, `add-block-to-page-tool`, `add-child-block-tool`, `delete-block-tool` | `pages.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](https://laravel.com/docs/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](#remote-access-http-transport)):

* 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.
