mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(mcp): fabricate resource_metadata challenge when upstream 401 omits WWW-Authenticate
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.
This commit is contained in:
parent
1b0b75ccd7
commit
064f31e826
2 changed files with 53 additions and 5 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue