fix(mcp): challenge delegate-auth OAuth servers with upstream resource_metadata - #31250
fix(mcp): challenge delegate-auth OAuth servers with upstream resource_metadata#31250tin-berri wants to merge 1 commit into
Conversation
…e_metadata An oauth2 MCP server with delegate_auth_to_upstream=true never prompted the user to sign in. On an unauthenticated initialize the gateway answered locally (200, no tools) and emitted no WWW-Authenticate, so clients like Claude Desktop either connected empty or hit "OAuth probe timeout after 10000ms". #30124 added a bare `continue` in _raise_preemptive_401_for_unauthenticated_servers to stop sending LiteLLM's gateway authorization_uri challenge for delegate-auth servers, expecting the upstream to emit its own challenge. On initialize the gateway never probes upstream, so no challenge ever reached the client. Replace the `continue` with a preemptive 401 carrying the proxied resource_metadata (RFC 9728) challenge, the same form passthrough servers and MCPUpstreamAuthError already use. This keeps #29770 fixed (still no authorization_uri) while restoring the upstream PKCE sign-in prompt.
Greptile SummaryThis PR fixes a regression in the MCP delegate-auth OAuth flow where unauthenticated
Confidence Score: 5/5Safe to merge — a tightly scoped two-file change that restores a missing 401 challenge for an edge-case auth path without touching any shared or unrelated infrastructure. The production change is a single-branch swap: one No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/server.py | Replaces the bare continue for delegate-auth servers in _raise_preemptive_401_for_unauthenticated_servers with a preemptive 401 carrying the correct resource_metadata= challenge; change is minimal and correctly scoped to the no-stored-token + delegate_auth branch |
| tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py | Tests updated to reflect corrected behavior: no-token path now asserts 401+resource_metadata challenge (session manager never reached); authenticated path correctly uses session_manager_stateful with an initialize body matching actual routing logic |
Reviews (1): Last reviewed commit: "fix(mcp): challenge delegate-auth OAuth ..." | Re-trigger Greptile
Greptile SummaryFixes a regression where
Confidence Score: 5/5Safe to merge — the change is a one-site, 12-line replacement of a The server change is minimal and precisely targeted: it replaces a silent skip with a challenge that reuses No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/server.py | Replaces the bare continue for delegate_auth_to_upstream OAuth2 servers in _raise_preemptive_401_for_unauthenticated_servers with a proper 401 carrying a resource_metadata= challenge built by _get_passthrough_www_authenticate, matching the form already used by passthrough servers and MCPUpstreamAuthError. The change is minimal (12 lines), targets the exact regression site, and the shared helper ensures both streamable-HTTP and SSE paths are covered. |
| tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py | Two tests updated to match the corrected behavior: (1) test_handle_streamable_http_mcp_delegated_server_without_token_returns_preemptive_resource_metadata_401 replaces the old test that locked in the buggy skip-to-session-manager path; it now asserts the 401 + resource_metadata= challenge and that the session manager is never reached. (2) test_handle_streamable_http_mcp_delegated_server_surfaces_upstream_challenge is re-focused to the authenticated path (token present but rejected by upstream), verifying the upstream challenge is surfaced. Both tests mock network IO and make no real network calls. |
Reviews (2): Last reviewed commit: "fix(mcp): challenge delegate-auth OAuth ..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Relevant issues
Regression introduced by #30124 (rolled up in #30202); keeps the original #29770 fix intact
Linear ticket
N/A; reported from Claude Desktop DCR ("OAuth probe timeout after 10000ms" on a delegate-auth MCP server)
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
make test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewCI (LiteLLM team)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Screenshots / Proof of Fix
An oauth2 MCP server with
delegate_auth_to_upstream: true(interactive, notclient_credentials) where the user has not signed in yet. Config used for the runBefore (bug): unauthenticated initialize connects with no challenge
No
www-authenticateheader. The gateway answersinitializeitself and never probes upstream, so the client (Claude Desktop) treats the server as not requiring OAuth; it shows "connected" with no tools and never opens the sign-in page, or it times out on the OAuth probeAfter (fixed): unauthenticated initialize returns the upstream RFC 9728 challenge
The challenge carries
resource_metadata=(the upstream-delegation form), not LiteLLM'sauthorization_uri=. Following that chain lands the client on the upstream IdP, proxied by the gatewayClaude Desktop now follows this to the upstream authorize page and signs in. An authenticated retry (token present) skips the preemptive check unchanged and reaches the session manager, where a token the upstream rejects still surfaces the upstream's own challenge via
MCPUpstreamAuthErrorType
🐛 Bug Fix
Changes
_raise_preemptive_401_for_unauthenticated_serversskipped the challenge entirely fordelegate_auth_to_upstreamoauth2 servers with a barecontinue(added in #30124 to avoid emitting the wrongauthorization_uri=form). Because the gateway answersinitializelocally and only contacts upstream ontools/listortools/call, no challenge was ever produced for the request that matters, so MCP clients got no sign-in promptThe
continueis replaced with a preemptive 401 carrying the proxiedresource_metadata=challenge built by_get_passthrough_www_authenticate, the same form pass-through servers andMCPUpstreamAuthError.to_http_exceptionalready emit. This restores the upstream PKCE prompt without reintroducing theauthorization_uri=form that #29770 removed. The fix lives in the shared helper, so both the streamable-HTTP and SSE handlers are coveredTests in
test_mcp_stale_session.pyare updated to match: the former..._reaches_session_managertest (which locked in the skipped challenge) becomes a regression test asserting the 401 +resource_metadatachallenge and that the session manager is never reached; the..._surfaces_upstream_challengetest now exercises the authenticated path, where a present-but-rejected token reaches the session manager and the upstream challenge is surfaced