Zuplo
On this page

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 discovers the authorization server's own metadata, obtains a client_id, and runs an authorization-code flow with PKCE, sending an RFC 8707 resource parameter on both the authorization and the token request. The server checks that the token's audience is itself. Every step but that one happens inside the MCP client, which is why the flow works in Claude, ChatGPT, Cursor, and VS Code, and fails 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 don't control, or listed in a connector directory
Specification
The specification's own flow — though the specification 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

  • 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 its 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 (carries the credential) access_token, aud = the MCP server
  13. MCP client to MCP server

    POST /mcp (carries the credential) 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 don't control, or listed in a connector directory: Anthropic's submission requirements say to use OAuth 2.0 for authenticated services.
  • The agent calls on behalf of a named person, and the audit trail must 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: it doesn't open a browser or run an interactive OAuth flow, and reports a challenged server as 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 specification's own flow, and the specification 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, and two of its changes alter this flow: Dynamic Client Registration is marked Deprecated in favor 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

A token proves who the caller is, and nothing about what they can call, what they did call, or how often — the next three tickets.

  1. Step 1.

    Let the gateway answer the 401

    The whole discovery chain — the 401, the protected resource metadata, the authorization server, PKCE, the resource parameter — is what the gateway serves. Point clients at the route and it publishes RFC 9728 metadata for that route itself.

    "url": "https://api.example.com/mcp"
  2. Step 2.

    Add the policies

    A list on the route, and the options that go with it. No SDK, no middleware, nothing imported into your server.

    "inbound": [
      "mcp-oauth-inbound",
      "require-user-claims-inbound",
      "mcp-capability-filter-inbound",
      "rate-limit-inbound"
    ]
  3. Step 3.

    Deploy

    Your MCP server keeps its code and stops having to implement the discovery chain itself.

    zuplo deploy

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

mcp-capability-filter-inbound

Only what you expose — and the list can differ per caller, keyed on the claims in the token you just validated. A hidden tool is refused with MethodNotFound before anything is forwarded.

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

require-user-claims-inbound

On the route, as one rule on the token's claims — group, role, tenant — answered with 403 before your server runs. Per-object questions ("may they refund this order?") go to an FGA or AuthZEN policy beside it.

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

mcp_auth_downstream_token_validated

That's one query: rejections carry named reason codes — missing_token, invalid_audience — tied to the subject and the route. Bearer tokens, authorization codes, and PKCE verifiers are never logged.

One agent looped and made 40,000 tool calls. Can we cap that per user?

rate-limit-inbound

Yes — the cap follows the identity the token established rather than an egress IP, so the runaway agent exhausts its own budget and nobody else's.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out. Your MCP server keeps the code and the authentication it has today.

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 specification revision is current, and what changed for OAuth?

2026-07-28 is the current protocol version. For authorization, it deprecates Dynamic Client Registration in favor 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?

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: registered credentials must be keyed by the issuer that granted them and re-registered when the authorization server changes, where a CIMD client ID is an HTTPS URL with 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, and reports a server that answers with an authorization challenge as 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 doesn't honor it: its v2.0 protocol uses scopes instead, so you ask for the audience with a scope — and Entra can refuse the parameter outright rather than ignoring it, with AADSTS901002, 'The resource request parameter isn't supported', which is in Microsoft's own error-code reference.

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 says MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server, and the authorization specification 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. So a missing header is not fatal on its own; a missing metadata document is.

Is PKCE optional if the authorization server doesn't advertise it?

No — 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.