mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
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:
parent
a86f6e7eb6
commit
326c6bb84f
2 changed files with 70 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue