MCP authentication
OAuth 2.1 through Clerk, or the same rvd_ API key the REST API takes — on the same Authorization header.
https://getrevised.com/api/mcp takes two credentials on the same Authorization: Bearer ... header. The server picks the right verifier from the token’s shape; you do not.
Either way the caller resolves to the same user id and the same plan, through the same lookup the REST API uses. There is no second code path in which a rule could be enforced differently: the reveal quota, the hold cooldown, the access-delay window and name disclosure all key off those two values and nothing else.
Option 1: OAuth 2.1 (recommended)
For anything with a browser to sign in from. Give the client the URL and nothing else — no key is ever pasted.
The client POSTs with no credential
It gets
401and a challenge naming where to look:http WWW-Authenticate: Bearer realm="revised", resource_metadata="https://getrevised.com/.well-known/oauth-protected-resource/api/mcp"It reads the protected-resource document
RFC 9728. The same document is also served at the bare
/.well-known/oauth-protected-resource, because several shipping clients probe the origin first.json { "authorization_servers": ["https://clerk.getrevised.com"], "bearer_methods_supported": ["header"], "resource": "https://getrevised.com/api/mcp", "resource_documentation": "https://getrevised.com/docs/api", "resource_name": "Revised", "scopes_supported": ["openid", "profile", "email"] }It registers and runs the flow
Dynamic client registration against Clerk, then the OAuth 2.1 authorisation-code flow with PKCE
S256./.well-known/oauth-authorization-serverpasses Clerk’s RFC 8414 metadata through from the Revised origin, for clients that probe there first; itsissuernames Clerk and is deliberately not rewritten.The person signs in and consents
A browser tab opens on the Revised sign-in, then the consent screen. The client stores the access token and sends it as
Authorization: Bearer <token>.
Every URL in that flow is built from the request’s own origin, so a preview hostname or a local server advertises itself rather than production.
Scopes
| Scope | Advertised | Required |
|---|---|---|
openid | yes | no |
profile | yes | yes |
email | yes | no |
profile is the one scope a token must carry. It is deliberately a single scope rather than a per-tool ladder: splitting reads from reveals across two scopes would look like least privilege and would not be one, since a client that can search can ask for the reveal scope at the next consent screen. The thing that actually bounds spending is the plan’s monthly reveal allowance, which no token can widen.
Clerk’s instance also offers public_metadata, private_metadata and offline_access. The first two are deliberately not advertised — this server never reads the user’s Clerk metadata through the token; it resolves the plan server-side. offline_access is the client’s business to request, not Revised’s to require.
Option 2: API key
For a script, a container, CI, or any client that cannot complete an OAuth flow. The same rvd_ keys the REST API takes, created at getrevised.com/account on any plan.
{
"mcpServers": {
"revised": {
"url": "https://getrevised.com/api/mcp",
"headers": {
"Authorization": "Bearer rvd_yourprefix_yoursecret"
}
}
}
}The exact configuration shape varies by client — see the install pages for Claude Code, Claude Desktop and Cursor.
A key carries no entitlement of its own: the owner’s current plan is resolved behind it and refreshed at least once a minute. See Authentication for the token format and the per-plan key ceilings.
Which one to use
| OAuth | API key | |
|---|---|---|
| Needs a browser to sign in | yes, once | no |
| Key stored in a config file | no | yes |
| Works in CI or a headless container | no | yes |
| Rate-limit counter | shared across everything that account has connected | per key |
| Entitlements | identical | identical |
whoami reports which one the session is using, as auth: "oauth" or auth: "api_key" — worth reading before telling someone where to change a setting.
Failures
Authentication failures arrive as an HTTP status with a JSON-RPC error body and no tool result at all. They are distinct from a tool that ran and refused, which returns a result with isError: true and an UPPER_SNAKE_CASE code.
| Status | WWW-Authenticate | Meaning | Fix |
|---|---|---|---|
| 401 | bare challenge | No credential was presented. | Start authenticating. |
| 401 | error="invalid_token" | A credential was presented and is wrong, expired or revoked. | Do not retry the same one. Re-authenticate, or check the key on the account page. |
| 403 | error="insufficient_scope", scope="profile" | The sign-in granted less than this server needs. | Disconnect and connect again, accepting the consent screen. |
| 429 | — | Over the per-minute ceiling. Carries Retry-After. | Wait and retry. On an OAuth connection the counter is shared with everything else this account has connected. See Rate limits. |
Every 401 and 403 carries the WWW-Authenticate challenge with a resource_metadata pointer built from the request’s own origin, so a client that lost its token can rediscover the authorisation server without being reconfigured.
CORS
The two discovery documents are public and cross-origin: a browser-hosted client fetches them from its own origin before it has a token. They carry Access-Control-Allow-Origin: *, and OPTIONS answers the preflight on the MCP endpoint itself. There is nothing to protect in them — they say which public authorisation server Revised uses.