Skip to content

How can I add OAuth to my MCP server?

Two discovery documents, dynamic client registration, PKCE with S256, and a 401 that points the way. Plus how to fix Cursor MCP OAuth errors.

By Catherine Williams-TreloarSep 2026·7 min read

You add OAuth to an MCP server by publishing two discovery documents, accepting dynamic client registration, running authorization code with PKCE and answering unauthenticated requests with a 401 that points at the first document. MCP clients (Claude Code, Cursor, Codex, ChatGPT, Windsurf) all walk the same chain, so once the chain is right, every client connects without a pre-shared secret.

This is the chain as Annsa implements it. It follows the MCP authorization spec, which in turn leans on 3 RFCs: 9728 for protected-resource metadata, 8414 for authorization-server metadata and 7591 for dynamic client registration.

01401 with a WWW-Authenticate pointerthe client’s first, tokenless request02Protected-resource metadataRFC 9728 · who protects this resource03Authorization-server metadataRFC 8414 · every endpoint, S256 only04Dynamic client registrationRFC 7591 · no pre-issued client id05Authorization code with PKCEa human approves on the consent screen06Access token and refresh token30 days and 90 days
Fig. 01: The discovery chain every MCP client walks

Step 1: Answer the first request with a pointer

A client's first call arrives with no token. Do not return a bare 401. Return one whose WWW-Authenticate header says where to look:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Annsa MCP",
  resource_metadata="https://app.annsa.ai/.well-known/oauth-protected-resource/mcp"

Everything the client needs from here on is derived from that URL. It never has to be told anything else.

Step 2: Publish protected-resource metadata

At the URL you pointed to, serve RFC 9728 metadata: what the resource is, which authorization server protects it, how to send the token and what scopes exist.

{
  "resource": "https://app.annsa.ai/mcp",
  "authorization_servers": ["https://app.annsa.ai"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["read", "write"]
}

Clients derive the path from the resource URL, so serve it at both /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp.

Step 3: Publish authorization-server metadata

At /.well-known/oauth-authorization-server on the origin you named, serve RFC 8414 metadata listing every endpoint:

{
  "issuer": "https://app.annsa.ai",
  "authorization_endpoint": "https://app.annsa.ai/mcp/auth",
  "token_endpoint": "https://app.annsa.ai/mcp/token",
  "registration_endpoint": "https://app.annsa.ai/mcp/register",
  "revocation_endpoint": "https://app.annsa.ai/mcp/revoke",
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "response_types_supported": ["code"],
  "code_challenge_methods_supported": ["S256"],
  "scopes_supported": ["read", "write"]
}

Step 4: Accept dynamic client registration

A coding agent on someone's laptop has no pre-issued client id. RFC 7591 lets it register itself: a POST to the registration endpoint with a name, redirect URIs and grant types, answered with a client id. Keep it open (no human approval, no secret) and use token_endpoint_auth_method: "none", because a public client cannot keep a secret anyway. Your security comes from PKCE, not from a client secret.

Step 5: Authorization code with PKCE, S256 only

The client sends the user to your authorization endpoint with a code_challenge. Require S256 and refuse plain. The user signs in and approves the scopes on your consent screen. You redirect back with a code; the client exchanges it with its verifier at the token endpoint and receives an access token and a refresh token.

Annsa's access tokens last 30 days and refresh tokens 90. Choose your own, but make refresh work: an agent that has to reopen a browser every day will be abandoned.

Step 6: Scope narrowly

Two scopes are enough for most servers. Annsa's are read (priorities, specs, survey results, search) and write (state changes: start building, mark shipped, share back). An agent that only answers questions should ask for read alone, and your consent screen should show the user exactly which it asked for.

Step 7: Keep protocol errors out of the auth path

This is the mistake that costs the most time. Once a client is authenticated, a call to a method you do not implement must come back as a JSON-RPC error, code -32601, inside a 200. If it comes back as a 401 (because the method fell through to an anonymous allow-list, say), every client reads it as an expired token and loops on re-authentication. We shipped that once. Auth errors are HTTP statuses; method errors are JSON-RPC bodies. Never mix them.

How to fix Cursor MCP OAuth authentication errors

When Cursor shows the server as failing to authenticate, it is almost always one of these, in this order:

  1. The 401 has no resource_metadata pointer. Cursor cannot start discovery. Check the header on an unauthenticated POST.
  2. Metadata is served on the wrong origin or path. The protected-resource document must be on the resource's origin; the authorization-server document must be on the issuer named inside it.
  3. PKCE method mismatch. The server refuses S256 or only accepts plain. Only S256 should be accepted.
  4. Redirect URI not registered. Cursor registers its own callback at registration time; a server that ignores the registered URIs and checks a fixed one will refuse the code.
  5. A protocol error returned as 401. See step 7. The symptom is a server that authenticates, works for one call, then asks to authenticate again.
  6. A stale token in the client. Re-authenticate from the client. In Cursor, remove and re-add the server; in Claude Code, /mcp → the server → Authenticate.

Annsa's own client-side notes for this are in common issues.

What Annsa does not implement, on purpose

Two things the auth.md convention allows are not offered: identity assertions from a third-party IdP and anonymous access. Annsa declares neither, so an agent never tries a method that would fail. Publishing what you do not support is part of adding OAuth well. The full agent-facing version of this document is at annsa.ai/auth.md.

Test the chain from outside

Run these against a fresh deployment:

  • Unauthenticated POST /mcp401 with resource_metadata in WWW-Authenticate
  • GET the protected-resource URL → names your authorization server
  • GET the authorization-server URL → lists all endpoints and S256
  • POST registration with no secret → a client id
  • Authorization with code_challenge_method=plain → refused
  • Valid token, unknown method → 200 with a -32601 body

If all six pass, Claude Code, Cursor and ChatGPT connect with the URL alone.

annsa

Feedback in. Specs out.

500 extra pieces of feedback in your first month.