feat(mcp)!: extend keyless gateway OAuth flow to per-server MCP URL paths - #34856
Conversation
The keyless flow (gateway as authorization server, no virtual key) worked
only at the aggregate /mcp scope: the session-bearer admission arm was
gated on _is_aggregate_mcp_scope, the 401 fallback only challenged at
aggregate scope, and per-server protected-resource metadata for plain
oauth2 servers pointed clients at the per-server relay, whose flow
returns the raw upstream token that ingress can never accept keylessly
(401 "LiteLLM Virtual Key expected. Received=gho_****").
Per-server spellings now join the same gateway flow for gateway-managed
oauth2 servers (auth_type oauth2 without delegate_auth_to_upstream, new
MCPServer.is_gateway_managed_oauth2 owner):
- the session-bearer arm admits at any MCP scope; downstream grant
resolution already intersects the admitted subject's servers with the
path or header targets fail-closed, so a narrower scope never broadens
- the 401 challenge is scope-aware: a single gateway-managed oauth2 path
target gets the per-server resource_metadata in the spelling the
request used, everything else gets the aggregate document; unknown
names, CSV multi-target paths, and every client-forwarded or delegated
mode keep their existing behavior
- per-server PRM for explicitly named gateway-managed oauth2 servers
advertises the gateway AS ({base}/mcp); delegate, passthrough, bridge,
OBO, and the root-resolved unnamed shape are byte-identical
- the preemptive 401 for an admitted keyless subject with no vaulted
token challenges with resource_metadata (re-entering the gateway flow,
whose authorize interlude vaults the upstream token) instead of the
relay authorization_uri, which cannot vault without a litellm key
The per-server challenge URL builder moved from server.py to
oauth_utils.py (shared with the auth module) and now inserts the
SERVER_ROOT_PATH segment exactly as the discovery routes do.
Resolves LIT-4864
Greptile SummaryExtends keyless gateway OAuth support to per-server MCP routes.
Confidence Score: 5/5The PR appears safe to merge, with no concrete changed-code failure identified. The changed admission, discovery, and challenge paths consistently preserve target parsing, delegated-mode separation, route spelling, and fail-closed OAuth behavior.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py | Broadens session-bearer admission and emits scope-aware OAuth challenges while retaining fail-closed target parsing. |
| litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py | Makes explicitly named gateway-managed OAuth2 resources advertise the aggregate gateway authorization server. |
| litellm/proxy/_experimental/mcp_server/oauth_utils.py | Centralizes protected-resource challenge URL construction with root-path and legacy-route support. |
| litellm/proxy/_experimental/mcp_server/server.py | Challenges admitted keyless subjects through per-server resource metadata when no stored upstream token exists. |
| litellm/types/mcp_server/mcp_server_manager.py | Adds a shared predicate identifying OAuth2 servers whose credential custody is gateway-managed. |
Reviews (1): Last reviewed commit: "feat(mcp): extend keyless gateway OAuth ..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 6be1b5e. Configure here.
There was a problem hiding this comment.
| Change | Risk (out of 10) | Explanation |
|---|---|---|
| Per-server PRM advertises gateway AS | 4 | Spec clients doing PRM discovery re-route to gateway sign-in |
| Bad-key 401 on oauth2 paths gains challenge | 3 | Keyed clients with stale keys may auto-launch gateway sign-in |
| Root-path challenge URLs gain suffix | 2 | Passthrough challenge bytes change under SERVER_ROOT_PATH |
| Anonymous x-mcp-servers gets challenge | 2 | Clients now prompt sign-in where they errored |
| Session-shaped bearers intercepted everywhere | 1 | Previously always failed, so nothing working changes |
Claude noted these potential backwards-incompatible changes. Of any of the higher risk backwards-incompatible changes, are any of these legit / should be addressed? Or are none of them legit?
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks!
Let's consider adding the breaking changes in the release notes
TLDR
Problem this solves:
/mcp/How it solves it:
Relevant issues
/mcp/scope to the per-server URL spellings/mcp/{server}and/{server}/mcpLinear ticket
Resolves LIT-4864
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Live proxy on
localhost:4864, real Postgres, config with three servers:deepwiki(real public upstream, auth none,allow_all_keys),github_oauth(auth_type: oauth2,oauth2_flow: authorization_code,allow_all_keys),locked_down(no grants). The session bearer in every step below was minted through the real keyless flow with curl only:POST /login->POST /register(DCR) ->GET /authorize(S256 PKCE) ->POST /authorize/complete->POST /tokenBefore, at the merge base
2a7885aee7(unfixed):After, at
6be1b5e291(this PR), same rig and the same session bearer:Type
🆕 New Feature
Changes
MCPServer.is_gateway_managed_oauth2owner (auth_type == oauth2 and not delegate_auth_to_upstream); the challenge gate and the PRM flip both read ituser_api_key_auth_mcp.py: the session-bearer arm admits at any MCP scope (downstream grant resolution already intersects path and header targets fail closed); the gateway DCR challenge is scope-aware and fires for the aggregate scope,x-mcp-serversscoped requests, and a single gateway-managed oauth2 path targetdiscoverable_endpoints.py: explicitly named gateway-managed oauth2 PRM advertises{base}/mcp; the root-resolved unnamed shape and every other mode are unchangedserver.pypreemptive 401: an admitted keyless subject with no stored user token is challenged with per-serverresource_metadata(the gateway flow vaults via the authorize interlude) instead of the relayauthorization_uri, whose token request cannot vault without a litellm keyget_passthrough_resource_metadata_urlmoved tooauth_utils.py(shared with the auth module) and now inserts theSERVER_ROOT_PATHsegment exactly as the discovery routes register itThings a reviewer will ask about: accepting the relayed upstream token itself at per-server ingress was considered and rejected; no gateway identity attaches to it (the relay token request carries no litellm credential, so nothing is vaulted and there is no user to admit), which is why it fails twice today. The challenge fires only for names whose per-server PRM already returns 200 publicly, so no new server-name enumeration surface is created. The keyed interactive flow keeps its
authorization_urichallenge and the per-server relay endpoints, unchangedFinal Attestation
Note
High Risk
Touches MCP authentication, OAuth discovery, and 401 challenge behavior across admission and route handlers; incorrect scoping could mis-route clients into gateway sign-in or change behavior for delegated/passthrough modes.
Overview
Extends the keyless gateway DCR flow so it works on per-server MCP URLs (
/mcp/{server},/{server}/mcp) andx-mcp-servers-scoped calls, not only aggregate/mcp.Gateway session bearer admission now runs at any MCP scope; grants still intersect with path/header targets so access cannot broaden. 401 handling is scope-aware: unauthenticated or failed auth on eligible scopes returns an RFC 9728
WWW-Authenticatechallenge pointing at either aggregate or per-server protected-resource metadata (URL spelling matches the request). A newis_gateway_managed_oauth2flag gates this to interactive/M2M oauth2 servers not using upstream-delegated auth; passthrough, delegate, OBO, and multi-target paths stay unchanged.Discovery changes so explicitly named gateway-managed servers advertise
{base}/mcpas the authorization server while keeping the per-serverresourceURL; root-resolved unnamed discovery is unchanged. Preemptive 401s for keyless admitted users without a vaulted upstream token now useresource_metadata(gateway sign-in) instead of the per-server relayauthorization_uri.Shared helpers
get_passthrough_resource_metadata_url/get_passthrough_www_authenticatemove tooauth_utils.py(withSERVER_ROOT_PATHalignment) and replace localserver.pycopies.Reviewed by Cursor Bugbot for commit 6be1b5e. Bugbot is set up for automated code reviews on this repo. Configure here.