Repository navigation
Client(mode="auto") falls back to legacy when interactive OAuth exceeds the fixed server/discover timeout #3601
Description
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
What happens
Client(mode="auto") probes server/discover before falling back to the legacy initialize handshake.
ClientSession.send_discover() applies a fixed internal timeout:
DISCOVER_TIMEOUT_SECONDS = 10.0When the first MCP request triggers an interactive OAuth authorization-code flow, that timeout includes the time spent waiting for the user to complete browser authorization.
If authorization takes longer than 10 seconds:
- The client sends a modern
server/discoverrequest. - The server returns
401, and the OAuth flow starts. - The discovery request times out while waiting for the user callback.
mode="auto"falls back to legacyinitialize.- OAuth completes and correctly retries the original authenticated
server/discover. - The retry receives a valid modern
DiscoverResult, but the client has already selected the legacy protocol.
As a result, a modern upstream is exposed as legacy only on cold interactive OAuth flows. Once credentials are cached, the same upstream negotiates 2026-07-28 correctly.
Expected behavior
Interactive OAuth should not cause a modern server to be classified as legacy solely because user authorization took longer than the discovery timeout.
Environment
mcp==2.1.1- Python 3.12
- Streamable HTTP transport
Client(mode="auto")OAuthClientProvider
The current v2.2.0 source still defines DISCOVER_TIMEOUT_SECONDS = 10.0 and uses it directly in send_discover().
Suggested direction
A public configuration point for the discovery timeout would let interactive clients choose an appropriate bound, for example:
Client(
transport,
mode="auto",
discover_timeout_seconds=300.0,
)That value would need to flow into ClientSession.send_discover() instead of relying only on the fixed module-level constant.
Alternatively, the SDK could avoid counting time spent in an interactive OAuth authorization flow against the discovery timeout.
Example Code
import asyncio
import json
import anyio
import httpx2
from mcp.client import Client
from mcp.client.streamable_http import streamable_http_client
class SlowInteractiveOAuth(httpx2.Auth):
"""Models an OAuth flow which takes longer than the 10s discover timeout."""
requires_response_body = True
async def async_auth_flow(self, request: httpx2.Request):
response = yield request
if response.status_code == 401:
# Simulate the user taking 11 seconds to complete browser authorization.
await anyio.sleep(11)
# Retry the exact original request, now authenticated.
request.headers["Authorization"] = "Bearer access-token"
yield request
async def handler(request: httpx2.Request) -> httpx2.Response:
payload = json.loads(request.content)
method = payload.get("method")
request_id = payload.get("id")
if method == "server/discover":
if "authorization" not in request.headers:
return httpx2.Response(
401,
headers={"WWW-Authenticate": "Bearer"},
)
# The authenticated retry is a valid modern response.
return httpx2.Response(
200,
json={
"jsonrpc": "2.0",
"id": request_id,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {},
"ttlMs": 0,
"cacheScope": "private",
},
},
)
if method == "initialize":
return httpx2.Response(
200,
json={
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"serverInfo": {
"name": "legacy-fallback",
"version": "1.0",
},
},
},
)
return httpx2.Response(202)
async def main() -> None:
async with httpx2.AsyncClient(
transport=httpx2.MockTransport(handler),
auth=SlowInteractiveOAuth(),
) as http_client, Client(
streamable_http_client(
"https://mcp.example.test/mcp",
http_client=http_client,
),
mode="auto",
) as client:
print(client.protocol_version)
asyncio.run(main())Expected output:
2026-07-28
Actual output:
2025-11-25
Python & MCP Python SDK
Python 3.12
mcp==2.1.1
Activity
HarshXAI commentedon Oct 1, 2026
Client(mode="auto") gives server/discover a fixed 10s budget, and that clock keeps running while the user is in the browser for OAuth. If login takes longer than 10s the client falls back to the legacy initialize handshake even though discover was the right path.
I'll pause that timeout, or start it only after the OAuth step returns, and add a test where authorization takes longer than DISCOVER_TIMEOUT_SECONDS and mode=auto still finishes the modern handshake.
BenRobinson-hub commentedon Oct 1, 2026
One source-level distinction may be useful before implementation.
I did a bounded read of current
main, and this looks slightly deeper than only “the 10s constant is too small”.Current path is effectively:
The dispatcher arms its response timeout after writing the JSON-RPC request, while Streamable HTTP handles the POST asynchronously. The OAuth provider then awaits the redirect/callback flow before it can retry the original request. So user authorization time is inside the pending
server/discoverresponse budget.When that local 10s budget expires,
JSONRPCDispatcherturns it intoMCPError(REQUEST_TIMEOUT). Thennegotiate_auto()treats almost everyMCPErroras sufficient reason to fall back toinitialize().That seems to collapse two different facts into one decision signal:
In other words, a locally generated timeout is being reused as evidence about the remote server's protocol era.
There is already a useful recovery test for a different topology: a timed-out probe can still lock a slow server modern, then the fallback
initializegets-32022, which triggers a corrective re-probe. But interactive OAuth does not guarantee that corrective signal;initializemay succeed before the authenticateddiscoverfinishes.That suggests three possible fix layers:
REQUEST_TIMEOUT.A bounded question for maintainers: should a locally generated discover timeout ever be considered affirmative evidence for legacy fallback, or should only peer/protocol evidence drive that resolution?
I’m not claiming this issue or starting implementation; @HarshXAI has already indicated intent to work on it. This analysis was materially AI-assisted, source-checked against current main, and human-reviewed before posting.
Jaswanth1902 commentedon Oct 2, 2026