Engineering Guide

MCP Authentication: What the Spec Actually Says (And What Most Guides Get Wrong)

Search for MCP authentication and you get the same two artifacts every time: the sentence MCP uses OAuth 2.1 with PKCE, and a sequence diagram of the authorization code flow. Both are lifted from the spec, which ships its own diagram, so neither is differentiation — and neither tells you the four things that will actually bite you. Authorization is optional, not mandatory. A separate authorization server is permitted, not required. Dynamic Client Registration — the thing every guide presents as the MCP registration story — is now a backwards-compatibility footnote. And the scope rule everyone cites gets quoted at half length, which is how it ends up meaning the opposite of what it says. This is the primary-source version, version-stamped, as of July 2026.

Sequence diagram of MCP authorization from the 401 challenge through discovery to a bearer token bound to a single server

Two housekeeping notes before anything normative, because both change what the rest of this article is allowed to claim.

Version. The current stable specification is 2025-11-25. A release candidate targeting 2026-07-28 is in flight; the MCP blog describes the coming release as the largest revision of the protocol since launch, with the RC locked on May 21, 2026 and a ten-week validation window before the final publishes. Every normative sentence below is attributed to a version on purpose. If you are reading this after July 28, re-check anything version-stamped against the new spec.

Governance. Do not write Anthropic's MCP. MCP was donated on December 9, 2025 to the Linux Foundation's newly formed Agentic AI Foundation, and spec pages now carry Model Context Protocol a Series of LF Projects, LLC. The nuance cuts both ways: AAIF is a directed fund co-founded by Anthropic, Block and OpenAI, MCP's governance model and SEP process continue unchanged, and the project retains technical autonomy. The Linux Foundation provided a neutral home, not a new roadmap.

Correction 1: authorization is optional

Here is the sentence that most MCP auth content skips, verbatim from the 2025-11-25 authorization spec: Authorization is OPTIONAL for MCP implementations.

The requirement levels then split by transport, and the split is sharper than the usual local versus remote hand-wave:

Note what is not there. There is no MUST anywhere in that list forcing a remote MCP server to speak OAuth. And the second overstatement is just as common: remote MCP is not OAuth-only. The architecture documentation says Streamable HTTP supports standard HTTP authentication methods including bearer tokens, API keys, and custom headers, and that MCP recommends using OAuth to obtain authentication tokens. Recommendation, not monopoly. Claude Code's static --header flag is the everyday proof — a bearer token in a config file, no OAuth anywhere in the picture. If you are standing one up, the practical trade-offs are covered in building a remote MCP server.

One more piece of hygiene while we are here: OAuth 2.1 is not a ratified standard. The spec references it as an IETF draft, and version discipline matters even here. 2025-11-25 pins draft-ietf-oauth-v2-1-13 consistently — all eighteen citations on the page agree, and that page has no refresh-token section at all. The draft feeding the RC is where it frays: its new Refresh Tokens section cites draft-ietf-oauth-v2-1-14 while the rest of that page still says -13. Say the OAuth 2.1 draft and move on; do not cite a specific draft revision as though it were stable.

The honest summary: authorization is opt-in, but it is not opt-in-halfway. Once you implement the authorization spec, large chunks of it flip to MUST — Protected Resource Metadata, PKCE, resource indicators, audience validation. The optionality is at the door, not inside the room.

Correction 2: separate roles, not separate servers

The brief version you will read elsewhere — MCP requires a separate authorization server — overstates the spec. What 2025-11-25 actually assigns is three roles:

Separation of roles is required. Separation of deployments is not. A single process can be both the MCP server and its own authorization server and be perfectly compliant. What you cannot do is blur the token audiences, which is what the rest of this article is really about. That distinction — roles versus deployments — is the same conceptual split that makes MCP different from a plain API integration: the protocol constrains the contract, not your topology.

