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. No developer portal step, 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
The two rungs below assumed the agent already knew where to authenticate and had an identifier there, which came from a world where a developer decided on an integration months in advance. It's the assumption agents break hardest, and the one place OAuth needed a new part.
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 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. An API can be protected by a server it doesn't operate, tokens are audience-bound to the resource, and an agent can verify the token it's about to send was issued for the thing it's calling.
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 is what the first wave of MCP servers used. The costs show up at scale: an unauthenticated write endpoint anyone can spray, a row per agent instance per server, and a newly minted, locally meaningless identifier. The consent screen says whatever the agent claims, because nothing authenticates the request.
Client ID Metadata Documents
The client's ID is an HTTPS URL serving the client's own metadata, and the authorization server fetches it. That's the whole mechanism, and it inverts the problem: 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, with no bilateral setup.
- It carries provenance a human can read. The consent screen can show the hostname, which is the one security signal users are 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, and the spec forbidsclient_secretand symmetric methods. Nothing in it is 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: a stable identifier that works everywhere, plus 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). That's 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 servers, and a malicious one tricks it into sending the wrong token. Resource indicators audience-bind each token at issuance, so the wrong one doesn't work: a class of agent bug becomes 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 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, resource indicators and issuer validation, and makes client ID metadata documents the preferred way for clients to identify themselves.
Worth noticing as a data point: the largest new agent protocol looked at this problem, and its answer was existing OAuth building blocks plus one new draft.