Zuplo

OAuth 2.1 Authentication for MCP Servers

The specification's own authentication method: an authorization-code flow the MCP client discovers and runs for itself, given nothing but the server's URL.

Method
OAuth 2.1
Platform
Any platform
MCP Specification
2026-07-28

How it works

The client sends an unauthenticated request and gets 401 with a WWW-Authenticate header naming the server's RFC 9728 protected resource metadata. That document names the authorization server; the client then discovers the authorization server's own metadata, obtains a client_id, and runs an authorization-code flow with PKCE and an RFC 8707 resource parameter on both the authorization and the token request. The server validates that the token's audience is itself. Every step but the last happens inside the MCP client, which is why this works in Claude, ChatGPT, Cursor and VS Code and does not work in an agent framework whose MCP configuration accepts only a URL and a headers map.

Best for
Servers that must be reachable by MCP clients you do not control, or listed in a connector directory
MCP spec
The spec's own flow — though the spec still makes authorization OPTIONAL
Works with
Claude, ChatGPT, Cursor and VS Code run it themselves; among agent SDKs, only those shipping an OAuth provider
Effort
Days — most of it is the authorization server, not MCP

The exchange

Each step in the exchange, in order.

  • MCP client in your agent or host
  • Browser the user signs in
  • Authorization server issues the token
  • MCP server resource server
  1. MCP client to MCP server

    POST /mcp no Authorization header
  2. MCP server to MCP client

    401 Unauthorized WWW-Authenticate: Bearer resource_metadata=…
  3. Two documents, two hops. RFC 9728 protected resource metadata names the authorization server; RFC 8414 metadata, or OpenID Connect Discovery, describes it. If code_challenge_methods_supported is absent from that second document, a conforming client MUST refuse to proceed.

    MCP client to MCP server

    GET …/oauth-protected-resource/mcp
  4. MCP server to MCP client

    200 OK authorization_servers, scopes_supported
  5. MCP client to Authorization server

    GET …/oauth-authorization-server then /.well-known/openid-configuration
  6. Authorization server to MCP client

    200 OK code_challenge_methods_supported: [S256]
  7. The browser step. There is no headless variant of it: a framework whose MCP configuration takes only a URL and headers stops here, and the request never reaches /authorize at all.

    Inside MCP client

    Get a client_id, build the PKCE pair, open a browser pre-registered, then CIMD, then DCR
  8. Browser to Authorization server

    GET /authorize code_challenge + resource=https://mcp.example.com/mcp
  9. Authorization server to Browser

    302 to redirect_uri code + iss
  10. Browser to MCP client

    Authorization code callback code, state, iss
  11. The resource parameter goes on both the authorization request and the token request, and the server checks the audience it produced. That check is what stops this token working at a different MCP route.

    MCP client to Authorization server

    POST /token code_verifier + resource
  12. Authorization server to MCP client

    200 OK access_token, aud = the MCP server
  13. MCP client to MCP server

    POST /mcp Authorization: Bearer <access-token>
  14. Inside MCP server

    Check the token was issued for this server
  15. MCP server to MCP client

    200 OK tool result
The client makes an unauthenticated request and is challenged with a 401 naming the server's protected resource metadata. It reads that document to find the authorization server, reads the authorization server's own metadata, obtains a client id, generates a PKCE pair, and sends the user to the authorization server in a browser. The authorization code comes back through the browser with an issuer parameter, the client exchanges it for an access token whose audience is the MCP server, and only then does an authenticated request reach the server.

Connect your agent

Pick your language and SDK.

Language
SDK
TypeScriptagent.ts
// v2: every OAuth symbol is in `@modelcontextprotocol/client`.
// v1 imported them from `@modelcontextprotocol/sdk/client/*.js`.
import {
  Client,
  StreamableHTTPClientTransport,
  UnauthorizedError,
} from "@modelcontextprotocol/client";

const url = new URL("https://mcp.example.com/mcp");
const client = new Client({ name: "my-app", version: "1.0.0" });

// `provider` implements OAuthClientProvider: token storage, the PKCE
// verifier, discovery state, and redirectToAuthorization(url). Key
// client credentials by issuer so one AS's client_id never reaches another.
const transport = new StreamableHTTPClientTransport(url, { authProvider: provider });

try {
  await client.connect(transport);
} catch (error) {
  if (!(error instanceof UnauthorizedError)) throw error;
  const params = new URL(await waitForCallback()).searchParams;
  // The SDK does not check `state` for you.
  if (params.get("state") !== provider.lastState) throw new Error("state mismatch");
  await transport.finishAuth(params); // also validates the RFC 9207 iss
  // A started transport cannot be restarted — reconnect on a fresh one.
  await client.connect(new StreamableHTTPClientTransport(url, { authProvider: provider }));
}

When to use something else

Use this when

  • The server has to be reachable by MCP clients you do not control, or listed in a connector directory: Anthropic's submission requirements say to use OAuth 2.0 for authenticated services.
  • Calls are made on behalf of a named person, and the audit trail has to identify that person rather than a key.
  • You already run an identity provider and want its sign-in, its consent screen and its revocation to govern MCP access too.

