modelcontextprotocol / modelcontextprotocol/csharp-sdk
Interactive OAuth flow is aborted by DiscoverProbeTimeout during dual-path connect, then fails with an authorization state mismatch
@PederHP is already working on this.
Since Aug 20, 2026.
- Dominant language
- C#
- Stars
- 4.5k
- Forks
- 814
- Avg merge
- 9d 19h
- Merged PRs (30d)
- 4
Description
Summary
When connecting to an OAuth-protected server with the default dual-path connect (ProtocolVersion unset), an interactive authorization flow started by the server/discover probe's 401 challenge is silently destroyed by McpClientOptions.DiscoverProbeTimeout (default 5s), and the connect then reliably fails with:
ModelContextProtocol.McpException: Failed to handle unauthorized response with 'Bearer' scheme. The authorization response state did not match the state sent in the authorization request.
Sequence (observed against a production MCP server, SDK 2.2.0)
- The client sends the
server/discoverprobe. The server answers 401, which entersClientOAuthProvider: metadata discovery, DCR, thenAuthorizationCallbackHandleris invoked. The host presents the authorization URL (stateS1) to the user, who starts logging in. - After 5 seconds,
DiscoverProbeTimeoutelapses. This timeout exists for servers that silently drop unknown methods — but here the cancellation propagates throughHandleUnauthorizedResponseAsyncinto the pending interactive flow and aborts it, even though the user is mid-login in a browser the SDK cannot close. - The client falls back to
initialize, which draws another 401 and starts a second interactive flow with a fresh stateS2and PKCE verifier. The host has no new popup to show — the user is still completing flow 1. - The user finishes logging in; the redirect arrives carrying
S1. The second flow expectsS2→ValidateStateResponsethrows the state-mismatch error above. The user-approved authorization code is unusable by flow 2 anyway (different PKCE verifier), so there is no way to recover the flow the user actually completed.
A second, related problem: the whole interactive login is bounded by InitializationTimeout (default 60s), which a real login (with MFA etc.) easily exceeds. That part is at least configurable, but nothing in the docs points hosts at it.
Expected behavior
Canceling the request whose challenge started an interactive authorization should not abort the flow itself — the user-facing browser flow is already in progress and only one flow can ever match the redirect the user completes. A subsequent challenge (from the initialize fallback, or any concurrent request) should join the in-flight flow and reuse its result instead of starting a competing flow the user never sees.
Suggested fix
In ClientOAuthProvider, memoize the in-flight authorization-code flow and detach it from the triggering request's cancellation token (bounding it by provider disposal instead); challenge handlers await it with their own token via Task.WaitAsync. The existing _tokenAcquisitionLock + cached-token re-check already handles the "another caller acquired the token" case once the flow survives.
Docs for DiscoverProbeTimeout / InitializationTimeout should also mention the interactive-authorization interplay.
I have a PR ready with this change plus regression tests (deterministic cancellation-survival test, an end-to-end probe-timeout scenario test, and a transport-dispose cancellation test).
Workaround (current releases)
For interactive connects, set DiscoverProbeTimeout = Timeout.InfiniteTimeSpan and raise InitializationTimeout above the host's authorization window.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.