The normative stack under 2025-11-25 is: the OAuth 2.1 draft, RFC 8414 (Authorization Server Metadata), RFC 7591 (Dynamic Client Registration), RFC 9728 (Protected Resource Metadata), and the OAuth Client ID Metadata Documents draft. The RC's list is longer, adding RFC 6750 (Bearer Token Usage), RFC 8707 (Resource Indicators), RFC 9207 (Authorization Server Issuer Identification), and the OpenID Connect Discovery and Dynamic Client Registration specs.

Auth by transport: the row nobody includes

There are exactly two standard transport mechanisms in 2025-11-25: stdio and Streamable HTTP. Clients SHOULD support stdio whenever possible. Streamable HTTP replaces the HTTP+SSE transport from protocol version 2024-11-05. Custom transports are permitted (MAY).

Most transport comparisons stop at env vars here, OAuth there. That is true and shallow. The axis that actually explains the two designs is where trust gets established — and the row nobody prints is what the client must validate in each case.

DimensionstdioStreamable HTTP
Clients per serverOne — local servers typically serve a single MCP clientMany — remote servers typically serve many MCP clients
When trust is establishedOnce, at process spawnPer request — authorization MUST be included in every HTTP request, even within one logical session
Is the authorization spec in scope?SHOULD NOT follow itSHOULD conform to it
Where credentials come fromThe environment (spec's own words)OAuth 2.1 draft (recommended), or bearer tokens / API keys / custom headers (permitted)
Primary threatArbitrary code execution from a malicious startup command — not token theftAudience confusion, SSRF via discovery, session hijacking
What the client must validateThe exact spawn command, shown to the user untruncated, before executingDiscovery URLs, PKCE support, redirect URI scheme, and (in the RC) the iss parameter
What the server must doPrefer stdio to limit access to just the MCP client; if HTTP, require a token or use unix sockets / IPC with restricted accessMUST validate the Origin header (403 if invalid); SHOULD bind to 127.0.0.1 when local; MUST verify all inbound requests
SessionsN/AMUST NOT be used for authentication; session IDs MUST be secure and non-deterministic

Read the primary threat row twice. Framing stdio as the transport where you use env vars instead of OAuth is a category error. The spec's named local-server scenario is an attacker includes a malicious startup command in a client configuration. Your API key hygiene is irrelevant to that attack. The control is the consent dialog: clients supporting one-click local server configuration MUST implement proper consent mechanisms prior to executing commands, and MUST show the exact command that will be executed, without truncation. stdio's boundary is process trust, not token trust. That is why picking servers carefully (see our MCP server roundup) does more for a local setup than any credential store, and why the broader threat model is worth reading in MCP security.

The 401-to-bearer flow, with the MUSTs marked

Everyone draws this flow. Almost nobody annotates the four places where the client can silently violate a MUST. Here it is with those marked.

MCP authorization: where the client owes a MUST MCP Client MCP Server resource server Authorization Server same host or separate — spec allows both 1  Request, no token 2  401 + WWW-Authenticate: resource_metadata=... MUST #1 — validate that URL before fetching it. Block 169.254.0.0/16, 127.0.0.0/8, 10.0.0.0/8, 172.16/12, 192.168/16, fc00::/7. 3  GET /.well-known/oauth-protected-resource 4  PRM → authorization_servers[ ] (RFC 9728) 5  Fetch AS metadata (RFC 8414 or OpenID Connect Discovery 1.0) MUST #2 — verify PKCE support before proceeding. No code_challenge_methods_supported in the metadata? Refuse to proceed. 6  /authorize + PKCE S256 + resource=https://mcp.example.com/mcp 7  authorization code (+ iss — RFC 9207, added in the RC) 8  /token + code_verifier + resource (again) MUST #3 — send resource in BOTH requests (RFC 8707). Send it regardless of whether the authorization server supports it. 9  access token — audience = this MCP server 10  Authorization: Bearer <token> MUST #4 — on every request, in the header, never in the query string. Even when requests are part of the same logical session. Server side: MUST only accept tokens valid for its own resources. Validate the audience per RFC 8707. MUST NOT accept or transit any other tokens.
The 2025-11-25 authorization flow, annotated with the four client-side MUSTs that are easiest to skip. Steps are illustrative of the normative text, not a wire trace. Chart: DataLLM Lab

A few details from that diagram that deserve prose, because they are where implementations quietly diverge from the spec:

Discovery is mandatory and dual-path. MCP servers MUST implement RFC 9728 Protected Resource Metadata, and clients MUST use it to discover the authorization server. The PRM document MUST include an authorization_servers field with at least one entry. A server can advertise its PRM location two ways — a WWW-Authenticate header with resource_metadata on a 401, or a well-known URI at /.well-known/oauth-protected-resource (optionally path-suffixed). Clients MUST support both and MUST prefer the WWW-Authenticate value when present, falling back to well-known probing otherwise. On the AS side, metadata comes from RFC 8414 or OpenID Connect Discovery 1.0, and clients MUST support both.

The canonical URI is fussy on purpose. Valid: https://mcp.example.com/mcp, https://mcp.example.com, https://mcp.example.com:8443. Invalid: mcp.example.com (missing scheme) and https://mcp.example.com#fragment (contains a fragment). Prefer no trailing slash. Get this wrong and your audience binding is wrong, which means every downstream MUST in the next section is built on sand.

Send resource even when it looks pointless. Clients MUST implement RFC 8707 Resource Indicators, and the parameter MUST appear in both the authorization request and the token request. The clause people miss: clients MUST send this parameter regardless of whether authorization servers support it. You do not get to skip it because your AS ignores it today.

Transport security is not optional inside the auth spec. All authorization server endpoints MUST be served over HTTPS. All redirect URIs MUST be either localhost or HTTPS. Redirect URIs must be pre-registered, and the AS must validate them with exact string matching — no wildcards.

Error codes. 401 means authorization is required or the token is invalid. 403 means invalid scopes or insufficient permissions. 400 means the authorization request was malformed. Insufficient scope at runtime returns 403 with WWW-Authenticate: Bearer error="insufficient_scope", plus scope and resource_metadata.

Correction 3: the scope rule everyone half-quotes

This is the section worth the click, because nearly every MCP auth post quotes half of one sentence and lands somewhere the spec never went.

The received wisdom is request minimal scopes, practice least privilege. The spec agrees. It says so first, and it says so about clients specifically. Here is the opening line of the 2025-11-25 Scope Selection Strategy — the very section everyone cites:

When implementing authorization flows, MCP clients SHOULD follow the principle of least privilege by requesting only the scopes necessary for their intended operations.

Then it gives a two-step priority order:

  1. Use the scope value from the 401 WWW-Authenticate challenge if it is present.
  2. If scope is not available, use all scopes defined in scopes_supported — omitting the scope parameter entirely if scopes_supported is undefined.

Step 2 is the sentence that circulates on its own, usually as proof that MCP threw out least privilege. It did not. The stated rationale is that general-purpose MCP clients typically lack domain-specific knowledge to make informed decisions about individual scope selection — which is obviously true once you say it out loud. Your MCP client is a code editor or a chat app. It has no idea whether files:write is scary in your deployment. So the spec moves the burden onto the field the server controls: scopes_supported is intended to represent the minimal set of scopes necessary for basic functionality. Request all of a minimal catalog and you are still practising least privilege. Request all of a bloated one and you are not. The fallback is only safe because the server is obliged to keep it small.

The load-bearing claim, then, is not clients should ask for everything. It is if you publish it, expect it to be requested.

The tension the spec has not resolved. Put the Security Best Practices document next to the authorization spec and they do not quite agree. Its Scope Minimization section addresses clients directly: begin with only baseline scopes (or those specified by initial WWW-Authenticate) — that is step 1 without step 2's fallback. And its attack description assigns blame to both parties: broad scopes get granted up front because the MCP server exposed every scope in scopes_supported and the client requested them all. One document's compliant fallback is the other document's named attack precondition. Anyone telling you the spec has a single clean answer here has read one page of it.

Practice has already picked a side. Claude Code shipped the fallback, then removed it. As of v2.1.196, when oauth.scopes is unset it requests only the scope supplied by the server's WWW-Authenticate header or its protected resource metadata, and sends no scope parameter when neither provides one. It no longer requests the full scopes_supported catalog from automatically discovered authorization server metadata. The reason is the most practical argument in this article: identity providers that advertise admin-only or template scopes rejected the authorization request outright with invalid_scope. The spec's fallback is not merely contested in theory. It broke against real identity providers, and the client with the largest MCP install base dropped it.

Where the spec is unambiguous is the server side. The Security Best Practices Common Mistakes list is server-side, every entry:

The recommendation is a minimal initial set (the spec's illustrative example is mcp:tools-basic) with step-up elevation on insufficient_scope. Mind the version here, because the two specs split the responsibility differently. Under 2025-11-25, the server carries it: the recommended approach is to include both existing relevant scopes and newly required scopes to prevent clients from losing previously granted permissions. The draft flips that to the client — scope accumulation across operations is a client-side responsibility, with clients computing the union of previously requested and newly challenged scopes — and tells servers to emit every scope needed for the current operation in a single challenge, because challenging incrementally forces multiple authorization round-trips for a single operation and degrades user experience.

One caveat so nobody over-reads this. files:read, files:write and mcp:tools-basic are illustrative examples in the spec. MCP defines no standard scope vocabulary. There is no registry of blessed scope names to copy.

The decision rule, compressed: if you are writing a client, treat the challenge as authoritative and the all-scopes fallback as a last resort that real identity providers may reject outright. If you are writing a server, send a scope in your 401 so no client ever reaches the fallback — and size scopes_supported so that a client which ignores you and asks for the lot is still a boring event.

Correction 4: DCR is on the way out

Dynamic Client Registration is presented in most MCP auth articles as the registration story. That was true in earlier spec versions. It is no longer where the protocol is heading, and the language differs by version — which is exactly why so much content is wrong here.

The 2025-11-25 spec gives clients a priority order: use pre-registered credentials if available; otherwise use Client ID Metadata Documents if the AS advertises client_id_metadata_document_supported; otherwise fall back to Dynamic Client Registration if the AS advertises a registration_endpoint; otherwise prompt the user.

Registration mechanism2025-11-25 (stable)Draft / 2026-07-28 RCWhat to build
Pre-registered credentialsFirst priority when availableUnchangedBest option if you control both ends
Client ID Metadata DocumentsSHOULD — preferred when the AS advertises supportSHOULD — the recommended pathBuild for this. It is where the spec is going.
Dynamic Client Registration (RFC 7591)MAY — included for backwards compatibility with earlier versions of the MCP authorization specDeprecated — retained only for authorization servers that do not support CIMDKeep as a fallback. Do not architect around it.
Prompt the userLast resortUnchangedFine for developer tooling

The version discipline matters: the word deprecated appears in the draft, not in 2025-11-25, which says MAY and calls it backwards compatibility. Writing DCR is deprecated without naming a version is wrong for the current stable spec. Writing DCR is the MCP registration story is wrong for both.

What replaces it: with Client ID Metadata Documents, the client_id is itself an HTTPS URL pointing to a JSON metadata document. The URL MUST use the https scheme and contain a path component. The document must include at least client_id, client_name, and redirect_uris, and the client_id inside the document MUST match the URL exactly. Authorization servers SHOULD fetch and cache the document and MUST validate redirect_uris against it. The appeal is obvious: no registration call, no per-AS client record, no stale credential to rotate — the client publishes its identity at a URL and the AS reads it.

Temper the enthusiasm slightly. CIMD rests on a -00 IETF draft — the earliest possible revision. The spec's recommended registration path is built on the newest ink in the room, and it will churn. Describe it functionally, pin nothing to draft mechanics, and expect the details to move.

Testing an MCP client against a real auth flow

Most of this article is only checkable by running an agent against a server that challenges it — and how a model handles a 403 insufficient_scope mid-task varies a lot by model. That is worth measuring rather than assuming. DataLLM Lab gives you 300+ models behind one OpenAI-compatible key at https://www.datallmlab.com/v1, so you can replay the same step-up harness across frontier models without opening six provider accounts.

The two-token rule

If you take one operational rule from this article, take this one. The spec defines token passthrough as an anti-pattern where an MCP server accepts tokens from an MCP client without validating that the tokens were properly issued to the MCP server, and passes them through to the downstream API.

The core is four lines, and they are worth quoting exactly because paraphrases keep getting them wrong:

Now the part that sloppy write-ups mangle into never forward tokens, which would forbid the correct pattern. Calling an upstream API is fine. The spec: if the MCP server makes requests to upstream APIs, it may act as an OAuth client to them. The access token used at the upstream API is a separate token, issued by the upstream authorization server. The MCP server MUST NOT pass through the token it received from the MCP client.

Two tokens, two audiences, one hop each. The client's token is for your server. Your server's token is for the upstream. They never touch. If you have ever wired provider credentials through an intermediary, this is the same discipline as implementing BYOK — the middle layer holds its own relationship with the upstream rather than laundering someone else's.

The spec names four risk categories for passthrough: Security Control Circumvention, Accountability and Audit Trail Issues, Trust Boundary Issues, and Future Compatibility Risk. The second one is the underrated one — upstream logs will show the client's identity, not your server's, so your incident response starts from a fiction.

Passthrough is also the enabling condition for the confused deputy chain, which the spec is unusually precise about. It requires all four of: an MCP proxy using a static client ID with a third-party AS; the proxy allowing MCP clients to dynamically register; the third-party AS setting a consent cookie after first authorization; and the proxy not implementing per-client consent. The mitigation is a MUST: MCP proxy servers MUST implement per-client consent. Consent cookies MUST use the __Host- prefix with Secure, HttpOnly and SameSite=Lax, and bind to the specific client_id. The state parameter MUST be single-use, short-lived (roughly ten minutes), and set only after consent approval.

And the one-liner that contradicts what a lot of hand-rolled Streamable HTTP servers actually do: MCP servers MUST NOT use sessions for authentication. Servers implementing authorization MUST verify all inbound requests. Session IDs MUST be secure and non-deterministic, and servers SHOULD bind them to user-specific information — the spec suggests a key format like <user_id>:<session_id>. A session ID is a correlation handle. It is not a credential. If your server is checking MCP-Session-Id and skipping token validation, you have built exactly the thing the spec forbids. This is the sort of thing a gateway layer can centralize for you, and it is also the sort of thing a gateway structurally cannot fix if the server itself is wrong.

The client is an attack surface too

Almost every MCP auth article is written as how do I protect my server. The spec's two sharpest attack chains run the other way: a malicious server attacking your client. Essentially nobody covers these.

SSRF via OAuth discovery. Look again at the flow diagram. The client dutifully fetches three URLs the server told it about: resource_metadata from the WWW-Authenticate header, authorization_servers from the PRM document, and token_endpoint from the AS metadata. A malicious server poisons any of them. The named targets are grim and specific: http://169.254.169.254/ (cloud metadata, which is IAM credential exfiltration), http://localhost:6379/ (Redis), private ranges generally, plus DNS rebinding and redirect chains. Clients SHOULD block 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, fc00::/7 and fe80::/10. The spec then adds the advice that saves you from yourself: avoid implementing IP validation manually, and suggests an egress proxy (it names Stripe's Smokescreen).

The javascript: authorization URL. Clients MUST only allow http:// and https:// schemes for authorization URLs (http only for loopback in development), and MUST reject javascript:, data:, file: and vbscript:. Clients also MUST NOT use shell commands (cmd.exe, sh, PowerShell) to open URLs. The documented escalation chain is worth reading slowly — and note the spec scopes it to proxy architectures specifically, not to direct stdio use: a malicious javascript: authorization URL causes XSS in the client, which steals the MCP proxy's auth token, and the proxy spawns arbitrary commands via stdio — remote code execution, reached from a field in an OAuth discovery document.

Token theft, both ends. Authorization servers SHOULD issue short-lived access tokens, and for public clients they MUST rotate refresh tokens. Clients and servers MUST implement secure token storage. One named threat deserves a callout because it is so mundane: tokens cached or logged on the server. Logging the Authorization header is not a hypothetical — it is an explicitly named risk in the spec.

Wiring it up in Claude Code

Enough spec. Here is what the flow looks like from a real client, as of July 2026. Version-gated behavior drifts, so treat the capability as the durable part and the version numbers as decoration.

Remote servers with OAuth. Use /mcp to authenticate, or run claude mcp login <name> from the shell (v2.1.186+); claude mcp logout <name> clears credentials. Claude Code flags a server as needing auth on 401 or 403. On a 401 for an already-authenticated server it refreshes the token, reconnects and retries once. A custom server returning WWW-Authenticate pointing to its authorization server gets the same automatic discovery as any other remote server — which is the payoff for implementing RFC 9728 properly. In non-interactive mode (claude -p) it cannot run the OAuth flow, and instead (from v2.1.196, with tool search enabled — the default) tells the model that the server's tools are unavailable pending authorization.

CIMD is already supported. Claude Code supports servers using a Client ID Metadata Document instead of Dynamic Client Registration, and discovers them automatically. If you go the DCR route and the AS does not support it, the error you will meet is Incompatible auth server: does not support dynamic client registration. For pre-configured OAuth credentials there are --client-id and --client-secret (masked prompt), the MCP_CLIENT_SECRET env var for CI, and --callback-port to match a pre-registered redirect URI of the form http://localhost:PORT/callback. These flags apply only to HTTP and SSE transports and have no effect on stdio servers.

Secrets never touch your config. The client secret is stored in the system keychain on macOS, or a credentials file — not in your config. Since .mcp.json is checked into version control, environment variable expansion keeps secrets out of it: ${VAR} expands, ${VAR:-default} supplies a fallback, and expansion works in url, headers, command, args and env. The documented example is exactly what you would hope: "Authorization": "Bearer ${API_KEY}". Local scope is the recommendation for servers with credentials you do not want in version control.

Static tokens and stdio. The non-OAuth path is one flag: --header "Authorization: Bearer your-token". Worth knowing: if a configured headers.Authorization is rejected, Claude Code reports a connection failure and does not fall back to OAuth. For stdio, credentials go in with claude mcp add --env KEY=value --transport stdio <name> -- <command>, where -- separates Claude's options from the server command. For schemes OAuth does not cover — Kerberos, short-lived tokens, an internal SSO — there is headersHelper, a command run at connection time whose JSON output merges into the connection headers. It runs fresh on every connection with no caching, and re-runs then retries once on a 401 or 403 (v2.1.193+). The docs carry the warning it deserves: headersHelper executes arbitrary shell commands.

Two different SSE deprecations, do not conflate them. At the spec level, the HTTP+SSE transport from protocol version 2024-11-05 was replaced by Streamable HTTP. Separately, at the client level, Claude Code marks its own --transport sse as deprecated while still supporting it. Different statements about different things. In JSON config, type accepts streamable-http as an alias for http, because the MCP specification uses that name for the transport. WebSocket (type: "ws") is header-only auth — no OAuth — and --transport does not accept ws.

One gateway caveat with teeth. Tool search is enabled by default, so MCP tool definitions are deferred rather than loaded into context upfront — the recycled claim that every MCP tool eats your context is false under defaults. But it is disabled when ANTHROPIC_BASE_URL points to a non-first-party host, since most proxies do not forward tool_reference blocks (it is also off by default on Google Cloud's Agent Platform). If you route through a gateway, you pay the full tool-definition cost. Budget for it.

Finally, the framing line the docs put right next to all of this: verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk. That is the bridge between this article and the wider threat model. Authentication answers who are you. It does not answer should this content be trusted. Perfect OAuth hygiene and a poisoned tool description coexist happily.

The do / do-not checklist

Everything above, compressed to something you can run down before shipping. Version-stamped to 2025-11-25 unless a line says otherwise.

If you are building an MCP server:

If you are building an MCP client:

What changes on 2026-07-28

The release candidate targeting 2026-07-28 is described by the MCP blog as the largest revision of the protocol since launch, and it touches authorization in six places. Per the RC announcement:

SEP-2468 is the one with real implementation weight, and the draft spells it out: the client MUST record the issuer from validated AS metadata alongside the PKCE verifier, and MUST validate iss before sending the authorization code to any token endpoint. If authorization_response_iss_parameter_supported is true and iss is absent, reject. Clients MUST NOT normalize before comparison — no case folding, no default-port elision, no trailing-slash or percent-encoding normalization. Validation applies to error responses too: on mismatch the client MUST NOT display error, error_description or error_uri. The spec notes a future revision is expected to upgrade the AS's iss inclusion from SHOULD to MUST.

The draft also restructures authorization from one page into sub-pages (authorization server discovery, client registration, security considerations) and adds a dedicated Refresh Tokens section: clients MUST keep refresh tokens confidential, SHOULD include refresh_token in grant_types, MAY add offline_access when advertised, and MUST NOT assume refresh tokens will be issued. Servers SHOULD NOT include offline_access in a WWW-Authenticate scope or in PRM scopes_supported. Beyond auth, the RC carries a stateless protocol core, an Extensions framework, Tasks, MCP Apps, and — welcome news for anyone maintaining integrations — a formal deprecation policy requiring at least twelve months between deprecation and earliest removal.

Two adjacent things worth a glance rather than a dependency. MCP Authorization Extensions live in a separate repo (modelcontextprotocol/ext-auth) and include Enterprise-Managed Authorization and Client Credentials; extensions are optional, additive, composable and versioned independently, and their status moves fast, so check before you build on one. And the MCP Registry is still in preview, not GA — its own docs warn that breaking changes or data resets may occur before general availability, and it is explicitly not intended for direct host-application consumption. It is the official centralized metadata repository for publicly accessible MCP servers, backed by contributors including Anthropic, GitHub, PulseMCP and Microsoft, using reverse-DNS namespaces with DNS, GitHub or HTTP verification. Namespace verification is an identity mechanism, not a security audit.

For scale, the most recent published figures are from the December 2025 donation post: over 97 million monthly SDK downloads, 10,000 active servers. Those are seven months old as of this writing — treat them as floors, not current counts. Ten official SDKs exist under a tier system: Tier 1 TypeScript, Python, C#, Go; Tier 2 Java, Rust; Tier 3 Swift, Ruby, PHP, Kotlin.

If you implement one thing from this article this week, make it the audience check on your server: reject any token that was not issued for you. It is a single MUST, it takes an afternoon, and it is the precondition that makes both the passthrough anti-pattern and the confused-deputy chain impossible on your infrastructure.

FAQ

Is authentication required for MCP servers?

No. The 2025-11-25 specification states plainly that authorization is OPTIONAL for MCP implementations. The requirement levels differ by transport: HTTP-based transports SHOULD conform to the authorization specification; implementations using the stdio transport SHOULD NOT follow it and should instead retrieve credentials from the environment; implementations using alternative transports MUST follow established security best practices for their protocol. So an unauthenticated MCP server is not a spec violation. It is a deployment decision — and for a local stdio server, it is the expected one.

Does MCP require OAuth for remote servers?

No, on two counts. First, authorization itself is optional, and the requirement for HTTP transports is SHOULD, not MUST. Second, OAuth is a recommendation rather than the only sanctioned path: the architecture documentation says Streamable HTTP supports standard HTTP authentication methods including bearer tokens, API keys and custom headers, and that MCP recommends using OAuth to obtain authentication tokens. Claude Code proves it in practice — you can attach a static token to a remote server with a header flag and never touch an OAuth flow. What is true: if you do implement the authorization spec, large parts of it become MUST. Protected Resource Metadata, PKCE, resource indicators and audience validation are all mandatory once you are in.

Is Dynamic Client Registration deprecated in MCP?

It depends which version you are reading, and this is the most outdated claim in circulating articles. In 2025-11-25, DCR (RFC 7591) is a MAY, and the spec says it is included for backwards compatibility with earlier versions of the MCP authorization spec. In the draft feeding the 2026-07-28 release, the language sharpens to: Dynamic Client Registration is deprecated and retained for backwards compatibility with authorization servers that do not support Client ID Metadata Documents. So writing DCR is deprecated without naming a version is wrong for the current stable spec. The direction is unambiguous either way — the preferred path is Client ID Metadata Documents, where the client_id is itself an HTTPS URL pointing to a JSON metadata document.

Should an MCP client request the minimum scopes?

Yes — and the spec says so in as many words, which is the half most guides drop. The 2025-11-25 Scope Selection Strategy opens with: MCP clients SHOULD follow the principle of least privilege by requesting only the scopes necessary for their intended operations. It then gives a two-step priority order: use the scope from the 401 WWW-Authenticate challenge if present, and if scope is not available, use all scopes defined in scopes_supported. That fallback is the sentence that circulates alone, as proof MCP abandons least privilege. It does not — the spec makes scopes_supported carry the burden instead, describing it as the minimal set of scopes necessary for basic functionality. Two caveats. The Security Best Practices document gives clients contrary advice (begin with only baseline scopes) and its Scope Minimization attack description blames both sides: the server exposed every scope and the client requested them all. And Claude Code removed the all-scopes fallback in v2.1.196 because identity providers advertising admin-only or template scopes rejected the request with invalid_scope. Follow the challenge; treat the fallback as a last resort. Server author? Send a scope in your 401 and keep scopes_supported minimal.

How do local stdio MCP servers authenticate?

Mostly they do not, and that is by design — the spec says stdio implementations SHOULD NOT follow the authorization specification and should retrieve credentials from the environment instead. But treating that as the security story is a category error. The threat model for a local server is not token theft, it is arbitrary code execution: the spec's named scenario is an attacker includes a malicious startup command in a client configuration. The controls that matter are consent and process isolation. Clients supporting one-click local server configuration MUST implement proper consent mechanisms prior to executing commands, and MUST show the exact command that will be executed, without truncation. Local servers SHOULD use the stdio transport to limit access to just the MCP client; if they expose HTTP, require an authorization token or use unix domain sockets or IPC with restricted access.

Can an MCP server forward the client's token to an upstream API?

No — and the distinction matters, because sloppy phrasing here scares people off the correct pattern. Token passthrough is defined as an anti-pattern where an MCP server accepts tokens from an MCP client without validating that the tokens were properly issued to the MCP server, and passes them through to the downstream API. The mitigation is a MUST NOT: MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server. But an MCP server calling an upstream API is fine and expected — it just has to do it as an OAuth client in its own right. The spec: the access token used at the upstream API is a separate token, issued by the upstream authorization server. The MCP server MUST NOT pass through the token it received from the MCP client. Two tokens, two audiences, one hop each.

Written by
Kevin Fan

Founder of DataLLM Lab, the unified LLM gateway. Kevin tests models the boring way — same prompts, real costs, unedited outputs — and writes up what the runs actually show.

One API for every model

One API, every model.

Get a single API key for Claude Opus 4.7, GPT-5.4, and 300+ more — with automatic price comparison and routing to the best model for every request.