Use something else when

  • The caller is a cron job or a CI step. The authorization-code grant needs a browser and a person, and there is no headless variant of it.
  • Your framework's MCP configuration takes only a URL and a headers map. The Claude Agent SDK documents this plainly: it does not open a browser or run an interactive OAuth flow, and a challenged server is reported needs-auth while the run continues without its tools.
  • Both ends belong to you and no end user's identity needs to reach the API behind the server, where a static key is less machinery for the same result.

This is the spec's own flow, and the spec still makes it optional: "Authorization is OPTIONAL for MCP implementations", with HTTP transports SHOULD — not MUST — conforming. 2026-07-28 is the current protocol version. Two of its changes alter the flow above: Dynamic Client Registration is marked Deprecated in favour of Client ID Metadata Documents, and clients MUST validate a present RFC 9207 iss parameter against the recorded issuer before redeeming the authorization code.

Zuplo

What lands on you next

A token proves who the caller is. It says nothing about what they may call, what they did call, or how often — and those are the next three tickets.

The token validates. Which of our tools can that caller reach?

mcp-capability-filter-inbound

A static per-route allow-list of tools, prompts and resources. A call to a hidden tool is refused with MethodNotFound before it is forwarded. There is no per-user axis here — that is the row below.

Only the finance group should reach this route. Where does that live?

require-user-claims-inbound

A rule on the validated token's claims — group, role, tenant, subject — evaluated on the route and answering 403. Per-object decisions go to an AuthZEN or FGA policy on the same route; OpenFGA is Enterprise.

A token was rejected at 2am. Which one, and why?

mcp_auth_downstream_token_validated

Auth-lifecycle events carry named reason codes, missing_token and invalid_audience among them. Bearer tokens, authorization codes and PKCE verifiers are never logged.

One agent looped and made forty thousand tool calls. Can we cap that per user?

rate-limit-inbound

rateLimitBy: "user" counts against the authenticated subject rather than a shared egress IP, so the budget follows the identity the token established.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.

Common questions

Does MCP require OAuth?

No. The specification states that authorization is OPTIONAL for MCP implementations, and that HTTP-based transports SHOULD — not MUST — conform to it. What OAuth buys you is reach: a hosted client completes the whole flow given nothing but your URL, and Anthropic's connector-directory submission requirements say to use OAuth 2.0 for authenticated services.

Which spec revision is current, and what changed for OAuth?

2026-07-28 is the current protocol version. For authorization, it deprecates Dynamic Client Registration in favour of Client ID Metadata Documents, requires clients to validate a present RFC 9207 iss parameter, requires an application_type on dynamic registration, and requires client credentials to be keyed by the issuer that granted them.

Client ID Metadata Documents or Dynamic Client Registration?

A client supporting everything should try a pre-registered client id first, then a Client ID Metadata Document if the authorization server advertises client_id_metadata_document_supported, then RFC 7591 registration if it advertises a registration_endpoint, and only then prompt the user. CIMD is a SHOULD in both revisions; registration is a MAY and is Deprecated as of 2026-07-28. A CIMD client id is also portable: 2026-07-28 requires registered credentials to be keyed by the issuer that granted them and re-registered when the authorization server changes, whereas a CIMD client id is an HTTPS URL and there is nothing to re-register.

Why does my agent framework never open a browser for OAuth?

Because it has no browser and no callback URL. The Claude Agent SDK says so directly: the SDK does not open a browser or run an interactive OAuth flow, and a server that answers with an authorization challenge is reported needs-auth while the run continues without its tools. Complete the flow in your own application and pass the resulting access token in the server's headers.

Do I still send the resource parameter if my provider ignores it?

Yes. MCP clients MUST send this parameter regardless of whether authorization servers support it, on both the authorization request and the token request, using the MCP server's canonical URI. The Microsoft identity platform is the common case that ignores it — its v2.0 protocol uses scopes instead of resource, so you ask for the audience with a scope — and the parameter still goes out.

Can my MCP server forward the client's token to the API behind it?

No, and this is the one prohibition in the whole flow. The security best practices document is explicit: MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server, and the authorization spec adds that a server acting as a client to an upstream API MUST NOT pass through the token it received. Mint or exchange a separate upstream credential.

What if the 401 has no WWW-Authenticate header?

The client keeps going. A server MUST implement one of two discovery mechanisms, and clients MUST support both: use the resource_metadata URL from the header when present, otherwise construct and request the well-known URIs — the path-scoped form /.well-known/oauth-protected-resource/mcp first, then the root. A missing header is therefore not fatal on its own; a missing metadata document is.

Is PKCE optional if the authorization server does not advertise it?

The opposite: absence is a stop condition. Clients MUST implement PKCE and MUST verify support before proceeding, and because neither OAuth 2.1 nor PKCE defines a discovery mechanism, that verification is the presence of code_challenge_methods_supported in the authorization server's metadata. If it is absent, clients MUST refuse to proceed. S256 is required wherever the client is technically capable of it.

One policy engine for APIs, AI, and MCP

Put your MCP servers behind a gateway that speaks every identity provider, filters tools per role, and logs every call.