Scenario 02 · adds runtime discovery
The agent meets a service nobody registered with
Mid-conversation, someone gives the agent a URL: an MCP server run by a company that has never heard of it. There is no developer portal step available and no human free to go do one. Two strangers have to bootstrap a trust relationship from a hostname, in under a second, and then get a user's consent.
This is the actual new problem
Everything on the two rungs below assumed the agent already knew where to authenticate and already had an identifier at that server. That assumption came from a world where a developer decided on an integration months in advance. It's the assumption agents break hardest, and it's the one place where OAuth genuinely needed a new part rather than a new configuration.
Two things have to happen at runtime:
- Find the authorization server. The agent has a resource URL and nothing else. Which authorization server protects it?
- Get an identity there. The authorization server has never seen this client. What identifier does it use, and how does it know anything about it?
The first was solved in 2025. The second is what Client ID Metadata Documents are for.
Part one
Discovery, from a bare URL
The agent calls the resource with no credentials. The resource replies 401 and, crucially, tells
it where to look — a WWW-Authenticate header pointing at its protected resource metadata
(RFC 9728).
POST /mcp HTTP/1.1
Host: mcp.acme-crm.com
→ 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata=
"https://mcp.acme-crm.com/.well-known/oauth-protected-resource"
GET /.well-known/oauth-protected-resource HTTP/1.1
Host: mcp.acme-crm.com
→ 200 OK
{
"resource": "https://mcp.acme-crm.com/mcp",
"authorization_servers": ["https://login.acme-crm.com"],
"scopes_supported": ["crm.contacts.read", "crm.deals.write"],
"bearer_methods_supported": ["header"],
"dpop_bound_access_tokens_required": true
}
From there, RFC 8414 gets the agent the authorization server's endpoints and capabilities. Two well-known documents and the agent knows how to authenticate to a service it learned about ten milliseconds ago.
It matters that the resource declares which authorization server protects it. It means an API can be protected by an authorization server it doesn't operate, tokens are audience-bound to the resource rather than to the server, and an agent can verify that the token it's about to send was issued for the thing it's about to call.
Part two
Getting an identity without registering
There are three ways for a never-before-seen client to have a client ID. They are not equally good.
Dynamic Client Registration
RFC 7591 has been around for a while, and it is what the first wave of MCP servers used. The costs show up at scale: an unauthenticated write endpoint that anyone can spray, a database row per agent instance per server, and — the part that actually hurts — an identifier that is newly minted and locally meaningless. A user's consent screen says whatever the agent claims to be, because there is no way to authenticate the request.
Client ID Metadata Documents
The client's ID is an HTTPS URL that serves the client's own metadata. The authorization server fetches it. That's the whole mechanism, and it inverts the problem: instead of the client asking the server for an identity, the client has an identity and the server looks it up.
// client_id: https://agent.example.com/clients/research-assistant
GET /clients/research-assistant HTTP/1.1
Host: agent.example.com
→ 200 OK
Content-Type: application/json
Cache-Control: max-age=3600
{
"client_id": "https://agent.example.com/clients/research-assistant",
"client_name": "Research Assistant",
"client_uri": "https://agent.example.com/",
"logo_uri": "https://agent.example.com/logo.png",
"redirect_uris": ["https://agent.example.com/callback"],
"jwks_uri": "https://agent.example.com/jwks.json",
"token_endpoint_auth_method": "private_key_jwt",
"grant_types": ["authorization_code", "refresh_token"]
}
The properties that make this the right primitive for agents:
- The identifier means the same thing everywhere. The same client ID at every authorization server on earth, resolvable by any of them, with no bilateral setup.
- It carries provenance a human can read. The consent screen can show the hostname. Users have been evaluating domains for thirty years; it's the one security signal they're actually trained on.
- There is no write endpoint. Nothing to spam, nothing to store per client, nothing to garbage-collect.
- It's revocable and reputable. Serve a 404 and the client is gone. Block a hostname and you've blocked a publisher, not a UUID.
- No shared secret exists. The document publishes a
jwks_uri; the spec forbidsclient_secretand symmetric methods. There is nothing in it worth stealing.
You are now making outbound HTTP requests to URLs supplied by strangers. Block special-use and internal
addresses, cap the response size (the draft suggests 5 KB), require a 200 and reject
every other status, honour cache headers but don't cache failures, and re-check when a document's keys or
redirect URIs change. This is the SSRF surface the mechanism buys you, and it is a known, bounded one.
Pre-registration
Still correct where it's possible. An enterprise can pre-register a client ID URL to grant it elevated trust, which gives you the best of both: a stable identifier that works everywhere, and an explicit local decision about which publishers get more than the baseline.
Putting it together
Zero to authorized
-
Agent→MCP Server
Calls it. Gets
401with aresource_metadatapointer. -
Agent→MCP Server
Fetches protected resource metadata. Learns the authorization server, the available scopes, and that DPoP is required.
-
Agent→Authorization Server
Fetches
/.well-known/oauth-authorization-server. Confirms the server supports client ID metadata documents and DPoP. -
Agent→Authorization Server
Starts an authorization request using its own URL as
client_id, with PKCE, andresourcenaming the MCP server. No registration call happens at any point. -
Authorization Server→itself
Fetches the client ID URL, validates the document, and renders a consent screen showing the client name, logo, and the hostname it came from.
-
Authorization Server→Agent
User approves. Authorization code comes back with an
issparameter the agent verifies (RFC 9207) — the defense against mix-up attacks when an agent is talking to several servers at once. -
Agent→MCP Server
Exchanges the code for a DPoP-bound token audience-restricted to this MCP server, and makes the call it was trying to make seven steps ago.
POST /par HTTP/1.1
Host: login.acme-crm.com
Content-Type: application/x-www-form-urlencoded
response_type=code
&client_id=https%3A%2F%2Fagent.example.com%2Fclients%2Fresearch-assistant
&redirect_uri=https%3A%2F%2Fagent.example.com%2Fcallback
&resource=https%3A%2F%2Fmcp.acme-crm.com%2Fmcp
&scope=crm.contacts.read
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Two attacks this rung has to answer
The confused deputy
An agent holds tokens for six different servers. A malicious server tricks it into sending the wrong one. Resource indicators mean each token is audience-bound at issuance, so the wrong token simply doesn't work — and a resource that validates its own audience turns a class of agent bug into a rejected request.
The mix-up
An agent starts a flow with one authorization server and gets a response that appears to come from another. Issuer identification puts the issuer in the authorization response, and the client checks it against the one it started with. Small parameter, whole attack class.
The 2026-07-28 revision of the Model Context Protocol authorization specification makes MCP servers OAuth 2.1 resource servers, requires protected resource metadata, requires resource indicators, requires issuer validation, and makes client ID metadata documents the preferred way for clients to identify themselves — with dynamic client registration retained only for servers that don't support CIMD yet.
That is worth noticing as a data point about this whole argument: the largest new agent protocol looked at the problem, and its answer was existing OAuth building blocks plus one new draft.