Start here, every time.
php please mcp:doctor It prints the endpoint and auth mode, then runs every check without stopping, so all problems are named at once. Each [FAIL] carries the same remedy text the endpoint answers with at runtime.
Look up a response#
Pick what the endpoint answered.
The token is missing, malformed, expired, or revoked, or its user was deleted. Every case answers the same way, so nobody can probe which tokens exist. In OAuth mode, Passport rejected the bearer or its user no longer resolves.
{
"error": "Unauthenticated."
}- Check the header is Authorization: Bearer mcp_… and the token is complete.
- Run php please mcp:tokens to see whether it still exists and when it expires.
- Issue a new one with php please mcp:token [email protected].
What the doctor checks#
| Check | What it looks at |
|---|---|
| Kill switch and mounting | Whether MCP is enabled and the route is actually mounted. “Enabled but not mounted” means the route, server, or middleware config has the wrong shape. The addon keeps your site up and reports Statamic MCP failed to mount; run php please mcp:doctor with the cause, and until the config is fixed, requests to the endpoint reach Statamic’s frontend route. |
| Route cache | Whether a cached route was built for another auth mode, or holds no MCP route at all. It can’t tell a cache from v1.0 apart from a current one, so rebuild the cache after updating. |
| Server class | Whether server names a subclass of the MCP server. |
| Middleware | Every entry has to be a string that resolves to a class, alias, or group. A typo mounts fine and then returns a 500 on every request. |
APP_URL | Warns on the Laravel default of http://localhost, and on plain http://, because bearer tokens over http travel unencrypted. |
| Leftover migrations | Warns about a second *_statamic_auth_tables.php, left by an interrupted OAuth setup, which would crash php artisan migrate. |
In token mode it also checks these.
| Check | What it looks at |
|---|---|
| Token store | That it’s writable, including the case where tokens.yaml was created by a different user, such as root. |
| Corrupt file | A corrupt tokens.yaml is reported with a recovery path instead of crashing. Meanwhile every request to the endpoint answers 500, so nobody gets in. |
| Token counts | Separates active tokens from expired and orphaned-user ones. “5 tokens issued but none are active” is a locked door that looks configured. |
In OAuth mode it checks these instead.
| Check | What it looks at |
|---|---|
| Passport | Whether it’s installed. |
| Database | Whether it’s reachable. If it isn’t, that’s the only OAuth check reported. |
| Encryption keys | Whether they’re available, naming the source: environment config, database, key files, or pending provision. There’s a dedicated failure when the stored key can’t be decrypted because APP_KEY changed. |
| Tables | Passport’s tables are migrated, with string user_id columns. |
| Consent view | A consent view is bound. |
There’s no user-repository check, because file users work in OAuth mode.
Common problems#
The agent can’t see a collection#
Three things can hide it. Check them in this order.
- Is the handle in
resources? A hidden handle reads as not found. - Does the user’s role have
view {collection} entries?statamic_overviewlists only what the user may view. - On multi-site, does the role have
access {site} site?
The agent says a tool doesn’t exist#
Tool [entries_delete] not found. means the tool isn’t registered. Delete tools need deletes, write tools disappear under read_only, and entries_localize only exists on multi-site. A client can also be holding an old tool list, so reconnect it.
Edits aren’t showing on the site#
On a revision-enabled collection, an edit to a live entry is a working copy. Look at result in the response, or has_working_copy on entries_get. It goes live with entries_publish.
A write was refused with an unknown field#
The key isn’t in the entry’s blueprint. The error lists the valid handles. slug, and date on dated collections, are top-level parameters and don’t go inside data.
A scheduled entry isn’t going live#
Check date_behavior in statamic_overview. The entry goes live without cron, because Statamic checks the date on every request, but the static cache and search index need schedule:run and a queue worker to refresh at that minute.
Every OAuth client has to reconnect#
The signing key changed. That happens when APP_KEY was rotated or when someone regenerated keys. See If APP_KEY changes.
Concurrent token writes#
Token writes from the command line and the Control Panel are serialized behind one of Laravel’s cache locks, so the default cache store must support atomic locks and be shared by every writer. Array and null cache stores, or per-server caches on multi-server deploys, degrade to no cross-process locking.
Use the MCP Inspector#
You can point the MCP Inspector at your server. This command comes from laravel/mcp.
php artisan mcp:inspector mcp/statamic