From e868947c80b42e1f9dad14bd26e1c85b6ae319f9 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Sat, 20 Jun 2026 16:55:13 -0700 Subject: [PATCH] fix(mcp/v2): surface MCPUpstreamError status codes and raise the env-var precondition on single-result reads Wire MCPUpstreamError into the three conversion sites (the streamable and SSE /mcp protocol handlers and the REST tool-call endpoint) so a non-auth upstream failure on a single-result op returns its semantic status (502 unreachable, 428 precondition, 500 misconfigured) instead of falling through to a blanket 500. Also make read_resource and get_prompt pass raise_on_missing_env=True like call_tool, so a missing per-user env var on any single-result op returns the 412 + setup URL rather than degrading to an under-authenticated request and a downstream 401. List ops keep degrading (non-blocking discovery). Validated: 8 unit tests; proxy boots with the wiring loaded; a live tools/call on a none-mode server returns the upstream result through the v2 path. --- .../mcp_server/mcp_server_manager_v2.py | 12 ++++++++++-- .../_experimental/mcp_server/rest_endpoints.py | 9 ++++++++- litellm/proxy/_experimental/mcp_server/server.py | 13 ++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager_v2.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager_v2.py index 39d6001b89d..d5ca5bc6a32 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager_v2.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager_v2.py @@ -285,7 +285,11 @@ class MCPServerManagerV2(MCPServerManager): server, url, mcp_auth_header, extra_headers, raw_headers ) conn = await self._v2_connection( - server, None, raw_headers=raw_headers, extra_headers=extra_headers + server, + None, + raw_headers=raw_headers, + extra_headers=extra_headers, + raise_on_missing_env=True, ) if isinstance(conn, Error): self._egress_item_failure(server, conn.error) @@ -315,7 +319,11 @@ class MCPServerManagerV2(MCPServerManager): raw_headers, ) conn = await self._v2_connection( - server, None, raw_headers=raw_headers, extra_headers=extra_headers + server, + None, + raw_headers=raw_headers, + extra_headers=extra_headers, + raise_on_missing_env=True, ) if isinstance(conn, Error): self._egress_item_failure(server, conn.error) diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 2149f079a3d..db41267f09a 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -18,7 +18,10 @@ import httpx from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from litellm._logging import verbose_logger -from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError +from litellm.proxy._experimental.mcp_server.exceptions import ( + MCPUpstreamAuthError, + MCPUpstreamError, +) from litellm.proxy._experimental.mcp_server.ui_session_utils import ( build_effective_auth_contexts, ) @@ -886,6 +889,10 @@ if MCP_AVAILABLE: "setup_url": e.setup_url, }, ) + except MCPUpstreamError as e: + # Non-auth upstream failure on the tool call; surface the semantic status + # (502 unreachable / 428 precondition / 500 misconfigured) instead of a blanket 500. + raise e.to_http_exception() except BlockedPiiEntityError as e: verbose_logger.error(f"BlockedPiiEntityError in MCP tool call: {str(e)}") raise HTTPException( diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 08e42e918e9..79a79e1cd36 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -40,7 +40,10 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( MCPRequestHandler, ) -from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError +from litellm.proxy._experimental.mcp_server.exceptions import ( + MCPUpstreamAuthError, + MCPUpstreamError, +) from litellm.proxy._experimental.mcp_server.discoverable_endpoints import ( get_request_base_url, ) @@ -4003,6 +4006,10 @@ if MCP_AVAILABLE: base_url=get_request_base_url(StarletteRequest(scope)), request_path=scope.get("_original_path") or scope.get("path"), ) + except MCPUpstreamError as e: + # Non-auth upstream failure on a single-result op; surface the semantic status + # (502 unreachable / 428 precondition / 500 misconfigured) instead of a blanket 500. + raise e.to_http_exception() except HTTPException: # Re-raise HTTP exceptions to preserve status codes and details raise @@ -4119,6 +4126,10 @@ if MCP_AVAILABLE: base_url=get_request_base_url(StarletteRequest(scope)), request_path=scope.get("_original_path") or scope.get("path"), ) + except MCPUpstreamError as e: + # Non-auth upstream failure on a single-result op; surface the semantic status + # (502 unreachable / 428 precondition / 500 misconfigured) instead of a blanket 500. + raise e.to_http_exception() except HTTPException: # Re-raise HTTP exceptions to preserve status codes and details # (e.g. 401 + WWW-Authenticate challenges from OAuth pass-through).