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 e2204ea7a21..5992d3e86de 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 @@ -207,7 +207,9 @@ class MCPRequestHandler: elif ( not litellm_api_key and MCPRequestHandler._target_servers_delegate_auth_to_upstream( # noqa: E501 - path=request.url.path, mcp_servers=mcp_servers + path=request.url.path, + mcp_servers=mcp_servers, + client_ip=IPAddressUtils.get_mcp_client_ip(request), ) ): # Operator opted this oauth2 server into upstream-delegated auth @@ -407,7 +409,7 @@ class MCPRequestHandler: @staticmethod def _target_servers_delegate_auth_to_upstream( - path: str, mcp_servers: Optional[List[str]] + path: str, mcp_servers: Optional[List[str]], client_ip: Optional[str] ) -> bool: """ True only when EVERY MCP server the request targets is configured for @@ -437,7 +439,9 @@ class MCPRequestHandler: return False for name in target_names: - server = global_mcp_server_manager.get_mcp_server_by_name(name) + server = global_mcp_server_manager.get_mcp_server_by_name( + name, client_ip=client_ip + ) if server is None or server.auth_type != MCPAuth.oauth2: return False # `is True` is intentional: opt-in must be an explicit boolean diff --git a/litellm/proxy/_experimental/mcp_server/exceptions.py b/litellm/proxy/_experimental/mcp_server/exceptions.py index 675f63113d6..cb1df1e92d6 100644 --- a/litellm/proxy/_experimental/mcp_server/exceptions.py +++ b/litellm/proxy/_experimental/mcp_server/exceptions.py @@ -2,6 +2,8 @@ from typing import Optional +from fastapi import HTTPException + class MCPUpstreamAuthError(Exception): """Raised when an upstream MCP server returns an authentication failure @@ -25,3 +27,19 @@ class MCPUpstreamAuthError(Exception): self.www_authenticate = www_authenticate self.server_name = server_name super().__init__(f"Upstream MCP server {server_name!r} returned {status_code}") + + def to_http_exception(self) -> HTTPException: + """Convert this upstream-auth error into an ``HTTPException`` that + preserves the upstream status code and any ``WWW-Authenticate`` + challenge, so standards-compliant MCP clients can trigger the + upstream OAuth flow. + """ + return HTTPException( + status_code=self.status_code, + detail="Unauthorized", + headers=( + {"www-authenticate": self.www_authenticate} + if self.www_authenticate + else None + ), + ) diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index b6bd990a2e5..0c318fceaf4 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -445,15 +445,7 @@ if MCP_AVAILABLE: except MCPUpstreamAuthError as e: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. - raise HTTPException( - status_code=e.status_code, - detail="Unauthorized", - headers=( - {"www-authenticate": e.www_authenticate} - if e.www_authenticate - else None - ), - ) + raise e.to_http_exception() except Exception as e: verbose_logger.exception(f"Error getting tools from {server.name}: {e}") return { @@ -546,15 +538,7 @@ if MCP_AVAILABLE: except MCPUpstreamAuthError as e: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. - raise HTTPException( - status_code=e.status_code, - detail="Unauthorized", - headers=( - {"www-authenticate": e.www_authenticate} - if e.www_authenticate - else None - ), - ) + raise e.to_http_exception() except Exception as e: verbose_logger.exception(f"Error getting tools from {server.name}: {e}") return { diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 071c6f35ad3..385ddee5941 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3183,15 +3183,7 @@ if MCP_AVAILABLE: except MCPUpstreamAuthError as e: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. - raise HTTPException( - status_code=e.status_code, - detail="Unauthorized", - headers=( - {"www-authenticate": e.www_authenticate} - if e.www_authenticate - else None - ), - ) + raise e.to_http_exception() except HTTPException: # Re-raise HTTP exceptions to preserve status codes and details raise @@ -3275,15 +3267,7 @@ if MCP_AVAILABLE: except MCPUpstreamAuthError as e: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. - raise HTTPException( - status_code=e.status_code, - detail="Unauthorized", - headers=( - {"www-authenticate": e.www_authenticate} - if e.www_authenticate - else None - ), - ) + 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).