MCP started as a trusted local subprocess with no auth story at all. Its authorization spec bolts on OAuth 2.1, PKCE, and resource indicators — here is what each piece actually prevents, including the token-passthrough bug that keeps showing up in early implementations.
Anyone who has wired an MCP server to a remote deployment has hit the same wall: the local, single-user story — spawn a subprocess over stdio, trust whoever's running the process — has no answer for "this server is reachable over HTTPS by many different agents, on behalf of many different users, and needs to know who's asking." Bolting on a static API key gets you something that works in a demo and fails an audit. MCP's authorization spec exists because that gap is real, and it's worth understanding the mechanics rather than treating "add OAuth" as a checkbox.
This post walks through what the spec actually requires, why it borrows so heavily from existing OAuth 2.1 machinery instead of inventing something bespoke, and where the sharpest edge case — token passthrough — actually bites.
The original MCP transport is a local subprocess talking JSON-RPC over stdin/stdout. There's no network hop, so there's no meaningful authentication question: whoever can launch the process already has whatever access the process has. Utilix's own MCP server, which exposes tools like Base64 encoding or JSON validation to an agent over stdio, works exactly this way — trust is inherited from the OS process boundary, and that's sufficient for a local dev tool.
Remote MCP servers over Streamable HTTP break that assumption completely. The server is now a shared endpoint. It has no OS-level signal about who's connecting — just an HTTP request. Without an authorization layer, "remote MCP server" is functionally an unauthenticated RPC endpoint that happens to speak a nice protocol, which is a bad place to expose anything that touches real data or paid APIs.
The spec's answer isn't a new auth scheme. It's a specific, opinionated profile of OAuth 2.1 — one that pins down the parts OAuth traditionally leaves as implementation choices, because an autonomous agent can't be expected to make the same judgment calls a human clicking through a login screen would.
Standard OAuth tutorials talk about a client and a server. MCP's profile is explicit about three roles, because collapsing them is where implementations go wrong:
Separating resource server from authorization server is what lets an MCP server operator plug into whatever auth infrastructure a company already has, instead of forcing every MCP server to become its own identity provider.
An agent that wants to talk to an MCP server it's never seen before has no prior knowledge of which authorization server to use, what scopes are valid, or what the token endpoint is. The spec solves this with two metadata documents, both served over well-known URIs:
GET /.well-known/oauth-protected-resource
→ { "resource": "https://mcp.example.com",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:tools:read", "mcp:tools:call"] }
GET https://auth.example.com/.well-known/oauth-authorization-server
→ { "authorization_endpoint": "...", "token_endpoint": "...",
"registration_endpoint": "...", "code_challenge_methods_supported": ["S256"] }
The first (RFC 9728, Protected Resource Metadata) tells the client which authorization server is authoritative for this MCP server. The second (RFC 8414) is the standard OAuth authorization server metadata document, telling the client where to send the actual auth requests. When an MCP client hits a protected endpoint without a token, the 401 response's WWW-Authenticate header points it at the first document — that's the entire bootstrap sequence, no out-of-band configuration required.
In classic OAuth 2.0, PKCE (Proof Key for Code Exchange) was originally a mobile-app mitigation for authorization code interception. OAuth 2.1 folded it in as mandatory for all clients, and MCP inherits that without exception — every authorization code flow between an agent and an MCP authorization server must use PKCE, full stop. Given that MCP clients are frequently CLI tools, desktop agents, or other contexts that can't reliably keep a client secret confidential, treating every client as effectively "public" and requiring PKCE is the only sane default.
A web app registers as an OAuth client once, by hand, in a developer console, and hardcodes the resulting client_id. That model doesn't scale to MCP, where an agent might discover and connect to an MCP server it has never talked to before, run by an operator the agent's developer has never contacted.
RFC 7591 Dynamic Client Registration solves this: the MCP client POSTs to the authorization server's registration endpoint, describing itself (redirect URIs, client name, grant types), and gets back a client_id on the spot — no human in the loop.
POST /register HTTP/1.1
Content-Type: application/json
{ "client_name": "My Agent",
"redirect_uris": ["http://localhost:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none" }
201 Created
{ "client_id": "8f3a...", "client_id_issued_at": 1735689600 }
DCR is listed as should support, not strictly mandatory, because an authorization server operator might reasonably want to keep registration behind manual approval for security reasons. But for the general "any agent should be able to connect to any compliant MCP server" story to work at all, DCR is effectively load-bearing. Without it, every MCP integration reverts to manual credential provisioning, which is exactly the friction OAuth was supposed to remove.
Here's the part of the spec that actually matters for security, not just plumbing: resource indicators.
Imagine an MCP server that, to do its job, calls out to a downstream API — a calendar API, a ticketing system, whatever. The naive implementation takes the bearer token the agent sent, and forwards that same token to the downstream API. This is the token passthrough anti-pattern, and it's a textbook confused-deputy vulnerability: the downstream API now has to trust a token it never issued, minted by an authorization server it may not even recognize, and any scope or audience restriction the original authorization server intended is gone. Worse, if that same access token is valid at multiple resource servers, a malicious or compromised MCP server can replay a user's token somewhere the user never authorized.
RFC 8707 Resource Indicators closes this by giving the client a way to tell the authorization server, at request time, exactly which resource server a token is for:
GET /authorize?...&resource=https://mcp.example.com
POST /token body includes: resource=https://mcp.example.com
The authorization server mints a token whose audience is bound to that specific MCP server. A resource server that receives a bearer token is expected to validate that the audience claim actually names it — not silently accept any validly-signed token regardless of who it was minted for. And critically, an MCP server that needs to call a downstream API on the user's behalf should not just relay the inbound token; it should either hold its own separately-scoped credential for that downstream call, or go through its own token exchange, so the two hops never share a bearer token. The diagram below traces the full flow, with the point where audience binding and non-forwarding matter called out.
If you're standing up a remote MCP server, the spec gives you a short, concrete checklist rather than a philosophy:
None of this is unique to MCP — it's standard OAuth 2.1 resource-server hygiene. What MCP's spec did was make the ambiguous parts of that hygiene non-optional for a protocol where the "user" clicking through consent screens is increasingly an agent acting semi-autonomously, and where the cost of a confused-deputy bug is an agent leaking a credential to a server it was never meant to talk to.
Takeaway: if your MCP server's auth story is "check for an API key in a header," it'll work until someone points a second MCP server at it, or until a downstream API call needs its own credential — at which point the token-passthrough trap is exactly the bug the resource-indicators requirement exists to prevent. Build the audience check in from the start; retrofitting it after a server is already load-bearing for real integrations is the harder path.