MCP authentication: OAuth 2.1, API keys, and what the specification actually requires
The spec says OAuth for remote servers and most of the world uses a bearer key. Here is what each arrangement actually does with your credential, where it ends up, and what revoking it costs.
"Connect your GitHub" is the same sentence in front of three completely different arrangements, and the difference between them is not security in the abstract. It is a concrete question with a concrete answer: where does the secret end up, and what do you have to do when it leaks? Everything below is that question asked of each way an MCP server can authenticate.
Local servers: the environment, and nothing else
A stdio server has no authentication of its own — the client that started it is the only thing that can talk to it. What it needs is a credential for whatever it reaches, and the specification is deliberately silent about how it gets one: the convention is an environment variable, set in the env block of the client’s config entry.
That convention has a consequence people discover late. The key is in a JSON file on disk, in plain text, in every place that file is synced or backed up, and in the process environment of every server the client starts. Rotating it means finding every copy. Revoking it means the same. A local server holding a long-lived key to a third-party service is the most common shape in the world and the worst one: the secret is on your disk like a local server, and the blast radius is somebody else’s production system like a remote one.
- Name the variables the server refuses to start without, and fail at startup rather than at the first call.
- Ask for the narrowest scope the tools use. Scopes are copied from a README and never trimmed, and anything that compares them with the tool surface will say so.
- Never put a key in args. It appears in the process list of the machine, which is broader than the file.
Remote servers: what the specification requires
Once a server is at an address, the specification has an opinion. The 2025-03-26 revision brought OAuth 2.1 to HTTP transports; the 2025-06-18 revision fixed what had been the awkward part by separating the roles: an MCP server is an OAuth resource server, not an authorization server. It does not issue tokens. It publishes, at a well-known address, which authorization server does — and the client goes there.
| Requirement | Standard | What it means in practice |
|---|---|---|
| Protected resource metadata | RFC 9728 | The server publishes /.well-known/oauth-protected-resource naming its authorization server, and a 401 points at it |
| Authorization server metadata | RFC 8414 | The client discovers endpoints rather than being configured with them |
| PKCE | OAuth 2.1 | Required on every authorization code flow, no exceptions for "trusted" clients |
| Resource indicators | RFC 8707 | The client says which server the token is for, so a token minted for one server is useless at another |
| Dynamic client registration | RFC 7591 | Recommended, so a client nobody pre-registered can still connect |
| No token passthrough | MCP specification | A server must not forward the token it received to an upstream API; it uses its own credential there |
What a client does with all of that: it sends a request, gets a 401 that names the resource metadata, reads which authorization server to use, registers itself if it has to, runs the authorization code flow with PKCE in a browser tab, and comes back with a token bound to that one server. The person sees one tab and one consent screen. The token lives in the client’s own store, per machine.
In practice: a bearer key
Most remote servers in the wild, and every gateway, take something simpler: an API key in an Authorization: Bearer header on every request. The specification does not forbid it — a bearer token is a bearer token, and the server is free to decide what mints one — it just does not describe it, because there is nothing to describe. You get a key from a settings page and paste it into a config file.
Which puts the secret back on disk, in the headers block this time rather than env. The difference from the local case is at the other end: a key issued by a service can be listed, scoped, expired and revoked by that service, per key, without touching the account it belongs to. That is a real improvement, and it holds only if the keys are actually issued that way — one per machine, with a scope and an expiry — rather than one long-lived key for everything, copied into every file.
{
"mcpServers": {
"issues": {
"type": "http",
"url": "https://…/mcp",
"headers": { "Authorization": "Bearer <a key for this machine>" }
}
}
}- One key per machine, not per person. Machines get lost, decommissioned and rotated independently; a key that belongs to a laptop can go with the laptop.
- A scope on the key — read-only where the work is reading — so that a leaked key cannot reach the tools that write.
- An expiry. A key that cannot expire is a key somebody will find in a backup in two years.
- Never in args, never in a URL. Both end up in logs the person who pasted the key never sees.
The brokered arrangement
There is a third shape, and it is the one that removes the secret from your disk altogether. A gateway sits between the client and the server: the client holds a key to the gateway and nothing else, and the gateway holds the credential for the upstream — the OAuth grant, the API key, whatever the service takes — on your behalf. Your config file contains one key, for one gateway, with one scope. The upstream credential never reaches your machine.
What that buys is revocation that means something. "Revoke this one integration" is one action at the gateway and changes nothing else; "revoke this machine" is one key, and every other machine keeps working. What it costs is a third party in the path of every call, which is either fine or unacceptable depending on what the calls carry, and that is a decision to make on purpose rather than by default.
The three shapes, side by side
| Arrangement | Where the secret lives | Revoking it means | A leak costs |
|---|---|---|---|
| Key in a local server’s environment | A config file on disk, and everywhere it syncs | Rotate at the service, then find every file | Whatever the key’s scope reaches, until you notice |
| OAuth, held by the client | The client’s own store, per machine | Revoke the grant, per machine | One machine’s grant, until revoked |
| Bearer key to a remote server | A config file on disk, per machine if issued that way | Revoke one key at the issuer | One key’s scope on one machine |
| Brokered through a gateway | With the gateway; never on your disk | One action, one integration or one machine | Access to the gateway, not the upstream credential |
Reading a refusal
When a call fails after all of this, the status code says which half of the arrangement failed. A 401 is about the credential itself — missing, expired, revoked, unknown — and nothing else; a server that wants OAuth answers it with the metadata pointer above, and a server that wants a key answers it with a sentence. A 403 means the credential was fine and something about what it may do was not: a scope it lacks, a tool that writes called with a read-only key, a plan that lapsed. Debugging a 403 by rotating the key is the most common wasted hour in this whole area.
The checklist
- Before pasting any key: what scope, what expiry, and is it this machine’s or everybody’s?
- Before installing a local server with a credential: what does that credential reach beyond what the tools use?
- Before trusting a remote server with OAuth: does its 401 point at real resource metadata, and does it promise not to pass your token upstream?
- Before choosing a gateway: what can it see, what does it log, and can you revoke one integration without touching the rest?
- After any of them: can you list, in under two minutes, every credential your agents hold and where each one is stored? If not, the arrangement is whichever one is hardest to inventory, and that is the first one.
The specification got the remote case right in 2025 by making the server a resource server rather than a bank. The rest is arrangement, not protocol — and the arrangement is decided by whoever pastes the key, which is the one part no specification can fix.