Skip to content

· 3 min read · Zain Akram

OAuth 2.0 for MCP servers: the checklist that passes a security review

What an MCP server needs before security will let an AI assistant touch real systems: discovery metadata, PKCE, dynamic client registration, scopes, consent, token lifetimes, idempotency and audit.

  • MCP
  • OAuth
  • Security
  • AI agents

Remote MCP servers are expected to protect themselves with OAuth 2.0. Most "it works on my machine" MCP servers skip that and use a static API key, which is fine for a demo and unacceptable the moment an assistant can deploy code or read customer data. This is the checklist I use before an MCP server goes anywhere near production. It is the same list that got the Coolify deployment server through a security review.

1. Discovery metadata

MCP clients find your authorization server through /.well-known/oauth-authorization-server (and increasingly /.well-known/oauth-protected-resource on the resource). Serve them correctly, with the real issuer, authorization and token endpoints, supported scopes, PKCE methods and registration endpoint. A single wrong field produces a vague "could not connect" in the client with no further detail, so test discovery first.

2. PKCE, required

Public clients cannot keep a secret, so the authorization code flow must use PKCE with the S256 method, and the server must reject requests without it. This is not optional for MCP.

3. Dynamic client registration, gated

Most MCP clients register themselves on first use. Allow it, but do not trust it: a newly registered client starts with no scopes until a human approves the grant on the consent screen. Keep registration rate limited and log every registration.

4. Scopes that name actions

Design scopes around what the agent may do, not around resources: deploy:staging, deploy:production, logs:read, status:read. Enforce them in the server on every tool call, after the model has decided what to call. A model cannot talk its way past a scope check that happens in your code.

5. A consent screen a human can read

List exactly which projects and actions the grant covers, in words. Destructive scopes should be visually distinct. Store the grant with who approved it and when.

6. Short-lived access tokens, rotating refresh tokens

Access tokens measured in minutes, refresh tokens that rotate on use and are revoked on reuse. Store only hashes. Tie every token to the grant and client that produced it.

7. Confirmation for dangerous tools

Anything irreversible returns a summary and a one-time confirmation token instead of acting. The client must call a confirm tool with that token. Users experience this as "the assistant asked me to confirm", which is exactly the behaviour you want.

8. Idempotency keys on every write

Models retry. A deploy tool that cannot tell a retry from a second request will deploy twice. Derive an idempotency key from the request and keep it for a window.

9. Rate limits per client and per user

Separate limits for reads and writes. Return clear errors with retry hints so the model backs off instead of hammering.

10. Audit everything

Each tool call writes the actor, grant, client, arguments, result and latency to an append-only table. When something breaks you want "who asked for what" answered in one query. Security teams will ask for this table before they ask for anything else.

11. Never let the model hold a credential

Upstream API tokens (Coolify's in my case) live in the server, encrypted, and are used only after the scope check. The model sees tools, never secrets.

12. Test the unhappy paths

Expired tokens, revoked grants, scope escalation attempts, replayed confirmation tokens, malformed discovery. Write tests for each. Most MCP incidents are not clever attacks; they are a missing check on a path nobody tried.

If you can tick all twelve, you have a server a security team can approve and an agent that can do useful, dangerous things without becoming the incident.