diff --git a/litellm/proxy/_experimental/mcp_server/v2_resolver_bridge.py b/litellm/proxy/_experimental/mcp_server/v2_resolver_bridge.py index 5d05e9a0f30..42d464eaea4 100644 --- a/litellm/proxy/_experimental/mcp_server/v2_resolver_bridge.py +++ b/litellm/proxy/_experimental/mcp_server/v2_resolver_bridge.py @@ -12,6 +12,7 @@ This lives on the v1 side so the v2 core keeps its no-v1-imports invariant. from __future__ import annotations +import base64 import functools import os from typing import TYPE_CHECKING, Dict, Optional @@ -146,17 +147,31 @@ def to_server_spec(server: MCPServer) -> Optional[ServerSpec]: key_source=SharedKey(value=SecretStr(token)), ), ) - if server.auth_type == MCPAuth.bearer_token: + # Static credential on the Authorization header; these differ only by scheme prefix (basic is + # base64(user:pass)). All reuse the api_key arm. authorization uses an empty prefix (verbatim), + # so test the prefix with `is not None`, not truthiness. + authorization_prefix = { + MCPAuth.bearer_token: "Bearer", + MCPAuth.token: "token", + MCPAuth.authorization: "", + MCPAuth.basic: "Basic", + }.get(server.auth_type) + if authorization_prefix is not None: token = server.authentication_token if not token: - return None # bearer_token with no token: let v1 handle it (parity-safe) + return None # static Authorization mode with no token: let v1 handle it (parity-safe) + value = ( + base64.b64encode(token.encode("utf-8")).decode() + if server.auth_type == MCPAuth.basic + else token + ) return ServerSpec( server_id=server.server_id, resource=resource, config=ApiKeyConfig( header_name="Authorization", - value_prefix="Bearer", - key_source=SharedKey(value=SecretStr(token)), + value_prefix=authorization_prefix, + key_source=SharedKey(value=SecretStr(value)), ), ) if server.auth_type == MCPAuth.aws_sigv4: diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_v2_resolver_bridge.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_v2_resolver_bridge.py index 439edc8b4ed..f53a177d5a3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_v2_resolver_bridge.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_v2_resolver_bridge.py @@ -5,6 +5,8 @@ headers must be byte-identical to what v1 produces, and every other mode (or any fall back to v1 unchanged. """ +import base64 + import httpx import pytest @@ -71,20 +73,34 @@ async def test_none_attaches_no_auth(v2_on): assert _v1_headers(MCPAuth.none, None) == _v1_headers(MCPAuth.none, v2_value) == {} -async def test_non_grafted_mode_defers_to_v1(v2_on): - # basic is not grafted yet -> v2 returns None so v1 handles it - assert await resolve_v2_auth_value(_server(MCPAuth.basic, "k")) is None - - -async def test_bearer_token_parity(v2_on): - token = "up-secret" - server = _server(MCPAuth.bearer_token, token) +@pytest.mark.parametrize( + "auth_type,token,expected", + [ + (MCPAuth.bearer_token, "up-secret", {"Authorization": "Bearer up-secret"}), + (MCPAuth.token, "up-secret", {"Authorization": "token up-secret"}), + ( + MCPAuth.authorization, + "Custom raw-value", + {"Authorization": "Custom raw-value"}, + ), + ( + MCPAuth.basic, + "user:pass", + {"Authorization": f"Basic {base64.b64encode(b'user:pass').decode()}"}, + ), + ], +) +async def test_static_authorization_modes_parity(v2_on, auth_type, token, expected): + server = _server(auth_type, token) v2_value = await resolve_v2_auth_value(server) - assert v2_value == {"Authorization": f"Bearer {token}"} + assert v2_value == expected # byte-identical to v1's final upstream headers - assert _v1_headers(MCPAuth.bearer_token, token) == _v1_headers( - MCPAuth.bearer_token, v2_value - ) + assert _v1_headers(auth_type, token) == _v1_headers(auth_type, v2_value) == expected + + +async def test_static_auth_without_token_defers_to_v1(v2_on): + # A static Authorization mode with no configured token defers to v1 (parity-safe). + assert await resolve_v2_auth_value(_server(MCPAuth.bearer_token, None)) is None async def test_api_key_without_token_defers_to_v1(v2_on):