---
url: /internals/mcp-server.md
---
# MCP server internals

The `mcp/` module exposes the CMS over the Model Context Protocol through `laravel/mcp`. The user side is on [MCP Server](/modules/mcp-server).

## Wiring

`mcp/src/Http/routes.php` registers `Mcp::local('evolve', EvolveServer::class)` always (stdio, `php artisan mcp:start evolve`) and `Mcp::web(config('mcp.http.path'), EvolveServer::class)` when `mcp.http.enabled`. `McpServiceProvider` sets `$defaultEnabled = false` but the shipped config says `'mcp' => true`, and the config wins; `requiredPackages()` names `laravel/mcp`, which the package only suggests since 2.21; without it the module does not boot. The provider publishes only `mcp.config`.

## The server

`mcp/src/Servers/EvolveServer.php` carries `#[Name]`, `#[Version('2.1.0')]` and `#[Instructions]` that describe every tool and its rules to the connecting agent. `$tools` lists thirteen tool classes from `mcp/src/Tools/` (list pages, get page structure, get block content, get SEO meta, list modules, list media, create page, update block content, set page published, add block to page, add child block, delete block, delete page), `$resources` two from `mcp/src/Resources/` (available block types, site config). `$publicTools` and `$publicResources` are the anonymous surface.

`boot()` runs per request: when `restrictsToPublicContent()` is true, the tool and resource lists are swapped for the public ones and `Support\PublicScope::enable()` marks the request. The decision: not the HTTP transport, trusted (stdio); HTTP without a configured `mcp.http.auth_guard`, public; otherwise `auth()->guard($guard)->check()`. Over HTTP, `boot()` also binds the authenticated user as the actor (`Racerfish\Mcp\Support\McpActor`, 2.18). Each write tool checks the actor against the permission catalogue before mutating (`pages.create`, `pages.delete`, `pages.publish`, `pages.edit` for the block tools) and honours Sanctum token abilities; a denial returns `isError: true` with the missing permission. Without an actor (stdio) the check passes.

## Public scope

`PublicScope` is a request-scoped container flag; the read tools call its query helpers to restrict pages and blocks to what the frontend would show (the same `public` and `published` scopes). It is never set for stdio.

## Tools

Each tool extends `Laravel\Mcp\Server\Tool`, declares its schema (page ID, locale, search, `confirm` for the delete tools, `is_published` for the publish tool) and returns JSON. `CreatePageTool` creates the page, its route, SEO title and a nested block tree in one call and returns `page_id`, path and `edit_url`. The delete tools hard-delete and descend into morph children themselves to avoid orphans. Shared behaviour sits in `mcp/src/Tools/Concerns`.

## Tokens

`mcp/src/Console/Commands/MakeMcpTokenCommand.php` (`mcp:token {user} {--name=mcp}`) issues a Sanctum personal access token for a user by ID or email. Sanctum is a dev dependency of the package only; a project installs it and adds `HasApiTokens` to its user model. Without Sanctum the endpoint stays read-only for everyone.

## Tests

`mcp/tests/`: public versus authenticated surface, the Sanctum path (2.3.1), each write tool, permission denials and narrow token abilities (2.18), and requests in the MCP 2026-07-28 shape (`_meta` plus headers), which skip below laravel/mcp 1.0. The constraint is `^0.6 || ^0.7 || ^0.8 || ^0.9.6 || ^1.0.1`. The starter bundle `core/stubs/claude/.mcp.json` wires `evolve` and `laravel-boost` over stdio for Claude Code.
