This document extends Authorization with the design rationale, implementation behavior, usage and integration, test examples, and limitations of multi-protocol authentication in the MCP Python SDK.
References (RFCs and specs):
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 6750 — Bearer Token Usage
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
- RFC 9449 — DPoP (Demonstrating Proof-of-Possession)
- MCP Specification — Model Context Protocol (authorization and transports)
The multi-protocol authorization extension aims to:
- Support multiple auth schemes — Allow a single MCP resource server to accept OAuth 2.0, API Key, and (optionally) Mutual TLS or other protocols, so that clients can choose the most appropriate method (e.g. API Key for automation, OAuth for user-delegated access).
- Preserve backward compatibility — Existing OAuth-only clients and servers (e.g.
OAuthClientProvider,simple-auth/simple-auth-client) continue to work unchanged; multi-protocol support is additive. - Unify discovery and selection — The server declares supported protocols and optional default and preferences; the client discovers them via standard metadata (PRM, WWW-Authenticate) and selects one without hardcoding a single scheme.
- Enable optional DPoP — When using OAuth, the client may bind the access token to a proof-of-possession key (DPoP, RFC 9449) to reduce token theft and replay risk.
- Replacing OAuth — OAuth remains the primary protocol for user-delegated access; API Key and mTLS are alternatives for machine or certificate-based auth.
- Implementing full mTLS in examples — The current examples use an mTLS placeholder (protocol declared, no real client certificate validation) only to demonstrate protocol selection.
- Defining new HTTP auth schemes — The implementation uses existing schemes (Bearer, DPoP, X-API-Key) and extends 401/403 response parameters (e.g.
auth_protocols,resource_metadata) as defined in RFC 9728 and MCP conventions.
Discovery answers: Which auth protocols does this resource support, and where is their metadata?
Sources (in priority order):
- WWW-Authenticate on 401 — The resource server may include
resource_metadata(PRM URL),auth_protocols,default_protocol, andprotocol_preferences(MCP extensions). See RFC 6750 (Bearer) and RFC 9728 (resource metadata). - Protected Resource Metadata (PRM) — RFC 9728 defines
/.well-known/oauth-protected-resource(optionally with a path suffix). PRM JSON includesauthorization_servers; the SDK extends it withmcp_auth_protocols,mcp_default_auth_protocol, andmcp_auth_protocol_preferences. - Unified discovery endpoint —
/.well-known/authorization_serversreturns a list of protocol metadata (MCP-style). The client tries the path-relative URL first, then the root URL (see protocol discovery order below).
Protocol discovery order (priority):
- Priority 1: PRM
mcp_auth_protocols— If the PRM was obtained and containsmcp_auth_protocols, use that list. - Priority 2: Path-relative unified discovery —
{origin}/.well-known/authorization_servers{resource_path}(e.g.http://localhost:8002/.well-known/authorization_servers/mcp). - Priority 3: Root unified discovery —
{origin}/.well-known/authorization_servers. - Priority 4: OAuth fallback — If unified discovery returns no protocol list and the PRM has
authorization_servers, fall back to OAuth protocol discovery.
Client-side logic (high level):
- On 401, extract
resource_metadatafrom WWW-Authenticate. - Build PRM URLs: (1)
resource_metadataif present, (2) path-based/.well-known/oauth-protected-resource{path}, (3) root/.well-known/oauth-protected-resource. Request each in turn until a PRM response is obtained. - For the protocol list: if the PRM has
mcp_auth_protocols, use it (priority 1). Otherwise try path-relative/.well-known/authorization_servers{path}, then root/.well-known/authorization_servers. If both fail and the PRM hasauthorization_servers, use OAuth fallback. - Merge the protocol list with WWW-Authenticate
auth_protocolsif present, then select one viaAuthProtocolRegistry.select_protocol(available, default_protocol, preferences).
There are three distinct URL trees involved:
| Host | Endpoint | Owner | Purpose |
|---|---|---|---|
| Authorization Server (AS) | /.well-known/oauth-authorization-server |
AS | OAuth 2.0 metadata (RFC 8414): authorization_endpoint, token_endpoint, registration_endpoint, etc. |
| Authorization Server (AS) | /authorize, /token, /register, /introspect |
AS | OAuth flows and token introspection |
| MCP Resource Server (RS) | /.well-known/oauth-protected-resource{path} |
RS | Protected Resource Metadata (RFC 9728): resource, authorization_servers, MCP extensions |
| MCP Resource Server (RS) | /.well-known/authorization_servers |
RS | Unified protocol discovery (MCP extension): protocols, default_protocol, protocol_preferences |
| MCP Resource Server (RS) | /{resource_path} (e.g. /mcp) |
RS | Protected MCP endpoint |
OAuth Authorization Server (http://localhost:9000)
├── /.well-known/oauth-authorization-server ← OAuth AS metadata
├── /authorize
├── /token
├── /register
├── /introspect
└── /login, /login/callback ← (example-specific)
MCP Resource Server (http://localhost:8002)
├── /.well-known/oauth-protected-resource/mcp ← PRM (path derived from resource_url)
├── /.well-known/authorization_servers ← Unified discovery (mounted at root)
└── /mcp ← Protected MCP endpoint
- On 401, read
resource_metadatafrom WWW-Authenticate (e.g.http://localhost:8002/.well-known/oauth-protected-resource/mcp). - If absent, try the path-based URL:
{origin}/.well-known/oauth-protected-resource{resource_path}(e.g.http://localhost:8002/.well-known/oauth-protected-resource/mcp). - If still absent, try the root URL:
{origin}/.well-known/oauth-protected-resource. - The PRM includes
authorization_servers(AS URL) and optionallymcp_auth_protocols; for OAuth, the client then fetches{AS}/.well-known/oauth-authorization-server. - For the protocol list, in order: (1) If the PRM has
mcp_auth_protocols, use it. (2) Otherwise try path-relative{origin}/.well-known/authorization_servers{resource_path}(e.g.http://localhost:8002/.well-known/authorization_servers/mcp). (3) Otherwise try root{origin}/.well-known/authorization_servers. (4) If all fail and the PRM hasauthorization_servers, use OAuth fallback.
Auth discovery logging: When discovery runs, the SDK emits debug-level logs with the [Auth discovery] prefix for each PRM and unified-discovery request (URL, status code, and on 200 a pretty-printed response body). Set LOG_LEVEL=DEBUG on the client to enable them. Implemented in mcp.client.auth.utils (format_json_for_logging, handle_protected_resource_response, discover_authorization_servers) and mcp.client.auth.multi_protocol (_parse_protocols_from_discovery_response, async_auth_flow).
References: RFC 9728 (PRM), RFC 8414 (OAuth AS metadata), SDK mcp.client.auth.utils (build_protected_resource_metadata_discovery_urls, discover_authorization_servers).
flowchart LR
subgraph 401
A[401 Response] --> B[Extract WWW-Authenticate]
B --> C[resource_metadata?]
end
C --> D[PRM discovery]
D --> E[Try resource_metadata URL]
E --> F[Try path-based well-known]
F --> G[Try root well-known]
G --> H[PRM obtained]
H --> I{PRM.mcp_auth_protocols?}
I -->|Yes| N[Select protocol]
I -->|No| J[Path-relative unified discovery]
J --> K{200 + protocols?}
K -->|Yes| N
K -->|No| L[Root unified discovery]
L --> M{200 + protocols?}
M -->|Yes| N
M -->|No| O{PRM.authorization_servers?}
O -->|Yes| P[OAuth fallback]
O -->|No| Q[Fail]
P --> N
The client uses MultiProtocolAuthProvider (httpx.Auth) to prepare each HTTP request and to handle 401/403 in one place.
Main flow:
- Initialization — On first use,
_initialize()runs (e.g. to register protocol classes). No network calls are made at this stage. - Before first request — Read credentials from
TokenStorage(get_tokens→AuthCredentials | OAuthToken). If credentials are present and valid (protocol.validate_credentials), callprotocol.prepare_request(request, credentials)and, if DPoP is enabled and the protocol supports it, attach a DPoP proof; then yield the request. - On 401 — (See discovery above.) After obtaining the protocol list and selecting a protocol:
- If OAuth2: Build an
OAuthClientProviderfrom config and drive the shared oauth_401_flow_generator (AS discovery, registration, authorization, and token exchange are performed by yielding requests that httpx sends and whose responses are injected back; no separate HTTP client is used, which avoids deadlock). OAuth2 supports two grant types in this flow: authorization_code (redirect/callback) and client_credentials (machine-to-machine). For client_credentials, the client supplies fixed_client_info (client_id, client_secret), so no dynamic registration is performed; the provider calls the token endpoint withgrant_type=client_credentials. - If API Key or other: Build
AuthContext, callprotocol.authenticate(context), store the returned credentials, then retry the original request withprepare_request.
- If OAuth2: Build an
- On 403 — The client parses
errorandscopefrom WWW-Authenticate for logging; the current implementation does not retry automatically. - TokenStorage contract — The storage backend may return
OAuthTokenorAuthCredentials. The provider converts OAuthToken → OAuthCredentials when reading and OAuthCredentials → OAuthToken when writing, so that backends that support only OAuthToken remain usable.
Protocol selection — AuthProtocolRegistry.select_protocol(available_protocols, default_protocol, protocol_preferences) restricts the choice to registered protocols, then applies the default protocol and the preference order (lower numeric value denotes higher priority).
References: mcp.client.auth.multi_protocol (MultiProtocolAuthProvider, async_auth_flow), mcp.client.auth._oauth_401_flow (oauth_401_flow_generator), mcp.client.auth.registry (AuthProtocolRegistry), mcp.client.auth.protocol (AuthProtocol, DPoPEnabledProtocol).
sequenceDiagram
participant HTTP as httpx
participant Provider as MultiProtocolAuthProvider
participant Registry as AuthProtocolRegistry
participant Protocol as AuthProtocol
participant Storage as TokenStorage
HTTP->>Provider: async_auth_flow(request)
Provider->>Storage: get_tokens()
alt has valid credentials
Provider->>Protocol: prepare_request(request, creds)
Provider->>HTTP: yield request
HTTP->>Provider: response
end
alt response 401
Provider->>HTTP: yield PRM request(s)
HTTP->>Provider: PRM response
Provider->>HTTP: yield discovery request
HTTP->>Provider: discovery response
Provider->>Registry: select_protocol(...)
alt OAuth2
Provider->>HTTP: yield OAuth requests (gen)
HTTP->>Provider: OAuth responses
else API Key / other
Provider->>Protocol: authenticate(context)
Protocol->>Storage: set_tokens(creds)
Provider->>HTTP: yield retry request
end
end
The server exposes protected MCP endpoints and declares supported auth methods via PRM and (optionally) unified discovery; credentials are verified on each request.
Routes and metadata:
- Protected Resource Metadata (PRM) —
create_protected_resource_routes(resource_url, authorization_servers, ..., auth_protocols, default_protocol, protocol_preferences)registers/.well-known/oauth-protected-resource{path}(RFC 9728). The handler returns JSON includingresource,authorization_servers, and MCP extensionsmcp_auth_protocols,mcp_default_auth_protocol,mcp_auth_protocol_preferences. - Unified discovery —
create_authorization_servers_discovery_routes(protocols, default_protocol, protocol_preferences)registers/.well-known/authorization_servers. The handler returns{ "protocols": [ AuthProtocolMetadata, ... ] }plus optional default and preferences. - 401 responses — Middleware (e.g. RequireAuthMiddleware) returns 401 with WWW-Authenticate including at least Bearer (and optionally
resource_metadata,auth_protocols,default_protocol,protocol_preferences).
| Item | Description |
|---|---|
/.well-known/oauth-authorization-server |
Must expose (RFC 8414). Returns JSON with authorization_endpoint, token_endpoint, and optionally registration_endpoint, scopes_supported. |
/authorize, /token |
Must implement — OAuth authorization code flow with PKCE. |
/register |
Optional — dynamic client registration. |
/introspect |
Required if the RS uses introspection — The RS calls this endpoint to validate Bearer/DPoP tokens. |
| DPoP | If DPoP is used, tokens must include cnf (e.g. jkt) so the RS can verify the DPoP proof. |
No changes to the AS are required for multi-protocol itself; the AS need only support standard OAuth 2.0 and (optionally) DPoP-bound tokens.
| Item | Description |
|---|---|
resource_url |
Base URL of the protected resource (e.g. http://localhost:8002/mcp). Used to build the PRM path: /.well-known/oauth-protected-resource{path}. |
authorization_servers |
List of AS URLs (e.g. ["http://localhost:9000"]). PRM references these so that clients know where to obtain tokens. |
auth_protocols |
List of AuthProtocolMetadata (protocol_id, protocol_version, metadata_url for OAuth, etc.). |
default_protocol |
Optional default protocol ID (e.g. "oauth2"). |
protocol_preferences |
Optional priority map (e.g. {"oauth2": 1, "api_key": 2}). |
| PRM route | Mount create_protected_resource_routes(...) so that /.well-known/oauth-protected-resource{path} is served. The path is derived from resource_url (e.g. /mcp → /.well-known/oauth-protected-resource/mcp). |
| Unified discovery route | Mount create_authorization_servers_discovery_routes(...) so that /.well-known/authorization_servers is served. Serving at origin root (e.g. http://localhost:8002/.well-known/authorization_servers) is recommended so that clients that try path-relative discovery first can fall back when that endpoint returns 404. |
| WWW-Authenticate on 401 | Include resource_metadata (PRM URL), auth_protocols, default_protocol, and protocol_preferences so that MCP clients can discover protocols without additional requests. |
Example config (simple-auth-multiprotocol):
# Resource server settings
server_url = "http://localhost:8002/mcp"
auth_server_url = "http://localhost:9000"
auth_protocols = [
AuthProtocolMetadata(protocol_id="oauth2", protocol_version="2.0",
metadata_url=f"{auth_server_url}/.well-known/oauth-authorization-server",
scopes_supported=["user"]),
AuthProtocolMetadata(protocol_id="api_key", protocol_version="1.0"),
AuthProtocolMetadata(protocol_id="mutual_tls", protocol_version="1.0"),
]
default_protocol = "oauth2"
protocol_preferences = {"oauth2": 1, "api_key": 2, "mutual_tls": 3}
# PRM URL: http://localhost:8002/.well-known/oauth-protected-resource/mcp
# Discovery URL: http://localhost:8002/.well-known/authorization_serversEnvironment / config (simple-auth-multiprotocol example):
| Env / CLI | Description |
|---|---|
--port |
RS port (default 8002). |
--auth-server |
AS base URL (e.g. http://localhost:9000). Used for authorization_servers and OAuth metadata_url. |
--api-keys |
Comma-separated API keys for APIKeyVerifier. |
--dpop-enabled |
Enable DPoP proof verification. |
server_url |
Derived as http://{host}:{port}/mcp; used for PRM path and 401 resource_metadata. |
Verification:
- MultiProtocolAuthBackend — Holds a list of
CredentialVerifierinstances. For each request it callsverifier.verify(request, dpop_verifier)in order; the first successful (non-None) result is used. - OAuthTokenVerifier — Reads
Authorization: Bearer <token>orAuthorization: DPoP <token>. Verifies the token (e.g. via introspection); if the token is DPoP-bound anddpop_verifieris set, it validates the DPoP proof (method, URI,ath, jti replay). See RFC 9449. - APIKeyVerifier — Reads
X-API-Keyfirst, then falls back toAuthorization: Bearer <key>and checks the value againstvalid_keys. It does not parse anApiKeyscheme. - DPoP — When enabled, the backend is constructed with a
DPoPProofVerifierand passes it into each verifier. The verifier uses it only when the token is DPoP-bound (e.g.Authorization: DPoPwith a valid proof).
References: mcp.server.auth.routes (create_protected_resource_routes, create_authorization_servers_discovery_routes), mcp.server.auth.verifiers (MultiProtocolAuthBackend, OAuthTokenVerifier, APIKeyVerifier), mcp.server.auth.dpop (DPoPProofVerifier).
flowchart TB
subgraph Request
R[Incoming request]
end
R --> Backend[MultiProtocolAuthBackend.verify]
Backend --> V1[OAuthTokenVerifier.verify]
V1 --> DPoP{DPoP header?}
DPoP -->|Yes| DPoPVerify[DPoPProofVerifier.verify]
DPoP -->|No| TokenCheck[Token valid?]
DPoPVerify --> TokenCheck
TokenCheck -->|OK| OAuthOK[Return AccessToken]
TokenCheck -->|Fail| V2[APIKeyVerifier.verify]
V2 --> ApiKey{X-API-Key or Bearer in valid_keys?}
ApiKey -->|Yes| ApiKeyOK[Return AccessToken]
ApiKey -->|No| V3[Next verifier / None]
V3 --> None[401 Unauthorized]
When the resource server uses OAuth 2.0 as one of the protocols:
- Expose OAuth 2.0 metadata — RFC 8414:
/.well-known/oauth-authorization-server(or/.well-known/openid-configurationif applicable). Must includeauthorization_endpoint,token_endpoint, and optionallyregistration_endpoint,scopes_supported. - Support authorization code + PKCE — Authorization endpoint, token endpoint, and (optional) dynamic client registration. MCP clients use authorization code with PKCE and optionally DPoP-bound tokens.
- Client credentials grant (optional) — If the resource server advertises OAuth2 and clients may use the client_credentials grant (e.g. machine-to-machine), the AS must include
client_credentialsingrant_types_supportedin its metadata and implement the token endpoint forgrant_type=client_credentials. - Token introspection (if used) — The resource server may call the AS introspection endpoint to validate Bearer/DPoP tokens. For DPoP-bound tokens, the AS must include
cnf(e.g.jkt) in the token or introspection response so the RS can verify the DPoP proof (RFC 9449).
No changes to the AS are required for multi-protocol itself; the AS need only support the OAuth flows and (if DPoP is used) token binding.
- Choose auth model — Use a single protocol (e.g. only OAuth via
OAuthClientProvider) or multi-protocol viaMultiProtocolAuthProvider. - Register protocols — Call
AuthProtocolRegistry.register(protocol_id, ProtocolClass)for each supported protocol (e.g.oauth2,api_key) before creating the provider. - Storage — Provide a
TokenStoragethat implementsget_tokens()→AuthCredentials | OAuthToken | Noneandset_tokens(AuthCredentials | OAuthToken). For OAuth-only storage, the provider converts to/from OAuthToken internally; or use an adapter. - Provider configuration — Construct
MultiProtocolAuthProviderwith storage, optionaldpop_enabled, optionaldpop_storage, and (for 401 flows) anhttp_clientthat will be used to send yielded requests. Attach the provider ashttpx.Client(auth=provider). - Environment / config — For the example client:
MCP_SERVER_URL,MCP_API_KEY(API Key),MCP_USE_OAUTH=1,MCP_DPOP_ENABLED=1(OAuth + DPoP). Protocol selection is determined by server discovery and the registry.
- Define protocols — Build a list of
AuthProtocolMetadata(protocol_id, protocol_version, metadata_url for OAuth, etc.) and optionallydefault_protocolandprotocol_preferences. - Mount PRM — Call
create_protected_resource_routes(resource_url, authorization_servers, ..., auth_protocols=auth_protocols, default_protocol=..., protocol_preferences=...)and mount the returned routes so that/.well-known/oauth-protected-resource{path}serves PRM JSON (RFC 9728 + MCP extensions). - Mount unified discovery (optional) — Call
create_authorization_servers_discovery_routes(protocols, default_protocol, protocol_preferences)and mount the returned routes so that/.well-known/authorization_serversreturns the protocol list. - Build backend — Instantiate
OAuthTokenVerifier,APIKeyVerifier, and (if needed) other verifiers; pass them intoMultiProtocolAuthBackend. If DPoP is used, create aDPoPProofVerifierand pass it intobackend.verify(request, dpop_verifier=...). - 401/403 responses — Use middleware that returns 401 with WWW-Authenticate (Bearer at minimum; add
resource_metadata,auth_protocols,default_protocol,protocol_preferencesfor MCP clients). Optionally return 403 witherrorandscopewhen appropriate.
Client-side protocol interface. All auth protocols (OAuth2, API Key, etc.) must implement it.
| Member | Type | Description |
|---|---|---|
protocol_id |
str |
Protocol identifier (e.g. "oauth2", "api_key"). |
protocol_version |
str |
Protocol version (e.g. "2.0", "1.0"). |
authenticate(context) |
async def |
Perform auth; return AuthCredentials. |
prepare_request(request, credentials) |
def |
Add auth headers (e.g. X-API-Key, Authorization: Bearer ...). |
validate_credentials(credentials) |
def |
Return True if credentials are still valid. |
discover_metadata(metadata_url, prm, http_client) |
async def |
Optional; return protocol metadata from server. |
AuthContext — Input to authenticate; includes server_url, storage, protocol_id, protocol_metadata, current_credentials, dpop_storage, dpop_enabled, http_client, resource_metadata_url, protected_resource_metadata, scope_from_www_auth.
DPoPEnabledProtocol — Extends AuthProtocol; adds supports_dpop(), get_dpop_proof_generator(), initialize_dpop() for DPoP-bound tokens.
Server-side verifier interface. Each verifier validates a single auth scheme.
| Member | Type | Description |
|---|---|---|
verify(request, dpop_verifier) |
async def |
Inspect request; return AccessToken on success, None on failure. |
Implementations:
- OAuthTokenVerifier — Reads
Authorization: Bearer <token>orAuthorization: DPoP <token>. Verifies token (e.g. introspection); if DPoP-bound anddpop_verifieris set, validates DPoP proof. - APIKeyVerifier — Reads
X-API-Keyfirst, thenAuthorization: Bearer <key>if value is invalid_keys. Constructor:APIKeyVerifier(valid_keys: set[str], scopes: list[str] | None = None).
MultiProtocolAuthBackend — Holds a list of CredentialVerifier instances; calls them in order; the first successful (non-None) result is used.
| Method | Signature | Description |
|---|---|---|
get_tokens() |
async def → AuthCredentials | OAuthToken | None |
Return stored credentials. |
set_tokens(tokens) |
async def |
Store AuthCredentials or OAuthToken. |
For storage backends that support only OAuthToken, the provider converts between OAuthToken and OAuthCredentials internally; no adapter is required.
If you use OAuthClientProvider or simple-auth-client and want to add multi-protocol support (e.g. API Key or OAuth + DPoP):
OAuthClientProvider,simple-auth, andsimple-auth-clientcontinue to work as before.- No code changes are required if you use only OAuth.
Before (OAuth only):
from mcp.client.auth.oauth2 import OAuthClientProvider
provider = OAuthClientProvider(...)
client = httpx.AsyncClient(auth=provider)After (multi-protocol):
from mcp.client.auth.multi_protocol import MultiProtocolAuthProvider, TokenStorage
from mcp.client.auth.registry import AuthProtocolRegistry
from mcp.client.auth.protocols.oauth2 import OAuth2Protocol
# Register protocols before creating the provider
AuthProtocolRegistry.register("oauth2", OAuth2Protocol)
# If using API Key: AuthProtocolRegistry.register("api_key", ApiKeyProtocol)
provider = MultiProtocolAuthProvider(
storage=your_storage, # must support AuthCredentials | OAuthToken
dpop_enabled=False, # set True for DPoP
)
client = httpx.AsyncClient(auth=provider)
# Pass http_client to provider if needed for 401 flows:
provider._http_client = clientIf your storage only handles OAuthToken:
- No change required; the provider converts internally.
- Alternatively, use
OAuthTokenStorageAdapterto wrap storage that supports only OAuthToken.
If you add API Key:
async def get_tokens(self) -> AuthCredentials | OAuthToken | None:
return self._creds # may be OAuthToken or APIKeyCredentials
async def set_tokens(self, tokens: AuthCredentials | OAuthToken) -> None:
self._creds = tokensBefore (OAuth only):
# Single OAuth verifier
token_verifier = TokenVerifier(...)
oauth_verifier = OAuthTokenVerifier(token_verifier)
# BearerAuthBackend or equivalentAfter (multi-protocol):
from mcp.server.auth.verifiers import (
MultiProtocolAuthBackend,
OAuthTokenVerifier,
APIKeyVerifier,
)
from mcp.server.auth.routes import (
create_protected_resource_routes,
create_authorization_servers_discovery_routes,
)
from mcp.shared.auth import AuthProtocolMetadata
# Build protocol list
auth_protocols = [
AuthProtocolMetadata(protocol_id="oauth2", protocol_version="2.0", metadata_url=as_url, ...),
AuthProtocolMetadata(protocol_id="api_key", protocol_version="1.0"),
]
# Verifiers
oauth_verifier = OAuthTokenVerifier(token_verifier)
api_key_verifier = APIKeyVerifier(valid_keys={"demo-api-key-12345"})
backend = MultiProtocolAuthBackend([oauth_verifier, api_key_verifier])
# PRM with MCP extensions
prm_routes = create_protected_resource_routes(
resource_url=resource_url,
authorization_servers=[as_url],
auth_protocols=auth_protocols,
default_protocol="oauth2",
)
# Optional: unified discovery
discovery_routes = create_authorization_servers_discovery_routes(
protocols=auth_protocols,
default_protocol="oauth2",
)
# Mount prm_routes and discovery_routesEnsure 401 WWW-Authenticate includes (when using multi-protocol):
resource_metadata— URL of PRM (e.g./.well-known/oauth-protected-resource/mcp)auth_protocols— Space-separated protocol IDs (e.g.oauth2 api_key)default_protocol— Optional default (e.g.oauth2)protocol_preferences— Optional priorities (e.g.oauth2:1,api_key:2)
See RequireAuthMiddleware and PRM handler in mcp.server.auth for how these are set.
Script: ./examples/clients/simple-auth-multiprotocol-client/run_multiprotocol_test.sh
Quick start (from repo root):
# API Key (non-interactive, default)
./examples/clients/simple-auth-multiprotocol-client/run_multiprotocol_test.sh
# OAuth (interactive — complete authorization in browser)
MCP_AUTH_PROTOCOL=oauth ./examples/clients/simple-auth-multiprotocol-client/run_multiprotocol_test.sh
# Mutual TLS placeholder (expect "not implemented" error)
MCP_AUTH_PROTOCOL=mutual_tls ./examples/clients/simple-auth-multiprotocol-client/run_multiprotocol_test.shThe script starts the multi-protocol RS on port 8002 (and AS on 9000 for OAuth), waits for PRM readiness, then runs the client with the selected protocol. For api_key and mutual_tls, the script is fully automated and prints PASS/FAIL. For oauth, the user completes OAuth in the browser, then runs list, call get_time {}, quit.
Env variables: MCP_RS_PORT (default 8002), MCP_AS_PORT (default 9000), MCP_AUTH_PROTOCOL (default api_key), MCP_SKIP_OAUTH=1 (skip manual OAuth test).
Demonstrates: PRM and optional unified discovery, protocol selection (API Key vs OAuth), and API Key authentication without an AS.
Script: ./examples/clients/simple-auth-multiprotocol-client/run_dpop_test.sh
Quick start (from repo root):
# Automated tests only (no browser)
MCP_SKIP_OAUTH=1 ./examples/clients/simple-auth-multiprotocol-client/run_dpop_test.sh
# Full test including manual OAuth+DPoP (requires browser)
./examples/clients/simple-auth-multiprotocol-client/run_dpop_test.shThe script starts AS on port 9000 and RS on port 8002 with --dpop-enabled, then runs automated curl tests:
- API Key request → 200 (DPoP does not affect API Key).
- Bearer token without DPoP proof → 401 (RS requires DPoP when token is DPoP-bound).
- Negative: fake token, wrong htm/htu, DPoP without Authorization → 401.
When MCP_SKIP_OAUTH is not set, the script also runs a manual OAuth+DPoP client test: the user completes OAuth in the browser, then runs list, call get_time {}, quit. Server logs should show "Authentication successful with DPoP".
Env variables: MCP_RS_PORT (default 8002), MCP_AS_PORT (default 9000), MCP_SKIP_OAUTH=1 (skip manual OAuth+DPoP test).
Demonstrates: DPoP proof verification on the server, rejection of Bearer tokens without a proof when DPoP is required, and a successful OAuth+DPoP flow with the example client.
Script: ./examples/clients/simple-auth-multiprotocol-client/run_oauth2_test.sh
Quick start (from repo root):
./examples/clients/simple-auth-multiprotocol-client/run_oauth2_test.shStarts the simple-auth AS and RS (OAuth-only, no multi-protocol), then runs simple-auth-client. The user completes OAuth in the browser, then runs list, call get_time {}, quit. Verifies that the existing OAuth-only path still works unchanged.
| Case | Auth type | Expected result |
|---|---|---|
| B2 | API Key | 200 (DPoP irrelevant) |
| A2 | Bearer, no DPoP | 401 when RS expects DPoP |
| A1 | OAuth + DPoP | 200 after browser OAuth |
| — | No auth | 401 |
| — | DPoP proof, fake token / wrong htm or htu | 401 |
- Mutual TLS — Implemented as a placeholder only: the protocol is advertised and selectable, but the example server does not perform client certificate validation. A full mTLS implementation would require TLS client certificate handling and a verifier that validates the certificate.
- Unified discovery URL — The client tries
/.well-known/authorization_serversin path-relative form (e.g.http://host:8002/.well-known/authorization_servers/mcp) then at the origin root (e.g.http://host:8002/.well-known/authorization_servers). Servers may expose only one of these; the ordered try and fallback to PRM’smcp_auth_protocolsaccommodate both. - 403 handling — The client parses 403 WWW-Authenticate for logging but does not retry automatically with a new scope or token; behavior could be extended for specific error or scope values.
- DPoP nonce — Server-side DPoP nonce (RFC 9449) is not yet implemented in the example; only jti replay protection is in place. Adding nonce would improve robustness against pre-replay.
- TokenStorage — The dual contract (OAuthToken vs AuthCredentials) and in-memory conversion are documented; a formal adapter type or storage interface versioning could simplify integration for new backends.
- Full mTLS example — Add client certificate validation and a verifier that maps the client certificate to an identity and scope.
- Discovery flexibility — Allow configurable discovery URL templates or multiple well-known paths so that both path-relative and origin-relative discovery work without relying solely on PRM fallback.
- 403 retry policy — Define retry rules for 403 (e.g.
insufficient_scope) and integrate with OAuth scope refresh or re-authorization. - DPoP nonce — Implement server-initiated nonce and client nonce handling per RFC 9449.
Related documentation: Authorization (overview), API Reference.
Examples: simple-auth-multiprotocol (includes the server variants prm_only, path_only, root_only, and oauth_fallback for testing each discovery path), simple-auth-multiprotocol-client, examples/README.md.