---
title: MCP OAuth
status: current
phase: 5
order: 3
---

# 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](../guides/connect-mcp.md).

It implements [MCP authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/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`

```json
{ "resource": "https://pluma.so/mcp", "authorization_servers": ["https://pluma.so"], "bearer_methods_supported": ["header"] }
```

`GET /.well-known/oauth-authorization-server`

```json
{
  "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`

```json
{ "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_uri` must be `https://`, or `http://` to `localhost`/`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 `201` with the `client_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](tools.md#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`.

```json
{ "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.
- `scope` is the permissions given by the role of whoever approved (the example is from an editor). See [What the agent can do](tools.md#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": "…"}`.
