fix(mcp): challenge delegate-auth OAuth servers with upstream resource_metadata - #31255
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 SummaryFixes a regression where
Confidence Score: 5/5Safe to merge — the change is a focused, single-site fix in a shared auth helper with solid before/after test coverage and no impact on the non-delegate code path. The diff is minimal: 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 a resource_metadata= challenge built by the existing _get_passthrough_www_authenticate helper — covering both streamable-HTTP and SSE handlers. |
| tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_stale_session.py | Two tests updated: the former "reaches_session_manager" test now asserts the preemptive 401 + resource_metadata= challenge (the fixed behaviour); the "surfaces_upstream_challenge" test now exercises the authenticated path (token present but rejected) to verify the upstream error propagates. |
Reviews (1): Last reviewed commit: "fix(mcp): challenge delegate-auth OAuth ..." | Re-trigger Greptile
…e_metadata (BerriAI#31255) 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". BerriAI#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 BerriAI#29770 fixed (still no authorization_uri) while restoring the upstream PKCE sign-in prompt.
…e_metadata (#31255) 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.
Regression introduced by #30124 (rolled up in #30202); keeps the original #29770 fix intact
Unauthorized passthrough servers when attempting connection via Third-Party client (eg: claude code) led to 200:ok instead of 401 to trigger authorization.
Relevant issues
Regression introduced by #30124 (rolled up in #30202); keeps the original #29770 fix intact
Linear ticket
Resolves LIT-4019; 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
https://www.loom.com/share/6fe22e05870947d9881511d6fa20201f
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