2.074.1 MCP Authorization for Remote Servers#

MCP Authorization for Remote Servers: How a remote MCP server becomes an OAuth 2.1 resource server, and what you run to issue the tokens

At a glance#

What the research found

  • Critical
  • Critical
  • High
  • High
  • High

Explainer

MCP Authorization, Explained#

Verified: 2026-09-01

For a reader who knows what an API is and has not implemented OAuth.


The situation#

An MCP server is a program that exposes tools to an AI assistant — search this database, file this ticket, read this document. MCP is the Model Context Protocol, the agreed format for that conversation.

A local MCP server runs on your own machine and the operating system already decides what it may touch. A remote MCP server runs somewhere on the internet and answers HTTP requests. This document is about the remote kind, because that is the kind that needs to know who is asking.

The awkward part: the thing asking is not a person. It is an assistant, acting for a person who set it up earlier and may have gone to bed. So the server needs a credential it can check, and somebody has to have handed that credential over on the user’s behalf, at a moment when the user was present and agreeing.

That is what all the machinery below is for.

The hotel analogy, and where it breaks#

A hotel gives you a key card. The front desk checks your ID and encodes the card. The door lock does not know who you are; it checks the card and opens or does not.

  • The front desk is the authorization server. It knows the humans.
  • The door lock is the resource server. It knows only whether a card is valid.
  • The card is an access token.

Splitting them is why hotels work. The door has no database, no staff, and no opinion about your identity. It reads a card.

In MCP, your server is the door lock. Anyone expecting to build a login has the wrong picture. You are not building a login. You are building a lock that reads cards somebody else issues, and your one hard job is checking that a card was cut for your door.

Where the analogy breaks is instructive. A hotel guest walks to the front desk knowing where it is. An AI client arrives holding nothing but your door’s address and has to work out where the front desk is, from the door itself, without asking anyone. Most of this specification is about that.

The vocabulary, in the order you meet it#

OAuth 2.1 — the rulebook for handing out access tokens. Not new; MCP adopts it rather than inventing anything.

Access token — the key card. A string the client sends on every request. Short-lived, on purpose.

Refresh token — a second string used to get a new access token when the first expires, without troubling the user again.

Resource server — the thing being protected. Your MCP server. It validates tokens; it does not issue them.

Authorization server — the front desk. Authenticates the human, shows the consent screen, issues tokens. The MCP specification explicitly declines to describe it, which is why choosing one is the actual decision in this category.

Client — the AI application. Claude, ChatGPT, an editor, an agent.

Scope — what a token is allowed to do. files:read rather than “everything”.

Audience — which server a token is for. The most important word in this document, and the one section below is entirely about.

Discovery — the process of a client working out, from a URL alone, where the authorization server is.

The handshake, step by step#

The client has one thing: your server’s URL. Watch what it learns.

1. It calls your server with no token. Your server answers 401 Unauthorized — the HTTP status meaning “you need to identify yourself” — and includes a header called WWW-Authenticate containing a web address.

The 401 matters. Not a 200 with an error message inside, not a 403. Clients look for the 401 specifically, and answering any other way means the handshake never starts.

2. It fetches the address from that header. This is your protected resource metadata document — a small JSON file that says, in effect: “I am the server at this exact URL, and the front desk that issues cards for me is over there.”

Two fields carry the weight. resource is your server’s own identity, and it has to match the URL the user typed, character for character. authorization_servers is a list, and clients use the first entry only.

3. It fetches the authorization server’s own description. Another JSON document, at the front desk’s address, listing where to send the user to log in, where to exchange a code for a token, and which optional features are supported.

4. It gets itself a client ID. The client and your authorization server have never met. Three ways to fix that, covered below.

5. It sends the user to log in. Browser opens, user signs in, user sees a consent screen naming the client and what it is asking for, user agrees.

6. It exchanges the result for a token and starts calling your server with it.

Six steps, and steps 1 through 4 all happen before a human sees anything. That is what makes this different from ordinary OAuth: it is a cold start between two parties with no prior arrangement.

PKCE, in one paragraph#

Pronounced “pixie”. Step 5 sends the user through a browser, and step 6 exchanges a short-lived code for a token. PKCE closes the gap between them: the client invents a random secret, sends a scrambled version of it in step 5, and reveals the original in step 6. An attacker who intercepts the code cannot use it, because they do not have the secret.

Every measured server supports it and it causes no trouble. Mentioned because you will see the acronym constantly and can then stop worrying about it.

Getting a client ID: three ways, and why they changed#

The client needs an identifier your authorization server recognizes. Since they have never met, this is awkward.

Pre-registration. Somebody sets it up by hand: you create a client, you send someone the ID. Old-fashioned, completely reliable, and first in the specification’s own preference order. If you know who your clients are, this is the answer, and it is skipped constantly because the conversation usually starts at the next one.

Dynamic Client Registration, or DCR. The client sends its details to a registration endpoint and gets an ID back, automatically. This was MCP’s original answer and almost every deployed server still supports it.

It has an unglamorous problem: a new client record on every fresh connection, and nothing deletes them. A popular server accumulates them without limit. That is why the 2026 revision deprecated it.

Client ID Metadata Documents, or CIMD. The replacement, and it turns the direction around. The client publishes a small JSON file on the web at a stable address, and that address is its ID. Your authorization server fetches it when it first sees the client.

No registration call, so nothing accumulates. And a quiet advantage worth knowing: because the ID is a URL the client owns rather than a number your server issued, it keeps working if you switch authorization servers. That makes CIMD the mechanism that lowers your exit cost, which is a better reason to want it than conformance is.

Deprecated does not mean gone. Measured across 23 deployed servers: 22 offer DCR, 5 offer CIMD. Anyone building now supports both.

Audience: the part that actually matters#

Here is the attack the whole design exists to prevent.

You run an MCP server. So does someone else. A user connects to both. If a token issued for their server also works at yours, then their server — or anyone who compromises it — can turn around and use that token on you. The token was legitimate. It was simply for the wrong door.

The fix has a name: RFC 8707 Resource Indicators. The client tells the authorization server which server the token is for, by sending a resource parameter naming your URL. The authorization server stamps that into the token’s audience. Your server checks the stamp.

Three parties, three duties:

  • The client must send resource. Always — the specification says to send it whether or not the authorization server supports it.
  • The authorization server should stamp it into the token.
  • Your server must check it. This is the one duty the specification puts on you, and it is not optional.

Why your check is not redundant#

The obvious question: if the authorization server stamps the audience, why check it?

Because it may not have. This survey measured two widely used authorization servers running locally:

  • Keycloak accepts the resource parameter, returns a perfectly normal success response, and issues a token stamped with its own default audience — which has nothing to do with your server. No error. No warning.
  • Ory Hydra stamps audiences correctly, but only when asked using a parameter it named audience before the standard existed. A client following the specification sends resource, and gets a token with no audience at all.

In both cases everything looks fine. A token comes back. The connection works. The security property is simply absent.

That is why your check is the one that counts, and why a test suite that only tries valid tokens proves nothing here. The test worth writing is the one that hands your server a token stamped for somewhere else and confirms it is refused.

What you actually have to build#

Shorter than the preceding pages suggest.

  1. Serve a small JSON document saying who you are and where your authorization server is.
  2. Return 401 with a pointer to it when a request has no valid token.
  3. Check the token: valid signature, not expired, and the audience is you.

That is the resource server. The MCP frameworks — the official SDKs, FastMCP — implement most of items 1 and 2 already.

Everything else in this survey is about item 3’s supporting cast: who issues the tokens, whether they stamp the audience correctly, and how a client that has never met you gets one in the first place.

The shortcut, and what it costs#

There is a way to skip all of it. Claude supports a fixed header credential — an administrator types an API key in once when adding your server, and it is sent on every request. ChatGPT accepts something similar. It is a supported feature, not a hack.

What you give up: no idea which user is calling, no consent screen, no expiry, no way to cut off one person without changing the secret for everybody. One credential, shared by an entire organization.

For a personal tool reaching only your own data, that list is a set of things you were not using. In front of data belonging to more than one person, it is one secret between any holder and all of it.

Never put it in the URL. Web addresses end up in server logs, proxy logs, browser history and screenshots, and the specification prohibits tokens there outright.

Five things worth knowing before you start#

The 401 is not decoration. Clients look for that exact status. A 200 with an error inside means the handshake never begins.

Trailing slashes have broken production systems. Your resource value must match the URL people type. Six of twenty-three measured servers get this wrong, and the failures look like mysteries.

Deprecated means “still required for years.” DCR is deprecated and 22 of 23 deployed servers use it.

The clients are not the specification. Claude and ChatGPT each have documented rules that go beyond it and differ from each other. A server that satisfies every requirement can still fail to connect, and the fix is to advertise more than the minimum.

Nobody will tell you the audience check is missing. Every other mistake here announces itself: the connection fails, an error appears, something is obviously broken. A missing audience check produces a system that works perfectly and is not protected. Write that check first.

S1: Rapid Discovery

The Category: Who Issues the Token#

Verified: 2026-09-01


The question splits in two, and most write-ups answer only one#

“How do I add authorization to my remote MCP server” sounds like one question. The specification answers it as two, and they have different costs, different vendors, and different failure modes.

The MCP server is an OAuth 2.1 resource server. Its job is to reject requests without a valid token, tell the client where to get one, and check that the token it eventually receives was minted for it and not for somebody else. That is a small, bounded piece of work — three endpoints’ worth of behavior, none of it involving a login screen.

The authorization server is the thing that authenticates a human, shows a consent screen, registers clients, issues and refreshes and revokes tokens, and publishes its own discovery document. That is an identity product. The specification declines to describe it: “the implementation details of the authorization server are beyond the scope of this specification.”

The two roles may run in one process. They usually should not. Almost every decision in this category is really a decision about the second role, and the loudest disagreements in the ecosystem — build or buy, DCR or not, which vendor — are arguments about who issues the token.

What the specification actually demands, and of whom#

The 2026-07-28 revision distributes its normative requirements across three parties. A matrix shows it better than prose does: a reader who owns only one of the three columns is being asked for much less than the literature implies.

RequirementResource server (the MCP server)Authorization serverClient
Protected Resource Metadata (RFC 9728)MUST publishMUST use for discovery
401 + WWW-Authenticate with resource_metadataMUST on unauthenticated requestsMUST parse
AS metadata (RFC 8414) or OIDC DiscoveryMUST serve at least oneMUST support both
Client ID Metadata DocumentsSHOULD supportSHOULD support
Dynamic Client Registration (RFC 7591)MAY (deprecated)MAY (deprecated)
Resource Indicators (RFC 8707)MUST validate audienceshould honor resourceMUST send resource
PKCE S256MUST support and advertiseMUST use
Issuer identification (RFC 9207)SHOULD emit issMUST validate when present
Bearer token in header, never in queryMUST reject query-string tokensMUST NOT send them there
Refresh token rotation for public clientsMUST rotate or sender-constrain
Step-up on insufficient_scopeSHOULD challenge with 403 + scopesSHOULD re-authorize

Two rows carry most of the weight.

RFC 9728 is the whole discovery mechanism. There is no registry, no well-known list of MCP authorization servers, no configuration the user pastes in. A client that has been given nothing but a URL sends a request, gets a 401, reads a header, fetches a JSON document, and learns where to go. Everything downstream depends on that first document being reachable and correct.

RFC 8707 is the whole security argument. Without a resource parameter binding a token to one MCP server, a token minted for server A can be replayed against server B — the confused-deputy problem the specification spends its security chapter on. It is also the requirement the ecosystem satisfies least consistently, which is the finding conformance-matrix.md develops.

Four ways to obtain an authorization server#

Every option in this survey is one of four architectures. The differences that matter are not feature-list differences; they are differences in what you operate and who you can blame.

Buy a managed authorization server. WorkOS, Auth0, Stytch, Descope, Scalekit. You get an issuer URL, a consent screen, and somebody else’s uptime. These vendors have raced each other to MCP-specific conformance since early 2026 and are, as a group, the most spec-current option available. You pay per user or per token, and your users’ identities live in a vendor’s database.

Run a general-purpose authorization server. Keycloak, Ory Hydra, ZITADEL. Mature OAuth implementations that predate MCP by years, now retrofitting the MCP-specific RFCs at varying speed. Free of license cost and of vendor lock-in; not free of operation. The retrofit gaps are real and specific rather than theoretical — see Keycloak’s own compliance statement.

Embed an authorization server library in your own process. Authlib, node-oidc-provider, Cloudflare’s workers-oauth-provider. The MCP server and the authorization server become one deployment. Attractive when you already authenticate users and do not want a second identity store; this is also the path with the most ways to be subtly wrong, because the parts you skip are the parts an audit asks about.

Delegate to a gateway. Put a proxy in front that terminates authorization and forwards an internal call. Covered in this survey only at the boundary; the gateway products themselves are 2.083’s subject.

A fifth answer exists and is not an architecture: do not use OAuth at all. Some clients accept a fixed credential in a request header. Whether that is a reasonable choice or a security incident waiting to happen depends entirely on who the credential belongs to, and client-conformance.md sets out what is actually on offer.

How each option is judged here#

Six axes, applied identically to a managed vendor and to a library:

  1. Conformance to the MCP-specific RFCs — 9728, 8707, 7591, the CIMD draft, 9207, PKCE. Claimed support and advertised support are different things, and the authorization server’s own metadata document settles the second one.
  2. What it leaves you to build. Every option leaves something. The useful comparison is which something.
  3. Client interoperability in practice. The specification is not the client. A server that satisfies every MUST can still fail to connect, and the failures are documented.
  4. Operational surface — what runs, what it stores, what happens when it stops.
  5. Cost shape — per user, per token, per instance, or per engineer-week.
  6. Exit cost. Tokens are short-lived and clients re-discover on every connection, so the switching cost here is unusually low compared to identity work generally. That is a finding, and it changes how much the vendor choice deserves to be agonized over.

What this survey does not cover#

Authorization decisions after the token is valid. Scopes appear throughout because the specification defines a scope challenge, but role modeling, per-tenant policy, and fine-grained permission systems are a different category.

Local servers. The specification tells STDIO implementations not to follow it, and to take credentials from the environment. Everything here is about HTTP transports.

The identity vendors as businesses. Pricing appears as a cost shape; funding, acquisition risk, and market share belong to 3.012 and to S4.


The Clients Are Not the Specification#

Verified: 2026-09-01


A server can satisfy every MUST in the authorization chapter and still fail to connect, because the thing on the other end is a product with its own selection rules, its own fallbacks, and its own documentation that diverges from the specification in places it names. Anthropic’s connector documentation says so in its first line: Claude’s auth support differs from the generic MCP specification in a few places, so read this page even if you know MCP auth.

That makes client behavior a first-class input to the choice of authorization server, and it is the input most write-ups omit.

Claude#

The most thoroughly documented client, and the one that publishes what it requires rather than only what it supports. Six named authentication types across Claude.ai, Desktop, mobile, Claude Code and Cowork, which share one backend:

