From 6030fcc01de592cfb6bfd5795af5a93b239169a7 Mon Sep 17 00:00:00 2001 From: joshua Date: Sat, 19 Sep 2026 06:22:52 +0000 Subject: [PATCH] refactor(mcp): keep tools failure handling in place and drop catalog fault log Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../mcp_server/mcp_server_manager.py | 65 +++++++++++++------ .../mcp_server/rest_endpoints.py | 1 - 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 2505ea3dca4..f8fae50b7bb 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1415,11 +1415,12 @@ def _upstream_failure_suffix(exc: BaseException) -> str: return f"\n upstream exchange: {detail}" if detail else "" -def _raise_single_server_list_failure(error: Exception, server: MCPServer, catalog: str) -> NoReturn: - """Relay a failed single-server catalog fetch: auth challenges (upstream, or a v2 resolver's - HTTPException 401/403 raised at client-build time) become ``MCPUpstreamAuthError`` with the - ``WWW-Authenticate`` kept (dropped for dcr_bridge servers, whose upstream challenge points at the - wrong metadata); anything else becomes a classified ``MCPServerListError``.""" +def _raise_single_server_list_failure(error: Exception, server: MCPServer) -> NoReturn: + """Relay a failed single-server catalog fetch the way ``_get_tools_from_server`` does: auth + challenges (upstream, or a v2 resolver's HTTPException 401/403 raised at client-build time) become + ``MCPUpstreamAuthError`` with the ``WWW-Authenticate`` kept (dropped for dcr_bridge servers, whose + upstream challenge points at the wrong metadata); anything else becomes a classified + ``MCPServerListError``. Callers log before delegating here.""" match error: case MCPUpstreamAuthError() if server.is_dcr_bridge and error.www_authenticate is not None: raise MCPUpstreamAuthError( @@ -1438,18 +1439,10 @@ def _raise_single_server_list_failure(error: Exception, server: MCPServer, catal server_name=server.name, ) from error case HTTPException(): - verbose_logger.warning("Failed to get %s from server %s: %s", catalog, server.name, error) raise MCPServerListError( ServerListFault(tag="internal", status_code=error.status_code), server.name ) from error case _: - verbose_logger.warning( - "Failed to get %s from server %s: %s%s", - catalog, - server.name, - type(error).__name__, - _upstream_failure_suffix(error), - ) raise_classified_list_failure(error, server.name, suppress_challenge=server.is_dcr_bridge) @@ -4446,8 +4439,40 @@ class MCPServerManager: return prefixed_or_original_tools + except MCPUpstreamAuthError as upstream_auth_error: + # Pass-through 401 must surface to single-server routes so the + # client triggers the upstream OAuth flow. The multi-server + # aggregator catches this explicitly to keep absorbing. + if server.is_dcr_bridge and upstream_auth_error.www_authenticate is not None: + raise MCPUpstreamAuthError( + status_code=upstream_auth_error.status_code, + www_authenticate=None, + server_name=upstream_auth_error.server_name, + ) from upstream_auth_error + raise + except HTTPException as e: + # A v2 resolver auth challenge (token_exchange's RFC 9728 401, authorization_code's + # browser-OAuth 401, or a 403) is raised at client-build time, inside this try. Route it + # through the same MCPUpstreamAuthError channel as pass-through so single-server routes + # surface the challenge (the client re-authenticates) while the aggregator keeps absorbing. + # Non-auth HTTP errors stay absorbed so one misconfigured server can't blank the listing. + if e.status_code in (401, 403): + headers: Final = e.headers or {} + challenge_header: Final = headers.get("WWW-Authenticate") or headers.get("www-authenticate") + raise MCPUpstreamAuthError( + status_code=e.status_code, + www_authenticate=None if server.is_dcr_bridge else challenge_header, + server_name=server.name, + ) from e + verbose_logger.warning("Failed to get tools from server %s: %s", server.name, e) + raise MCPServerListError(ServerListFault(tag="internal", status_code=e.status_code), server.name) from e + except MCPServerListError: + raise except Exception as e: - _raise_single_server_list_failure(e, server, "tools") + verbose_logger.warning( + "Failed to get tools from server %s: %s%s", server.name, type(e).__name__, _upstream_failure_suffix(e) + ) + raise_classified_list_failure(e, server.name, suppress_challenge=server.is_dcr_bridge) async def _resolve_list_headers( self, @@ -4569,9 +4594,9 @@ class MCPServerManager: items: Final = await self._prompt_discovery_cache.get(key, fetch) return self._create_prefixed_prompts(items, server, add_prefix=add_prefix) except Exception as error: - if raise_on_error: - _raise_single_server_list_failure(error, server, "prompts") verbose_logger.warning("Failed to get prompts from server %s: %s", server.name, error) + if raise_on_error: + _raise_single_server_list_failure(error, server) return [] async def get_resources_from_server( @@ -4613,9 +4638,9 @@ class MCPServerManager: items: Final = await self._resource_discovery_cache.get(key, fetch) return self._create_prefixed_resources(items, server, add_prefix=add_prefix) except Exception as error: - if raise_on_error: - _raise_single_server_list_failure(error, server, "resources") verbose_logger.warning("Failed to get resources from server %s: %s", server.name, error) + if raise_on_error: + _raise_single_server_list_failure(error, server) return [] async def get_resource_templates_from_server( @@ -4657,9 +4682,9 @@ class MCPServerManager: items: Final = await self._template_discovery_cache.get(key, fetch) return self._create_prefixed_resource_templates(items, server, add_prefix=add_prefix) except Exception as error: - if raise_on_error: - _raise_single_server_list_failure(error, server, "resource templates") verbose_logger.warning("Failed to get resource_templates from server %s: %s", server.name, error) + if raise_on_error: + _raise_single_server_list_failure(error, server) return [] async def read_resource_from_server( diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index ff66f8816aa..a6452cbbef0 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -1121,7 +1121,6 @@ if MCP_AVAILABLE: error: MCPServerListError, server: MCPServer, catalog: Literal["prompts", "resources"] ) -> HTTPException: fault: Final = classify_list_exception(error) - verbose_logger.info("Listing %s from %s failed with a %s fault", catalog, server.name, fault.tag) return HTTPException( status_code=list_fault_http_status(fault), detail={