fix(mcp): add upstream auth pre-flight in SSE handler

Mirror handle_streamable_http_mcp by calling _check_passthrough_upstream_auth
after the cold-start 401 emitter so expired/invalid upstream tokens surface a
proper 401 + WWW-Authenticate challenge before the SSE session commits 200
headers, instead of letting list_tools silently return [] when the upstream
rejects the token.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude 2026-05-20 17:31:20 +00:00
parent a86f6e7eb6
commit 326c6bb84f
No known key found for this signature in database
2 changed files with 70 additions and 0 deletions

View file

@ -3230,6 +3230,13 @@ if MCP_AVAILABLE:
user_api_key_auth=user_api_key_auth,
client_ip=_sse_client_ip,
)
# Pre-flight auth check for pass-through servers: surface upstream
# 401/403 as a proper challenge before the SSE session commits 200
# headers, so clients can refresh their OAuth token instead of
# being stuck with a silently empty tool list.
await _check_passthrough_upstream_auth(
scope, user_api_key_auth, mcp_servers, _sse_client_ip
)
set_auth_context(
user_api_key_auth=user_api_key_auth,
mcp_auth_header=mcp_auth_header,

View file

@ -504,6 +504,69 @@ async def test_sse_mcp_handler_mock():
)
@pytest.mark.asyncio
async def test_sse_mcp_handler_propagates_passthrough_401():
"""SSE handler must raise 401 + WWW-Authenticate when the upstream
pass-through probe rejects the client's bearer token, instead of letting
the SSE session start and silently return empty tool lists."""
from fastapi import HTTPException
from litellm.proxy._types import UserAPIKeyAuth
mock_scope = {
"type": "http",
"method": "GET",
"path": "/mcp/sse",
"headers": [(b"accept", b"text/event-stream")],
"query_string": b"",
"server": ("localhost", 8000),
"scheme": "http",
}
mock_receive = AsyncMock()
mock_send = AsyncMock()
mock_auth_result = (UserAPIKeyAuth(), None, None, {}, {}, [])
challenge = HTTPException(
status_code=401,
detail="Unauthorized",
headers={"WWW-Authenticate": "Bearer authorization_uri=https://example/"},
)
with (
patch(
"litellm.proxy._experimental.mcp_server.server._SESSION_MANAGERS_INITIALIZED",
True,
),
patch(
"litellm.proxy._experimental.mcp_server.server.sse_session_manager",
AsyncMock(),
),
patch(
"litellm.proxy._experimental.mcp_server.server.extract_mcp_auth_context",
new=AsyncMock(return_value=mock_auth_result),
),
patch(
"litellm.proxy._experimental.mcp_server.server.set_auth_context",
),
patch(
"litellm.proxy._experimental.mcp_server.server._raise_preemptive_401_for_unauthenticated_servers",
new=AsyncMock(),
),
patch(
"litellm.proxy._experimental.mcp_server.server._check_passthrough_upstream_auth",
new=AsyncMock(side_effect=challenge),
),
):
from litellm.proxy._experimental.mcp_server.server import handle_sse_mcp
with pytest.raises(HTTPException) as excinfo:
await handle_sse_mcp(mock_scope, mock_receive, mock_send)
assert excinfo.value.status_code == 401
assert excinfo.value.headers and "WWW-Authenticate" in excinfo.value.headers
def test_generate_stable_server_id():
"""
Test the _generate_stable_server_id method to ensure hash stability across releases.