From 0447ba6a9b7f6d8a7b7e3279dd53289b071e1833 Mon Sep 17 00:00:00 2001 From: gym-cmd <186399764+gym-cmd@users.noreply.github.com> Date: Thu, 21 May 2026 00:30:45 +0100 Subject: [PATCH] fix(mcp): use resource metadata for passthrough reauth --- .../proxy/_experimental/mcp_server/server.py | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 610edb3be9e..800b7d9f0ee 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -2834,6 +2834,30 @@ if MCP_AVAILABLE: ) return user_api_key_auth.model_copy(update={"object_permission": updated_op}) + def _get_passthrough_resource_metadata_url(scope: Scope, server_name: str) -> str: + request = StarletteRequest(scope) + base_url = get_request_base_url(request) + _path = scope.get("_original_path") or scope.get("path", "") or "" + + if _path.startswith(f"/{server_name}/mcp"): + return f"{base_url}/.well-known/oauth-protected-resource/{server_name}/mcp" + return f"{base_url}/.well-known/oauth-protected-resource/mcp/{server_name}" + + def _get_passthrough_www_authenticate( + scope: Scope, + server_name: str, + invalid_token: bool = False, + ) -> str: + resource_metadata_url = _get_passthrough_resource_metadata_url( + scope=scope, + server_name=server_name, + ) + params = [] + if invalid_token: + params.append('error="invalid_token"') + params.append(f'resource_metadata="{resource_metadata_url}"') + return "Bearer " + ", ".join(params) + async def _raise_preemptive_401_for_unauthenticated_servers( scope: Scope, mcp_servers: Optional[List[str]], @@ -2894,18 +2918,10 @@ if MCP_AVAILABLE: server, oauth2_headers, mcp_server_auth_headers ) ): - request = StarletteRequest(scope) - base_url = get_request_base_url(request) - _path = scope.get("_original_path") or scope.get("path", "") or "" - - # Pick the well-known resource-metadata form that matches the inbound - # route pattern so the metadata's `resource` field round-trips to what - # the client actually hit (RFC 9728 §3.2). - if _path.startswith(f"/{server_name}/mcp"): - resource_metadata_url = f"{base_url}/.well-known/oauth-protected-resource/{server_name}/mcp" - else: - resource_metadata_url = f"{base_url}/.well-known/oauth-protected-resource/mcp/{server_name}" - www_authenticate = f'Bearer resource_metadata="{resource_metadata_url}"' + www_authenticate = _get_passthrough_www_authenticate( + scope=scope, + server_name=server_name, + ) raise HTTPException( status_code=401, detail="Unauthorized", @@ -3040,19 +3056,20 @@ if MCP_AVAILABLE: for srv in passthrough_servers ] ) - request = StarletteRequest(scope) - base_url = get_request_base_url(request) for srv, (probe_status, _) in zip(passthrough_servers, probe_results): if probe_status == 401: - # Token is missing or expired — direct the client to re-authorize. - authorization_uri = ( - f"Bearer authorization_uri=" - f"{base_url}/.well-known/oauth-authorization-server/{srv.name}" + # Token is missing or expired: keep pass-through clients on the + # protected-resource discovery flow so they re-authorize against + # the upstream IdP metadata proxied by LiteLLM. + www_authenticate = _get_passthrough_www_authenticate( + scope=scope, + server_name=srv.name, + invalid_token=True, ) raise HTTPException( status_code=401, detail="Unauthorized", - headers={"WWW-Authenticate": authorization_uri}, + headers={"WWW-Authenticate": www_authenticate}, ) if probe_status == 403: # Token is valid but the caller lacks permission — do not hint