From 719adfd0439f8a607b3749b99835c5a73d0caa3e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 18:19:28 +0000 Subject: [PATCH] fix(mcp): handle passthrough OAuth metadata and startup auth errors - discoverable_endpoints: For pass-through MCP servers, when upstream oauth-protected-resource returns a non-200/non-dict response, raise HTTP 502 instead of falling through to default gateway metadata. Falling through would direct MCP clients at the gateway, which is not the authorization server for pass-through configs. - mcp_server_manager: Wrap _get_tools_from_server in startup tool name mapping with try/except. Since _get_tools_from_server now re-raises MCPUpstreamAuthError, an upstream 401 from a pass-through server at startup (when no user token is present) would otherwise abort the loop and leave subsequent servers unmapped. Co-authored-by: Yassin Kortam --- .../mcp_server/discoverable_endpoints.py | 16 ++++++++++++++++ .../mcp_server/mcp_server_manager.py | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index a9fa10197ea..92f43e782d0 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -893,6 +893,22 @@ async def _build_oauth_protected_resource_response( response = {**upstream_metadata, "resource": resource_url} return response + # Upstream responded but with non-200 or non-dict payload. For + # pass-through servers the gateway is NOT the authorization server, + # so we must not fall through to the default gateway metadata — + # that would point clients at the wrong IdP. + verbose_logger.warning( + "Upstream oauth-protected-resource metadata unavailable for " + f"pass-through MCP server {mcp_server.name!r}" + ) + raise HTTPException( + status_code=502, + detail=( + "Upstream oauth-protected-resource metadata unavailable " + f"for MCP server {mcp_server.name!r}" + ), + ) + return { "authorization_servers": [ ( diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 21bd474b45c..ccdebd95af6 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -3060,7 +3060,23 @@ class MCPServerManager: if server.needs_user_oauth_token: # Skip OAuth2 servers that rely on user-provided tokens continue - tools = await self._get_tools_from_server(server) + try: + tools = await self._get_tools_from_server(server) + except MCPUpstreamAuthError as e: + # Pass-through servers expect a user-supplied bearer token; + # at startup we have none, so an upstream 401 is normal. + # Swallow it so we keep mapping the remaining servers. + verbose_logger.debug( + f"Skipping tool name mapping for server {server.name} " + f"due to upstream auth error: {str(e)}" + ) + continue + except Exception as e: + verbose_logger.warning( + f"Failed to get tools from server {server.name} during " + f"tool name mapping initialization: {str(e)}" + ) + continue for tool in tools: # The tool.name here is already prefixed from _get_tools_from_server # Extract original name for mapping