TypeWhat it isAvailability
oauth_dcrdynamic client registrationout of the box
oauth_cimdClient ID Metadata Documentout of the box
oauth_anthropic_credsa client ID and secret you mail to Anthropic, which it holdson request
custom_connectionURL or credentials supplied at connection timeon request
static_headersa fixed API key or bearer token an admin enters oncebeta
noneauthlesssupported

Five behaviors matter more than the list.

A fixed credential is a supported path, not a hack. static_headers lets an organization administrator enter an API key or bearer token once when adding the connector; Claude sends it on every request. The credential belongs to the organization rather than to a user, which is the whole trade: no per-user identity, no consent screen, no refresh, and one secret whose blast radius is the whole organization. authorization and x-api-key are accepted header names; anything else needs review before an administrator can save the connector. Credentials in the connector URL are called out as a vulnerability, and the specification prohibits them outright.

Machine-to-machine is not available. A pure client_credentials grant with no user in the loop is unsupported: every connection requires user consent. A server designed around service-to-service tokens does not have a path here, and oauth_anthropic_creds is the consent-gated substitute — Anthropic holds your client credentials and completes the token exchange on behalf of a consenting user, which gives you a stable registered client without implementing DCR or CIMD.

CIMD requires a conjunction, not a flag. Claude selects CIMD only when the authorization server advertises client_id_metadata_document_supported and lists none in token_endpoint_auth_methods_supported, because its CIMD client authenticates as a public client. Miss either and it falls back to DCR. This is the rule behind the measured finding that five deployed servers advertise CIMD and four can be used with it.

The 401 is load-bearing and a 200 will not do. Claude does not honor a WWW-Authenticate header on a 200 response. Without a resource_metadata pointer it probes two well-known paths on the MCP server’s origin — the path-suffixed form first — and if neither answers, no metadata is found and the connection fails with a message about not reaching the server, even though the server answered.

Deadlines are published. Ten seconds for discovery, registration and token endpoints; thirty for refresh. Tokens refresh reactively on a 401 with a proactive attempt up to five minutes before expiry, and a refresh failure must return invalid_grant rather than a custom error code. The token endpoint must accept form-encoded bodies while the registration endpoint takes JSON, which is a real trap in frameworks that default to JSON-only parsing.

Claude Code differs from the hosted surfaces in one way worth designing for: it is a native client using a loopback redirect on an ephemeral port, and it declares http://localhost/callback and http://127.0.0.1/callback in its own Client ID Metadata Document, so an authorization server must match both ignoring the port.

ChatGPT#

Supports OAuth, no authentication, and a mixed mode in which initialize and tool listing are unauthenticated while individual tools require a token according to their security schemes. That mixed mode is the lazy-authentication pattern as a first-class client feature, and it is the shape a server should target if it wants to be browsable before it is connected.

Its CIMD client differs from Claude’s in a way that matters: it supports both public-client token exchange (none) and signed client assertion (private_key_jwt).

The consequence is a real interoperability split. An authorization server that supports CIMD only with private_key_jwt works with ChatGPT and is invisible to Claude, which requires none. An authorization server that supports both works everywhere. This is the single most actionable client-conformance finding in the pass, and neither client’s documentation mentions the other.

Static credentials are used when supplied, and DCR remains available when configured.

VS Code#

Attempts dynamic client registration first and falls back — to a built-in client ID, or to another flow, depending on the authorization server. The practical consequence is that VS Code exercises the DCR path harder than the other clients, and its issue tracker carries a steady stream of registration failures against servers that do not implement it.

The direction of travel is the same as everywhere else: the priority order the specification defines is pre-registration, then CIMD, then DCR, then ask the user. Clients are converging on it at different speeds and from different starting points.

Cursor#

Shipped browser-based OAuth for remote servers in mid-2025, among the first editors to do so, and also accepts static OAuth client credentials written into its configuration file — the pre-registration branch of the priority order, exposed as a user-facing setting.

That option deserves more attention than it gets. Pre-registration is first in the specification’s own priority list, it requires nothing of the authorization server beyond ordinary OAuth, and it sidesteps both DCR and CIMD entirely. For a server with a small, known set of clients it is the simplest correct answer available.

What the clients jointly require#

Reading the four together, the intersection a server must hit to work everywhere is smaller than the specification and sharper:

  1. Answer 401, never 200, and put a resource_metadata pointer in the header. The well-known fallback works but costs round-trips and depends on your platform serving /.well-known/*.
  2. Make resource in the metadata match the URL a user types, including the path and including the trailing slash or its absence.
  3. Support PKCE S256 and advertise it. Universal, and the one thing nobody argues about.
  4. Support at least one registration mechanism that at least one client will choose. DCR still reaches every client. CIMD reaches Claude only with none, and ChatGPT with either none or private_key_jwt.
  5. Return invalid_grant on a dead refresh token, and rotate refresh tokens for public clients.
  6. Respond inside ten seconds.

And the option outside all of it: a fixed header credential, supported by Claude in beta and by ChatGPT as static credentials, which trades every property of OAuth for an afternoon of work. Whether that is a shortcut or a mistake is a question about who owns the credential, and S3 answers it per persona rather than in general.


What Deployed Servers Actually Advertise#

Verified: 2026-09-01 Method: harness/2-074-1-mcp-authorization/ — 26 production remote MCP servers, probed 2026-09-01. Rung: measured-local.


Every claim elsewhere in this pass is a vendor statement or a repository reading. This one is a measurement, and it is available for an unusual reason: every document in the discovery chain is public by specification. A client that has never seen a server before has to be able to read them, so a probe can walk the same path with no account, no credential and no container.

What the probe reads: whether an unauthenticated call draws a 401, whether the challenge points at protected resource metadata, whether that document is reachable at all, whether its resource matches the endpoint, and then, from the authorization server’s own metadata document, the flags a client branches on.

What it cannot read: anything requiring a token. Whether a server rejects a token minted for a different resource — the specification’s central security requirement — is invisible from outside. That needs two accounts and two tokens, and it is filed rather than guessed.

-- below means the document did not advertise the field. RFC 8414 defines defaults for some fields, so absence is silence, not denial.

Counts#

servers probed26
answered 401 unauthenticated24/26
protected resource metadata reachable23/26
… of those, via the WWW-Authenticate pointer20/23
authorization server metadata reachable23/26
advertises a registration endpoint (DCR)22/23
advertises client_id_metadata_document_supported5/23
… and also accepts a public client, so CIMD is usable4/23
advertises PKCE S25623/23
advertises authorization_response_iss_parameter_supported3/23
sends a scope in the WWW-Authenticate challenge1/26
resource in PRM differs from the endpoint URL6/23
authorization server on the same host as the MCP server18/23

Five findings#

The deprecated mechanism is the deployed one#

DCR is advertised by 22 of 23. Client ID Metadata Documents, the mechanism that replaced it in the 2026-07-28 revision, is advertised by 5 and usable by 4.

The ratio is the finding, not the direction. A specification deprecating something does not retire it, and a server author reading the current specification will implement CIMD into an ecosystem where the fallback path is the one everything actually runs. Both mechanisms have to work for years. The corollary for anyone choosing an authorization server: CIMD support is a reason to prefer one vendor over another and not yet a reason to reject one without it.

Advertising CIMD is not the same as supporting it usefully#

Five servers set client_id_metadata_document_supported. Four can be used with it.

The gap is a conjunction that lives in two different documents. Claude selects CIMD only when the authorization server advertises the flag and lists none in token_endpoint_auth_methods_supported, because a CIMD client authenticates as a public client at the token endpoint. Hugging Face advertises the flag and does not accept a public client, so it advertises a mechanism it cannot be used with, and a client reading only the first field would choose a path that fails at the last step.

This is the clearest case in the survey of a conformance claim that a feature list gets wrong and a live document gets right.

PKCE is settled and issuer identification is not#

S256 is advertised by 23 of 23 — the only unanimous result in the measurement, and a fair description of PKCE as no longer a decision.

RFC 9207 issuer identification is advertised by 3. The specification makes it a SHOULD, states that a future revision is expected to raise it to MUST, and asks implementers to emit it now to ease the transition. Almost nobody has. That is a dated prediction the survey can be re-run against: this number should move, and if it has not moved by the time the requirement hardens, the transition will be noisy.

Two servers challenge and cannot be discovered#

Atlassian and Intercom both answer an unauthenticated call with 401, and neither exposes protected resource metadata by any route the probe tries — no resource_metadata pointer in the header, and 404 at both well-known paths.

That is the failure Anthropic’s own troubleshooting describes from the client side: the MCP server receives the request, the authorization server sees no traffic at all, and the connection fails with a message about not reaching the server. Seen from outside it is a missing document rather than a mystery. Both are large vendors with working connectors, so the reasonable inference is that some other arrangement carries them — a directory entry with credentials held by the client vendor, most likely — which is itself the finding: the published handshake is not the only way a connector gets authorized, and a server that skips it is betting on a private arrangement.

The role separation is mostly notional#

The specification is careful that the MCP server and the authorization server are distinct roles, and the literature follows it. In deployment, 18 of 23 put both on the same host.

Both readings are true and the practical one matters more: a team choosing an architecture should not conclude from the specification’s care that it must run two things. The role split is about which duties belong to which code, not about how many hostnames appear in the metadata. The five that do separate — GitHub most cleanly, with a resource server on one domain and an issuer on another — are mostly organizations that already had an authorization server before they had an MCP server.

The full tables#

Regenerate with uv run summarize.py in the harness; out/probe.json holds every field read from every document.

The discovery chain#

Server401PRM found viaresource matches endpointAS metadata
Linearyesheaderyesyes
Notionyesheaderyesyes
Sentryyesheaderyesyes
GitHubyesheaderyesyes
Atlassianyes
Asanayesheadernoyes
Intercomyes
PayPalyesheadernoyes
Squareyesheaderyesyes
Cloudflare docsno (200)
Neonyesheaderyesyes
Webflowyesheaderyesyes
Wixyesheaderyesyes
Canvayesheaderyesyes
Vercelyesheaderyesyes
Hugging Faceno (200)well-knownyesyes
Figmayesheaderyesyes
MCP exampleyesheadernoyes
Stripeyesheaderyesyes
Zapieryeswell-knownyesyes
Sanityyesheaderyesyes
Netlifyyesheaderyesyes
Prismayeswell-knownnoyes
Mondayyesheadernoyes
Cloudflare bindingsyesheaderyesyes
Globalpingyesheadernoyes

Two rows need reading rather than counting. Cloudflare’s docs server answers 200 because it is authless — a legitimate configuration the specification allows, and a reminder that authorization is optional. Hugging Face answers 200 and still publishes protected resource metadata, which is the lazy-authentication pattern: connect freely, challenge when a request needs a scope. Neither is a conformance failure and a probe that counted 401s alone would report both as one.

What the authorization servers advertise#

ServerissuerDCRCIMD flagpublic clientCIMD usablePKCE S256iss
Linearmcp.linear.appyesyesyesyesyes
Notionmcp.notion.comyesyesyesyesyes
Sentrymcp.sentry.devyesyesyesyesyes
GitHubgithub.com/login/oauthnonoyesyes
Asanamcp.asana.comyesyesnoyes
PayPalmcp.paypal.comyesnoyesnoyes
Squaremcp.squareup.comyesnoyesnoyes
Neonmcp.neon.techyesyesnoyes
Webflowmcp.webflow.comyesnoyesnoyes
Wixmcp.wix.comyesyesnoyes
Canvamcp.canva.comyesyesyesyesyes
Vercelvercel.comyesnonoyes
Hugging Facehuggingface.coyesyesnonoyes
Figmaapi.figma.comyesnonoyes
MCP exampleexample-server.modelcontextprotocol.io/yesyesnoyes
Stripeaccess.stripe.com/mcpyesyesnoyes
Zapiermcp.zapier.comyesyesnoyes
Sanitymcp.sanity.ioyesyesnoyes
Netlifynetlify-mcp.netlify.app/yesyesnoyes
Prismamcp.prisma.ioyesyesnoyes
Mondayauth.monday.com/mcpyesnonoyesyes
Cloudflare bindingsbindings.mcp.cloudflare.comyesnoyesnoyesyes
Globalpingmcp.globalping.devyesnoyesnoyes

GitHub advertises neither registration mechanism. No registration_endpoint and no CIMD flag, which leaves a client with no way to obtain a client ID except a pre-registered one — and there is a corresponding open bug against a GitHub connector reporting exactly that: no client ID available, falling back to a DCR the server does not support.

Two Cloudflare-hosted servers disagree with each other. Sentry advertises CIMD and the Cloudflare bindings server does not, though both are understood to run the same underlying provider. Version skew between two deployments of one library is the ordinary explanation, and it is why a table like this dates quickly.

A note on the reference server#

The specification’s own example server advertises endpoints built by naive concatenation onto an issuer that ends in a slash: .../authorize, /token and /register all arrive with a doubled slash. Most HTTP stacks tolerate it. It is a small thing, recorded because the same class of URL-construction and canonicalization error accounts for the six servers whose advertised resource does not match their own endpoint, for an open bug in Cloudflare’s provider about strict string comparison of equivalent URIs, and for a full section of the specification about trailing slashes. The most common conformance defect in this category is not cryptographic. It is string handling.


Authorization Servers as a Library in Your Own Process#

Verified: 2026-09-01


The third architecture puts the authorization server inside the MCP server’s own deployment. It is the option chosen by teams who already authenticate users and do not want a second identity store, and it is the option where the gap between “supports OAuth” and “supports the MCP profile of OAuth” is widest — because a library ships the RFCs somebody asked for, and until 2025 nobody was asking for these.

The MCP SDKs are listed here too, in their own section, because they occupy a position that confuses people: they implement the resource server thoroughly and are not authorization servers at all.

Authlib (Python)#

BSD-3-Clause, ~5.4k stars, actively maintained. The general answer for OAuth in Python and the one a FastAPI or Flask MCP server reaches for first.

Its module layout states what it implements, and reading it beats paraphrasing it. authlib/oauth2/ contains directories named for the RFCs it ships: 6749, 6750, 7009 revocation, 7521, 7523, 7591 dynamic client registration, 7592, 7636 PKCE, 7662 introspection, 8414 authorization server metadata, 8628 device flow, 8693 token exchange, 9068 JWT access tokens, 9101.

There is no rfc8707 and no rfc9728.

Both MCP-specific RFCs are absent. Resource indicators — the audience binding the specification’s security chapter rests on — and protected resource metadata — the entire discovery mechanism — are the two things Authlib does not give you. RFC 9728 has been tracked as an open issue since April 2025 and was still open on the verification date, sixteen months later.

That is not a criticism of Authlib, which implements what its users have wanted with unusual thoroughness. It is a statement of what the Python build path costs: the authorization endpoint, token endpoint, PKCE, registration and discovery come from the library, and the two pieces that make it an MCP authorization server are yours. Neither is large — protected resource metadata is a static JSON document and audience binding is a claim to set and a claim to check — but both are load-bearing and neither is tested by anything you did not write.

node-oidc-provider#

MIT, ~3.8k stars, committed to on the verification date. The most complete standalone OAuth and OIDC implementation available as a library in any language, and the reference other projects are measured against.

Unlike Authlib it ships resource indicators as a first-class configuration surface, and it carries the client_id_metadata_document_supported flag. A Node MCP server can get to MCP-profile conformance without writing OAuth internals, which is not true of the Python equivalent.

The cost is conceptual weight. This is a full OIDC provider with a large configuration surface and strong opinions, and adopting it means adopting all of it. Teams routinely underestimate the distance between “the library supports it” and “our deployment is configured correctly,” and this is a library where that distance is long.

The reason to pick it: Node, no vendor, and you want the RFCs to be somebody else’s problem. The reason not to: you wanted a small dependency.

Cloudflare workers-oauth-provider#

MIT, ~1.9k stars, and the most MCP-shaped library in this survey — it exists because Cloudflare needed remote MCP servers on Workers and there was nothing to use.

It ships Client ID Metadata Documents with a dedicated test file and a separate conformance suite, developed through the summer of 2026. Set that against the most-cited vendor comparison of this category, published in May 2026, which rates its CIMD support as absent: the rating was fair when written and is now wrong, which is the general hazard of comparison tables in a category moving this fast.

Its open issues are unusually informative about where this whole category is difficult. One reports that resource matching uses strict string equality and therefore rejects URIs that RFC 3986 considers equivalent, breaking token refresh. That is the same trailing-slash and canonicalization hazard the specification devotes a section to, showing up as a live bug in a competent implementation — and the measured probe finds six deployed servers whose advertised resource does not string-match their own endpoint.

The constraint is the obvious one: it targets Workers. Outside that runtime it is a reference to read rather than a dependency to install.

The MCP SDKs and FastMCP — the resource-server half#

The official Python and TypeScript SDKs and FastMCP all implement protected resource metadata and the 401 challenge, and FastMCP ships a growing collection of named auth providers wiring the resource server to an external authorization server.

Their position is the one to be clear about: these implement the resource server well and are not authorization servers. They give you the half of the handshake that is small, and they do it correctly so you do not have to. They do not authenticate a human, render consent, or issue a token.

Read that as good news. The resource-server half is the half where a framework can plausibly be complete, and every option elsewhere in this survey assumed you would build it. If your MCP server is on one of these frameworks, the work in front of you is choosing an authorization server, not writing an integration.

The velocity around FastMCP’s auth providers is worth a note: new named providers arrive roughly weekly, tracking whichever vendor shipped something. That is a healthy signal about the framework and an unhealthy one about the stability of what it is wrapping.

Where the group nets out#

LicenseAS or RSDiscovery (9728)Resource indicators (8707)DCRCIMD
AuthlibBSD-3authorization servernonoyesno
node-oidc-providerMITauthorization servervia configyesyesyes
workers-oauth-providerMITboth, on Workersyesyesyesyes
MCP SDKs / FastMCPMIT / Apache-2.0resource server onlyyesvalidatesn/an/a

The finding that organizes this group: language decides how much you build. A Node or Workers deployment can reach MCP-profile conformance out of libraries. A Python deployment cannot, today, without writing the two RFCs that matter most — which is the strongest argument in this survey for a Python MCP server to put its authorization server somewhere other than in its own process.


Managed Authorization Servers#

Verified: 2026-09-01


Five identity vendors have built MCP-specific products since the specification acquired an OAuth chapter in March 2025. They compete on the same axis and it is not the one their marketing leads with: not “do you support OAuth” — all of them have for years — but how quickly they track a specification that has been rewritten in every MCP revision.

That makes currency the distinguishing property of this group, and it is the reason to consider them at all. A vendor that shipped Client ID Metadata Documents within weeks of the 2026-07-28 draft has done work that a self-hosted deployment will do later or never.

What every vendor in this group leaves you, without exception: the resource server is still yours. You publish the protected resource metadata document, you return the 401 with the right header, you validate the token’s audience. Vendors describe this as a small integration and they are right. Buying an authorization server buys the authorization server, and not the MCP server’s half of the handshake.

WorkOS AuthKit#

The most aggressive MCP positioning of the five, and the most complete claimed conformance: protected resource metadata, resource indicators with configurable defaults, dynamic client registration retained for compatibility, and Client ID Metadata Documents. WorkOS also publishes the ecosystem’s most-cited comparison of its own competitors, which is worth reading and worth checking — its May 2026 edition rates Cloudflare’s CIMD support as absent, and Cloudflare merged CIMD with a dedicated conformance suite over the following summer.

Cost is usage-based rather than per seat, which fits an MCP server whose users connect a client once and then let an agent hold a token. Identity data lives with WorkOS.

The reason to pick it: you want the vendor most likely to have already implemented whatever the next revision adds. The reason not to: you are buying into a directory and an enterprise-SSO product whose surface is much larger than the piece you need.

Auth0#

The incumbent, and the option with the most existing deployments to inherit. Claimed conformance across the same four RFCs, with fine-grained controls over what dynamic registration is permitted to create — which matters more than it sounds, because DCR’s practical failure mode is client-table growth rather than a security breach.

Pricing is quote-based at the tier where the MCP features live. That is a real consideration for a small deployment: the other four vendors in this group publish a free tier and Auth0’s MCP story lives above the line where you talk to sales.

The reason to pick it: your organization already runs Auth0 and the MCP server should inherit the identities that exist. The reason not to: nothing about MCP argues for Auth0 specifically, and if you are choosing fresh, the newer vendors are cheaper and at least as current.

Stytch#

Positions its Connected Apps product as purpose-built for agent access rather than as a CIAM product with an MCP feature. The distinction it draws is worth taking seriously: Connected Apps can sit on top of an existing identity provider rather than replacing it, so an organization that already authenticates users somewhere else adds an authorization server without migrating its user store.

That is the shape most in-house identity systems need, and it is the least-discussed capability in this category. It is also the one to verify hardest against your own setup, because “layer on top” covers a range of integration depths.

Conformance is strong on discovery, registration and resource indicators; CIMD support has been reported as partial and is worth confirming against the live metadata document rather than the docs.

Descope#

The most generous free tier of the five by a wide margin — thousands of monthly active users before anything is owed — which makes it the practical choice for a server whose audience is small or unproven. Ships language-specific MCP SDKs rather than only a generic OAuth integration, so the resource-server half comes with more of it written.

Descope acts as the full authorization server: publishes discovery metadata, registers clients by CIMD or DCR, runs login and consent, issues and refreshes tokens, and validates audience. It also stores third-party OAuth tokens on your behalf, which is a different problem — the one where your MCP server needs to call somebody else’s API as the user — and having both in one vendor is a genuine convenience.

The reason to pick it: the cost curve starts at zero and the SDK coverage is the best in the group. The reason not to: it is the youngest identity company of the five, and S4 weighs that.

Scalekit#

The narrowest and most MCP-native of the five. Supports both DCR and CIMD out of the box and is explicit that it integrates with an existing identity system — Entra, Google Workspace, Cognito, Auth0, Keycloak — for the actual user authentication, while Scalekit issues the scoped tokens.

That is the same architecture as Stytch’s Connected Apps, arrived at independently, and the convergence is itself informative: two vendors building for this category specifically both concluded that the authorization server should be separable from the identity store. An organization with users already somewhere is the common case, not the exception, and the products that assume otherwise are making you migrate for no reason connected to MCP.

The reason to pick it: you want the smallest product that solves exactly this. The reason not to: the smallest product is also the smallest company, and the surrounding platform is thinner if your needs grow.

Where the group nets out#

Discovery (9728)Resource indicators (8707)DCRCIMDFree tierSits on existing IdP
WorkOSclaimedclaimedyesclaimedyespartial
Auth0claimedclaimedyes, with controlsclaimednot at this tieryes
Stytchclaimedclaimedyesreported partialyesyes, by design
Descopeclaimedclaimedyesclaimedyes, largeyes
Scalekitclaimedclaimedyesclaimedyesyes, by design

Every cell in the first four columns says claimed, and that word is doing real work. These are vendor statements about vendor products, which the evidence ladder tops out at cited. conformance-matrix.md reports what deployed authorization servers actually advertise, and the two do not always agree.

The summary of this group: the differences between them matter less than the difference between the group and the alternatives. All five are current, all five leave you the same resource-server work, all five hold your users’ identities, and switching between them costs a metadata document and a re-consent. The decisions that matter are whether to be in this group at all, and whether you need one that sits on top of the identity store you already have.


The Shortlist#

Verified: 2026-09-01


Twelve options across four architectures, reduced to the ones worth a serious look and the reason each survives.

Worth evaluating#

Why it survivesThe reservation
DescopeThe largest free tier in the group and the best per-language SDK coverage, so the resource-server half arrives partly writtenYoungest identity company of the five
ScalekitBuilt for this specifically; integrates with the identity store you already have rather than replacing itSmallest product and smallest company; the identity-store mode is the $99/month BYOA tier, not the free plan (F008)
StytchSame separable architecture, larger company behind itCIMD support reported partial — confirm against the live metadata document
WorkOSMost complete claimed conformance and the fastest tracker of specification changesBuying a directory and an enterprise SSO product to get one piece
Ory HydraThe self-hosted version of the separable shape: OAuth without a user storeAdvertises no registration endpoint publicly; binds audiences only under a non-standard parameter
KeycloakFree, ubiquitous, often already running, and documents its own gapNo RFC 8707; metadata omits none; CIMD absent from the current stable release
node-oidc-providerThe only general-purpose library that ships resource indicators and the CIMD flagA full OIDC provider’s configuration surface
workers-oauth-providerThe most MCP-shaped library here, with a CIMD conformance suiteWorkers only

Not on the list, and why#

Auth0 — claims the same conformance as the others, and its MCP features sit above the tier where pricing is published. Keep it if the organization already runs it; nothing about MCP recommends adopting it fresh.

ZITADEL — capable and multi-tenant by design, but AGPL-3.0 on an authorization server in a hosted product’s request path is a licensing question before it is a technical one, and its MCP-specific conformance is less documented than Keycloak’s.

Authentik — the largest community of the self-hosted set and the least MCP-specific work done. Reasonable if already deployed; not a reason to deploy.

Authlib — ships neither rfc8707 nor rfc9728. It remains the right OAuth library for Python generally and is not, by itself, a path to an MCP authorization server.

MCP SDKs and FastMCP — excluded from this list because they are not competing in it. They implement the resource server, they do it well, and they do not issue tokens.

The two shortlists that actually matter#

Not one ranking, because the architectures answer different questions and mixing them into a single list is what makes most comparisons in this category unhelpful.

If identity may live with a vendor: Descope, Scalekit, Stytch or WorkOS, and the differences between them are small enough that the tiebreaker should be whether they sit on top of an identity store you already have.

If it may not: Ory Hydra or Keycloak, with the audience check written first and tested against a knowingly wrong token — because S2 measures both of them failing to bind it for a conformant client, in different ways, and neither failing loudly.


Self-Hosted Authorization Servers#

Verified: 2026-09-01


These four predate MCP by years and implement OAuth thoroughly. The question is not whether they do OAuth well — they do — but whether they have absorbed the handful of RFCs MCP added on top, and the answer separates them more sharply than anything else in this survey.

The pattern across the group: dynamic client registration was already there, and resource indicators were not. DCR is an old RFC with pre-MCP demand behind it; RFC 8707 had almost no constituency until a specification made it a client MUST. So the mature self-hosted servers arrive with the deprecated mechanism implemented and the load-bearing one missing.

Keycloak#

Apache-2.0, Red Hat-backed, the default answer for self-hosted OAuth in enterprises. Keycloak publishes its own MCP compliance statement, which is the most useful document any project in this survey has produced, because it states a gap rather than a capability:

MCP 2025-03-26: fully supported. MCP 2025-06-18 and later: partially supported without Resource Indicators for OAuth 2.0.

Supported: OAuth 2.1, RFC 8414 authorization server metadata, RFC 7591 dynamic client registration, RFC 9207 issuer identification, and Client ID Metadata Documents as an experimental feature that the project warns may break across versions.

Not supported: RFC 8707 resource indicators. The suggested workaround is to model resources with scopes instead.

That gap deserves to be stated plainly rather than filed as a caveat. Resource indicators are how a token gets bound to one MCP server; without them, a token minted for your server carries whatever audience Keycloak decided to put in it, and the confused-deputy attack the specification’s security chapter is built around is not structurally prevented. The scope workaround changes what a token permits, not what it is for, and a client that sends the resource parameter as the specification requires will have it ignored.

An MCP server behind Keycloak can still validate audience — the specification puts that duty on the resource server, and a custom mapper can populate the claim. It is work you would not otherwise do, in the one place where getting it wrong is the documented attack.

The reason to pick it: it is free, it is everywhere, your organization may already run it, and it documents its own gap. The reason not to: the missing RFC is the important one.

Ory Hydra#

Apache-2.0, ~17.5k stars, actively maintained. A narrow product by design: Hydra is an OAuth 2.0 and OIDC server and nothing else — it does not store users or render login screens, and delegates both to an application you write or to Ory Kratos.

That separation is the reason it appears in MCP write-ups more often than its size would predict. An organization that already authenticates users has exactly the shape Hydra expects, and does not have to migrate anyone to adopt it. It is the self-hosted answer to the same problem Stytch and Scalekit solve commercially.

The cost is that the piece Hydra declines to do is the piece with the user-visible surface: login, consent, account recovery. “Bring your own login” means writing one.

Practical reports of Hydra behind an MCP server are among the more detailed available and surface a recurring interop problem rather than a Hydra defect: a client that does not send the resource parameter produces a token with an empty audience, and a resource server validating audience correctly then rejects it. The specification makes the parameter a client MUST; the failure appears at the server.

The reason to pick it: you already have identity, you want no vendor, and you accept operating one more service. The reason not to: it is a component, not a product, and the distance between it and a working consent screen is real.

ZITADEL#

AGPL-3.0 with a commercial license available, ~15k stars, developing fast. The most complete self-hosted option in the sense of doing what Keycloak does and what Hydra declines to do, in one binary, with multi-tenancy built in rather than bolted on.

The license is the thing to look at before the features. AGPL on an authorization server sitting in the request path of a hosted product is a question for whoever answers those questions at your organization; ZITADEL sells a commercial license because the answer is often “not that one.” Keycloak and Hydra are both Apache-2.0 and this question does not arise.

MCP-specific conformance is less clearly documented than Keycloak’s, which is a real difference: Keycloak tells you what it does not do.

The reason to pick it: multi-tenancy is a first-class concept and you want one service rather than three. The reason not to: the license, and thinner MCP-specific documentation.

Authentik#

~25k stars, the largest community of the four, and the one most often deployed by individuals and small teams rather than enterprises. Strong on the breadth of protocols a homelab needs; weakest of the four on the MCP-specific RFCs and on published conformance.

Include it in a shortlist if it is already running in your environment. Do not adopt it for MCP — the two better-documented options above have done more of this specific work.

Where the group nets out#

LicenseDCRCIMDResource indicators (8707)Brings its own loginDocuments its gaps
KeycloakApache-2.0yesexperimentalnoyesyes, explicitly
Ory HydraApache-2.0yesnot documentedpartialno, by designpartly
ZITADELAGPL-3.0yesnot documentednot documentedyesno
Authentikpermissive-ishyesnot documentednot documentedyesno

The group’s shared summary: self-hosting an authorization server for MCP means accepting that the MCP-specific parts are the least finished parts. That is a reasonable trade when the reason to self-host is data residency, cost at scale, or an existing deployment. It is a poor trade when the reason is “how hard can it be,” because the specific hardness is concentrated in exactly the RFCs these projects have not finished.

S2: Comprehensive

How the Handshake Is Built#

Verified: 2026-09-01


S1 established what exists and what it advertises. This pass is about mechanism: what the handshake does step by step, which steps have more than one correct implementation, and where the options in S1 diverge in ways a feature matrix cannot show.

Four sections, in the order a connection encounters them:

  • discovery-mechanics.md — how a client that has been given nothing but a URL finds the authorization server, the two well-known path constructions that are both correct, the cross-host case, and the browser constraint that follows from none of it being designed for a page.
  • registration-mechanisms.md — how a client that has never met the server obtains a client ID, and why the specification changed its mind about that in 2026.
  • audience-binding.md — measured. Whether the authorization server puts the right audience in the token, tested against two servers in containers.
  • token-lifecycle.md — refresh, rotation, revocation and step-up, for a client that is a background process rather than a person at a browser.

The organizing observation#

Every hard part of this specification is a name resolution problem, and the cryptography is the easy part.

PKCE is settled: 23 of 23 measured servers advertise S256, nobody argues about it, and no implementation surveyed gets it wrong. Signature verification is a solved library call. Neither appears in any failure documented in this survey.

What fails, repeatedly and across every architecture, is agreement about what something is called:

  • A resource value that differs from the endpoint URL by a trailing slash, in six of twenty-three deployed servers.
  • A well-known path built by appending where the RFC says insert, or the reverse.
  • A registration_endpoint with a doubled slash, in the specification’s own reference server.
  • Strict string comparison rejecting URIs that RFC 3986 calls equivalent, as a live bug in a competent implementation.
  • An audience parameter spelled audience instead of resource, silently discarding the security property, measured.
  • A metadata document that omits none from a list, making a mechanism unreachable regardless of whether it is implemented.

Six distinct failures, one shape. The MCP authorization handshake is a chain of documents that identify each other by URL, and every link is a string comparison somebody has to get exactly right. That is the frame for reading the rest of this pass, and it is why the sections below spend more space on URL construction than on tokens.

What “correct” means here, and why it is contested#

The specification is unusually specific about identifiers — it devotes a section to canonical server URIs, names the trailing-slash case explicitly, and tells implementations to prefer the form without one unless the slash is semantically significant. It is correspondingly vague about what a server should do when a client gets it slightly wrong.

Both stances are defensible and they compose badly. RFC 3986 defines equivalence rules that would resolve most of these mismatches, and applying them is neither required nor forbidden. So one implementation normalizes, another compares byte for byte, and both believe they are conformant — which is how a refresh that worked yesterday stops working after a proxy adds a trailing slash.

This survey does not take a side on which behavior is right. It reports that the ambiguity is where the defects are, and that a deployment should pin its resource value once and never let anything reconstruct it.


Audience Binding: What Happens When a Conformant Client Meets a Non-Conformant Server#

Verified: 2026-09-01 Method: harness/2-074-1-mcp-authorization/rig/ — Keycloak 26.4.7 and Ory Hydra 2.3.0, each in one container, driven through real grants. Rung: measured-local.


The question the documentation leaves open#

RFC 8707 is the mechanism that makes an MCP access token usable at one server and refused at every other. The specification makes sending resource a client MUST — “regardless of whether authorization servers support it” — and makes checking the audience a resource server MUST. Both duties sit at the ends. The authorization server in the middle is the only party that can put the right value in the token, and the specification cannot compel it, because the authorization server is out of scope by design.

Keycloak states in its own compliance documentation that it does not implement RFC 8707. That is useful and it stops one step short of the operational question:

When a conformant client sends resource to a server that does not implement it, does the request fail, or does a token come back?

The two outcomes are not variations of the same problem. A rejection is a loud failure at integration time, on the first connection, in front of the engineer wiring it up. A successful issue is a token that looks correct, carries whatever audience the server picked on its own, and moves the entire burden onto a resource-server check the operator has to have written unaided and correctly.

What was measured#

Two self-hostable servers, each provisioned with the smallest realm that answers the question: one confidential client with a service account, so the audience can be inspected without a browser, and — where the server ships a login — one public client and one user, so the authorization-code flow MCP uses can be driven end to end with PKCE S256.

Tokens are decoded without verifying their signatures. The claim under test is what the server put in the token; verifying its own signature adds nothing.

Keycloak 26.4.7#

Grantresource sentHTTPaud in the issued token
client_credentialsno200account
client_credentialsyes200account
authorization_code + PKCEno200account
authorization_code + PKCEyes200account

The parameter is accepted and has no effect. The full flow an MCP client performs — authorization code, PKCE S256, resource on both the authorization and token requests — completes with a 200 and returns a token whose audience is account, Keycloak’s own default, which has no relationship to the MCP server the token was requested for.

No warning is emitted. No error is returned. Nothing in the exchange tells the client that the one parameter carrying the security property was discarded.

Ory Hydra 2.3.0#

Parameter sentaud in the issued token
none[]
resource=https://mcp.example.com/mcp[]
audience=https://mcp.example.com/mcp["https://mcp.example.com/mcp"]
both, with different values["https://mcp.example.com/mcp"] — its own parameter wins

Hydra is the more interesting result, and it changes the shape of the finding. The capability is present and the spelling is wrong. Hydra binds audiences correctly under a parameter it named audience before RFC 8707 existed, and ignores the standard name.

A conformant MCP client — which by specification sends resource and does not know audience exists — receives a token with an empty audience from a server that was fully capable of binding it. Send both and the non-standard parameter wins.

This is the mechanism behind a field report that otherwise reads as a client bug: an operator running Hydra behind an MCP server finds the audience coming back empty and audience validation failing. The token is unbound, the resource server is right to refuse it, and every component behaved as designed.

What this establishes#

“Does not implement RFC 8707” understates the problem in both directions. Keycloak does not implement it and issues tokens anyway. Hydra effectively implements it and cannot be reached by a conformant client. Neither is a server that fails loudly, and a feature matrix with a “no” in one cell describes neither situation.

The failure is silent by construction, not by oversight. Unknown parameters are ignored in OAuth; that is ordinary and correct behavior everywhere else. Applied to resource, the ordinary behavior turns the specification’s central security requirement into a no-op that returns 200.

The resource server’s audience check is the only thing standing. The specification already says so — the MCP server MUST validate that a token was issued for it — and the measurement shows why that requirement is not redundant with the client’s. A deployment on either of these servers has exactly one line of defense against the confused-deputy attack, and it is a line the operator writes.

The practical consequence for anyone self-hosting: write the audience check first, and test it against a token you know is wrong. A test that only exercises the happy path passes identically whether the audience is bound or not, because the same 200 comes back either way.

Two adjacent findings from the same rig#

Keycloak’s metadata omits none. Its token_endpoint_auth_methods_supported lists private_key_jwt, client_secret_basic, client_secret_post, tls_client_auth and client_secret_jwt, and not none — even though the realm hosts a working public client. Claude selects CIMD only when the authorization server advertises the CIMD flag and lists none. So a client following the documented selection rule will never choose CIMD against Keycloak, and shipping CIMD would not change that on its own. The advertised metadata, not the capability, is what a client branches on.

client-id-metadata-document is not a feature flag in 26.4.7. Attempting to enable it produced the server’s full list of recognized features, and it is absent — the experimental CIMD support described in Keycloak’s documentation is not in the current stable release. That is a sharper statement than “experimental,” and it has a date on it.

Hydra advertises no registration endpoint. DCR exists on Hydra’s admin API and is not in its public discovery document, so an MCP client has no advertised registration mechanism at all: no registration_endpoint, no CIMD flag. Every client falls through to pre-registration or to prompting the user.

Neither server advertises what the other does. Keycloak advertises a registration endpoint and RFC 9207 issuer identification, and omits public-client support. Hydra advertises public-client support and omits both of the others. Between the two most recommended self-hosted options, the union is conformant and neither is — and each is missing what the other has.

What this does not establish#

That a token bound to server A is actually refused by server B. That needs two registered resources and two tokens, and it is filed as a follow-up rather than inferred. An empty or wrong aud claim is strong evidence the binding did not happen; it is not the same as watching the replay succeed.

Anything about the five managed vendors. All of them claim RFC 8707 support and none of them can be tested without an account, which is the coverage gap the measurement plan records rather than hides. The two servers measured here are the two a reader can check without paying anyone, which is also why they are the two most likely to be running behind an MCP server whose operator chose not to.

Two ways to give per-project resources an audience#

A platform exposing one MCP endpoint per project, tenant or workspace has a multi-tenancy problem before it has an authorization problem, and it lands on exactly the requirement the measurements above show the category handling worst. Every tenant boundary runs through the audience claim: a token that is not audience-bound works at every project its holder can reach, which is the multi-tenant model collapsing into one credential.

The endpoints are typically created at runtime, so pre-registering each one as a separate resource server is not available. Two mechanisms remain, and they put the burden in different places.

Register one server; carry resource as a claim; enforce at the resource server. This is the pattern Scalekit documents (F009). The authorization server knows about one resource and passes the client’s requested resource through as a claim the resource server reads and checks against its own identity. It needs no per-project registration and works with a managed authorization server that has no concept of your project namespace.

The cost is that the check is now application code in every resource server, and the specification’s aud machinery is not doing the work. A resource server that forgets the check accepts anything.

Echo the client’s resource into aud, behind a prefix allow-list. An authorization server you control validates that the requested resource begins with a namespace you own, then writes it into the token’s aud. Every resource server’s check collapses to aud == my own URL — a string comparison against a value it already knows, with no knowledge of the tenancy model.

This is the stronger property, because it moves the boundary into the standard claim that libraries already validate, and it needs no vendor feature. It is also only as good as the prefix list: an allow-list that is too permissive mints tokens for resources that do not exist, and one that is wrong in the other direction breaks discovery. The list is the security control and should be reviewed like one.

Which to choose follows from who runs the authorization server. With a managed one, the first is generally all that is on offer, and the resource-server check is non-delegable work you were going to write anyway. With one you run, the second is available and is worth the prefix list, because it turns a per-application invariant into a per-token one.

Neither mechanism is exercised by the two self-hosted servers measured above: Keycloak issues its own default audience regardless of resource, and Hydra never sees the parameter. Both patterns therefore assume an authorization server that reads resource at all, which — per the conformance matrix — is a narrower set than the vendor claims suggest.


Discovery: Finding an Authorization Server From a URL Alone#

Verified: 2026-09-01


A user pastes a URL into a client. The client has no configuration, no prior relationship with the server, and no registry to consult. Everything it needs has to be derivable from that one string.

That constraint is what makes MCP authorization different from ordinary OAuth, where a developer reads documentation and copies an issuer URL into a config file. Here the discovery is machine-driven and cold, and it is the reason RFC 9728 — a small, recent, and until 2025 barely-used specification — became the load-bearing piece.

The chain#

client                          MCP server              authorization server
  |  POST (no token)  --------->  |
  |  <-------  401 + WWW-Authenticate: Bearer resource_metadata="..."
  |
  |  GET the protected resource metadata document  ---->  |
  |  <-------  { resource, authorization_servers: [...], scopes_supported }
  |
  |  GET the authorization server's metadata  ------------------------->  |
  |  <-------  { issuer, authorization_endpoint, token_endpoint,
  |              registration_endpoint?, client_id_metadata_document_supported?,
  |              code_challenge_methods_supported, ... }

Three documents, two of which the MCP server does not own. Each step has a failure mode that produces the same user-visible symptom — the connection does not work — and a different fix.

Step 1: the challenge#

The 401 is not advisory. Claude does not honor a WWW-Authenticate header on a 200 response, and the specification names 401 as the trigger. A server that returns 200 with an error body, or 403, or a JSON-RPC error object describing an auth failure, has not started the flow.

The header’s resource_metadata parameter is the pointer. Parsing it is less trivial than it looks: RFC 6750 auth-params are comma-separated, values may be quoted strings that themselves contain commas and equals signs, and a naive split on , loses the pointer on every server that puts it last. Measured across 26 deployed servers, resource_metadata appears in varying positions and alongside realm, error and error_description in several orders.

The specification also SHOULDs a scope parameter in the challenge, so the client knows what to ask for. One server in twenty-six does it. The rest leave the client to fall back on scopes_supported from the metadata document, which the specification describes as the minimal set for basic functionality rather than the set for the operation at hand — so the common case is a client requesting either too little or everything.

Step 2: the protected resource metadata document#

Two ways a client finds it, and the difference matters operationally.

The pointer, from the 401 header. This is the reliable path and the one to build for, because the URL it names does not have to be on the MCP server’s origin. It can be any HTTPS location serving the JSON. That property is what rescues deployments on platforms that cannot serve /.well-known/* at the root — edge functions, Lambda function URLs with a path prefix, Workers without a dedicated route.

The well-known fallback, probed on the MCP server’s origin when the pointer is absent: the path-suffixed form /.well-known/oauth-protected-resource/<path> first, then the bare /.well-known/oauth-protected-resource. Measured, 20 of 23 discoverable servers are found via the pointer and 3 only via the fallback. It works, it costs round-trips on every connection, and it requires a platform that serves well-known paths.

The document’s resource field is the identity of the MCP server, and it is compared. The specification requires it to match the URL as the user entered it, including the path. Six of twenty-three deployed servers advertise a resource that does not string-match their own endpoint — most commonly the origin without the path, or with a trailing slash the endpoint lacks.

Whether that breaks a given client depends on whether the client normalizes, which the specification neither requires nor forbids. This is the ambiguity approach.md names, and it is live: an open bug against Cloudflare’s provider reports strict string equality rejecting URIs that RFC 3986 considers equivalent, breaking token refresh.

authorization_servers is a list, and clients use the first entry without falling back. Listing a secondary issuer does not buy redundancy; it buys a document that misleads whoever reads it next.

Step 3: the authorization server’s metadata#

Two specifications describe this document and a client must support both: RFC 8414 authorization server metadata, and OpenID Connect Discovery.

They construct their paths differently, and the difference is the single most avoidable implementation error in the chain. For an issuer https://as.example.com/tenant/7:

path
RFC 8414https://as.example.com/.well-known/oauth-authorization-server/tenant/7
OIDC Discoveryhttps://as.example.com/tenant/7/.well-known/openid-configuration

RFC 8414 inserts the well-known segment before the issuer’s path. OIDC appends it. Getting that backwards reports conformant servers as broken, which is why the harness tries both constructions for both documents rather than assuming either.

An issuer with no path component makes the two forms identical, which is the common case in the measured set — 18 of 23 deployments put the authorization server on the MCP server’s own host, usually at the root. So the distinction is invisible in most deployments and appears exactly where multi-tenancy makes issuers path-scoped, which is where it hurts most.

The flags a client reads from this document decide the rest of the flow, and S1’s measured tables report what deployed servers put in them.

The cross-host case#

Nothing special is required of an authorization server on a different host: the authorization_servers field says where it is and the client goes there. The work is entirely on the MCP server’s side — making the protected resource metadata findable.

Two constraints are worth knowing before choosing this topology. The authorization server must serve its own discovery metadata at its well-known paths, and it must be reachable from the client vendor’s egress range — discovery requests to the authorization server come from the same IPs as requests to the MCP server, so a WAF in front of the identity provider breaks the flow while the MCP server stays reachable. The diagnostic signature is distinctive: the MCP server sees the request and the authorization server sees no traffic at all.

GitHub is the cleanest measured example of the topology done deliberately — resource server on api.githubcopilot.com, issuer on github.com/login/oauth — and it is also the measured example of the topology’s other cost: its authorization server advertises neither a registration endpoint nor CIMD, leaving a cold client with no way to obtain a client ID. Separating the roles means the authorization server was built for something else first.

The browser constraint#

Checked rather than assumed, because it decides whether an MCP client can be a web page.

The metadata documents are CORS-open. Linear, Notion and Hugging Face reflect an arbitrary Origin on their well-known documents; Sentry sends *. A page can fetch and parse both metadata documents directly.

The pointer inside the 401 is not readable. WWW-Authenticate is not a CORS-safelisted response header, and neither Linear nor Notion sends Access-Control-Expose-Headers on its challenge. A page observes the 401 status and cannot read the header that tells it where to go.

The consequence: a browser-based MCP client cannot use the specification’s primary discovery mechanism. It is confined to the well-known fallback paths — which work only when the platform serves them, and which the specification itself describes as a fallback — or to a server-side proxy, which reintroduces a backend to a client that was trying not to have one.

Preflight support on the MCP endpoints themselves is uneven: Linear and Notion permit a cross-origin POST, Sentry answers the preflight with no CORS headers at all, so a browser cannot even reach the challenge.

No document in this category mentions any of this. It is a real constraint on where an MCP client can live, and it follows from a specification designed for a native client and a server-side agent, applied to a runtime neither anticipated.


Measurement Plan#

Written: 2026-09-01, before S2. Ladder: docs/map/17-the-evidence-ladder.mdrepeated -> cited -> measured-local -> measured-browser.


This category has an unusual property that decides the whole plan. The evidence is public. Every document in the MCP authorization handshake has to be readable by a client that has never seen the server before, so the conformance surface of any deployed server can be inspected by anyone, without an account. Almost nowhere else in the corpus does the cheapest rung reach production systems directly.

The offsetting property is equally structural: the security claim is not public. Whether a server rejects a token minted for a different resource — the one requirement the specification’s security chapter exists for — cannot be seen from outside, because it takes two tokens to test. So the cheap rung reaches everything and settles everything except the thing that matters most, and the plan has to say so rather than let coverage imply completeness.

The levels#

L0 — registry, repository and vendor documentation · cited#

What S1 banked: module directories, issue states and ages, licenses, star counts, and what each vendor’s own documentation claims.

Settles: what a project has implemented, when the repository answers directly. Authlib shipping no rfc8707 or rfc9728 directory is a fact about the library, not a claim about it. Keycloak’s published statement that MCP 2025-06-18 and later is “partially supported without Resource Indicators” is a vendor admission, which is the strongest form a vendor claim takes.

Does not settle: anything a vendor asserts about itself favorably. Five managed vendors claim conformance to the same four RFCs. A claim tops out at cited and the tables say so.

Cost: hours. Coverage: all 12 options.

L1 — the public discovery chain · measured-local · TAKEN#

harness/2-074-1-mcp-authorization/probe.py, run 2026-09-01 against 26 production remote MCP servers.

Settles: whether an unauthenticated call is challenged; whether the resource_metadata pointer is present; whether protected resource metadata is reachable by any route; whether its resource matches the endpoint; and, from the authorization server’s own metadata, the registration mechanisms, PKCE methods, public-client acceptance and issuer identification. It settled the survey’s sharpest S1 question — CIMD advertised versus CIMD usable — which no amount of documentation reading would have.

Cost: minutes. No container, no account, no credential.

Coverage: every deployed server, and zero coverage of the options as products. The probe sees a Cloudflare-hosted deployment, not workers-oauth-provider; it sees an issuer host, not the vendor behind it. Two deployments of the same library disagreed with each other in the results, which is the coverage limit made visible.

L2 — a local conformance rig · measured-local · TAKE#

One container: each self-hostable authorization server next to a minimal resource server, driven end to end by a scripted client through registration, authorization, token exchange and refresh.

Settles what L1 structurally cannot, because it needs a token:

  • Whether a registration attempt actually succeeds, against a registration_endpoint that merely exists.
  • What audience lands in the issued token when a client sends resource, and what lands when it does not. This is the direct test of Keycloak’s documented RFC 8707 gap: the question is not whether the parameter is rejected but whether it is silently ignored, which is the shape that produces a token a correct resource server must refuse.
  • Whether refresh rotation happens for a public client, and what error code a dead refresh token returns — Claude requires invalid_grant specifically.
  • Whether the CIMD conjunction holds end to end on the servers that advertise it.

Cost: a working session. Infrastructure: one container, no external service, no second machine — at the default cut line, so it is taken rather than deferred.

Coverage, and the gap that is a finding: reaches Keycloak, Ory Hydra, ZITADEL, node-oidc-provider and workers-oauth-provider under workerd. It reaches none of the five managed vendors, because using one means creating an account, which is an external service by definition.

That gap is not an omission. The group with the strongest claimed conformance is the group whose conformance cannot be checked without becoming its customer, and any comparison that rates managed and self-hosted options on one scale is mixing a measured column with an asserted one. This survey keeps them in separate tables for that reason.

L3 — cross-audience token replay · measured-local · DEFER#

Hold a token minted for MCP server A and present it to MCP server B behind the same authorization server; confirm B refuses it. Repeat with a token whose resource was omitted.

Settles: the confused-deputy claim the entire security chapter rests on, and the only thing that distinguishes an authorization server that implements RFC 8707 from one that advertises it.

Why deferred: it needs two registered resources and at least two client identities, and against a managed vendor it needs a paid or trialed account. Against the self-hosted set it is reachable at L2’s cost plus a second resource, so it is the first thing to promote if L2’s audience results are ambiguous.

Follow-up: filed on the survey’s bead.

L4 — real-client interoperability matrix · DEFER#

Connect Claude, ChatGPT, VS Code and Cursor to one test server, cycling it through pre-registration, DCR, CIMD-with-none, CIMD-with-private_key_jwt, and a static header, and record which combinations connect.

Settles: client-conformance.md’s central prediction empirically — that a CIMD-only server supporting private_key_jwt alone works in ChatGPT and is invisible to Claude. That finding currently rests on two vendors’ documentation, neither of which mentions the other.

Why deferred: accounts on four commercial products and a publicly reachable HTTPS host with a stable domain. Two external services minimum, well above the cut line.

Follow-up: filed on the survey’s bead. This is the level a future refresh should buy first, because it is the one that would convert the survey’s most actionable claim from cited to measured.

The floor model, and the browser limit that shapes it#

The floor model is not gated on finishing this ladder, and here it derives from L1, the cheapest rung — a page where a reader types any remote MCP server URL and watches the discovery chain resolve.

Checked before assuming it, because the answer is not obvious: the metadata endpoints are CORS-open. Linear, Notion and Hugging Face reflect an arbitrary Origin on their well-known documents and Sentry sends *, so a browser can fetch and parse the chain directly, with no Pyodide and no proxy.

One step of it cannot be done from a browser, and the constraint is worth building the lesson around rather than working around. WWW-Authenticate is not a CORS-safelisted response header, and neither Linear nor Notion sends Access-Control-Expose-Headers on its 401. A page can observe the 401 and cannot read the pointer inside it.

The consequence generalizes past the floor model: a browser-based MCP client cannot use the specification’s primary discovery mechanism. It is confined to the well-known fallback paths, or to a server-side proxy — which is a real constraint on where an MCP client can live, and one that no document in this category mentions.

Preflight support is uneven on the MCP endpoints themselves: Linear and Notion permit a cross-origin POST, Sentry answers the preflight with no CORS headers at all. So the floor model reports three distinct outcomes rather than two — challenged, CORS-blocked, and undiscoverable — and the middle one is a property of the page, not of the server.

What is deliberately not measured#

Latency and throughput of token issuance. Real, and a vendor-selection input, and unmeasurable without accounts on all five.

Anything about the identity vendors as businesses. S4’s subject, from public sources.

Authorization after the token validates. Scopes, roles, per-tenant policy. A different category with a different literature.


How the Options Differ, Technically#

Verified: 2026-09-01


S1 compared what the options claim. This is what the mechanism sections establish about how they differ once you are inside the handshake — the differences that survive a feature matrix.

The work does not divide the way the architecture diagram suggests#

Every architecture leaves the same resource-server work: publish the protected resource metadata document, return 401 with a correct pointer, validate the token’s audience. That is true of a managed vendor, a self-hosted server, and a library in your own process. It is small, and the MCP SDKs and FastMCP implement most of it.

What differs is the authorization server’s conformance, and the measurements say the gap between architectures is smaller than the gap inside them:

discovery correctregistration reachableaudience actually bound
Managed vendorsclaimed by all fiveclaimed by all fiveclaimed, untestable without an account
Keycloak 26.4.7yesDCR yes, CIMD unreachablemeasured: no, silently
Ory Hydra 2.3.0yesnone advertisedmeasured: only under a non-standard name
node-oidc-providervia configyes, bothyes, native
workers-oauth-provideryesyes, bothyes, with an open equivalence bug
Authlibyou write itDCR yesyou write it

Read down the last column. It is the specification’s central security property, and it is the column where the self-hosted options measurably fail, the library options split by language, and the managed options cannot be checked at all.

Three technical selection rules#

If the deployment is Python and in-process, it is not in-process#

Authlib ships no rfc8707 and no rfc9728. Both MCP-specific RFCs are absent from the general Python OAuth answer, and RFC 9728 has been an open issue since April 2025. A Python MCP server that embeds its authorization server is writing protected resource metadata and audience binding itself — the discovery mechanism and the security property, unaided, and untested by anything the team did not write.

That is not a large amount of code. It is the two pieces where a mistake is invisible: a malformed metadata document fails at connection time, which is at least loud, but an unbound audience produces a working system with the security property missing.

Node and Workers do not have this problem. node-oidc-provider ships resource indicators as a configuration surface and carries the CIMD flag; workers-oauth-provider ships CIMD with a conformance suite. Language decides how much you build, and it is the single most under-discussed input to this decision.

If you self-host, the audience check is your only line#

Measured: Keycloak accepts resource and discards it, returning 200 and a token whose audience is its own default. Hydra binds audiences correctly under audience and ignores resource, so a conformant client gets an empty audience from a server that could have bound it.

Neither fails loudly. Both produce a token. The resource server’s own audience validation is the only thing between that token and the confused-deputy attack, and a happy-path test passes identically whether the binding happened or not.

The operational rule that follows: write the audience check first, and test it with a token you know is wrong. For Hydra specifically there is a second rule — either send its non-standard parameter from a client you control, or accept that a standards-conformant client cannot get a bound token from it.

Advertised metadata is the interface, not capability#

This is the sentence that covers the largest number of measured failures.

Keycloak hosts working public clients and omits none from token_endpoint_auth_methods_supported, so Claude’s documented CIMD rule can never select it. Hugging Face advertises the CIMD flag without accepting a public client, so it advertises a mechanism it cannot be used with. Hydra implements DCR on its admin API and does not advertise a registration_endpoint, so no client finds it. GitHub advertises neither mechanism and has a corresponding open bug reporting exactly that.

In every case the capability question and the advertisement question have different answers, and the client only ever sees the advertisement. Any evaluation that reads documentation instead of fetching two JSON documents will get these wrong, which is why the harness exists and why S1’s tables are measured rather than collected.

Where the architectures actually diverge#

Stripping out what they share, three real differences remain.

Who holds the users. Managed vendors hold them. Self-hosted servers and in-process libraries do not. Stytch’s Connected Apps and Scalekit both sit on top of an identity store you already have, and Ory Hydra is the self-hosted version of that shape — it does OAuth and declines to store users or render login. Two commercial vendors and one open-source project converged on that architecture independently, which is a reasonable signal that “the users are already somewhere else” is the common case rather than the exception.

Who tracks the specification. The managed vendors do, quickly. The self-hosted servers do slowly and unevenly — Keycloak documents its gap, Hydra has not adopted the standard parameter name, and both were caught by the measurement. The libraries split: the ones written for MCP are current, the general-purpose ones are behind.

What a migration costs. Less than identity work usually costs, and this is the finding that should lower the temperature of the whole decision. Tokens are short-lived, clients re-run discovery on every connection, and the MCP server’s own half does not change. Moving authorization servers means editing one field in one JSON document and having users re-consent. The exception is client identity: a DCR-issued or pre-registered client ID is bound to the issuer that minted it and must be re-registered, while a CIMD client ID is a self-hosted URL that survives the move. CIMD is the exit-cost mechanism, which is a better reason to want it than conformance.

The technical bottom line#

The resource server is small and mostly written for you. The authorization server is the decision, and the property to select on is audience binding, verified rather than claimed — because it is the one thing the specification depends on, the one thing that fails silently, and the one thing three of the four measurable options get wrong.


Client Registration: How a Stranger Gets a Client ID#

Verified: 2026-09-01


OAuth was designed around an assumption that does not hold here: that a developer registers an application with a provider once, by hand, and receives a client ID to paste into a config file. MCP breaks it in both directions. The client is an AI application the server author has never heard of, and the server is one the client vendor has never heard of, and a user expects the two to connect from a pasted URL in under a minute.

Three mechanisms address that, the specification ranks them, and the ranking changed in 2026.

The priority order#

The specification’s own list, for a client that supports everything:

  1. Pre-registered credentials, if the client already has them for this server.
  2. Client ID Metadata Documents, if the authorization server advertises client_id_metadata_document_supported.
  3. Dynamic Client Registration, if the authorization server advertises a registration_endpoint.
  4. Ask the user to type in client details.

Note what is first. Pre-registration is not a legacy fallback in this list; it is the preferred answer whenever it applies, and it requires nothing of the authorization server beyond ordinary OAuth. Cursor exposes it as a user-facing setting, and Claude accepts an optional client ID and secret when adding a custom connector, which scopes the OAuth client to the organization that entered it. For a server with a small, known set of clients this is the simplest correct answer available, and it is routinely skipped because the discussion starts at DCR.

Dynamic Client Registration, and why it was deprecated#

RFC 7591 lets a client POST its metadata to a registration endpoint and receive a client ID with no human involved. It is old, widely implemented, and was MCP’s original answer.

The 2026-07-28 revision deprecated it. The reason is operational rather than cryptographic: DCR registers a new client on every fresh connection. A server listed in a client vendor’s directory accumulates client records at the rate its users connect, and nothing prunes them. Anthropic’s guidance to high-traffic servers is explicit — prefer CIMD or held credentials over DCR, because DCR “can result in very large numbers of registered clients on your authorization server.”

There is a second cost the specification handles separately. A registration endpoint open to the internet accepts arbitrary metadata from anyone, including client_name and logo_uri that a consent screen will render. Auth0’s fine-grained controls over what dynamic registration may create exist for that reason.

One conformance trap produces a failure confusing enough to name. An authorization server implementing OIDC Dynamic Client Registration may enforce redirect-URI constraints keyed on application_type, which defaults to web when omitted — and a native client registering loopback redirects under the web default is rejected. A client must send application_type: native for desktop, mobile, CLI and localhost-served applications. Non-OIDC servers ignore the parameter safely, so the failure appears only against OIDC-based authorization servers and looks like an arbitrary rejection.

Client ID Metadata Documents#

The replacement inverts the direction of the registration. Instead of the client sending metadata to the server, the client hosts a JSON document at an HTTPS URL and that URL is the client ID. The authorization server fetches it on first sight.

The document must carry at least client_id, client_name and redirect_uris, and its client_id must equal the URL it is served from exactly. The authorization server validates that equality, validates the presented redirect_uri against the list, and caches the document per HTTP cache headers.

Three properties follow, and the third is the one that matters most in practice.

No registration call. No client records accumulate, which is the problem DCR was deprecated for.

A stable, inspectable identity. The consent screen can name the client from a document the client vendor controls and any auditor can fetch.

Portability across authorization servers. A DCR-issued or pre-registered client ID is bound to the issuer that minted it — the specification requires clients to key such credentials by issuer and to re-register when the authorization server changes. A CIMD client ID is a self-hosted URL, so it survives the migration. For anyone who might change identity vendors, this is the strongest argument in the mechanism’s favor, and it is rarely the one cited.

Claude Code is the worked example: it declares http://localhost/callback and http://127.0.0.1/callback in its own published Client ID Metadata Document, and an authorization server must match both ignoring the port, because it binds an ephemeral loopback port per session. RFC 8252 requires port-agnostic matching for the IP-literal form; the same treatment for localhost is what makes Claude Code work.

A Client ID Metadata Document does not authenticate the client. Any local process can bind a port and claim to be a legitimate loopback client, so the specification requires the consent screen to display the redirect URI hostname and recommends an extra warning when the only registered redirects are loopback addresses. The document identifies; it does not attest.

Why the deployed ratio runs the other way#

Measured across 23 deployed authorization servers: 22 advertise DCR, 5 advertise CIMD, 4 can actually be used with it.

The specification deprecated the mechanism that 96% of deployments offer, in favor of one that 17% offer. That is not a criticism of the change — the operational argument for CIMD is sound and the accumulation problem is real — but it sets the transition period, and the transition is where anyone building now has to live. Both mechanisms have to work for years, and a server that implements only CIMD is unreachable by clients that have not adopted it while a server that implements only DCR is following deprecated guidance and still connects to everything.

The conjunction, and the client split#

The gap between 5 advertising CIMD and 4 usable is a rule that lives in two documents at once.

Claude selects CIMD only when the authorization server advertises client_id_metadata_document_supported: true and lists none in token_endpoint_auth_methods_supported — the second because its CIMD client authenticates as a public client at the token endpoint. Miss either and it falls back to DCR. Hugging Face advertises the flag and does not accept a public client, so it advertises a mechanism it cannot be used with.

ChatGPT’s CIMD client accepts both public-client exchange (none) and signed client assertion (private_key_jwt).

The consequence is an interoperability split that neither vendor’s documentation mentions: an authorization server offering CIMD only with private_key_jwt works with ChatGPT and is invisible to Claude. Supporting both makes the difference disappear, and supporting none alone is sufficient for both.

Keycloak shows the failure from the other side. Its metadata omits none entirely, even though the realm hosts a working public client — so a client following the documented rule will never choose CIMD against Keycloak, and shipping CIMD would not change that by itself. The advertised metadata, not the capability, is what a client branches on, and that sentence covers most of what goes wrong in this section.

The comparison#

who holds the recordsurvives an AS changeconsent screen names the client fromaccumulates recordsreaches every client today
Pre-registrationthe authorization servernowhat an admin typednoyes
DCRthe authorization servernowhat the client POSTedyesyes
CIMDthe client, at a URLyesa document anyone can fetchnono — 4 of 23

The reading for a server author today: implement DCR because everything speaks it, implement CIMD because the specification is going there and it is the only portable identity, and advertise none at the token endpoint so the CIMD you implemented is reachable. The third clause is the one that gets forgotten, and it is measurable from outside in one HTTP request.


Token Lifecycle for a Client That Is Not a Person#

Verified: 2026-09-01


OAuth’s refresh and revocation machinery was designed around a browser session: a user is present, a redirect is cheap, and re-prompting costs a click. An MCP client is a background process holding a token on behalf of a user who left hours ago, in a conversation that may resume at any time. The same machinery, different assumptions.

Refresh, and the errors that break it#

The client vendor’s behavior is the specification here, because the specification says little about timing. Claude’s is published and is the most detailed available:

  • Reactive refresh on a 401, with a proactive attempt up to five minutes before the stored expiry.
  • Thirty seconds for a refresh request to complete; ten for discovery, registration and token endpoints. Past the window the flow fails even if the server eventually answers.
  • invalid_grant, specifically, when a refresh token is no longer valid. Returning invalid_request or a custom code — which many implementations do, since RFC 6749’s error taxonomy is honored loosely — leaves the client unable to distinguish “re-authorize the user” from “retry later,” and it retries instead of prompting.

That last point is the most common self-inflicted failure in this section. A server that returns the wrong error code on an expired refresh token produces a connector that appears to hang rather than one that asks the user to sign in again.

Refresh token rotation is required, not advisory, for the clients MCP produces. Both DCR and CIMD register the client as public — no secret — and OAuth 2.1 requires public-client refresh tokens to be rotated or sender-constrained. If you rotate, the new refresh token must come back in the same response that invalidates the old one, or a client that crashes between the two calls is permanently locked out with no signal.

The content-type trap#

The token endpoint must accept application/x-www-form-urlencoded, per RFC 6749. The registration endpoint takes application/json, per RFC 7591.

Frameworks that default to JSON-only body parsing return 415 Unsupported Media Type from the token endpoint while the registration endpoint works, which reads as a mysterious partial failure. Name it, because the two endpoints usually live in the same router, written the same afternoon, by someone reasonably assuming they take the same body format.

Scopes and step-up#

The specification defines an incremental model that almost nobody uses yet, and it is the part of the design most suited to how agents actually work.

A server responds to an under-scoped request with 403 and a WWW-Authenticate carrying error="insufficient_scope", the scopes needed for this operation, and the resource_metadata pointer again. The client computes the union of what it already had and what was just demanded — preserving previously granted permissions — and re-authorizes.

Two requirements keep this from degrading. Servers SHOULD emit every scope an operation needs in a single challenge; challenging one scope at a time forces a separate consent round-trip per scope and is a bad experience delivered one step at a time. And servers MUST account for scope hierarchies, so a token carrying a broader scope satisfies a narrower demand without a pointless re-authorization.

scopes_supported in the protected resource metadata is meant to be the minimal set for basic functionality, with everything else requested incrementally. Measured, one server in twenty-six sends a scope in its challenge at all — so in practice clients fall back on scopes_supported, which was never intended to be the whole ask, and the incremental design is mostly unexercised.

The consequence for a server author is a choice the specification does not force but the ecosystem does: either advertise a scopes_supported broad enough that a client asking for all of it can do useful work, and accept over-permissioning, or implement challenges properly and be among the few servers whose least-privilege story is real.

Revocation#

The weakest part of the chain, and the specification is nearly silent on it.

Revocation reaches the client the same way expiry does: the next request returns 401, the client attempts a refresh, the refresh fails, and — if the error code was invalid_grant — the user is asked to sign in again. There is no push, no back-channel notification, and no defined way for a server to tell a client that a grant is gone.

For a browser session, that latency is invisible. For an agent holding a token across sessions, the window between an administrator revoking access and the client noticing is bounded only by how long the access token was valid. Access token lifetime is therefore a revocation-latency decision as much as a security-margin one, and it is the single configuration knob with the most effect on how quickly a deprovisioning takes effect.

Short-lived access tokens with rotating refresh tokens make revocation prompt at the cost of refresh traffic — which then runs into the thirty-second budget and the invalid_grant requirement. The three interact, and getting one right while leaving another wrong produces a system that mostly works and fails unpredictably.

What none of this covers#

Machine-to-machine. Claude does not support a client_credentials grant with no user in the loop; every connection requires user consent. A server designed around service tokens has no path through the OAuth flow, and the substitute is either Anthropic-held client credentials, which keeps consent while removing registration, or a fixed header credential, which removes both.

A fixed header credential is the bottom of the ladder and a supported rung: static_headers in beta, entered once by an organization administrator, sent on every request. No user identity, no consent, no refresh, no revocation short of rotating the secret — and one credential whose blast radius is the whole organization. Everything in this section is what you give up. S3 judges when that trade is reasonable, because the answer depends entirely on who the credential belongs to and what it can reach.

S3: Need-Driven

Five Situations#

Verified: 2026-09-01


The solo author with one tool and no users yet, who mostly needs permission to skip all of this. The directory-listed vendor whose users arrive by clicking, for whom registration volume is a real cost. The team that already authenticates its own users and needs to turn a session-issuing identity service into a token-issuing authorization server. The security reviewer who decides whether any of it ships. The client author at the other end, implementing the handshake rather than publishing one.

Why these five, and not use cases#

The natural instinct is to divide by what the MCP server does — a database tool, a document search, a ticketing integration. That division produces five files with identical contents, because the authorization decision does not turn on what the tools do.

It depends on two things, and these five are the corners of that grid:

Who holds the credential. One person holding a key to their own data is a different security object from one organization-wide secret in front of a multi-tenant store, even when the mechanism is byte-for-byte the same. This is what separates the solo author from the other four, and it is why a fixed header credential is a reasonable answer in one row and a finding in another.

Whether the users already exist somewhere. A vendor with a customer database, a team with an in-house identity service, and a developer with no users at all face different work — and the products in this category divide along the same line, which is why Stytch’s Connected Apps, Scalekit and Ory Hydra all exist in the shape they do.

The reviewer and the client author sit outside the grid on purpose. The reviewer is the adoption decision-maker in most organizations and is served by none of the vendor-facing literature. The client author is a small population whose choices determine whether every other row’s server can be connected to at all.

What is not a persona here#

Industry or company size. Neither changes any answer in this category.

The specific triggering project. A survey is a comparison of a category for any reader. Where a situation below resembles a particular deployment, the resemblance is the point of the persona and not a description of it.


What Each Situation Should Do#

Verified: 2026-09-01


SituationRegistrationAuthorization serverThe thing most likely to go wrong
Solo author, own data onlynone, or a fixed header credentialnonePutting the credential in the URL
Solo author, a few known peoplepre-registrationa managed free tierReaching for DCR when nobody needed it
Directory-listed vendorCIMD and DCRmanaged, chosen on tracking speedAdvertising CIMD without none at the token endpoint
Existing in-house identitypre-registration, then CIMDone that layers on the existing storeAssuming a self-hosted OAuth server binds the audience
Regulated, no vendor permittedDCR todayKeycloak or ZITADELThe audience check, which is the only line left
Client authorimplement all four, expect DCRn/aOne well-known path construction; the browser CORS wall

The two answers most often skipped#

Nothing. Authorization is optional in the specification and one of the twenty-six measured deployments is authless. The question is not whether a server is public but what a request can do.

Pre-registration. First in the specification’s own priority order, requires nothing unusual of the authorization server, and is the correct answer whenever the set of clients is known and small. It gets skipped because the conversation in this category habitually opens at dynamic registration, which exists to handle strangers at scale.

The one instruction that applies to all six rows#

Validate the token’s audience, and write the test that hands your server a token minted for somewhere else.

Every other mistake in this survey announces itself — the connection fails, an error appears, a support ticket arrives. Two widely deployed authorization servers were measured issuing tokens with the audience unbound while returning a normal success response, so a suite that only exercises valid tokens passes identically whether the protection exists or not.


The Author of an MCP Client#

Verified: 2026-09-01


Who Needs This#

Someone building the other end: an agent framework, an IDE integration, an internal tool that consumes remote MCP servers, or a web application that wants to reach them from a browser. They are implementing the discovery chain rather than publishing one.

A small population and a disproportionately important one, because every conformance finding in this survey is ultimately about whether a client can connect.

Why They Need It#

Because the specification tells them what to do and the deployed ecosystem tells them what they will actually meet, and the two differ enough to determine whether a client works.

Measured across 26 production deployments: 24 challenge with 401, 23 publish discoverable protected resource metadata, 22 of 23 advertise dynamic client registration, 5 advertise Client ID Metadata Documents, 4 can be used with it, 23 of 23 advertise PKCE S256, 3 advertise RFC 9207 issuer identification, and 1 sends a scope in its challenge.

A client built strictly to the current specification — CIMD preferred, DCR deprecated, issuer validation expected — meets an ecosystem running the previous one.

What This Means for the Implementation#

Implement the full priority order, and expect to land on DCR. Pre-registration first, then CIMD when the authorization server advertises both the flag and none at the token endpoint, then DCR, then ask the user. Today the third branch carries almost all the traffic.

Parse the challenge header properly. RFC 6750 auth-params are comma-separated with quoted values that may themselves contain commas and equals signs. A naive split loses resource_metadata on every server that puts it last, and several do.

Implement both well-known constructions for both documents. RFC 8414 inserts the well-known segment before the issuer’s path; OpenID Connect Discovery appends it. Supporting one form works against 18 of 23 measured deployments, because most put the authorization server at a host root where the two forms coincide — so this bug ships, passes testing, and surfaces against the first path-scoped multi-tenant issuer.

Decide the normalization question on purpose, and write the decision down. Six of 23 deployments advertise a resource that does not string-match their own endpoint, usually by a trailing slash or a missing path. RFC 3986 defines equivalence rules; applying them is neither required nor forbidden. A client that compares byte for byte rejects working servers, and one that normalizes freely weakens an identifier check. Cloudflare’s provider carries an open bug on exactly this, from the server side.

Send resource regardless. The specification requires it whether or not the server supports it, and the measured consequence is that some servers will silently discard it. That is not a reason to stop sending it; it is a reason not to treat a successful token exchange as evidence the token is bound.

Validate iss when it is present, and do not require it. Three of 23 advertise support. The specification’s table is explicit: compare when present, reject an absent one only when the server advertised support.

The Browser Case, Which Is Worse Than It Looks#

A client that runs as a web page hits a wall the specification does not mention.

The metadata documents are reachable — measured, several deployments reflect an arbitrary Origin and one sends *. But WWW-Authenticate is not a CORS-safelisted response header, and no measured deployment sends Access-Control-Expose-Headers on its 401. A page can observe the challenge and cannot read the pointer inside it.

The consequences, in order of severity:

  1. The primary discovery mechanism is unavailable. A browser client is confined to the well-known fallback paths, which work only when the server’s platform serves /.well-known/* — the exact case the pointer exists to rescue.
  2. Preflight support is uneven. Some deployments permit a cross-origin POST and others answer the preflight with no CORS headers, so the challenge itself is unreachable.
  3. The remaining option is a server-side proxy, which gives a browser-based client the backend it was built to avoid.

This is not a defect in any implementation. It follows from a specification designed for native clients and server-side agents, meeting a runtime it did not anticipate. A client author choosing a runtime should know it before choosing, because it is not fixable from the client side.

What Would Change the Answer#

Servers exposing WWW-Authenticate via Access-Control-Expose-Headers. One header, entirely within each server’s control, and it would make browser-based MCP clients ordinary. Nothing in the specification asks for it, which is why nobody does it, and it is the single cheapest ecosystem-level improvement this survey identifies.

The CIMD ratio inverting. When most servers advertise it, a client can stop maintaining the DCR path — and can stop registering a new client on every connection, which is the problem the deprecation exists to solve.


The Vendor Whose Server Is Listed in a Client Directory#

Verified: 2026-09-01


Who Needs This#

A software company that already sells a product with accounts, and is now publishing a remote MCP server so its customers can reach that product from an AI client. The server appears in a connector directory, users add it by clicking rather than by pasting a URL, and connections arrive from people the company has never provisioned individually.

Most of the twenty-six deployments measured in S1 are this persona: Linear, Notion, Stripe, Figma, Canva, Zapier and the rest.

Why They Need It#

Three pressures that do not apply to anyone else in this survey.

Volume changes which registration mechanism is viable. Dynamic client registration creates a client record on every fresh connection and nothing prunes them. For a server with a handful of users that is invisible. For one in a directory it is unbounded growth in the authorization server’s client table, and it is the reason the specification deprecated DCR. Anthropic’s guidance names this persona specifically: servers expecting high traffic from the directory should prefer CIMD or held credentials.

The consent screen is a product surface. It is the first thing a user sees with the company’s name on it, and what appears there comes from client metadata — a name and a logo the authorization server renders. A dynamically registered client supplies its own; a Client ID Metadata Document supplies one from a URL anyone can fetch and audit. That is a brand-safety difference as much as a security one.

Support cost is dominated by discovery failures. Every measured failure mode in S1 produces the same user-visible symptom — it does not connect — and lands in a support queue as an unreproducible complaint. Two of twenty-six deployments answer 401 with no discoverable metadata at all, which from the outside is indistinguishable from an outage.

What Fits#

A managed authorization server, chosen for currency rather than features. All five vendors surveyed claim conformance to the same RFCs; what separates them is how fast they implement whatever the next revision adds, and this persona feels that lag directly because its users are on whatever the client vendor shipped last week.

CIMD, with DCR retained. Not either-or. CIMD removes the accumulation problem and gives the consent screen a stable identity; DCR is what 22 of 23 deployed servers still advertise and what every client can still use. A directory-listed server that implements only CIMD is unreachable by clients that have not adopted it, and one that implements only DCR is following deprecated guidance.

Advertising none at the token endpoint. Measured, and the cheapest correctness fix in this survey: Claude selects CIMD only when the CIMD flag and none in token_endpoint_auth_methods_supported are both present. One deployment advertises CIMD without none and is therefore never selected for it. This is one line in one JSON document and it is checkable from outside in a single request.

A scope in the challenge. One server in twenty-six does it. For a product with more than one permission level, doing it is what makes least-privilege real rather than aspirational — and it is what lets the client ask for exactly what an operation needs instead of everything the metadata advertises.

What Does Not Fit#

Self-hosting, unless it was already there. The measured conformance of the free self-hosted servers is materially behind, and this persona is exposed to that gap on every new client release. Keycloak’s missing resource indicators and Hydra’s non-standard audience parameter are both invisible until a client that follows the specification arrives.

A fixed header credential. An organization-wide shared secret does not survive contact with a customer base, cannot express per-user permissions, and cannot be revoked for one customer without rotating it for all of them.

What Would Change the Answer#

An enterprise customer asking to bring their own identity provider. At that point the question becomes whether the authorization server can sit on top of an existing identity store, which is exactly what Stytch’s Connected Apps and Scalekit are built for, and what a vendor chosen only for MCP conformance may not do gracefully.

Regulatory or residency constraints on where identities live. That is the one argument that reliably overrides the conformance gap and pushes this persona toward self-hosting — in which case the audience check stops being a formality and becomes the thing the security review turns on.


The Team That Already Authenticates Its Own Users#

Verified: 2026-09-01


Who Needs This#

An organization running several first-party applications behind one identity service they built or deployed themselves. Users sign in once and reach every app. The identity service already does the hard parts of a login: credentials, sessions, a grant list saying which applications a given user may reach.

Now they want to expose MCP endpoints — often one per project, tenant or workspace — to clients they do not control.

This is the most common shape in the category and the least well served by its literature, which is written either for a vendor with no identity of its own or for an enterprise buying a platform.

Why They Need It#

Because what they have and what MCP needs overlap almost entirely and differ in exactly one place, and the difference is easy to miss.

An in-house SSO service typically issues browser session cookies to applications it controls. It authenticates a human, it decides what that human may reach, it redirects. It is often shaped like an authorization code flow already: a redirect, a one-time code, a server-to-server exchange, a grant list consulted at the end.

MCP needs it to issue bearer tokens to third-party clients, scoped to one resource. Same authentication, same consent, same grant list — different credential, different audience, different holder. The user is the same person; the thing holding the credential is software the organization has never seen.

That gap is smaller than it looks in one direction and larger in another. The login is done. What is missing is the OAuth surface a stranger can discover and use: a discovery document, a registration mechanism, PKCE, refresh with rotation, and audience binding.

What Fits#

An authorization server that sits on top of the identity store, rather than replacing it. This is the shape that matches, and three products in this survey converged on it independently: Stytch’s Connected Apps and Scalekit are built for it commercially, and Ory Hydra is the open-source version — it does OAuth and OIDC and declines by design to store users or render login, delegating both to an application you already have.

The convergence is the signal. Two vendors building for this category and one long-running open-source project all concluded that the authorization server should be separable from the identity store, because “the users are already somewhere else” is the common case rather than the exception. A product that assumes otherwise is asking for a migration that has nothing to do with MCP.

Per-resource identity, planned before the first endpoint ships. A team exposing one endpoint per project has a multi-tenancy problem before it has an authorization problem: the resource value has to name the specific endpoint, protected resource metadata has to be served per endpoint with a matching resource, and the audience check has to compare against the right one. Getting this right at one endpoint and generalizing later means reworking the discovery layer, because the resource a client sends is the endpoint URL it was given.

Reusing the existing grant list as scopes. The mapping is usually direct — a per-user list of which applications are reachable becomes the scopes a token may carry — and it is the part of the work already done.

What Does Not Fit#

Extending the existing service into a full authorization server in-process, if it is Python. The measured finding: Authlib ships neither rfc8707 nor rfc9728, so this path means writing protected resource metadata and audience binding personally. The first fails loudly. The second fails silently, and it is the one the specification’s security chapter exists for.

The same path in Node is materially different — node-oidc-provider ships resource indicators as a configuration surface — which is an uncomfortable thing for a decision to hinge on and is nevertheless what the evidence says.

Assuming a self-hosted OAuth server closes the gap. It closes most of it and not the part that matters. Measured: Keycloak accepts the resource parameter and discards it, returning a token bound to its own default audience; Hydra binds audiences only under a parameter name that predates the RFC, so a conformant client gets an empty audience. Either one leaves the audience check as the only line of defense, and that check is written by this team.

Machine-to-machine tokens. A team used to service-to-service credentials between its own applications will reach for client_credentials and find it unavailable: Claude does not support a grant with no user in the loop. Every connection requires consent, which is a design constraint rather than a configuration problem.

What Would Change the Answer#

How many third-party clients, and how identifiable they need to be. For a handful, pre-registration is first in the specification’s priority order and requires nothing exotic: issue a client ID by hand, hand it over, done. The dynamic mechanisms exist for strangers at scale, and a team that knows all its clients by name is paying for a problem it does not have.

Whether identity may leave the building. If it may, a managed authorization server on top of the existing store removes the conformance risk entirely and is the shortest path. If it may not, the work is real and the audience check is the part to build first and test with a token known to be wrong — because everything else in the flow announces its own failures and that one does not.

What It Costs To Build Instead#

The separable managed option and the in-process build are close enough in cost that the comparison is worth stating in hours and dollars rather than in principle.

The build is about a day, for a team already running the identity half. PyJWT and cryptography are enough; Authlib ships neither RFC 8707 nor RFC 9728 and does not shorten it. What that day buys: RFC 8414 metadata at both well-known forms, RFC 7591 registration persisted, PKCE S256 only, a consent screen, refresh rotation with family revocation, a JWKS endpoint, and audience binding. Write the audience check first and test it with a token known to be wrong — everything else in the flow announces its own failures and that one does not.

The managed option’s floor is a subscription, not a free tier. Scalekit’s identity-store mode is its BYOA tier at $99/month rather than the free plan (F008), and the equivalent shape at the other vendors sits above their free tiers too. Below a few hundred users, a fixed monthly fee competes with a day of work plus the conformance risk you then own. Above that, the vendor’s argument regains its weight: they track the specification’s revisions so you do not, and this specification is revising.

The login half is the same either way, which is the part most estimates get wrong. A BYOA integration is a redirect to your existing login URL carrying a request id and state, a server-to-server POST of the authenticated user’s claims, and a redirect back to the vendor’s callback. A team that has an SSO code exchange has already written that shape. Choosing the vendor does not remove the work of connecting your identity store to it — it removes the OAuth surface, which is the other half.

One risk stays open until measured, either way: whether the clients you care about resolve a path-aware issuer, /.well-known/oauth-authorization-server/<path>, or only the root form. If they do not, an authorization server mounted under a path is undiscoverable and has to move to the root. Test this against a real client before committing to a layout.


The Reviewer Who Decides Whether This Ships#

Verified: 2026-09-01


Who Needs This#

The person who signs off — a security engineer, an architect, a compliance lead — reviewing a proposal to expose an internal system through a remote MCP server. They did not choose the technology and often did not ask for it. They are the actual adoption decision-maker, and the proposal fails or ships on their reading.

They need to know what the specification guarantees, what it delegates, and which of the delegated parts the proposing team has actually done.

Why They Need It#

Because the reassuring sentence in every vendor page — “MCP uses OAuth 2.1” — is true and describes less than it sounds like.

OAuth 2.1 is a framework, and the MCP profile of it distributes duties across three parties. Two of them are outside the reviewing organization’s control: the client is a product from an AI vendor, and the authorization server may be a third party. What the organization controls is the resource server, and the specification places exactly one security-critical duty there: validate that the token was issued for this server and no other.

That duty is not redundant with anything the authorization server does, and the measurements in this survey are why.

The Questions Worth Asking#

“Show me the audience check, and show me the test that proves it fails a wrong token.”

This is the question. The specification’s central protection against a token minted for one MCP server being replayed against another is RFC 8707 audience binding, and the measured finding is that two widely deployed self-hosted authorization servers do not deliver it to a conformant client. Keycloak accepts the resource parameter, returns HTTP 200, and issues a token bound to its own default audience. Ory Hydra binds audiences correctly only under a parameter name predating the RFC, so a standards-conformant client receives an empty audience.

Neither fails loudly. Both return a working token. A happy-path integration test passes identically whether the binding happened or not, which means a green test suite is not evidence here and a test that presents a knowingly wrong token is.

“Which authorization server, and can we verify its conformance or only read about it?”

Self-hosted servers can be measured, and this survey measured two of them. The five managed vendors all claim conformance and none can be verified without an account — a real asymmetry, and one a review should say out loud rather than let a comparison table imply that claimed and measured are the same kind of fact.

“What is the revocation latency?”

There is no back channel. When access is revoked, the client finds out on its next request, attempts a refresh, and fails — assuming the server returns invalid_grant rather than some other code, without which the client retries instead of prompting. The window between deprovisioning a user and the client losing access is bounded by the access token lifetime, and nothing else. For an agent holding a token across sessions this is the number to ask for, and it is a configuration value rather than a property of the protocol.

“Is there a shared credential anywhere in this design?”

A fixed header credential is a supported path and an organization-wide secret. It is defensible when it can only reach resources the whole organization already shares, and it is a single point of total compromise in front of anything per-user or multi-tenant. The question is not whether the secret is stored well; it is what one holder of it can reach.

“What can a tool actually do with a valid token?”

Outside this survey’s scope and inside the reviewer’s. Authorization ends when the token validates; what the token permits is the resource server’s own policy. One server in twenty-six sends a scope in its challenge, so the common case is a client holding whatever the metadata advertised rather than what any operation needed.

What This Survey Does Not Settle#

Whether a bound token is actually refused elsewhere. An empty or wrong audience claim is strong evidence the binding did not happen. Watching a replay fail is a different test, it needs two registered resources, and it is filed as follow-up work rather than asserted.

The trust problem inside the protocol. Tool description poisoning, rug pulls and shadowing are a separate class — the question of whether a server the organization already authorized can be trusted with what it says. Survey 2.074’s security chapter is that subject, and a review that clears the authorization design has cleared the perimeter and not the interior.

The Shortest Defensible Position#

Approve when the audience check exists and has a failing-token test; when the access token lifetime is short enough that revocation latency is acceptable; when no shared credential sits in front of per-user data; and when the team can name which authorization server issues the tokens and what it advertises.

Ask for more work when the answer to the audience question is “the authorization server handles that.” Measured, for the two self-hosted options a team can adopt without a purchase order, it does not.


The Solo Author With One Tool and No Users Yet#

Verified: 2026-09-01


Who Needs This#

One developer, one MCP server, exposing something they built — a database query tool, a document search, a wrapper around an internal API. The server runs on a small host somewhere. The expected audience is themselves, and possibly a handful of people they know.

They arrive at this category because a client refused to connect, or because they read that remote MCP servers need OAuth and want to know how much trouble they are in.

Why They Need It#

They mostly do not, and finding that out is the most valuable thing this survey can give them.

Authorization is optional in the specification. It says so directly. A server exposing public or non-sensitive data can answer every request and never issue a challenge — one of the measured deployments, Cloudflare’s documentation server, does exactly that and connects fine. If nothing behind the server is worth protecting, nothing here applies.

The decision point is not “is this server public” but “what can a request do.” A search over public documentation needs nothing. A tool that reads one person’s files, spends money, or writes anywhere needs an answer, and the answer’s size depends on how many people will ever hold the credential.

What Fits#

One person, one credential: a fixed header. Claude supports static_headers in beta — an administrator enters an API key or bearer token once and it is sent on every request. authorization and x-api-key work without review. ChatGPT accepts static credentials similarly. This takes an afternoon, requires no authorization server, and is a supported path rather than a workaround.

What it costs, stated plainly because the trade is the whole decision: no user identity, so the logs cannot say who did anything; no consent screen; no expiry; no revocation short of rotating the secret and updating every client. For one person that list is a set of properties they were not going to use. For five people sharing one credential it is a liability that grows with the group.

Never in the URL. A credential in a query string leaks through server logs, proxy logs, browser history and screenshots, and the specification prohibits access tokens there outright.

A few known people: pre-registration. First in the specification’s own priority order, and consistently overlooked because the discussion starts at dynamic registration. Register one OAuth client by hand with whatever identity provider is already in play, hand the client ID to the people who need it — Cursor takes static client credentials in its configuration, Claude accepts an optional client ID when adding a custom connector. No DCR, no CIMD, nothing dynamic, and real per-user identity.

Growing past that: a managed vendor’s free tier. Descope’s free tier runs to thousands of monthly active users; Scalekit, Stytch and WorkOS all have entry tiers. At this size the choice between them barely matters, and the reason to move is that per-user identity has become something worth having rather than that the previous approach broke.

What Does Not Fit#

Self-hosting an authorization server. Keycloak, Hydra and ZITADEL are each another service to run, patch, back up and debug, and the measured findings say the MCP-specific parts are the least finished parts. Adopting one to serve five users trades an afternoon of work for an ongoing operational commitment and gets a worse conformance story.

Writing an authorization server in-process. In Python especially: Authlib ships neither of the two MCP-specific RFCs, so this path means implementing protected resource metadata and audience binding personally, correctly, with nothing to test against. Both are small. One of them fails silently when wrong, which is the wrong kind of small.

What Would Change the Answer#

The trigger is not user count. It is the first time a second person’s data is reachable through the server. A shared credential that can only reach the holder’s own resources is a convenience. The same credential in front of a multi-tenant data store is a single secret standing between any holder and every record in it, and no amount of care about where the secret is stored changes that shape.

At that moment the question stops being this persona’s and becomes the one in use-case-existing-identity-service.md or use-case-directory-listed-vendor.md, depending on who the users are.

S4: Strategic

What This Pass Weighs#

Verified: 2026-09-01


Two questions, kept apart because conflating them is how technology choices get made on the wrong evidence.

Is the standard stable enough to build on? standard-viability.md. What has changed in MCP’s authorization chapter across five revisions, what direction the changes run in, which parts have stopped moving, and the state of the IETF draft the newest mechanism depends on.

Will the thing you picked still be there? durability.md. Maintenance signals on the open-source options, and what a migration between authorization servers actually costs — a number that turns out to be small, and that changes how much the vendor choice deserves.

Durability answers will this survive and never is this good. The conformance measurements answer the second question and this pass does not revisit them. A well-funded vendor with a missing RFC still has a missing RFC, and a small project that ships the specification correctly still ships it correctly.

The predictions#

A survey with a fast decay class should say what it expects, so a re-run can score it rather than re-narrate it. standard-viability.md records four, each with a measured starting number from 2026-09-01: RFC 9207 issuer identification at 3 of 23, CIMD at 5 of 23, machine-to-machine support at zero across the major clients, and the string-handling defect class at 6 of 23.

Those numbers are the point. A prediction without one is a mood.


Will the Thing You Picked Still Be There#

Verified: 2026-09-01


Durability answers will this survive, and it never answers is this good. The conformance measurements answer the second question and this file does not revisit them. A well-funded vendor with a missing RFC is still missing the RFC.

The open-source options#

licensestarslast commitinbound patches unanswered
AuthlibBSD-3-Clause~5.4k2026-08-31RFC 9728 open since 2025-04-24
node-oidc-providerMIT~3.8k2026-09-01no
Ory HydraApache-2.0~17.5k2026-07-29no
ZITADELAGPL-3.0~14.9k2026-09-01no
KeycloakApache-2.0Red Hat-backedactiveno
workers-oauth-providerMIT~1.9k2026-08-19no
Authentikpermissive~25.3k2026-09-01no

All active, none stalled. The stall signal this corpus watches for — inbound patches accumulating with nothing merged — does not fire on any of them.

One entry deserves reading rather than scanning. Authlib’s RFC 9728 issue has been open for sixteen months against a library committed to within the last day. That is not abandonment; it is a maintained project whose users have not asked for this. The signal is about demand rather than health, and it is the reason to expect the gap to persist: nothing in Authlib’s constituency is pushing for the MCP-specific RFCs, because most of its constituency is not building MCP servers.

The two projects written for this problem — workers-oauth-provider and the MCP SDKs — are the ones tracking the specification, and workers-oauth-provider is the smallest and youngest thing in the table. Its durability rests on Cloudflare needing it for its own platform, which is a real dependency and a stable one for as long as that need holds.

The managed vendors#

Five companies, and the summary of their durability is that it matters less here than it would anywhere else in identity, for a reason specific to this category.

Ordinary identity migrations are expensive because the user records, the credentials, the sessions and the integrations all move together. That is what 2.060 measured for OAuth provider portability generally, and the numbers were large.

An MCP authorization server is a much smaller commitment:

  • Tokens are short-lived, so nothing long-lived is stranded.
  • Clients re-run discovery on every connection, so pointing at a new issuer is editing one field in one JSON document.
  • The MCP server’s own half — protected resource metadata, the 401, the audience check — does not change at all.

What does not survive a move is client identity. A DCR-issued or pre-registered client ID is bound to the issuer that minted it; the specification requires clients to key such credentials by issuer and re-register when the authorization server changes. A CIMD client ID is a self-hosted URL and survives. So the exit cost is a re-consent for every user, plus re-registration for every client that is not on CIMD.

That reframes the vendor decision. Choosing among five vendors who all claim the same conformance is a decision with a low reversal cost, and treating it as a long commitment over-weights it. The commitments worth agonizing over in this category are architectural — whether identity leaves the building, whether the authorization server sits on top of an existing store — not which of five similar products issues the tokens.

The residual durability question worth asking of a vendor is narrow: how fast did they ship the last revision. In a category whose specification changed four times in two years, tracking speed is the property that decays into a problem, and it is observable from outside — their metadata documents say what they support today.

The concentration nobody chose#

One structural risk sits outside every vendor’s control.

The measurements in this survey are shaped by what two client vendors decided. Claude’s documented CIMD rule — requiring none at the token endpoint — is the reason a server advertising CIMD may be unreachable by it. ChatGPT accepting private_key_jwt as well is the reason the two clients disagree. Neither behavior is in the specification, both are defensible, and together they define what an authorization server must advertise to be usable.

The specification says what is legal; two products decide what works. That is the ordinary condition of a young protocol with concentrated clients, it is not a criticism of either vendor, and it is the single fact most likely to invalidate a conformance decision made from the specification alone. A server author who reads only the RFCs will build something legal that does not connect.

The mitigation is unglamorous and effective: advertise the union rather than the minimum. Support DCR and CIMD, list none at the token endpoint, emit iss, and put a scope in the challenge. Every one of those is cheap, and each one removes a dependency on a decision somebody else gets to change.


Choosing#

Verified: 2026-09-01


The decision, in the order it should be made#

Most write-ups in this category open with a vendor comparison. That is the fourth question, and three cheaper ones come first.

1. Does this server need authorization at all?#

The specification says it is optional. One of the twenty-six measured deployments is authless and connects fine. If nothing behind the server is worth protecting, stop here.

2. If it does, is a fixed credential defensible?#

A fixed header credential is a supported path — Claude’s static_headers in beta, ChatGPT’s static credentials — and it takes an afternoon. The test is not the size of the audience but the reach of the secret: defensible when it can only reach what the whole organization already shares, and a single point of total compromise in front of anything per-user or multi-tenant. Never in a URL.

3. Are the clients known?#

If a handful of named clients will ever connect, pre-registration is first in the specification’s own priority order, requires nothing of the authorization server beyond ordinary OAuth, and sidesteps DCR and CIMD entirely. Cursor exposes it as a setting; Claude accepts an optional client ID on a custom connector. This is the most-skipped correct answer in the category, skipped because the discussion usually starts at dynamic registration.

4. Only now: which authorization server?#

And the property to select on is audience binding, verified rather than claimed.

The recommendation, by situation#

Users already live in an identity store you run → an authorization server that sits on top of it rather than replacing it. Commercially, Stytch’s Connected Apps or Scalekit — noting that Scalekit’s version of this is its BYOA tier at $99/month and not on the free plan (F008, 2026-09-02); in open source, Ory Hydra, which does OAuth and declines to store users. Three products converged on this architecture independently, which says the shape is common rather than special. If self-hosting, treat Hydra’s non-standard audience parameter as a known defect to work around, not a surprise to discover.

Publishing to a client directory → a managed vendor, selected on how fast they shipped the last revision rather than on feature parity, since all five claim the same RFCs. Support CIMD and DCR together, and advertise none at the token endpoint so the CIMD is reachable.

Small, private, few users → a fixed header credential or pre-registration. Do not adopt an authorization server for five people; the operational cost is permanent and the conformance story is worse than a managed free tier.

Regulatory or residency constraints forbid a vendor → Keycloak or ZITADEL, with the audience check written first and tested with a knowingly wrong token, because the measured finding is that Keycloak issues a 200 and a default audience when a conformant client sends resource. Check ZITADEL’s AGPL license against your deployment before anything else.

Building the authorization server in your own process → Node or Workers, or reconsider. node-oidc-provider ships resource indicators; workers-oauth-provider ships CIMD with a conformance suite. In Python, Authlib ships neither MCP-specific RFC, so this path means writing discovery and audience binding personally — one of which fails silently when wrong.

The four things to do regardless#

Cheap, measurable from outside, and each one removes a dependency on somebody else’s decision.

  1. Return 401 with a resource_metadata pointer. Not 200, not a JSON-RPC error. The pointer beats the well-known fallback and works on platforms that cannot serve /.well-known/*.
  2. Make resource match the endpoint URL exactly, trailing slash included, and pin it once so nothing reconstructs it. Six of 23 deployments get this wrong.
  3. Validate the audience, and write the test that presents a wrong token. A happy-path test passes identically whether the binding happened or not.
  4. Advertise the union, not the minimum: DCR and CIMD, none at the token endpoint, iss in authorization responses, a scope in the challenge. One server in twenty-six does the last of these.

What would change this advice#

CIMD adoption crossing half the ecosystem. Then DCR becomes the compatibility path rather than the default one, and the accumulation problem it was deprecated for stops being theoretical for high-traffic servers.

RFC 9207 becoming a MUST, which the specification says to expect. At 3 of 23 advertised today, that transition is disruptive unless the number moves first.

Keycloak implementing RFC 8707. It is the most-deployed self-hosted authorization server and the gap is its most consequential property for this use. Its own documentation states the gap, which is the best available evidence that the project knows.

A server exposing WWW-Authenticate through Access-Control-Expose-Headers. One header, entirely within each server’s gift, and it would make browser-based MCP clients ordinary rather than impossible. Nothing asks for it, so nobody does it, and it is the cheapest ecosystem-level improvement this survey found.


Is This Stable Enough to Build On?#

Verified: 2026-09-01


The revision record#

MCP’s authorization chapter has been rewritten in substance at every revision it has existed for.

RevisionWhat happened to authorization
2024-11-05none — the specification had no authorization
2025-03-26OAuth 2.1 introduced; the MCP server was also the authorization server
2025-06-18roles separated — MCP server becomes a resource server; RFC 8707 added
2025-11-25resource-server model tightened
2026-07-28DCR deprecated for Client ID Metadata Documents; discovery requirements hardened

Five revisions, four changes, in under two years. The most recent one deprecated the registration mechanism that 22 of 23 measured deployments run today.

Reading that as instability would be the wrong conclusion, and the direction is what makes the difference. Every change has moved toward delegating more and specifying less. The 2025-06-18 separation removed the requirement that an MCP server be an authorization server — which was the single largest reduction in what an implementer must build. The 2026-07-28 deprecation replaced a mechanism the server must run with one the client hosts. The trajectory is consistently toward the MCP server doing less.

An adopter is therefore exposed to churn in a shrinking surface. That is a materially better position than churn in a growing one, and it is the strongest argument in this survey for building now rather than waiting.

The dependency that is not settled#

The specification’s normative reference for Client ID Metadata Documents is draft-ietf-oauth-client-id-metadata-document-00.

The IETF OAuth working group has adopted that draft and is at revision 02, dated 6 July 2026, expiring 7 January 2027. There is no document shepherd, no last call, no IESG telechat, and the intended RFC status is unassigned.

Two things follow, and neither is alarming on its own.

MCP pins a two-revision-old draft. A specification that normatively references -00 while the working group publishes -02 has either made a deliberate pin or has not tracked it. Either way, an implementation reading the MCP specification and an implementation reading the current IETF draft may not agree, and that disagreement lives in the mechanism MCP just made its preferred one.

A pre-last-call draft can still change. CIMD is the direction of travel for MCP client registration and it is not yet a standard. The measured 5-of-23 adoption is consistent with an ecosystem that has noticed.

The practical reading: implement CIMD, keep DCR, and do not build anything that assumes the CIMD document format is frozen. The mechanism is durable; the wire details are not guaranteed.

What is settled#

Three things have stopped moving, and separating them from the rest is most of what planning here amounts to.

PKCE. S256 is advertised by 23 of 23 measured deployments and appears in no documented failure in this survey. It is not a decision.

The resource-server role. Established in 2025-06-18 and unchanged through two revisions. The MCP server validates tokens and does not issue them. Every subsequent change has reinforced it.

RFC 9728 as the discovery mechanism. A 401, a pointer, a metadata document. Stable since 2025-06-18 and hardened rather than replaced in 2026-07-28.

An adopter can treat these three as fixed and confine planning to registration mechanisms and the details of audience binding.

What is predicted, with dates that can be checked#

Recording these because a survey with a fast decay class should say what it expects, so a re-run can score it rather than re-narrate.

RFC 9207 issuer identification will become a MUST. The specification says a future revision is expected to upgrade it and asks implementers to emit iss now. Measured today: 3 of 23. If that number has not moved substantially before the requirement hardens, the transition breaks connections.

The CIMD ratio will invert, and slowly. 5 of 23 advertise it, 4 usably. DCR has to keep working for years regardless, because deprecation is not removal and 22 of 23 deployments run it.

Machine-to-machine will stay unsupported on the major clients. Every connection requiring user consent is a product position rather than a technical limitation, and nothing in the specification’s direction argues against it. A design that needs service tokens should not plan around this changing.

The string-handling defects will not go away by themselves. Six of 23 deployments advertise a resource that does not match their endpoint; the reference server ships doubled slashes; a competent implementation carries an open bug on URI equivalence. The specification is specific about canonical form and silent about what a server should do with a near-miss, and until that silence is filled the defects are structural rather than incidental.

The verdict on the standard#

Safe to build on, with the work concentrated where the specification stops.

The parts MCP specifies are stable and shrinking. The parts it delegates — what the authorization server does with resource, whether a near-miss identifier is normalized, how revocation propagates — are where the measured failures are, and delegation is a deliberate design choice rather than an omission the next revision will fix.

The adopter inherits one non-expiring obligation: the resource server’s audience check is load-bearing and nobody else will do it. That was true in 2025-06-18, it is true now, and no plausible revision makes it untrue, because the authorization server is out of scope by construction.

Published: 2026-09-01 Updated: 2026-09-01