MCP OAuth Kit: Protect a Remote MCP Server (OAuth 2.1)

Make Claude, Cursor and other MCP clients sign in to your server, and prove the flow works before you ship.

MCP OAuth Kit: Protect a Remote MCP Server (OAuth 2.1) product illustration

What's inside

The screenshot is real output from the kit’s own commands. Tested from a fresh download of this exact zip: ./run_tests.sh ran 34 tests OK on Python 3.13 and 3.10.

Real terminal output from MCP OAuth Kit: Protect a Remote MCP Server (OAuth 2.1): demo and passing tests

Add OAuth to a remote MCP server the way the MCP authorization spec wants it (2025-11-25, and the 2026-07-28 revision), then prove it works with a checker that runs the whole flow. Python 3.9+, standard library only: no dependencies, nothing to sign up for.

PartWhat it does
mcp_oauth/resource.pyProtectedResource: RFC 9728 Protected Resource Metadata, 401/403 WWW-Authenticate challenges (resource_metadata, scope, error), RS256 JWT validation against your IdP's JWKS (issuer, audience = your server URL, exp/nbf with leeway, scopes), JWKS rotation, or RFC 7662 introspection for opaque tokens
mcp_oauth/asgi.pyOAuthMiddleware: the same in front of any ASGI app (FastAPI, Starlette, the HTTP app of an MCP SDK), with per-request scope rules for step-up auth
mcp_oauth/devserver.pyA local OAuth 2.1 authorization server for development and CI: PKCE S256 only, resource indicators, Client ID Metadata Documents, Dynamic Client Registration, iss in the response, refresh-token rotation with reuse detection, consent page or --auto-approve
mcp_oauth/jwt.pyRS256 sign and verify, RSA key generation, JWK (cross-checked against the cryptography library)
tools/mcp_oauth_check.pyTest any remote MCP server (yours or someone else's): challenge, metadata, discovery, PKCE, registration options, token handling (garbage, tampered, alg: none, token-in-URL), and with --flow a full authorization code + PKCE run. --json and exit codes for CI
examples/notes_server.pyA small protected MCP server with a read scope and a write scope (step-up)
examples/broken_server.pySeven common mistakes behind --bug flags, to see what the checker reports

60-second demo

bash tools/demo.sh          # dev auth server + protected example + checker (full flow) + a broken server
bash run_tests.sh           # 34 tests, loopback only

Protect your server

from mcp_oauth.resource import ProtectedResource, AuthError

pr = ProtectedResource(
    "https://mcp.example.com/mcp",                       # the exact URL clients connect to
    ["https://auth.example.com"],                        # your authorization server's issuer
    jwks_uri="https://auth.example.com/.well-known/jwks.json",
    scopes_supported=["files:read", "files:write"],
    default_scopes=["files:read"],                       # sent in the 401 challenge
)

# 1. Serve pr.metadata() as JSON at every path in pr.metadata_paths() (GET, no auth).
# 2. On each MCP request:
try:
    claims = pr.authenticate(request.headers.get("Authorization"), required_scopes=["files:read"])
except AuthError as e:
    return Response(e.body(), status=e.status, headers={"WWW-Authenticate": e.www_authenticate,
                                                         "Content-Type": "application/json"})

ASGI (FastAPI, Starlette, an SDK's streamable HTTP app):

from mcp_oauth.asgi import OAuthMiddleware

def scopes_for(msg):            # step-up: writing needs more than connecting
    if isinstance(msg, dict) and msg.get("method") == "tools/call" and msg["params"].get("name") in {"delete_file", "write_file"}:
        return ["files:write"]
    return ["files:read"]

app = OAuthMiddleware(mcp_app, pr, mcp_path="/mcp", scopes_for=scopes_for)
# validated claims: scope["state"]["oauth"]  (request.state.oauth in Starlette)

A client that gets a 403 insufficient_scope re-authorizes with the scopes listed in the challenge.

Use a real identity provider

The resource side works with any authorization server that issues RS256 JWT access tokens (most do) or offers token introspection. Check three things in your IdP:

  • Audience. The token's aud must be your MCP server URL. MCP clients send resource=<server URL> (RFC 8707). If your IdP ignores resource and uses its own "audience" or "API identifier" setting instead, set that to the same URL. Never accept tokens minted for another API: that is the token-passthrough / confused-deputy problem the spec calls out.
  • Discovery. authorization_servers in your metadata must equal the IdP's issuer string exactly (trailing slashes matter), and its metadata must list code_challenge_methods_supported: ["S256"], or MCP clients refuse to continue.
  • Client registration. MCP clients prefer Client ID Metadata Documents, then pre-registration, then Dynamic Client Registration (deprecated in 2026-07-28). If your IdP has none of these for public clients, only clients you pre-register can connect.

Then run the checker against your deployed server: python3 tools/mcp_oauth_check.py https://mcp.example.com/mcp.

The dev authorization server

python3 -m mcp_oauth.devserver --port 9000 --resource http://127.0.0.1:8000/mcp --scopes "notes:read notes:write"

Opens a consent page per authorization (add --auto-approve for CI). --key-file dev-key.json keeps the signing key across restarts. --allow-http-cimd accepts http://localhost client metadata documents for local client development. It keeps everything in memory and has no user accounts: it is for development and tests only.

The checker

python3 tools/mcp_oauth_check.py https://mcp.example.com/mcp                   # discovery and challenge
python3 tools/mcp_oauth_check.py https://mcp.example.com/mcp --token "$TOKEN"  # plus token handling
python3 tools/mcp_oauth_check.py https://mcp.example.com/mcp --flow            # full flow in your browser
python3 tools/mcp_oauth_check.py http://127.0.0.1:8000/mcp --flow --headless --json   # CI against the dev server

--flow registers through DCR when available; otherwise pass --client-id (a pre-registered id, or a CIMD URL whose redirect_uris include http://127.0.0.1/callback; loopback redirects may use any port). Exit code 0 means no errors, 1 means errors, 2 means it could not run. The token tests only send requests that a correct server rejects; they never try to bypass anything else.

See docs/spec-checklist.md for every requirement the kit covers and where.

Demo output (preview)

bash tools/demo.sh          # dev auth server + protected example + checker (full flow) + a broken server
bash run_tests.sh           # 34 tests, loopback only

How it was tested

bash run_tests.sh runs 34 tests (loopback only). Tested from a fresh Estage CDN download on Python 3.13 and 3.10: both exited 0.

Honest limits

  • RS256 only for JWT validation. ES256 or EdDSA-signed tokens need introspection or a JOSE library.
  • The example servers implement the JSON-RPC subset needed to show auth (initialize, ping, tools/list, tools/call), not the full MCP protocol. Use your SDK for the protocol and this kit for the auth layer.
  • No DPoP or mTLS sender-constrained tokens, no token exchange.
  • The dev server is not hardened and stores nothing; do not expose it to the internet.
  • A clean checker run is evidence, not a security audit.

Format and license

Zip, 39 KB, v1.0, 19 files. You may use and modify this kit for your own work and for clients you serve, including paid client work. Not affiliated with or endorsed by the MCP project or Anthropic.

FAQ

Does it work with Auth0, Okta, Keycloak or Entra ID?
The resource side works with any authorization server that issues RS256 JWTs or supports introspection; set the audience to your MCP server URL.
Do I need FastAPI?
No. Use ProtectedResource in any Python server, or OAuthMiddleware with any ASGI app.
Is the dev authorization server for production?
No. It is for development and CI; use your identity provider in production.
How do I get the files?
Pay by card right here on youraitools.co: your download links appear on this page the moment the payment goes through (claim window: 72 hours, up to 5 downloads per 7 days). Need the files re-sent later? Email hello@youraitools.co.
Refunds?
Email hello@youraitools.co within 14 days if it doesn’t work and we can’t fix it.
Can I pay with crypto?
Yes. Pay the same price in USDC or USDT on Base, Ethereum, Arbitrum, Optimism, Polygon or Solana from the "Pay with crypto" button, with no account or KYC. We check the payment on-chain and show your download links as soon as it confirms.

Get MCP OAuth Kit: Protect a Remote MCP Server (OAuth 2.1)

Protect a remote MCP server with OAuth 2.1 per the MCP spec: protected resource metadata, JWT validation, step-up scopes, a dev auth server and a checker.

Pairs well with

Download$119

Agent Builder Bundle

Swarm Operator Kit + x402 Builder Skills + MCP Starter Kit. $119 instead of $157.

View Agent Builder Bundle

Related

1 email a week: new x402 + MCP tools and free kits.

Short weekly digest of new x402 + MCP tools and free kits, including the MCP OAuth Checker. No spam, unsubscribe any time.