How to add OAuth to an MCP server
You have an MCP server. You want it to enforce who can call which tool. OAuth 2.1 is the standard way; this page walks through the moving parts and the shortest path from "open server" to "scoped 200s."
If you only want the AuthSec-specific instructions, jump to From zero to a launched MCP server.
The five things that need to exist
To protect an MCP server with OAuth, you need:
- An authorization server. Issues access tokens. Anything OAuth 2.1-compliant works — Ory Hydra, Keycloak, Auth0, your own. AuthSec uses Hydra under the hood and adds the MCP-specific layer (scope matrix, tool inventory, manifest) on top.
- A resource server. Your MCP server, validating tokens on every tool call.
- A token validation strategy. Either local JWT verification against a JWKS endpoint, or runtime introspection (
POST /oauth/introspect), or both. See JWT vs token introspection for the tradeoff. - A scope model. What scopes exist, which tool requires which. The hard part — see Scope design for MCP tools.
- An OAuth client. Whatever's calling your MCP server — Claude Desktop, Codex, your own UI. The client gets a user to consent to specific scopes, exchanges that for an access token, and includes it on each request.
The minimal flow
A user opens Claude Desktop, points it at your MCP server, and tries to call a tool. End-to-end:
- Client sends
POST /mcpwith no Authorization header. - Server responds
401withWWW-Authenticate: Bearer resource_metadata="https://your-server/.well-known/oauth-protected-resource/mcp". - Client fetches the metadata, finds the authorization server's URL.
- Client does the OAuth authorization-code flow: redirect to
/oauth/authorize, user logs in and consents to scopes, redirect back with a code, exchange the code at/oauth/tokenfor an access token. - Client retries the original
POST /mcpwithAuthorization: Bearer <token>. - Server validates the token, checks if the user's granted scopes include what this tool requires, and either runs the tool or returns 403.
That's the whole loop. Steps 1–3 are RFC 9728. Steps 4–5 are RFC 6749 + 7636 (PKCE). Step 6 is what your SDK does for you.
Doing it yourself
If you want to implement this without a library:
- Step 6 — token validation. Decode the JWT, verify against the issuer's JWKS, check
audagainst your resource URI, checkexpis in the future, check thescopeclaim contains what you need. ~50 lines of code in any language with a JWT library. - Step 2 — the 401 challenge. Set the WWW-Authenticate header correctly. The format matters —
resource_metadata="..."is the RFC 9728-defined parameter. - The /.well-known/oauth-protected-resource endpoint. Serve a JSON document listing
authorization_servers,scopes_supported,bearer_methods_supported. Clients use this to discover where to send the user.
Hand-rolling all of this is feasible but boring. Most teams end up with a library; that's what AuthSec's Go SDK is for. The point of using a library isn't the OAuth dance — it's the policy layer on top (per-tool scope mapping, drift detection, manifest publishing) that you'd otherwise build yourself badly.
Where things commonly go wrong
In rough order of frequency:
audmismatch. The token'saudclaim says one thing; your server checks for something else. See Troubleshoot OAuth tokens for the four-claim checklist.- Scope name drift. You scoped the token at issuance time, but the server is checking for a different string. This is why scope IDs should be stable and namespaced — see Scope design.
- Token validation always passes. You forgot to actually call the validation function in the middleware. Test with
Authorization: Bearer garbage— you should get 401, not 200. tools/listfiltering not applied. Your validation runs but you still return every tool to every client. Filtering is a separate concern from authorization — the SDK does both.
When you outgrow the basics
The minimal flow above gets you to "authorized tool calls." The next thing you'll want is:
- Per-scenario policy. "User Alice with the Claude-Desktop client can do read but not write." This is what a scope matrix is for.
- Dynamic Client Registration. Letting any MCP client register without an admin step. RFC 7591.
- Audience-bound tokens. Tokens that are only valid for one specific MCP server, so they can't be replayed to another. The
audclaim, when checked, gives you this. See Preventing confused-deputy attacks. - Token revocation. Killing a leaked token before it expires. Switch your
ValidationModeto include introspection. - Audit logging. Who called what, when. The Go SDK emits structured logs you can pipe into anything; AuthSec's Monitor tab tracks policy changes.
Doing it in 10 lines
If your MCP server is in Go, the SDK collapses all of the above into ~10 lines of Config. If you're in Python or TypeScript, see Python SDK and TypeScript SDK for the current stopgaps.
Related
- From zero to a launched MCP server — the AuthSec-specific walkthrough
- Scope design for MCP tools
- JWT vs token introspection
- Troubleshoot OAuth tokens