How to Secure an MCP Server with OAuth 2.1
Before You Start
You need an MCP server running with HTTP transport and an OAuth 2.1 provider. Major providers include Auth0, Okta, Keycloak, and cloud-native options like AWS Cognito and Google Identity Platform. If you already have an identity provider in your organization, use that. If not, Auth0 and Keycloak both have free tiers suitable for development and small deployments.
This guide covers the server-side implementation. MCP clients that support OAuth (including Claude Code and Cursor) handle the client-side flow automatically when they detect that a server requires OAuth authentication.
Step-by-Step Implementation
Your OAuth provider is the authorization server that issues tokens. It handles user authentication, consent, and token management. You configure your MCP server to trust tokens from this provider and validate them on each request.
Key requirements for your provider: it must support OAuth 2.1 (specifically the authorization code flow with PKCE), it must issue JWTs (JSON Web Tokens) as access tokens (so your server can validate them locally without calling the provider on every request), and it must expose a JWKS (JSON Web Key Set) endpoint for token signature verification.
In your OAuth provider's dashboard, register your MCP server as an API or resource server. Define the scopes that represent different levels of access. Each scope maps to a set of tools that the token holder is allowed to invoke.
Scopes:
mcp:read - Can invoke read-only tools (recall, search, status)
mcp:write - Can invoke write tools (store, update, forget)
mcp:admin - Can invoke administrative tools (reflect, configure)Create a client application registration for each MCP client that will connect to your server. The client registration includes a client ID, a redirect URI for the OAuth callback, and the allowed scopes. The redirect URI depends on the MCP client implementation; check the client's documentation for the expected callback URL.
Add middleware to your HTTP server that intercepts every MCP request, extracts the bearer token from the Authorization header, validates the token's signature using the provider's JWKS, checks the token's expiration, and extracts the user identity and scopes. Reject requests with missing, invalid, or expired tokens with a 401 response.
import jwt
import requests
from functools import lru_cache
OAUTH_ISSUER = "https://your-provider.auth0.com/"
AUDIENCE = "https://mcp.yourserver.com"
@lru_cache(maxsize=1)
def get_jwks():
url = f"{OAUTH_ISSUER}.well-known/jwks.json"
return requests.get(url).json()
def validate_token(auth_header):
if not auth_header or not auth_header.startswith("Bearer "):
return None
token = auth_header[7:]
jwks = get_jwks()
header = jwt.get_unverified_header(token)
key = None
for k in jwks["keys"]:
if k["kid"] == header["kid"]:
key = jwt.algorithms.RSAAlgorithm.from_jwk(k)
break
if not key:
return None
try:
payload = jwt.decode(
token, key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=OAUTH_ISSUER
)
return payload
except jwt.InvalidTokenError:
return NoneAfter validating the token, check which scopes it contains before allowing a tool invocation. Each tool handler should verify that the calling user has the required scope. Return a 403 response if the token is valid but lacks the required scope for the requested tool.
TOOL_SCOPES = {
"recall": "mcp:read",
"search": "mcp:read",
"status": "mcp:read",
"store": "mcp:write",
"update": "mcp:write",
"forget": "mcp:write",
"reflect": "mcp:admin",
}
def check_scope(token_payload, tool_name):
required = TOOL_SCOPES.get(tool_name, "mcp:admin")
granted = token_payload.get("scope", "").split()
return required in grantedAccess tokens expire, typically after 15 minutes to an hour. When a client receives a 401 response to a previously working token, it should use the refresh token to obtain a new access token without requiring the user to re-authenticate. MCP clients that support OAuth handle this automatically, but your server needs to return consistent 401 responses so the client knows when to refresh.
Make sure your 401 responses include a WWW-Authenticate header that tells the client what went wrong. This helps the client distinguish between an expired token (refresh and retry) and an invalid token (re-authenticate from scratch).
WWW-Authenticate: Bearer realm="mcp",
error="invalid_token",
error_description="Token has expired"Walk through the complete authentication cycle: obtain an authorization code, exchange it for tokens, make an MCP request with the access token, verify scope enforcement, let the token expire, refresh it, and verify the refreshed token works. Use curl or a simple HTTP client to test each step independently before testing through an MCP client.
API Keys vs OAuth
For single-user or small-team deployments where simplicity matters more than enterprise features, API keys are a valid alternative to OAuth. The tradeoff is clear: API keys are simpler to implement (just compare strings) but lack token expiration, per-user scoping, and audit trails that OAuth provides natively.
Use API keys when: you have a small number of known users, you manage key rotation manually, and you do not need fine-grained access control. Use OAuth when: you have many users, you need automatic token lifecycle management, you need different permission levels, or your organization requires audit compliance.
Multi-Tenant Considerations
For servers that serve multiple organizations, each with their own data and access policies, the token's claims should include a tenant identifier. Your tool handlers use this identifier to scope all data access to the correct tenant. This prevents one organization's users from accessing another organization's data through the same MCP server.
Adaptive Recall handles multi-tenancy through API keys that are scoped to individual accounts. Each key maps to an isolated memory store, entity graph, and configuration set. The MCP tools automatically scope all operations to the key's account without any additional configuration from the user.
Skip the auth implementation. Adaptive Recall handles authentication, rate limiting, and multi-tenant isolation out of the box.
Get Started Free