Access and safety

Security model

What protects the endpoint, what fails closed, and a checklist before you expose a site.

On this page

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#

WhoWhat they tryWhat happens
An anonymous scannerProbe the endpoint or guess tokensThe 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 scannerSend a huge Authorization header to burn CPU on hashingIn token mode, a header over 256 characters is rejected before any parsing or hashing. In OAuth mode, only the throttle applies.
An attacker timing responsesLearn which token ids existIn 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 backupTake tokens from tokens.yamlThe 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 jobCall tools it shouldn’tEvery call checks the acting user’s Statamic permissions, and the denial names what’s missing.
An agent with a stale tool listCall a tool that read_only or deletes has since hiddenHandlers re-check the switch on every call. The tool answers not found.
An agent looking for hidden contentName a collection outside resourcesA 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 agentFetch http://169.254.169.254/ or an internal host through assets_uploadPrivate 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 pathRead outside the container with ../The tool answers with an error, and Statamic refuses the path before it touches the disk.
A rogue OAuth clientRegister itself as “Claude” and hope an editor approvesThe 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 siteLoad the consent page in a hidden framelaravel/mcp 1.1.2 sends X-Frame-Options: DENY and frame-ancestors 'none' with it.
A client registering tricky redirect URIsSlip 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 registrationFill Passport’s tablesRegistration and discovery allow 60 requests a minute per address, whatever middleware says.
Your own SPA or mobile API tokenReuse it as an MCP credentialThe 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 outputHarvest editor emailsUsers 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 adminRewrite the site-wide guidelinesOnly 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, or middleware value of the wrong shape keeps the route unmounted, so no unauthenticated route gets left behind. The addon reports Statamic MCP failed to mount and 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, and mcp:doctor reports 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 enabled and auth on every request, so an old cache can’t keep serving an old value. With enabled off 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#

  1. Set APP_URL to the public https address, so generated snippets are right and tokens aren’t sent in the clear.
  2. Give each agent its own user and role. Keep your super admin account out of it.
  3. Turn on revisions for any collection whose live entries must wait for a person, and leave Reorder entries off the role.
  4. Limit resources to what the agents need. A list like ['blog', 'pages'] exposes only those.
  5. Leave deletes off unless an agent has a real job that needs it.
  6. Set an expiry on tokens, and revoke the ones nobody uses under Tools → MCP → Connections.
  7. Set uploads.source_allowlist if agents only ever upload from known hosts. It’s the strongest limit on URL uploads. Add a mimes rule to containers agents upload to.
  8. Run php please mcp:doctor in your deploy. It exits non-zero on a failure.