"Add auth to the MCP server" sounds like one task. In OAuth terms it's often two, and they point in opposite directions.
On one side, an MCP client — Claude, Cursor, an IDE — connects to your server. Your server has to decide whether to let it in. On the other, your server's tools call your billing API, your CRM, your internal services. Now your server is the one asking to be let in, and the question is: as whom?
Getting those two confused is how MCP servers end up forwarding tokens they shouldn't, or calling everything with one all-powerful key.
Role one: the MCP server is an API
When an MCP client connects, your server is a resource server. It receives a bearer token and checks it, like any API.
The MCP authorization spec builds this on standard OAuth. The client discovers your authorization server, registers itself with Dynamic Client Registration (because you can't pre-register every MCP client in existence), runs the authorization code flow with PKCE, and requests a token for your server using a resource indicator.
Your server's job is simple and familiar: verify the token's signature, issuer, and audience, and check it grants what the tool needs.
If this is all your server does — its tools read from its own database, or call nothing that needs a user identity — it is not an agent. It's an API. In AuthAction that means registering it as an API and allowing dynamic clients, and that's the end of it.
Role two: the MCP server acts for the user
Now suppose a tool does something like "reconcile last month's payouts," and the payouts live behind a different API. Your server needs a token for that API, and the payouts API needs to know which user is asking — because what they're allowed to see depends on it.
This is where things go wrong, in one of two ways.
Mistake one: forwarding the user's token
The token your server received was issued for your server. Its audience is your MCP server. Passing it along to the payouts API means either the payouts API accepts tokens meant for someone else — so it's no longer checking audience — or the token was issued with a broad audience to begin with.
Either way, any server that sees the token can replay it anywhere else that accepts it. The MCP security best practices call this token passthrough and say plainly not to do it.
Mistake two: a shared service key
The other instinct is to give the MCP server its own credentials and call the payouts API as itself. Now the audience is right, but the user has vanished. The payouts API sees "the MCP server" for every request, from every user. It can't apply per-user permissions, and the audit log records a service where there should be a person.
Worse, the key can do anything it's allowed to do, for anyone. If the model is talked into calling a tool it shouldn't, nothing downstream can tell.
The fix: exchange the token
OAuth 2.0 Token Exchange (RFC 8693) exists for exactly this. Your server sends the user's token and its own identity to the authorization server, and gets back a new token:
{
"sub": "ops@acme.eu",
"act": { "sub": "payouts-mcp-server" },
"aud": "https://payouts.acme.eu",
"exp": "5 minutes from now"
}
- The audience is the payouts API, so it's valid there and nowhere else.
- The subject is still the user, so per-user permissions apply.
- The
actclaim names your server, so the audit trail records both who asked and what acted. - It's short-lived, and can't outlive the user's original token.
In this role, your MCP server is an agent: a client acting on a person's behalf. That's a larger power than calling an API as yourself, which is why AuthAction only lets applications registered as agents perform the exchange — an ordinary machine-to-machine client is refused, even with a valid secret.
So which is it?
| Your MCP server… | It is | Set up |
|---|---|---|
| Accepts connections from MCP clients | An API | API with dynamic clients |
| Calls other APIs as the signed-in user | Also an agent | Agent, with those APIs enabled |
| Calls other APIs as itself, with no user in the picture | A machine-to-machine client | M2M application |
Most non-trivial MCP servers are the first two at once. That's fine — they're separate registrations for separate jobs, and keeping them separate is what stops one role's credentials leaking into the other.
One more thing: the agent's own secret
Token exchange fixes the user's side of the audit trail. It still leaves your server authenticating with a client secret in an environment variable — permanent, anonymous, and the credential an attacker actually wants.
If your MCP server runs on Kubernetes, EKS, GKE, or GitHub Actions, the platform
already gives it a short-lived, rotating identity token. In AuthAction you register
that platform as a trusted issuer, then link it on the agent's Trusted
Issuers tab with the workload's exact subject — say,
system:serviceaccount:prod:payouts-mcp — and the audience it's minted for. The
server presents that token instead of a secret, and there's nothing left to leak.
The dashboard steps are in Agents & Trusted Issuers, and the exchange itself, with an MCP server example, is in Token Exchange for AI Agents.