The endpoint is on the public internet, so it faces more than your intended agent. This page lists who can reach it, what stops each of them, and what’s left for you to decide. To see the checks in order, send a request through the request path.
Who can reach it, and what stops them#
| Who | What they try | What happens |
|---|---|---|
| An anonymous scanner | Probe the endpoint or guess tokens | The throttle runs before authentication, at 60 requests a minute by default. Every bad token gets the same 401, so nothing can be enumerated. |
| An anonymous scanner | Send a huge Authorization header to burn CPU on hashing | In token mode, a header over 256 characters is rejected before any parsing or hashing. In OAuth mode, only the throttle applies. |
| An attacker timing responses | Learn which token ids exist | In token mode, the hash compare runs in constant time, and against a random value when the id doesn’t exist. |
| Someone who reads your disk or a backup | Take tokens from tokens.yaml | The file holds SHA-256 hashes only, at mode 0600. A token issued in the Control Panel does pass once through the session so the page can show it, so a file or database session store holds it briefly. |
| A logged-in agent acting outside its job | Call tools it shouldn’t | Every call checks the acting user’s Statamic permissions, and the denial names what’s missing. |
| An agent with a stale tool list | Call a tool that read_only or deletes has since hidden | Handlers re-check the switch on every call. The tool answers not found. |
| An agent looking for hidden content | Name a collection outside resources | A hidden handle reads exactly like a missing one, and opening a hidden entry by id answers not found. Agents get no title, URL, or status for hidden items in navigation menus, augmented reads, or blueprint field options. Stored values stay, so see what’s left to you. |
| A prompt-injected agent | Fetch http://169.254.169.254/ or an internal host through assets_upload | Private and special-purpose addresses are refused, the connection goes to the checked IP whatever host curl reads from the URL, and redirects are re-checked. The guard knows ranges, not your network, so read how URL uploads stay off your network. |
| An agent with a path | Read outside the container with ../ | The tool answers with an error, and Statamic refuses the path before it touches the disk. |
| A rogue OAuth client | Register itself as “Claude” and hope an editor approves | The consent screen shows the redirect URI’s host in full. Any caller can register under any name, so the host is what to check. |
| Another site | Load the consent page in a hidden frame | laravel/mcp 1.1.2 sends X-Frame-Options: DENY and frame-ancestors 'none' with it. |
| A client registering tricky redirect URIs | Slip past mcp.redirect_domains with http://localhost:[email protected]/ | Registration is limited to ten redirect URIs of up to 1,024 characters, and a site that limits redirect_domains to http://localhost no longer accepts that URI. The default in laravel/mcp’s config is *, which allows any host, so publish that config to limit them. |
| Anyone flooding OAuth registration | Fill Passport’s tables | Registration and discovery allow 60 requests a minute per address, whatever middleware says. |
| Your own SPA or mobile API token | Reuse it as an MCP credential | The endpoint requires the mcp:use scope or Passport’s * superscope, and it only accepts tokens issued for Statamic users. A token with narrower scopes gets a 403. A * token issued for a Statamic user passes, because * is a deliberate full-access grant, so don’t mint those. |
| Someone reading augmented output | Harvest editor emails | Users come back as id, name, and api_url, with email only for a caller who has view users. |
| An editor who isn’t a super admin | Rewrite the site-wide guidelines | Only super admins can open the Guidelines page, and it refuses text Statamic would run as a template. Other routes to the agent remain, so see what’s left to you. |
What fails closed#
- No token, no access. Nothing is reachable until you issue one.
- A broken config. A
route,server, ormiddlewarevalue of the wrong shape keeps the route unmounted, so no unauthenticated route gets left behind. The addon reportsStatamic MCP failed to mountand your site stays up. Any other exception while registering the routes surfaces, as it would from any other package. - A corrupt
tokens.yaml. Every request answers 500, so nobody gets in, andmcp:doctorreports it with a recovery path. - A token store lock that can’t be acquired. Nothing gets written.
- Missing Passport or Passport keys, or an unreachable database that holds the keys. The endpoint answers 503 with the remedy, and only that route is affected. Missing Passport tables aren’t caught at runtime and give a 500 during the connector flow, so run
mcp:doctor. - A stale route cache. The routes check
enabledandauthon every request, so an old cache can’t keep serving an old value. Withenabledoff and routes cached, every verb answers 404. - A deleted user. Their tokens stop working, and there’s nothing to clean up.
- A save that a listener cancels. The tool reports what wasn’t stored under
not_stored, or says nothing was created, instead of reporting success.
What’s left to you#
The addon can’t decide these for you.
Whether edits to live entries wait for a person. Without revisions they don’t. A role with edit permission can change a live entry at once, and it can re-date a published entry, which can schedule or expire it. If that matters, enable revisions, which need Statamic Pro. Revisions don’t cover moves. A move with parent is live at once, so leave Reorder entries off the role too.
Which writes go live without a draft. Terms, globals, assets, and navigations have no draft state. A role that can edit them changes the live site.
How long a token lives. A token without --expires-days never expires. Pick an expiry for anything that isn’t your own machine.
Where the token sits. claude mcp add puts it in your shell history, and .cursor/mcp.json holds it in plain text. Keep that file out of version control.
Who gets a preview link. Anyone with the URL can view that entry for an hour, without logging in. Treat it as you would the draft itself.
What your agent reads. Form submissions are written by anonymous visitors, and entries by whoever can edit them. An agent treats what it reads as context, so a submission can carry instructions aimed at the agent. Keep write tools away from an agent whose job is reading submissions.
Who can steer agents. Field, set, and section instructions reach agents through blueprints_get, so anyone who can edit a blueprint can write to them. While the Guidelines page is empty, agents also read a global set named guidelines if its blueprint has exactly site and resources fields, so a role that can edit that set can too.
What resources promises. It stops an agent opening or listing hidden items. It doesn’t scrub stored values. A raw read returns an entry id, term slug, or asset path outside the exposure as stored, and navigations_get returns a linked entry’s id and any title set on the branch. entries, terms, and link fields don’t check it, so writes can store a value outside it. A computed value that returns plain data, such as a string or an entry’s toArray(), comes back as written, so keep those out of collections an agent may read. statamic_overview also lists every site with its name and URL, marking the ones the user can’t access with can_access: false.
What agents can store and read back. Without a mimes rule on a container, a caller with upload permission can store what a URL returned as x.txt and read it through the asset’s URL. Set one on containers agents upload to.
Personal data. Submissions often hold names and emails, and they reach whichever AI client the agent runs in. Use resources.forms or the role to keep forms away from agents that don’t need them. submissions_delete exists for erasure requests, if you enable deletes.
Before you expose a site#
- Set
APP_URLto the public https address, so generated snippets are right and tokens aren’t sent in the clear. - Give each agent its own user and role. Keep your super admin account out of it.
- Turn on revisions for any collection whose live entries must wait for a person, and leave Reorder entries off the role.
- Limit
resourcesto what the agents need. A list like['blog', 'pages']exposes only those. - Leave
deletesoff unless an agent has a real job that needs it. - Set an expiry on tokens, and revoke the ones nobody uses under Tools → MCP → Connections.
- Set
uploads.source_allowlistif agents only ever upload from known hosts. It’s the strongest limit on URL uploads. Add amimesrule to containers agents upload to. - Run
php please mcp:doctorin your deploy. It exits non-zero on a failure.