How to Implement OAuth 2.1 for an MCP Server
A wire-level implementation walkthrough for OAuth 2.1 on a remote MCP server: what the discovery documents actually contain, CIMD vs. Dynamic Client Registration in your server code, per-SEP detail from the 2026-07-28 hardening set, and token-validation mechanics.
- The discovery chain is two JSON documents a client fetches before it ever authenticates:
/.well-known/oauth-protected-resource(RFC 9728), which lists the authorization servers your server trusts, and/.well-known/oauth-authorization-server(RFC 8414) on that AS, which returnsauthorization_endpoint,token_endpointandregistration_endpoint— get either response wrong and no client can complete the flow. - A Client ID Metadata Document (CIMD) replaces server-side client registration with a fetch: the
client_idyour server receives is itself an HTTPS URL, and your authorization server resolves it by fetching that URL for a JSON document describing the client, instead of looking up a row a Dynamic Client Registration call previously wrote to a database. - Of the six 2026-07-28 authorization-hardening SEPs, three change what a server must actively validate or emit: SEP-2468 requires checking the
issclaim against RFC 9207, SEP-837 adds anapplication_typefield your registration handler has to read, and SEP-2350 requires accepting incremental scope requests during step-up re-authorization instead of forcing a full re-grant. - Token validation on every request is three checks, not one: audience (does the
audclaim match this server's URL), issuer (doesissmatch the authorization server your Protected Resource Metadata actually names), and signature/validity (via the AS's published keys or an introspection call) — a token that passes only the first check is still forgeable or replayable. - A rejected request should look like a signpost, not a dead end: an HTTP 401 whose
WWW-Authenticateheader points back at your Protected Resource Metadata document, so a compliant client can restart discovery instead of failing silently.
The auth section of MCP server in production covers what a compliant OAuth 2.1 flow requires: PKCE, RFC 8707 audience binding, the confused-deputy prohibition. It is a checklist, not a wire trace — it tells you the five steps exist without showing what actually crosses the network at each one, or what changes in your server's own code between the two client-registration paths. This article is that wire trace, plus what the six 2026-07-28 hardening SEPs concretely make you implement.
Everything below assumes a remote Streamable HTTP server; a stdio server has no OAuth surface at all, and that decision is covered in stdio vs. Streamable HTTP.
What the discovery documents actually contain
A client does not guess your authorization endpoints — it fetches two static JSON documents, in order, before it can authenticate at all.
/.well-known/oauth-protected-resource, served from your MCP server's own base URL, is the Protected Resource Metadata defined by RFC 9728. Its job is narrow: tell the client which authorization server(s) your server trusts. A client reads it once at the start of the flow to know where to go next — nothing in it is a credential; it is pure discovery.
/.well-known/oauth-authorization-server, served from the authorization server's own base URL (not yours), is the Authorization Server Metadata defined by RFC 8414. This is the document a client actually parses to build the flow: it returns the authorization_endpoint a client redirects a user to for login and consent, the token_endpoint it POSTs to for the code exchange, and the registration_endpoint it uses if Dynamic Client Registration is in play. If either document is missing, malformed, or wrong, no client can complete the flow regardless of how correctly it implements everything downstream — the most common early-implementation failure is getting PKCE and audience binding right while serving a broken or absent metadata document that never lets a client reach that code at all.
Once discovery resolves, the authorization and token requests each carry two things worth naming because they are easy to omit silently: the resource parameter from RFC 8707, set to your MCP server's own URL, which is what lets the authorization server bind the issued token's audience to your server specifically; and the PKCE pair — a code_verifier the client generates and keeps, and a code_challenge derived from it (S256 only, per the pillar's security guidance) that travels in the authorization request. Your server never sees either PKCE value directly — that exchange happens between the client and the authorization server — but the token your server later validates is the downstream product of both being present and correct.
CIMD vs. DCR: what actually changes in your server's code
The pillar names the priority order between these two client-registration paths; what it does not show is what each one actually requires you to build, and that is where the real implementation cost differs.
Dynamic Client Registration (RFC 7591) requires your authorization server to run a registration endpoint: a POST handler that accepts a JSON payload describing the client (redirect URIs, a display name, a client type), validates it, mints a new client_id (and, for confidential clients, a secret), persists a client record somewhere durable, and returns the credentials. From then on your AS owns a client-record table it has to manage — updating entries when a redirect URI changes, pruning stale registrations, and treating the endpoint itself as attack surface, since it is an unauthenticated POST that creates state.
A Client ID Metadata Document (CIMD) removes that table entirely. The client_id a CIMD-using client presents is itself an HTTPS URL — something the client's own operator controls and serves — pointing at a JSON document describing the client the same way a DCR payload would (redirect URIs, a display name). Your authorization server's job changes from "look up a row this client_id should match" to "fetch this URL and parse what comes back": an HTTP GET at authorization time, rather than a database read against a row a registration call wrote earlier. No registration endpoint to write or secure and no stale-registration cleanup — but a new obligation to fetch that document reliably (sane timeout, TLS verification) every time it is needed, since there is no local "this client is known good" cache the way a database row provides.
If you are implementing the authorization-server side yourself rather than delegating to a provider such as Auth0 or Keycloak: DCR means writing and hardening a registration handler; CIMD means writing a document fetcher and validator, treating the fetched JSON with the same untrusted-input discipline as any external content. Both are legitimate simultaneously — CIMD as the default, DCR retained for clients that have not adopted it — the priority order the pillar describes.
Three of the six 2026-07-28 hardening SEPs, in implementation detail
The pillar lists all six 2026-07-28 authorization SEPs in one paragraph. Three of them require you to actually write new validation or registration-handling code, not just be aware the SEP exists (MCP goes stateless):
- SEP-2468 —
issvalidation per RFC 9207. Your token-validation code needs an explicit check, separate from audience validation, that theissclaim names the authorization server your Protected Resource Metadata lists as trusted. Without it, a token issued by a different AS your server never declared trust in — but that still passes signature verification — could be redeemed against you. This closes a wrong-server token-redemption bug class that audience binding alone does not. - SEP-837 —
application_typeon Dynamic Client Registration. A DCR-capable registration endpoint now needs to read anapplication_typefield from the payload and branch on it, rather than defaulting every client to "web." The failure this fixes: servers assuming "web" for every client were rejectinglocalhostredirect URIs from desktop and CLI clients, for whomlocalhostis the normal pattern, not a misconfiguration. - SEP-2350 — incremental step-up scopes. If your server supports scoped permissions, the re-authorization path needs to accept a request for additional scopes on top of what a client already holds, instead of forcing a full re-grant from zero — a distinct, supported code path, not a config flag.
The other three — SEP-2351 (discovery-suffix clarification), SEP-2352 (credential reissuance on AS migration), SEP-2207 (refresh tokens from OIDC-style servers) — are narrower and situational, and don't reshape every server's validation code the way the three above do.
Token validation: what your handler actually checks, in order
Every authenticated request needs the same sequence of checks, and skipping or reordering them is where implementations quietly become insecure rather than obviously broken:
- Audience. Does
audmatch this server's own URL — theresourceparameter (RFC 8707) set at authorization time? A token that passes every other check but names a different audience must still be rejected; this is the check that stops a confused-deputy replay. - Issuer. Does
issmatch the authorization server your Protected Resource Metadata names as trusted, per RFC 9207 and SEP-2468? A token can have the right audience and still fail here if issued by an AS you never declared trust in. - Signature and validity. Is the token genuine and unexpired — a local signature check against the AS's published signing keys, or a live introspection call to the AS, depending on the token format it issues? The corpus this article draws from specifies the audience- and issuer-binding requirements but not one mandated token format, so confirm which mechanism your AS supports before building against it.
A request failing any of these three gets an HTTP 401, and a compliant response does more than reject it: the WWW-Authenticate header should point back at your Protected Resource Metadata document, so the client can restart discovery from a known-good location. Confirm exact header syntax against RFC 9728 directly before shipping it — the shape is a signpost back to /.well-known/oauth-protected-resource, not a bare rejection.
An honest example, extended
ChangeGamer's own /mcp endpoint is unauthenticated as of August 2026 — no OAuth flow, no RFC 9728 metadata — because it is public and read-only, with paid content gated at the application layer instead. What that gate actually covers — the current premium resource list and its pricing — is published separately at access and pricing, not embedded in the MCP tool schema. Adding OAuth would concretely mean: standing up the two discovery documents above (a minimal Protected Resource Metadata pointing at one trusted AS), choosing CIMD over a registration endpoint (ChangeGamer only needs to vet its own clients, not arbitrary ones), and replacing the current api_key argument check with the three-step token validation above — a materially larger surface than today's single string comparison. That gap is why the pillar treats unauthenticated-but-key-gated as legitimate for a narrow, read-only case, not a shortcut every server should take.
Where this leaves you
The discovery documents, CIMD's document-fetch model, the three SEPs that add real validation code, and the three-check token-validation sequence above are the parts of OAuth 2.1 for MCP that only show up once you are implementing it, not just deciding to require it. For the decision-level version — whether you need OAuth at all, the confused-deputy rule, the CIMD/DCR priority order — see the auth section of MCP server in production; for the broader security posture beyond auth specifically, see the agentic security checklist.
Frequently asked questions
- What exactly does a client read from oauth-protected-resource and oauth-authorization-server?
- The Protected Resource Metadata document at `/.well-known/oauth-protected-resource` on your MCP server's base URL exists to answer one question for the client: which authorization server(s) do you trust for this resource. The client then takes that authorization server's URL and fetches `/.well-known/oauth-authorization-server` on it — the Authorization Server Metadata defined by RFC 8414 — to get back the three endpoints it needs to actually run the flow: `authorization_endpoint` (where to send the user to log in and consent), `token_endpoint` (where to exchange a code for a token), and `registration_endpoint` (where to register a client, if Dynamic Client Registration is in play at all). Both documents are static JSON your server or its authorization server serves at a fixed, well-known path — no request parameters, no session state.
- Do I need to write a client registration endpoint if I use CIMD instead of DCR?
- No — that is the entire appeal of CIMD from a server-operator's perspective. Dynamic Client Registration (RFC 7591) requires your authorization server to expose a `registration_endpoint` that accepts a POST, validates the payload, persists a new client record, and returns a `client_id` your server now has to store and manage indefinitely. A Client ID Metadata Document sidesteps all of that: the client already has an HTTPS URL it controls, that URL serves a static JSON document describing the client, and your authorization server treats the URL itself as the `client_id` — resolving it with an HTTP fetch at authorization time instead of a database lookup. You still need to fetch and validate that document, but there is no registration handler, no client-record table, and no registration endpoint to secure.
- What does token audience and issuer validation actually check, mechanically?
- Audience validation confirms the `aud` claim on the presented token names your specific MCP server URL — set via the `resource` parameter (RFC 8707) at authorization time — so a token minted for a different server cannot be replayed against yours. Issuer validation, tightened under SEP-2468 in the 2026-07-28 spec revision, confirms the `iss` claim matches the authorization server your own Protected Resource Metadata document actually names, per RFC 9207, closing a bug class where a token gets redeemed against the wrong server entirely. Neither check tells you the token is genuine and unexpired on its own — that third check is a signature verification against the authorization server's published keys, or a live introspection call to the AS, depending on which token format your AS issues.