-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Description
Description
Problem
OAuthClientProvider.async_auth_flow in mcp/client/auth/oauth2.py only triggers OAuth discovery when it receives a 401 response. There's no way to tell the client "I already know where the auth server is — skip straight to OASM discovery and token exchange."
This forces an unnecessary round-trip in environments where the authorization server URL is already known (config file, env var, service registry, etc.).
Current behavior
In OAuthClientProvider.__init__:
OAuthClientProvider(
server_url="http://mcp-server:3002/mcp",
client_metadata=OAuthClientMetadata(...),
storage=my_storage,
redirect_handler=...,
callback_handler=...,
)Then inside async_auth_flow (line ~500):
response = yield request # unauthenticated
if response.status_code == 401:
# Only NOW does it start discovery:
# 1. Extract WWW-Authenticate
# 2. Discover PRM (/.well-known/oauth-protected-resource)
# 3. Set self.context.auth_server_url from PRM
# 4. Discover OASM (/.well-known/oauth-authorization-server)
# 5. Register client (DCR or CIMD)
# 6. Authorization code + PKCE
# 7. Retry with tokenSteps 1–3 exist purely to resolve auth_server_url. When the caller already has that URL, they're wasted work.
Proposed change
Add an optional authorization_server_url parameter to OAuthClientProvider.__init__:
OAuthClientProvider(
server_url="http://mcp-server:3002/mcp",
client_metadata=OAuthClientMetadata(...),
storage=my_storage,
redirect_handler=...,
callback_handler=...,
authorization_server_url="http://auth-server:9000", # NEW
)When provided:
- Set
self.context.auth_server_urlat init time - In
async_auth_flow, skip the initial unauthenticated request and jump directly to OASM fetch → client registration → token exchange → send authenticated request
When omitted, behavior stays identical to today.
Rough implementation sketch
In OAuthContext:
@dataclass
class OAuthContext:
# ... existing fields ...
auth_server_url: str | None = None # already exists, just needs to be settable at initIn OAuthClientProvider.__init__:
def __init__(self, ..., authorization_server_url: str | None = None):
self.context = OAuthContext(...)
if authorization_server_url:
self.context.auth_server_url = authorization_server_urlIn async_auth_flow, before response = yield request:
if self.context.auth_server_url and not self.context.is_token_valid():
# Auth server already known — go straight to OASM + token exchange
# (skip sending unauthenticated request and waiting for 401)
...Why this matters
- Latency — One fewer round-trip before the client can do real work.
- Controlled environments — Enterprise setups always know their auth server. Discovery via 401 adds no value there.
- Complements existing features —
client_metadata_url(CIMD) already lets you skip DCR. This is the same idea applied one step earlier in the chain.
Alternatives considered
| Approach | Limitation |
|---|---|
CIMD (client_metadata_url) |
Skips DCR (step 5), but the 401 trigger + PRM discovery still happens |
Pre-populating TokenStorage |
We don't necessarily manage the TokenStorage |
Using httpx.Auth with a static Bearer token |
Works, but loses the full OAuth lifecycle (refresh, re-auth, etc.) |
References
OAuthClientProvider—mcp/client/auth/oauth2.pyline 217OAuthContext.auth_server_url— already exists as a field (line ~107), just populated from PRM todayasync_auth_flow— the 401 branch starts at line ~504- MCP spec — Authorization
- RFC 9728 — Protected Resource Metadata
- RFC 8414 — OAuth Authorization Server Metadata