Skip to content

Allow OAuthClientProvider to accept a pre-configured auth server URL #2121

@martimfasantos

Description

@martimfasantos

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 token

Steps 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_url at 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 init

In 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_url

In 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 featuresclient_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

  • OAuthClientProvidermcp/client/auth/oauth2.py line 217
  • OAuthContext.auth_server_url — already exists as a field (line ~107), just populated from PRM today
  • async_auth_flow — the 401 branch starts at line ~504
  • MCP spec — Authorization
  • RFC 9728 — Protected Resource Metadata
  • RFC 8414 — OAuth Authorization Server Metadata

Metadata

Metadata

Assignees

Labels

authIssues and PRs related to Authentication / OAuthenhancementRequest for a new feature that's not currently supportedneeds confirmationNeeds confirmation that the PR is actually required or needed.

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions