From 78cb412419f6028ad599c50af2129fc8eb45a8ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 17:32:46 +0000 Subject: [PATCH] fix(mcp): tighten cold-start bypass against CSV paths + dedupe upstream auth probe - Return None from _parse_mcp_server_names_from_path for CSV multi-server paths (/mcp/a,b). The regex previously truncated at the first comma and silently passed a single server name to the cold-start gate. - Switch _is_mcp_passthrough_cold_start to all-targets semantics, matching _target_servers_use_oauth2: one non-passthrough target in a co-targeted set must not flip the anonymous-admission bypass open for the others. - Drop the redundant HTTPStatusError block in _extract_upstream_auth_failure - any HTTPStatusError carries a .response, so the preceding generic block already handles 401/403 detection. Co-authored-by: Yassin Kortam --- .../mcp_server/auth/user_api_key_auth_mcp.py | 33 +++++++++++++------ .../mcp_server/mcp_server_manager.py | 10 ------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 1a7bc6a499a..f76b0c8acc8 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -19,10 +19,18 @@ from litellm.proxy.auth.ip_address_utils import IPAddressUtils def _parse_mcp_server_names_from_path(path: str) -> Optional[List[str]]: """Parse a single MCP server name from /mcp/{name} or /{name}/mcp path patterns. - Returns None for the aggregate /mcp route (no bypass for multi-server paths).""" - m = re.match(r"^/mcp/([^/,?#]+)", path) + Returns None for the aggregate /mcp route (no bypass for multi-server paths). + + Multi-server CSV paths like ``/mcp/server1,server2`` also return ``None`` — + the cold-start bypass must not activate when any of the co-targeted servers + might not be passthrough-eligible. The regex stops at ``/?#`` only; the + comma check is handled explicitly below.""" + m = re.match(r"^/mcp/([^/?#]+)", path) if m: - return [m.group(1)] + segment = m.group(1) + if "," in segment: + return None + return [segment] m = re.match(r"^/([^/,?#]+)/mcp", path) if m: return [m.group(1)] @@ -32,10 +40,15 @@ def _parse_mcp_server_names_from_path(path: str) -> Optional[List[str]]: def _is_mcp_passthrough_cold_start( scope: Scope, mcp_servers: Optional[List[str]], client_ip: Optional[str] ) -> bool: - """True when the request targets a pass-through server with no auth headers — - the cold-start OAuth discovery case per RFC 9728 / MCP Authorization spec. - Lets the route handler's 401 emitter produce the spec-compliant WWW-Authenticate - challenge instead of surfacing a generic admission error.""" + """True only when EVERY targeted server is a pass-through server with no + auth headers — the cold-start OAuth discovery case per RFC 9728 / MCP + Authorization spec. Lets the route handler's 401 emitter produce the + spec-compliant WWW-Authenticate challenge instead of surfacing a generic + admission error. + + Uses "all" semantics (mirrors :meth:`MCPRequestHandler._target_servers_use_oauth2`): + one non-passthrough target in a co-targeted set must not flip the bypass + open for the others. Fails closed when any target cannot be resolved.""" if not mcp_servers: return False from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( @@ -46,9 +59,9 @@ def _is_mcp_passthrough_cold_start( server = global_mcp_server_manager.get_mcp_server_by_name( name, client_ip=client_ip ) - if server is not None and getattr(server, "is_oauth_passthrough", False): - return True - return False + if server is None or not getattr(server, "is_oauth_passthrough", False): + return False + return True def _is_litellm_auth_admission_error(exc: Exception) -> bool: diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 9e607c257ac..21bd474b45c 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -153,16 +153,6 @@ def _extract_upstream_auth_failure( www_authenticate = None return status_code, www_authenticate - if isinstance(current, HTTPStatusError): - status_code = getattr(current.response, "status_code", None) - if isinstance(status_code, int) and status_code in (401, 403): - www_authenticate = None - try: - www_authenticate = current.response.headers.get("www-authenticate") - except Exception: - pass - return status_code, www_authenticate - # anyio / PEP 654 ExceptionGroup sub_exceptions = getattr(current, "exceptions", None) if sub_exceptions: