OAuth mode is for connector clients that can’t send static headers, such as individual-plan claude.ai and Claude Desktop connectors, and ChatGPT. Dynamic client registration, PKCE, metadata discovery, and the consent screen are delegated to laravel/mcp and Laravel Passport.
Your users stay exactly where they are, and file users work. The addon brings its own auth guard. Passport’s ResourceServer validates the bearer token’s signature, expiry, and revocation, and the addon resolves the user through the Statamic repository instead of Eloquent. There’s no user migration, no HasApiTokens trait, and no api guard to configure. The addon defines its guard (statamic-mcp) in memory, so config/auth.php is never touched.
What it needs#
laravel/passport, for the OAuth endpoints and token machinery.- A database for Passport’s own tables, meaning clients, tokens, and auth codes. SQLite is fine. Your users don’t live there.
- Passport’s encryption keys, which the addon manages. It stores the pair in the database, encrypts the private key with
APP_KEY, and provisions a pair on first use. - String
user_idcolumns on Passport’s tables. Statamic ids are UUID strings and Passport’s stock columns are bigint. The addon ships a migration that converts them, loaded automatically in OAuth mode and safe for integer ids too.
Connector clients reach your site from the internet, so it also needs a public HTTPS URL.
The wizard#
php please mcp:setup --oauth The wizard installs Passport, sets STATAMIC_MCP_AUTH=oauth, runs the migrations, and creates the keys. It asks before each step and finishes by running mcp:doctor. Running it again is safe, because it skips what’s already done. Add --yes to run it unattended. Each edit is still printed.
php please mcp:setup --oauth --yes Then add https://example.com/mcp/statamic as a connector in your client. It discovers the OAuth server, registers itself, and sends you through a Statamic login and the consent screen. The resulting token maps to that real Statamic user, so permissions work exactly as in token mode. The mcp:use scope gates entry, then Statamic’s permissions decide everything else.
Manual setup#
composer require laravel/passport STATAMIC_MCP_AUTH=oauth php artisan vendor:publish --tag=passport-migrations php artisan migrate php please mcp:keys The .env line in the second block has to be in place before the final php artisan migrate, because the addon’s migrations only load in OAuth mode. Running migrate again after the change fixes an accidental early run. mcp:keys is optional. It provisions the keys now instead of on the first request.
Deploying#
Set the mode in each environment and let your pipeline run migrations. There is no key step.
STATAMIC_MCP_AUTH=oauth php artisan migrate --force The keys live in the database, so every server of an environment shares them, and they survive releases and read-only filesystems such as Vapor. Each environment gets its own pair. php please mcp:doctor verifies each environment and exits non-zero on a failure, so it fits in a deploy step.
Keys#
The addon looks for keys in this order, and only generates a pair when none exist anywhere.
| Source | When it’s used |
|---|---|
PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY | Always take precedence when set |
The statamic_mcp_oauth_keys table | The normal case |
storage/oauth-*.key files | Adopted into the database on first use. From then on the database copy is used. |
To use explicit environment keys instead, say one pair shared across apps, export them. --json pipes into secret-store CLIs, and --write fills a local .env. If the database can’t be read, mcp:keys refuses to generate a pair, because writing it to .env would override the stored one and every connected client would have to reconnect.
php please mcp:keys If APP_KEY changes#
The stored key is encrypted with APP_KEY, so rotating APP_KEY makes it undecryptable. mcp:doctor and the endpoint’s 503 both name that state. Restore the previous APP_KEY, or delete the row in statamic_mcp_oauth_keys to provision a fresh pair, after which every connected client reconnects.
The consent screen#
The addon binds a working, self-contained consent screen automatically. Passport 12 and newer ship no default consent view, so without this /oauth/authorize would return a 500. mcp:doctor verifies it’s bound.
The screen shows the client’s name, the account that approves, the access the client asks for, and where approving sends the editor. That’s the redirect URI as the browser will reach it, lowercase and with punycode for non-Latin hosts, with the host in dark text. A URI over 64 characters folds after its host into one line that expands on click, and the host always shows in full.
Read the host before you approve. Any caller can register a client under any name, so the host is what tells a real connector from a lookalike called “Claude”.
Restyle it#
Publish the Blade view and edit the copy. There’s no need to publish laravel/mcp’s own view, which depends on a compiled Vite and Tailwind bundle and returns a 500 on sites without one.
php artisan vendor:publish --tag=statamic-mcp-views The view lands in resources/views/vendor/statamic-mcp/oauth/authorize.blade.php. To supply your own entirely, call Passport::authorizationView(...) in your AppServiceProvider::boot(), and the addon steps aside. A view you publish or supply yourself should show the redirect URI too.
When something is missing#
If Passport isn’t installed, its keys are missing or undecryptable, or the database that holds them can’t be reached, the MCP endpoint answers 503 with the exact remedy and a pointer to mcp:doctor. Only that route is affected. The rest of your site is untouched, and token mode keeps working if you switch back.
{
"error": "MCP OAuth mode is misconfigured.",
"remedy": "Passport's encryption keys are missing. Run 'php artisan migrate' — the addon provisions a pair into the database automatically — or 'php please mcp:keys' to generate one now.",
"doctor": "Run 'php please mcp:doctor' to check every OAuth prerequisite at once."
} The endpoint doesn’t check Passport’s tables or user_id columns at runtime. If those migrations haven’t run, the connector flow fails with a 500 at the handshake or at consent. mcp:doctor reports both, so run it after every OAuth change.
Which tokens sign in#
- A token needs the
mcp:usescope, or Passport’s*superscope. A token minted for your own SPA or mobile API with narrower scopes gets a 403. A*token is a deliberate full-access grant and passes, so don’t mint those for users who shouldn’t reach MCP. - Only tokens issued for Statamic users sign in. A token from a Passport client of another user provider, or a client credentials token, doesn’t.
Client registration and discovery keep their own limit of 60 requests a minute per address, whatever your middleware setting says. Registration shares that budget with /oauth/token and the MCP route’s default throttle.
Connections and disconnecting#
Tools → MCP → Connections shows one row per connected user and client pair: client name, user, first connected, last token refresh, and whether the connection is still usable. A live refresh token counts as usable, since the connector can come back without consenting again. Users see and disconnect their own connections, and super admins see everyone’s. See Control Panel.
Disconnect revokes the pair’s access tokens and their refresh tokens. The connector gets a 401 on its next request and has to go through login and consent again. Passport doesn’t track per-request usage, so “last refreshed” reflects token issuance, not the last MCP call.
Dead rows accumulate as tokens expire and rotate. That’s Passport’s housekeeping, so schedule passport:purge.
Turning MCP off#
STATAMIC_MCP_ENABLED=false turns off the MCP endpoint, OAuth discovery, client registration, and the Tools → MCP pages. If Passport is installed, the routes it registers itself stay: /oauth/authorize, /oauth/token, and the device routes. An existing token still can’t reach MCP, because the endpoint is gone.
With MCP off at boot, the addon doesn’t load its database-managed keys or bind the consent view, so on a site that relies on either, Passport’s routes answer 500. That includes the site’s own API if it uses Passport with those keys.
To remove Passport’s routes as well, call Passport::ignoreRoutes() in the register() method of a service provider. MCP’s OAuth mode needs those routes, so take the call out before turning it back on, and leave it out if the site’s own API uses Passport.
Coming from the Eloquent setup#
Earlier versions required database users, the Passport api guard, and HasApiTokens on the user model. All of that keeps working, and existing tokens stay valid, but none of it is required anymore. The leftover guard and trait are harmless, so remove them when convenient.
Run php artisan migrate once after upgrading, so the user_id conversion applies and the key table gets created. Existing keys are never touched. Environment keys keep precedence, and key files are adopted into the database on first use.