fix(mcp): match sanitized per-server alias at the connect-time preemptive 401

The connect gate resolved x-mcp-{alias}-authorization by matching the raw
lowercased alias/server_name/name only, but dashboard clients send
x-mcp-{sanitize_mcp_alias_for_header(alias)}-authorization, and egress resolves
those through lookup_mcp_server_auth_in_headers, which also tries the sanitized
alias. So a per-server token bound with a sanitized alias (e.g. alias 'pt-server'
arriving as header key 'pt_server') was forwarded at egress but still triggered a
preemptive 401 at connect. _client_has_per_server_auth_header now resolves through
the same lookup_mcp_server_auth_in_headers egress uses, so connect and egress
agree on which header names match.
This commit is contained in:
Tin 2026-07-08 16:22:05 -07:00
parent ddec3b2b8b
commit b2ea36f4f1
2 changed files with 35 additions and 16 deletions

View file

@ -1449,25 +1449,25 @@ if MCP_AVAILABLE:
header for this server. This is the multi-server binding: it names one
upstream, so it is unambiguously the caller's upstream token regardless of
auth mode (never the LiteLLM admission credential).
Resolves through the same ``lookup_mcp_server_auth_in_headers`` egress uses, so
the connect gate and egress agree on which per-server header names match: a
dashboard client sends ``x-mcp-{sanitize_mcp_alias_for_header(alias)}-authorization``,
and matching only the raw alias here would 401 a token egress would forward.
"""
if not mcp_server_auth_headers:
return False
for key in (server.alias, server.server_name, server.name):
if not key:
continue
server_headers = None
for k, v in mcp_server_auth_headers.items():
if k.lower() == key.lower():
server_headers = v
break
if server_headers is None:
continue
if isinstance(server_headers, str) and server_headers.strip():
return True
if isinstance(server_headers, dict):
for hk in server_headers.keys():
if hk.lower() == "authorization":
return True
from litellm.proxy._experimental.mcp_server.utils import (
lookup_mcp_server_auth_in_headers,
)
server_headers = lookup_mcp_server_auth_in_headers(
mcp_server_auth_headers, alias=server.alias, server_name=server.server_name
)
if isinstance(server_headers, str):
return bool(server_headers.strip())
if isinstance(server_headers, dict):
return any(isinstance(hk, str) and hk.lower() == "authorization" for hk in server_headers)
return False
def _client_has_passthrough_authorization(

View file

@ -1293,6 +1293,25 @@ async def test_handle_streamable_http_mcp_per_server_header_skips_preemptive_cha
assert challenged is False
@pytest.mark.asyncio
@pytest.mark.parametrize("auth_type", [MCPAuth.oauth_delegate, MCPAuth.true_passthrough])
async def test_handle_streamable_http_mcp_sanitized_per_server_header_skips_preemptive_challenge(auth_type):
"""A dashboard client sends x-mcp-{sanitize_mcp_alias_for_header(alias)}-authorization, so the
alias 'pt-server' arrives as the header key 'pt_server'. Egress resolves that via the sanitized
alias, so the connect gate must too, or it 401s a token egress would forward."""
try:
from litellm.proxy._experimental.mcp_server.server import handle_streamable_http_mcp # noqa: F401
except ImportError:
pytest.skip("MCP server not available")
challenged, _ = await _run_passthrough_connect(
auth_type=auth_type,
server_names=["pt-server"],
mcp_server_auth_headers={"pt_server": {"Authorization": "Bearer upstream-token"}},
)
assert challenged is False
@pytest.mark.asyncio
@pytest.mark.parametrize("auth_type", [MCPAuth.oauth_delegate, MCPAuth.true_passthrough])
async def test_handle_streamable_http_mcp_aggregate_does_not_preemptively_challenge(auth_type):