Skip to main content

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:

  1. 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.
  2. A resource server. Your MCP server, validating tokens on every tool call.
  3. 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.
  4. A scope model. What scopes exist, which tool requires which. The hard part — see Scope design for MCP tools.
  5. 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:

  1. Client sends POST /mcp with no Authorization header.
  2. Server responds 401 with WWW-Authenticate: Bearer resource_metadata="https://your-server/.well-known/oauth-protected-resource/mcp".
  3. Client fetches the metadata, finds the authorization server's URL.
  4. 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/token for an access token.
  5. Client retries the original POST /mcp with Authorization: Bearer <token>.
  6. 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 aud against your resource URI, check exp is in the future, check the scope claim 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:

  • aud mismatch. The token's aud claim 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/list filtering 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 aud claim, when checked, gives you this. See Preventing confused-deputy attacks.
  • Token revocation. Killing a leaked token before it expires. Switch your ValidationMode to 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.