From 064f31e826dad3b3e2fca2761f321fedbfef214a Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 21 May 2026 19:38:19 +0000 Subject: [PATCH] fix(mcp): fabricate resource_metadata challenge when upstream 401 omits WWW-Authenticate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an upstream pass-through MCP server returns 401 without a WWW-Authenticate header (non-compliant per RFC 7235 §3.1), to_http_exception() now produces a synthetic Bearer challenge pointing at the gateway's standard-pattern oauth-protected-resource well-known endpoint for that server. This keeps MCP clients on the RFC 9728 discovery flow instead of receiving a bare 401 with no recovery hint. --- .../_experimental/mcp_server/exceptions.py | 18 ++++++--- .../test_mcp_oauth_passthrough_tools.py | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/exceptions.py b/litellm/proxy/_experimental/mcp_server/exceptions.py index cb1df1e92d6..9d0f21d37d2 100644 --- a/litellm/proxy/_experimental/mcp_server/exceptions.py +++ b/litellm/proxy/_experimental/mcp_server/exceptions.py @@ -33,13 +33,21 @@ class MCPUpstreamAuthError(Exception): preserves the upstream status code and any ``WWW-Authenticate`` challenge, so standards-compliant MCP clients can trigger the upstream OAuth flow. + + 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. """ + challenge: Optional[str] = self.www_authenticate + if challenge is None and self.status_code == 401: + challenge = ( + "Bearer resource_metadata=" + f'"/.well-known/oauth-protected-resource/mcp/{self.server_name}"' + ) return HTTPException( status_code=self.status_code, detail="Unauthorized", - headers=( - {"www-authenticate": self.www_authenticate} - if self.www_authenticate - else None - ), + headers={"www-authenticate": challenge} if challenge else None, ) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough_tools.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough_tools.py index 0e00443d15d..2d7ba622288 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough_tools.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough_tools.py @@ -112,6 +112,46 @@ async def test_fetch_tools_from_passthrough_returns_tools_on_success(): assert tools == [tool] +def test_to_http_exception_preserves_upstream_www_authenticate(): + err = MCPUpstreamAuthError( + status_code=401, + www_authenticate='Bearer resource_metadata="https://upstream/.well-known/oauth-protected-resource"', + server_name="sample_docs", + ) + + http_exc = err.to_http_exception() + assert http_exc.status_code == 401 + assert http_exc.headers == { + "www-authenticate": 'Bearer resource_metadata="https://upstream/.well-known/oauth-protected-resource"' + } + + +def test_to_http_exception_fabricates_resource_metadata_when_upstream_omits_header(): + err = MCPUpstreamAuthError( + status_code=401, + www_authenticate=None, + server_name="sample_docs", + ) + + http_exc = err.to_http_exception() + assert http_exc.status_code == 401 + assert http_exc.headers == { + "www-authenticate": 'Bearer resource_metadata="/.well-known/oauth-protected-resource/mcp/sample_docs"' + } + + +def test_to_http_exception_skips_challenge_for_non_401_status(): + err = MCPUpstreamAuthError( + status_code=403, + www_authenticate=None, + server_name="sample_docs", + ) + + http_exc = err.to_http_exception() + assert http_exc.status_code == 403 + assert http_exc.headers is None + + @pytest.mark.asyncio async def test_fetch_tools_from_gateway_managed_swallows_errors(): """Regression guard: non-pass-through servers keep returning [] on errors."""