The Model Context Protocol authorization spec changed in November 2025. Most blog posts and tutorials still describe the June 2025 model. The mismatch matters: the spec now prefers a registration approach (Client ID Metadata Documents) that did not exist six months ago, and demotes the one most existing implementations use (Dynamic Client Registration) to "backwards compatibility."
I wrote a primer on MCP earlier this month. This is the deeper sequel: an honest ranking of every way an MCP server can handle authentication in 2026, scored on criteria that matter (where the credential physically lives, how badly it fails when leaked, whether it works for more than one user) rather than which RFC the README cites.
Why this is actually a hard question now
The MCP spec has revised three times in a year:
- June 2025 (
2025-06-18): Separated the MCP server (resource server) from the authorization server cleanly. Made RFC 9728 Protected Resource Metadata mandatory. - November 2025 (
2025-11-25): Introduced Client ID Metadata Documents (CIMD) via SEP-991 as the preferred registration approach when client and server have no prior relationship. Demoted Dynamic Client Registration (DCR, RFC 7591) fromSHOULDtoMAY(kept for backwards compatibility). Aaron Parecki's writeup is the canonical reference. - 2026-07-28 release candidate is being prepared for the next stable revision.
What this means in practice: the canonical MCP servers that launched on Cloudflare in MCP Demo Day, May 2025 (Asana, Atlassian, Block, Intercom, Linear, PayPal, Sentry, Stripe, Webflow) were built against an earlier spec. Most use DCR. Some don't implement Resource Indicators (RFC 8707) correctly. There's an open issue (#1614) debating whether the resource parameter should be MUST or OPTIONAL. The spec is still finding its shape.
So when a team asks "which MCP auth approach should I use?", the right answer depends on three things the spec doesn't decide for you: where you want the credential to physically live, how many users you serve, and what you're willing to operate.
The seven approaches, and how I rank them
I'll cover these in order from least to most secure (by my criteria below), with where each fits and how each fails.
- No auth (stdio local trust)
- Env-var pass-through (stdio)
- API key in HTTP header
- OAuth with manually pre-registered client
- OAuth + Dynamic Client Registration (DCR)
- OAuth + Client ID Metadata Documents (CIMD)
- Agent-side credential broker injection
Ranking criteria
Each approach is scored on:
- Credential boundary. Where does the secret physically sit at request time? On the user's laptop, in the MCP server's environment, in a remote vault, or nowhere (placeholder pattern)?
- Setup friction. How many steps does the end user take? Browser flow, paste-a-token, or zero-touch?
- Multi-user. Can one running instance of the server serve more than one identity safely?
- Multi-account per user. Personal GitHub plus work GitHub from the same setup?
- Refresh handling. What happens when the credential expires? Manual rotation, silent refresh, or "doesn't expire" (which is its own problem)?
- Failure mode. When the credential leaks, what's the blast radius?
- Spec alignment. Does the approach map to the current MCP authorization spec (
2025-11-25draft)?
The composite ranking is mine, not the spec's. The spec is mostly silent on which of these should exist; it tells you what the protocol-level requirements are if you choose to do auth at all.
7. No auth (stdio local trust)
How it works. The MCP server runs as a subprocess of the host (Claude Code, Cursor, etc.), communicating over stdin/stdout. The trust boundary is the OS user account.
Examples. All seven official reference servers (Everything, Fetch, Filesystem, Git, Memory, Sequential Thinking, Time) use no auth.
Spec status. Authorization is OPTIONAL for MCP implementations. STDIO transport SHOULD NOT follow the authorization spec; it SHOULD retrieve credentials from the environment instead. This is explicit in the current draft.
When it's right. Single-user local tooling that does not reach external paid APIs and does not hold credentials. The reference servers are the canonical case: they manipulate files, time, memory graphs. There's no third-party secret to protect.
When it's wrong. Any server that touches an external API, talks to a multi-user system, or runs anywhere but the user's single-user machine. "No auth" is fine for Fetch; "no auth" with an embedded OPENAI_API_KEY is theater.
Failure mode. The auth model isn't the failure here. The failure is what's underneath: a prompt-injection that walks the agent into reading ~/.aws/credentials from a Filesystem server that "doesn't need auth" because it inherits the user's OS permissions.
Score: Acceptable in its narrow lane. Dangerous when copied as a pattern for servers that do hold secrets.
6. Env-var pass-through (stdio)
How it works. The host config passes secrets into the server's environment at spawn time:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
}
}
}
Examples. Most community MCP servers in the wild: GitHub, Postgres, Stripe (unofficial), Linear (the local variant), the entire wave of "wrap-this-API-as-an-MCP-server" projects.
Spec status. Allowed but not specified. The spec says STDIO SHOULD retrieve credentials from the environment; the spec does not tell you to write them into a JSON config file that gets committed.
Where the credential lives. In a JSON file on the user's machine (~/.claude.json, ~/.cursor/...). GitGuardian's 2026 State of Secrets Sprawl found 24,008 unique secrets exposed in MCP-related configuration files on public GitHub, including 2,117 valid live credentials (8.8% of findings). That is the failure mode at scale.
Multi-account per user. Impossible without spawning a second copy of the server. There is no per-call account switching.
Refresh handling. None. Tokens go stale. Static long-lived secrets are the norm.
Failure mode when leaked. The full blast radius of whatever scope the token has. PATs at repo scope read every private repo the user can see. The lifetime is "until you remember to rotate."
Score: The path of least resistance for shipping an MCP server. The path of least security for running one.
5. API key in HTTP header
How it works. Bearer token, no flow. The user pastes a key into the host config; the host sends Authorization: Bearer <token> on every HTTP request to the MCP server.
Examples. Many early remote MCP servers, particularly internal/private deployments.
Spec status. Not really compliant with the current draft, which assumes OAuth 2.1 for HTTP transports. But the spec is OPTIONAL, so a server can choose not to follow it and just expect a bearer.
Credential boundary. The token lives in the client config and is sent on every request. Slightly better than env-var-in-stdio because the server is centrally managed (rotation is one-place), but the client still has to hold the token.
Multi-user. Workable if the token is scoped per user, terrible if it's a single shared key.
Failure mode. Same as any bearer token. Anyone with the token can call the API as the user, indefinitely, until rotation.
Score: Works for "I built a private MCP server and want to limit access without thinking about it." Not a serious answer for anything public.
4. OAuth with manually pre-registered client
How it works. Classical OAuth: a human registers an app in the authorization server's admin UI, gets a client_id (and sometimes a client_secret), and pastes them into the MCP client config. Then the normal Authorization Code + PKCE flow runs.
Spec status. Supported and SHOULD be available as a fallback. Per the current draft, MCP clients are expected to prefer pre-registered credentials first (priority 1 in the registration approach order) if they have them, before trying CIMD or DCR.
When it's right. Internal enterprise deployments where IT registers each MCP client centrally. Anywhere you can pay the per-app registration cost up front.
When it's wrong. Public MCP ecosystems where any agent might want to connect to any server. The N-clients × M-servers registration matrix becomes operationally unmanageable fast.
Multi-user. Yes, this is the OAuth strength. Each user gets their own auth flow, their own tokens, their own consent.
Refresh. Standard OAuth refresh tokens. The MCP server (or its auth server) handles rotation.
Failure mode. If the client_secret leaks (and it lives in config files, so it leaks), an attacker can impersonate the client and complete OAuth flows against any user they can social-engineer. Better than bearer leakage but not great.
Score: Reliable, well-understood, ops-heavy. The right choice when you control both client and server populations.
3. OAuth + Dynamic Client Registration (RFC 7591)
How it works. The client POSTs its metadata (redirect URIs, grant types, etc.) to the auth server's registration_endpoint, gets back a fresh client_id (and sometimes client_secret), then runs normal OAuth.
Spec status. MAY support (demoted from SHOULD in the November 2025 update, kept for backwards compatibility). The current spec recommends CIMD first.
Examples. Notion's mcp.notion.com is the cleanest production example. The user runs one command, a browser opens, they consent in Notion's UI, and they're authenticated. Per Notion's MCP docs, access tokens expire after one hour, with refresh-token rotation on every use. Most of the launches from Cloudflare's MCP Demo Day also use DCR.
When it's right. Public MCP servers that want zero-friction onboarding for users and don't want to coordinate client registration out of band.
When it's wrong. Two cases. First, the /register endpoint is a public write surface; servers need to think hard about abuse, rate limits, and DOS. Second, every new short-lived client (think: ephemeral CI agents) registers a fresh client_id that lives forever in the server's database unless explicitly garbage-collected. This is the scaling concern that motivated CIMD.
Multi-user, refresh, failure mode. Same as pre-registered OAuth once registration completes.
Score: The pre-November-2025 default. Still works. Newer deployments should reach for CIMD instead.
2. OAuth + Client ID Metadata Documents (CIMD)
How it works. Instead of a server-issued client_id, the client hosts its OAuth metadata as a JSON document at a stable HTTPS URL. That URL is the client_id. The authorization server fetches the document on demand, validates it, and caches it.
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"redirect_uris": ["http://127.0.0.1:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Spec status. MCP clients and authorization servers SHOULD support CIMD (draft-ietf-oauth-client-id-metadata-document-00, referenced from the MCP authorization spec). Authorization servers advertise support via client_id_metadata_document_supported: true in their oauth-authorization-server metadata.
Implementations as of May 2026. WorkOS AuthKit ships native CIMD. Auth0 and Authlete have shipped or announced implementations. Keycloak has experimental support.
Why it wins over DCR (where it works). Four reasons, all real:
- Stateless. No registration database to maintain. The metadata fetch is the registration.
- No public write endpoint. Reduces attack surface vs DCR's
POST /register. - Portable client identity. One
client_idURL works across every CIMD-supporting authorization server. No per-server re-registration. - Trust anchored to domain control. The same model OAuth already uses for redirect URI validation.
When it's wrong. Desktop and CLI agents that don't naturally host an HTTPS endpoint. You can work around this with a redirect host or a tiny static file on a known domain, but it's friction. Also: the spec is draft-00, so expect churn through 2026-2027.
Score: The right default for new MCP servers and clients in 2026 if both sides support it. The honest caveat is that "both sides support it" still excludes most of the deployed ecosystem.
1. Agent-side credential broker injection
How it works. The MCP server gets a placeholder (or no token at all). A credential broker running on the agent's machine sits on the outbound HTTPS path, matches the destination URL, and substitutes the real Authorization header at the request boundary. The MCP server, the host, and the agent process never see the real value.
Disclosure. I run authsome, which is one tool in this category. Agent Vault, Clawvisor, and OneCLI are others. They differ in deployment shape and bundled provider counts but share this core mechanic. See the broker comparison post for the long form.
Spec status. Orthogonal. The broker pattern doesn't compete with the MCP authorization spec; it operates one layer down. An MCP server still does whatever OAuth or API-key flow it does; the broker handles where the resulting token lives.
Credential boundary. The encrypted vault on the user's machine. Never in the MCP server, the host config, the environment, or the agent process. Substitution happens at the proxy boundary on outbound requests.
Multi-account per user. Yes, this is the broker's specialty. --connection work and --connection personal against the same provider, picked per call.
Refresh. The broker holds the refresh token and rotates silently.
Failure mode. The attack surface collapses to "the user's machine." Prompt-injection still steals whatever's reachable from the agent's environment, but the environment doesn't contain real credentials anymore. The agent reads OPENAI_API_KEY=authsome-proxy-managed and exfils that. Useless.
When it's right. Local agents with multiple identities or multiple providers. Anywhere you don't want to trust the MCP server to hold credentials safely. Anywhere you've adopted a "the agent should not hold secrets" stance.
When it's wrong. Fully remote SaaS MCP deployments where there is no agent-side machine to run a broker on. In that case you're stuck with whatever auth the server speaks, which makes CIMD or DCR the right answer.
Score: Best blast radius of any approach when it fits. Doesn't fit every deployment shape.
A decision tree
Three questions get you 90% of the way to an answer.
Q1: Is the MCP server local (stdio) or remote (HTTP)?
- Local. You're choosing between #7 (no auth, for servers that hold no secrets), #6 (env-var, for servers that hold static secrets), or #1 (broker, for servers that need fresh credentials). The broker pattern is the only one that handles multi-account and automatic refresh cleanly.
- Remote. You're in OAuth territory. Move to Q2.
Q2: Is the server private (you control the client population) or public (anyone could connect)?
- Private. #4 (manually pre-registered client) is operationally simplest and the spec's first preference. You take on the registration ops; you avoid the public-endpoint risks.
- Public. You need CIMD (#2) or DCR (#3). Pick CIMD if your authorization server supports it. Fall back to DCR otherwise.
Q3: How many users will one server instance serve?
- One. Bearer tokens or single-user OAuth are fine.
- Many. You need real OAuth with per-user tokens, never a shared key. The Resource Indicator (RFC 8707) requirement is doing real work here: it ensures the user's GitHub token can't be replayed against the user's Linear MCP server.
What real production servers actually use today
The MCP Demo Day cohort (Asana, Atlassian, Block, Intercom, Linear, PayPal, Sentry, Stripe, Webflow) built on Cloudflare's stack. Sentry's writeup confirms it uses Cloudflare's OAuth Provider Library directly with Durable Objects for state; the others are likely similar but the public details vary. Most of these launched against the pre-June-2025 spec when DCR was the recommended approach; the migration to CIMD is just starting.
Notion's mcp.notion.com is the cleanest reference DCR implementation. Refresh-token rotation, one-hour access tokens, real refresh handling. If you want to read someone else's code that gets OAuth right, start there.
Among community MCP servers (the much larger long tail), env-var pass-through (#6) dominates. Most ship with a README that says "set X_API_KEY and paste this into your config." This is why GitGuardian found 24,008 secrets in public MCP configs.
The seven official reference servers all use no auth (#7), which is correct for what they are but creates an unfortunate impression that MCP servers don't need auth in general. They do.
What's actually broken in the wild
Four honest critiques worth surfacing.
1. Resource Indicators (RFC 8707) compliance is patchy. The spec says MCP servers MUST verify the audience claim on incoming tokens. Many servers don't. The open issue #1614 is debating whether to relax this requirement. Until it's resolved, token confusion attacks (a token meant for service A being replayed against service B) remain theoretically possible against non-compliant servers.
2. CIMD adoption is slow on the client side. Authorization servers (WorkOS, Auth0, Authlete) shipped CIMD support faster than MCP clients have updated to use it. The result: even where the server speaks CIMD, the client falls back to DCR.
3. Stdio MCP servers with secrets are a stable attack pattern. Every prompt-injection writeup from late 2025 and early 2026 walks the same path: agent reads a malicious README, the README tells it to dump ~/.claude.json or .env to a URL, the agent does it. The credentials are in config files because that's where the spec implicitly suggests they go.
4. "Auth at the MCP server" doesn't say anything about auth between the MCP server and the underlying service. A perfectly OAuth-compliant MCP server can still hold a hardcoded GitHub PAT for the GitHub API on the other side. The auth spec covers the host-to-server hop, not the server-to-API hop. That's where the broker pattern (#1) keeps mattering even when the MCP layer does the right thing.
The shorter version
The MCP authorization spec landed real improvements in November 2025: CIMD as default, DCR demoted to backwards-compat, Resource Indicators (where actually implemented) preventing token confusion. The deployed ecosystem is still catching up, mostly on DCR, with a long tail of community servers that ship env-var pass-through and ignore the spec entirely.
Pick by where you want the credential to live, not by which RFC the README cites. For local servers with secrets, the broker pattern keeps credentials out of the process. For remote servers, prefer CIMD if both sides support it, DCR otherwise. For internal deployments, manual pre-registration is still the operationally simplest answer.
If you take one thing from this post: a tutorial that tells you to paste an API key into your MCP host config is teaching you to publish that key, even if it doesn't say so out loud.
Next steps
Quickstart
Install authsome and route an MCP server's credentials through a vault on your machine.
What is MCP? A developer's primer
The companion primer: protocol, architecture, transports, and the auth patterns at a higher level.
How prompt injection becomes credential exfiltration
Why putting credentials in config files alongside an MCP server is the same problem as putting them in env vars.
Further reading
The agent auth wave: auth.md, ID-JAG, AAuth, CIMD, and why OAuth is finally growing up in 2026
Six months ago, agent authentication was a gap in the OAuth ecosystem. As of May 2026, five separate efforts are converging on a coherent shape. Here is the map, the timeline, and what is still missing.
Read postMay 26, 2026MCP gateways in 2026, compared: the twelve you should actually know about
Twelve real MCP gateways in 2026, sorted by deployment model and use case. Self-hosted open source first, then hosted SaaS, then cloud-platform managed. With an honest take on what is and isn't actually a gateway.
Read postMay 26, 2026Building a self-hosted MCP server: the auth checklist nobody publishes
Every blog post tells you how to use an MCP server. This is the post about how to build one that does not show up on a security report. Stdio and HTTP, OAuth and env-var, with a real checklist.
Read post