The Model Context Protocol made it trivial to hand an agent new tools. Point a client at a server, and the model can suddenly query a database, open tickets or move money. What MCP did not make trivial is deciding with whose authority those tools run. Most servers in the wild answer that question with a single API key in an environment variable, which means every tool the server exposes runs with the full power of that key — for as long as the key lives.
This guide walks through the authentication patterns MCP servers actually use, where each one breaks, and how to get tools down to scoped, per-service access that a human can revoke.
Two transports, two auth stories
MCP servers run in one of two shapes, and the auth story is different for each.
- Local (stdio). The client launches the server as a child process on the same machine. There is no network hop, so there is no protocol-level auth: the server reads whatever credentials it needs from its environment, usually a key pasted into a JSON config file.
- Remote (streamable HTTP). The server lives at a URL and many clients connect to it. Here the MCP specification defines an authorization flow based on OAuth 2.1, and the server behaves as an OAuth resource server: it accepts access tokens and rejects requests without them.
Read-only, public servers can skip auth entirely. Notlogin's own MCP endpoint is an example: its tools list the vendor catalog and verify credentials, nothing that belongs to a user, so it is advertised at /.well-known/mcp.json as read-only with no auth. The moment a tool touches a user's data or money, that stops being acceptable.
Pattern 1: the key in the config file
The stdio default is a long-lived API key in env. It is easy and it works, which is why it is everywhere. Its problems are the ones covered in the authentication methods overview: the key is usually account-wide, it never expires, it sits in plain text on disk next to configs for a dozen other servers, and every tool on the server shares it.
The last point is the MCP-specific one. A server might expose search_issues and delete_repository side by side. If both run on the same key, the read tool is exactly as dangerous as the destructive one once the model is steered by a malicious page or document — the situation described in the confused deputy problem. Telling the model to be careful does not change what the key can do.
If you must ship a stdio server with a key, the minimum is: ask for the narrowest key type the upstream API offers, document which scopes each tool needs, and refuse to start if the key is broader than the tools you expose.
Pattern 2: OAuth 2.1 for remote servers
For remote servers the spec settles on standard OAuth, which is good news: nothing exotic, and every piece is an existing RFC. The flow looks like this:
- The client calls the server without a token and gets
401with a pointer to the server's protected resource metadata, which names the authorization server it trusts. - The client fetches the authorization server's metadata from
/.well-known/oauth-authorization-server(RFC 8414) to learn its endpoints and supported grants. - The human signs in and consents in a browser using the authorization-code grant with PKCE, which is mandatory: the client is usually a public app that cannot keep a secret.
- The client receives an access token bound to that specific server and sends it as a bearer token on every MCP request. Short expiry plus a refresh token keeps the session alive without re-prompting.
This is a real improvement over the config-file key: the human approves a consent screen, the token expires, and it can be revoked. Notlogin implements the authorization-server side of this — it publishes RFC 8414 metadata at the path MCP clients probe first, requires PKCE with S256, and issues signed tokens with refresh rotation — so generic OAuth clients, including MCP connectors, can configure themselves from discovery alone.
The rule people break: no token passthrough
An MCP server often fronts another API. The tempting shortcut is to take the token the client sent and forward it upstream. The spec explicitly forbids this, and for good reason. A token minted for your MCP server should be accepted only by your MCP server; if downstream services also accept it, a token stolen from one hop works at every hop, audit logs lose track of who acted, and any per-server limits you enforce are bypassed by calling upstream directly.
Instead, the server validates the token it received (issuer, audience, expiry, scope), then uses its own credential for the upstream call — one that is scoped to that upstream and ideally tied to the same user and grant. That is the gap most implementations leave open, because it means holding a second, separate credential per upstream service.
Pattern 3: one narrow grant per service
This is where an auth broker fits. Rather than an MCP server collecting a powerful key for every service it touches, the human pre-authorizes each service once, and the agent presents a credential minted for that service alone. In Notlogin that credential is a signed verifiable credential that states the principal, the vendor, the scope, the expiry, an optional spending budget and the level of human proof behind it. The receiving service verifies the signature locally — no shared secret, no call back to us on the hot path.
Mapped onto MCP tools, the properties you want are:
- Per service. The tool that reads issues cannot carry authority into your mail provider. A compromised tool or an injected instruction finds nothing to borrow outside its lane.
- Per scope. Read tools hold read grants. A destructive tool needs a grant the human approved separately, and the upstream enforces the difference, not the model.
- Budgeted. Any tool that spends money spends against a ceiling signed into the grant. The runaway loop stops at the cap; see how agents pay for services safely.
- Expiring and revocable. Grants carry their expiry, and the human revokes one service from a dashboard without rotating anything else.
A checklist for MCP server authors
- Public, read-only tools: no auth is fine. Say so in your discovery document.
- Remote servers that touch user data: implement the spec's OAuth flow, PKCE included.
- Validate audience on every token. Reject tokens minted for anyone else.
- Never forward the client's token upstream. Use a separate, narrowly scoped credential.
- Group tools by the permission they need, and require a separate grant for destructive ones.
- Put money limits in the credential, not in a tool description the model can be talked out of.
- Treat everything a tool returns as untrusted input to the next model turn — that is where credential theft via prompt injection starts.
MCP solved how agents discover and call tools. Authorization is the part that decides whether that is safe to do with real accounts. If you run agents and want each tool on its own narrow grant, start with Notlogin. If you run an API or an MCP server and want requests that state their own scope, budget and human proof, register as a vendor.