mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
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:
parent
ddec3b2b8b
commit
b2ea36f4f1
2 changed files with 35 additions and 16 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue