MCP OAuth
MCP clients (ChatGPT, Claude, Claude Code, Cursor, VS Code, Codex and others) connect with OAuth 2.1: they open a Pluma screen, you pick the site and approve, and the client is connected. No copying tokens. This page is the technical reference; the guide for people is Step 3 · Connect the MCP.
It implements MCP authorization: protected resource metadata (RFC 9728), authorization server metadata (RFC 8414), dynamic client registration (RFC 7591), and authorization code with required PKCE S256.
Discovery
A request to /mcp without a token responds 401 with:
WWW-Authenticate: Bearer resource_metadata="https://pluma.so/.well-known/oauth-protected-resource/mcp"
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-protected-resource/mcp
{ "resource": "https://pluma.so/mcp", "authorization_servers": ["https://pluma.so"], "bearer_methods_supported": ["header"] }
GET /.well-known/oauth-authorization-server
{
"issuer": "https://pluma.so",
"authorization_endpoint": "https://pluma.so/oauth/authorize",
"token_endpoint": "https://pluma.so/oauth/token",
"registration_endpoint": "https://pluma.so/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
Client registration
POST /oauth/register
{ "client_name": "Claude Code", "redirect_uris": ["http://localhost:53682/callback"] }
- Public clients: no
client_secret(token_endpoint_auth_method: none). PKCE provides the security. - Each
redirect_urimust behttps://, orhttp://tolocalhost/127.0.0.1(native apps). - Loopback addresses (
http://127.0.0.1,http://localhost) match on any port, as in RFC 8252: native clients pick a free port each time. Scheme, host and path still have to match. Everything else must match exactly. - Without
client_name, the client is called "MCP client". - Responds
201with theclient_id.
Authorization
GET /oauth/authorize
Parameters: response_type=code, client_id, redirect_uri (must match a registered one), code_challenge and code_challenge_method=S256, state, and optionally resource. Pluma accepts https://pluma.so/mcp or the bare origin https://pluma.so (ChatGPT sends the origin); any other value is rejected.
Pluma asks you to sign in (if you haven't), shows you which client is asking for access and which redirect_uri it will return to, and lets you pick the site. Anyone on the site can connect a client: the agent can do the same as their role (see What the agent can do).
POST /oauth/authorize
This is the button on the screen. On approve, it returns to redirect_uri?code=…&state=…; on cancel, ?error=access_denied&state=…. The code works once and for 10 minutes.
Token
POST /oauth/token
Form-encoded: grant_type=authorization_code, code, redirect_uri, client_id, code_verifier.
{ "access_token": "pluma_agt_…", "token_type": "Bearer", "scope": "read_published read_drafts write publish" }
- The token belongs to a new agent on the chosen site, named after the client. It shows up under Agents and you revoke it there.
scopeis the permissions given by the role of whoever approved (the example is from an editor). See What the agent can do.- It doesn't expire on its own: it lives until you revoke it. There is no
refresh_token. - Reusing a code revokes the agent that code created (protection against stolen codes).
Token errors use the OAuth format: {"error": "invalid_grant", "error_description": "…"}.