mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
fix(mcp): filter local openapi headers
This commit is contained in:
parent
907e6534fa
commit
f82e588a50
2 changed files with 75 additions and 24 deletions
|
|
@ -429,6 +429,7 @@ if MCP_AVAILABLE:
|
|||
_caller_authorization_fans_out,
|
||||
_client_forwarded_authorization_headers,
|
||||
_is_gateway_attribution_header,
|
||||
_openapi_forwarded_extra_headers,
|
||||
_should_strip_caller_authorization,
|
||||
_without_authorization,
|
||||
global_mcp_server_manager,
|
||||
|
|
@ -2854,30 +2855,11 @@ if MCP_AVAILABLE:
|
|||
else:
|
||||
auth_header_value = f"Bearer {mcp_auth_header}"
|
||||
|
||||
# Forward named client headers to OpenAPI tool upstream requests.
|
||||
# MCPServer.extra_headers lists header names to copy from raw_headers.
|
||||
# The strip decision is centralized in _should_strip_caller_authorization so this
|
||||
# OpenAPI/local path agrees with the managed paths: M2M and the resolver-owned modes
|
||||
# (token_exchange's raw subject token, authorization_code's stored token) must never
|
||||
# have the caller's Authorization forwarded verbatim upstream.
|
||||
forwarded_headers: dict[str, str] | None = None
|
||||
if mcp_server and mcp_server.extra_headers and raw_headers:
|
||||
normalized_raw: Final = {str(k).lower(): v for k, v in raw_headers.items() if isinstance(k, str)}
|
||||
skip_caller_authorization: Final = _should_strip_caller_authorization(
|
||||
mcp_server=mcp_server,
|
||||
raw_headers=raw_headers,
|
||||
user_api_key_auth=user_api_key_auth,
|
||||
)
|
||||
for header_name in mcp_server.extra_headers:
|
||||
if not isinstance(header_name, str):
|
||||
continue
|
||||
if skip_caller_authorization and header_name.lower() == "authorization":
|
||||
continue
|
||||
value = normalized_raw.get(header_name.lower())
|
||||
if value is not None:
|
||||
if forwarded_headers is None:
|
||||
forwarded_headers = {}
|
||||
forwarded_headers[header_name] = value
|
||||
forwarded_headers: Final = _openapi_forwarded_extra_headers(
|
||||
mcp_server=mcp_server,
|
||||
raw_headers=raw_headers,
|
||||
user_api_key_auth=user_api_key_auth,
|
||||
)
|
||||
|
||||
resolved_auth_headers: dict[str, str] | None = None
|
||||
if mcp_server:
|
||||
|
|
|
|||
|
|
@ -6675,6 +6675,75 @@ async def test_execute_mcp_tool_sets_model_in_model_call_details():
|
|||
assert litellm_logging_obj.model == "MCP: list_pets"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_mcp_tool_local_openapi_does_not_forward_gateway_attribution_headers():
|
||||
from litellm.proxy._experimental.mcp_server import server as mcp_module
|
||||
from litellm.proxy._experimental.mcp_server.openapi_to_mcp_generator import (
|
||||
_request_extra_headers,
|
||||
)
|
||||
|
||||
server = MCPServer(
|
||||
server_id="local-openapi-server",
|
||||
name="local_openapi",
|
||||
server_name="local_openapi",
|
||||
url="https://example.com",
|
||||
transport=MCPTransport.http,
|
||||
spec_path="/specs/openapi.yaml",
|
||||
extra_headers=["x-request-id", "X-LiteLLM-Tags", "X-LiteLLM-End-User-Id"],
|
||||
)
|
||||
captured_extra_headers = None
|
||||
|
||||
async def capture_local_tool(*args, **kwargs):
|
||||
nonlocal captured_extra_headers
|
||||
captured_extra_headers = _request_extra_headers.get()
|
||||
return []
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mcp_module.global_mcp_server_manager,
|
||||
"_get_mcp_server_from_tool_name",
|
||||
return_value=server,
|
||||
),
|
||||
patch.object(
|
||||
mcp_module.global_mcp_server_manager,
|
||||
"pre_call_tool_check",
|
||||
new=AsyncMock(return_value={}),
|
||||
),
|
||||
patch.object(
|
||||
mcp_module.global_mcp_server_manager,
|
||||
"resolve_openapi_upstream_auth",
|
||||
new=AsyncMock(side_effect=lambda **kwargs: (None, kwargs["forwarded_headers"])),
|
||||
),
|
||||
patch.object(
|
||||
mcp_module.global_mcp_tool_registry,
|
||||
"get_tool",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
patch(
|
||||
"litellm.proxy._experimental.mcp_server.server._handle_local_mcp_tool",
|
||||
new=AsyncMock(side_effect=capture_local_tool),
|
||||
),
|
||||
patch.object(
|
||||
mcp_module.MCPRequestHandler,
|
||||
"is_tool_allowed",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
await mcp_module.execute_mcp_tool(
|
||||
name="local_openapi-list_pets",
|
||||
arguments={},
|
||||
allowed_mcp_servers=[server],
|
||||
start_time=datetime.now(),
|
||||
raw_headers={
|
||||
"x-request-id": "request-123",
|
||||
"x-litellm-tags": "application:orders,service:checkout",
|
||||
"x-litellm-end-user-id": "user-123",
|
||||
},
|
||||
)
|
||||
|
||||
assert captured_extra_headers == {"x-request-id": "request-123"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_mcp_tool_rest_unresolved_prefixed_name_routes_to_requested_server():
|
||||
"""A prefixed REST name that resolves to no tool must still dispatch to the server_id.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue