diff --git a/litellm/proxy/_experimental/mcp_server/exceptions.py b/litellm/proxy/_experimental/mcp_server/exceptions.py index 67bb9f5d387..fd8fc3d5e58 100644 --- a/litellm/proxy/_experimental/mcp_server/exceptions.py +++ b/litellm/proxy/_experimental/mcp_server/exceptions.py @@ -28,7 +28,11 @@ class MCPUpstreamAuthError(Exception): self.server_name = server_name super().__init__(f"Upstream MCP server {server_name!r} returned {status_code}") - def to_http_exception(self, base_url: Optional[str] = None) -> HTTPException: + def to_http_exception( + self, + base_url: Optional[str] = None, + request_path: Optional[str] = None, + ) -> 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 @@ -36,21 +40,38 @@ class MCPUpstreamAuthError(Exception): When the upstream 401 omits ``WWW-Authenticate`` (non-compliant per RFC 7235 §3.1) we fabricate a ``Bearer resource_metadata=`` challenge - that points at the gateway's standard-pattern well-known endpoint for - this server, so MCP clients can still initiate RFC 9728 discovery - against the upstream IdP via the gateway's proxied metadata. Callers - must pass ``base_url`` (the gateway origin, no trailing slash) so the - fabricated URI is absolute as RFC 9728 §3.2 requires; if ``base_url`` - is missing we skip fabrication entirely rather than emit a relative - URI that strict clients reject in the Bearer challenge. + that points at the gateway's well-known endpoint for this server, so + MCP clients can still initiate RFC 9728 discovery against the upstream + IdP via the gateway's proxied metadata. Callers must pass ``base_url`` + (the gateway origin, no trailing slash) so the fabricated URI is + absolute as RFC 9728 §3.2 requires; if ``base_url`` is missing we + skip fabrication entirely rather than emit a relative URI that strict + clients reject in the Bearer challenge. + + When ``request_path`` is supplied and matches the legacy + ``/{server_name}/mcp`` MCP transport route, the fabricated URI uses + the matching legacy well-known form + ``/.well-known/oauth-protected-resource/{server_name}/mcp``. Otherwise + we default to the standard form + ``/.well-known/oauth-protected-resource/mcp/{server_name}``. This + keeps the ``resource_metadata`` URI aligned with the resource pattern + the client originally targeted, matching the path-aware behaviour of + ``_get_passthrough_resource_metadata_url`` in ``server.py``. """ challenge: Optional[str] = self.www_authenticate if challenge is None and self.status_code == 401 and base_url: prefix = base_url.rstrip("/") - challenge = ( - "Bearer resource_metadata=" - f'"{prefix}/.well-known/oauth-protected-resource/mcp/{self.server_name}"' - ) + if request_path and request_path.startswith(f"/{self.server_name}/mcp"): + resource_metadata_url = ( + f"{prefix}/.well-known/oauth-protected-resource/" + f"{self.server_name}/mcp" + ) + else: + resource_metadata_url = ( + f"{prefix}/.well-known/oauth-protected-resource/" + f"mcp/{self.server_name}" + ) + challenge = f'Bearer resource_metadata="{resource_metadata_url}"' detail = "Forbidden" if self.status_code == 403 else "Unauthorized" return HTTPException( status_code=self.status_code, diff --git a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py index 052607cc46e..7b9a6073233 100644 --- a/litellm/proxy/_experimental/mcp_server/rest_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/rest_endpoints.py @@ -505,7 +505,10 @@ 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 e.to_http_exception(base_url=get_request_base_url(request)) + raise e.to_http_exception( + base_url=get_request_base_url(request), + request_path=request.scope.get("_original_path") or request.url.path, + ) 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 67a112a9324..783d8d15055 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3251,7 +3251,8 @@ if MCP_AVAILABLE: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. raise e.to_http_exception( - base_url=get_request_base_url(StarletteRequest(scope)) + base_url=get_request_base_url(StarletteRequest(scope)), + request_path=scope.get("_original_path") or scope.get("path"), ) except HTTPException: # Re-raise HTTP exceptions to preserve status codes and details @@ -3356,7 +3357,8 @@ if MCP_AVAILABLE: # Pass-through server returned 401 — surface it to the client so # standards-compliant MCP clients trigger the upstream OAuth flow. raise e.to_http_exception( - base_url=get_request_base_url(StarletteRequest(scope)) + base_url=get_request_base_url(StarletteRequest(scope)), + request_path=scope.get("_original_path") or scope.get("path"), ) except HTTPException: # Re-raise HTTP exceptions to preserve status codes and details