Ask five different MCP deployments how they handle mcp authentication and you will get four different answers, because the spec leaves more of this open than most people assume. Some of that variation is by design, matched to where the server actually runs. Some of it is teams reaching for whatever was fastest to wire up, and not noticing that the pattern they picked leaks in a specific, predictable way once real data is behind it.
The four patterns actually in use
Environment credentials for STDIO servers. A server launched as a local subprocess reads its credentials from the environment it was started in, the same way any command-line tool does. The MCP authorization specification explicitly says implementations using STDIO transport SHOULD NOT attempt to layer its OAuth flow on top, because the process already inherits the trust boundary of whoever launched it.
A static bearer token. An HTTP-based server checks one header against one stored value. Anyone holding the token gets in, and once in, every call runs with the same access as the token itself. This is the fastest pattern to build and the one most likely to be mistaken for something stronger than it is, because "bearer token" sounds like infrastructure even when the implementation behind it is a single shared secret.
An API key that resolves to a member. Structurally similar to a bearer token in transport, the difference is what happens on the server after the header is read: the key is looked up, resolved to a specific person with a role and a set of grants, and everything downstream runs against that identity rather than against an undifferentiated "the app."
Full OAuth 2.1 with PKCE. The pattern the spec actually documents for HTTP transports: a client obtains a short-lived, scoped token through an authorization code flow, proving both that it holds a valid client registration and that a resource owner consented to the specific access requested.
Which mcp authentication pattern is most secure?
OAuth 2.1 is the strongest of the four at the question it actually answers: proving who is opening a session, without a long-lived shared secret sitting in a config file somewhere. But "most secure" depends on what you are defending against. A leaked static bearer token and a leaked OAuth access token both grant access for as long as they remain valid; the meaningful difference is that an OAuth token is typically short-lived and scoped, so the blast radius of a leak is smaller and self-limiting, where a static token often is not.
Where each one actually leaks
| Pattern | What it proves | What leaks, and how |
|---|---|---|
| STDIO environment credentials | The process is running under whatever account launched it | Anything with local access to the environment or process list; the trust boundary is the machine, not the protocol |
| Static bearer token | Someone holds the token | The token itself, in full, for as long as it is valid; every caller behind it is indistinguishable |
| API key resolved to a member | This specific person's credential is in use | Still the credential itself, but a leak is attributable to one person, and revocation does not affect anyone else's key |
| OAuth 2.1 with PKCE | A specific client, with a specific consented scope, for a limited time | A stolen access token, but typically short-lived; the authorization code and refresh token exchange are harder to intercept than a static value |
Table 1: what each authentication pattern actually guarantees, and the specific thing that goes wrong when it is compromised.
"MCP clients MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707 to explicitly specify the target resource for which the token is being requested."
Model Context Protocol specification, Authorization
That requirement exists precisely to stop a token issued for one MCP server being replayed against another, which is the specific failure mode that makes a bearer token dangerous once an agent is juggling several tool connections at once. Naming the resource explicitly closes a hole that a generic OAuth implementation, not written with MCP's multi-server reality in mind, would otherwise leave open.
The gap every pattern shares
Every one of these four proves something about the connection. None of them, on their own, decides what a given call is allowed to return. That is a second, separate design choice, made by whoever operates the server, not answered by the choice of authentication pattern. A server can run flawless OAuth and still respond to every authenticated caller identically, because nothing after the handshake resolves the caller down to a specific person with specific scopes.
Does adding mcp oauth remove the need for a permissions layer?
No. It changes what a compromised credential is worth (a short-lived scoped token instead of a permanent shared secret) but it does not add per-caller entitlement on the data a tool call returns. Those are additive, not substitutable: a server can and often should run OAuth for the connection and a separate entitlement check for every call, because they defend against different things.
Practical guidance for choosing a pattern
- If the server only ever runs as a local subprocess, environment credentials are the correct pattern, not a shortcut. Trying to force OAuth onto STDIO adds complexity without adding a trust boundary that was not already there.
- If you are behind a static bearer token today because it was fastest to ship, the highest-value next step is usually not switching to OAuth. It is making the credential resolve to a specific member with specific grants, so a leak is attributable and revocation is scoped to one person.
- If multiple untrusted or semi-trusted clients need to connect over a network, OAuth 2.1 with PKCE and resource indicators is worth the implementation cost, specifically because it limits both the lifetime and the scope of what a stolen token is good for.
- Whatever you choose, treat it as answering one question only. The second question, what a given caller may see once connected, needs its own explicit answer, checked on every call rather than assumed from the fact that the connection succeeded.
What a resolved API key actually looks like in a client config
The third pattern, an API key that resolves to a specific member rather than an undifferentiated application, is worth showing concretely rather than describing in the abstract, because the difference from a static bearer token is entirely in what happens on the server, not in the shape of the request itself:
{
"mcpServers": {
"contextely": {
"url": "https://your-deployment/api/mcp",
"headers": { "Authorization": "Bearer ctx_sk_..." }
}
}
}
At the wire level that is indistinguishable from a static bearer token. The header is the same shape either way. What differs is entirely server-side: whether that value is checked against a single stored secret, or looked up against a table of members, each with their own role and their own explicit grants. Two servers can accept the exact same request format and diverge completely in what a caller can subsequently retrieve, which is precisely why "we use bearer tokens" is not, on its own, a meaningful answer to how a system authenticates.
For how that second question gets answered once a caller is authenticated, mcp authorization covers the per-person entitlement layer this post deliberately leaves out. The MCP pillar page states the same distinction in the context of the whole product, /docs/mcp has the actual connection config for the API-key pattern described above, and /security documents how a credential is stored and rotated once you have one.
