mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(mcp): forward x-mcp-auth headers during MCP tool list retrieval
Previously, _get_mcp_tools_from_manager() hardcoded mcp_auth_header=None and mcp_server_auth_headers=None when calling _get_tools_from_mcp_servers(). This meant that x-mcp-auth and x-mcp-<server>-* headers from the HTTP request were never forwarded during tool list fetching, causing MCP servers that require authentication to fail with 424 (Failed Dependency). The auth headers were already correctly forwarded for tool execution (call_tool) but were missing from the tool discovery path. Changes: - _get_mcp_tools_from_manager: Accept and forward mcp_auth_header, mcp_server_auth_headers, oauth2_headers, raw_headers parameters - _process_mcp_tools_without_openai_transform: Thread auth headers through to _get_mcp_tools_from_manager - _process_mcp_tools_to_openai_format: Thread auth headers through - aresponses_api_with_mcp (responses/main.py): Extract MCP auth headers from secret_fields BEFORE tool list retrieval, not just for auto-execute - acompletion_with_mcp (chat_completions_handler.py): Same fix - extract headers early and pass to tool processing Tests: - test_get_mcp_tools_from_manager_forwards_auth_headers - test_process_mcp_tools_without_openai_transform_forwards_auth_headers Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
parent
6d10233fd3
commit
2a1983aa37
4 changed files with 152 additions and 26 deletions
|
|
@ -177,6 +177,18 @@ async def aresponses_api_with_mcp(
|
|||
"litellm_metadata", {}
|
||||
).get("user_api_key_auth")
|
||||
|
||||
# Extract MCP auth headers from the request for tool list retrieval
|
||||
secret_fields: Optional[Dict[str, Any]] = kwargs.get("secret_fields")
|
||||
(
|
||||
mcp_auth_header,
|
||||
mcp_server_auth_headers,
|
||||
oauth2_headers,
|
||||
raw_headers_from_request,
|
||||
) = ResponsesAPIRequestUtils.extract_mcp_headers_from_request(
|
||||
secret_fields=secret_fields,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
# Get original MCP tools (for events) and OpenAI tools (for LLM) by reusing existing methods
|
||||
(
|
||||
original_mcp_tools,
|
||||
|
|
@ -185,6 +197,10 @@ async def aresponses_api_with_mcp(
|
|||
user_api_key_auth=user_api_key_auth,
|
||||
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy,
|
||||
litellm_trace_id=kwargs.get("litellm_trace_id"),
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||
oauth2_headers=oauth2_headers,
|
||||
raw_headers=raw_headers_from_request,
|
||||
)
|
||||
openai_tools = LiteLLM_Proxy_MCP_Handler._transform_mcp_tools_to_openai(
|
||||
original_mcp_tools
|
||||
|
|
@ -288,18 +304,6 @@ async def aresponses_api_with_mcp(
|
|||
"user_api_key_auth"
|
||||
)
|
||||
|
||||
# Extract MCP auth headers from the request to pass to MCP server
|
||||
secret_fields: Optional[Dict[str, Any]] = kwargs.get("secret_fields")
|
||||
(
|
||||
mcp_auth_header,
|
||||
mcp_server_auth_headers,
|
||||
oauth2_headers,
|
||||
raw_headers_from_request,
|
||||
) = ResponsesAPIRequestUtils.extract_mcp_headers_from_request(
|
||||
secret_fields=secret_fields,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
tool_results = await LiteLLM_Proxy_MCP_Handler._execute_tool_calls(
|
||||
tool_server_map=tool_server_map,
|
||||
tool_calls=tool_calls,
|
||||
|
|
|
|||
|
|
@ -120,7 +120,18 @@ async def acompletion_with_mcp( # noqa: PLR0915
|
|||
(kwargs.get("metadata", {}) or {}).get("user_api_key_auth")
|
||||
)
|
||||
|
||||
# Process MCP tools
|
||||
# Extract MCP auth headers early so they can be used for both tool fetching and execution
|
||||
(
|
||||
mcp_auth_header,
|
||||
mcp_server_auth_headers,
|
||||
oauth2_headers,
|
||||
raw_headers,
|
||||
) = ResponsesAPIRequestUtils.extract_mcp_headers_from_request(
|
||||
secret_fields=kwargs.get("secret_fields"),
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
# Process MCP tools (with auth headers for servers that require authentication)
|
||||
(
|
||||
deduplicated_mcp_tools,
|
||||
tool_server_map,
|
||||
|
|
@ -128,6 +139,10 @@ async def acompletion_with_mcp( # noqa: PLR0915
|
|||
user_api_key_auth=user_api_key_auth,
|
||||
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy,
|
||||
litellm_trace_id=kwargs.get("litellm_trace_id"),
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||
oauth2_headers=oauth2_headers,
|
||||
raw_headers=raw_headers,
|
||||
)
|
||||
|
||||
openai_tools = LiteLLM_Proxy_MCP_Handler._transform_mcp_tools_to_openai(
|
||||
|
|
@ -143,17 +158,6 @@ async def acompletion_with_mcp( # noqa: PLR0915
|
|||
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy
|
||||
)
|
||||
|
||||
# Extract MCP auth headers
|
||||
(
|
||||
mcp_auth_header,
|
||||
mcp_server_auth_headers,
|
||||
oauth2_headers,
|
||||
raw_headers,
|
||||
) = ResponsesAPIRequestUtils.extract_mcp_headers_from_request(
|
||||
secret_fields=kwargs.get("secret_fields"),
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
# Prepare call parameters
|
||||
# Remove keys that shouldn't be passed to acompletion
|
||||
clean_kwargs = {k: v for k, v in kwargs.items() if k not in ["acompletion"]}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth: Any,
|
||||
mcp_tools_with_litellm_proxy: Optional[Iterable[ToolParam]],
|
||||
litellm_trace_id: Optional[str] = None,
|
||||
mcp_auth_header: Optional[str] = None,
|
||||
mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None,
|
||||
oauth2_headers: Optional[Dict[str, str]] = None,
|
||||
raw_headers: Optional[Dict[str, str]] = None,
|
||||
) -> tuple[List[MCPTool], List[str]]:
|
||||
"""
|
||||
Get available tools from the MCP server manager.
|
||||
|
|
@ -106,6 +110,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
Args:
|
||||
user_api_key_auth: User authentication info for access control
|
||||
mcp_tools_with_litellm_proxy: ToolParam objects with server_url starting with "litellm_proxy"
|
||||
mcp_auth_header: Legacy auth header for MCP server (from x-mcp-auth)
|
||||
mcp_server_auth_headers: Server-specific auth headers (from x-mcp-<server>-* headers)
|
||||
oauth2_headers: OAuth2 headers for MCP server
|
||||
raw_headers: Raw HTTP headers from the original request
|
||||
|
||||
Returns:
|
||||
List of MCP tools
|
||||
|
|
@ -133,9 +141,11 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
|
||||
tools = await _get_tools_from_mcp_servers(
|
||||
user_api_key_auth=user_api_key_auth,
|
||||
mcp_auth_header=None,
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
mcp_servers=mcp_servers,
|
||||
mcp_server_auth_headers=None,
|
||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||
oauth2_headers=oauth2_headers,
|
||||
raw_headers=raw_headers,
|
||||
log_list_tools_to_spendlogs=True,
|
||||
list_tools_log_source="responses",
|
||||
litellm_trace_id=litellm_trace_id,
|
||||
|
|
@ -245,6 +255,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth: Any,
|
||||
mcp_tools_with_litellm_proxy: List[ToolParam],
|
||||
litellm_trace_id: Optional[str] = None,
|
||||
mcp_auth_header: Optional[str] = None,
|
||||
mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None,
|
||||
oauth2_headers: Optional[Dict[str, str]] = None,
|
||||
raw_headers: Optional[Dict[str, str]] = None,
|
||||
) -> tuple[List[Any], dict[str, str]]:
|
||||
"""
|
||||
Centralized method to process MCP tools through the complete pipeline.
|
||||
|
|
@ -253,6 +267,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth: User authentication info for access control
|
||||
mcp_tools_with_litellm_proxy: ToolParam objects with server_url starting with "litellm_proxy"
|
||||
litellm_trace_id: Optional trace ID for linking list_mcp_tools spend logs to parent request
|
||||
mcp_auth_header: Legacy auth header for MCP server (from x-mcp-auth)
|
||||
mcp_server_auth_headers: Server-specific auth headers (from x-mcp-<server>-* headers)
|
||||
oauth2_headers: OAuth2 headers for MCP server
|
||||
raw_headers: Raw HTTP headers from the original request
|
||||
|
||||
Returns:
|
||||
List of tools in OpenAI format ready to be sent to the LLM
|
||||
|
|
@ -265,6 +283,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth,
|
||||
mcp_tools_with_litellm_proxy,
|
||||
litellm_trace_id=litellm_trace_id,
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||
oauth2_headers=oauth2_headers,
|
||||
raw_headers=raw_headers,
|
||||
)
|
||||
|
||||
openai_tools = LiteLLM_Proxy_MCP_Handler._transform_mcp_tools_to_openai(
|
||||
|
|
@ -278,6 +300,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth: Any,
|
||||
mcp_tools_with_litellm_proxy: List[ToolParam],
|
||||
litellm_trace_id: Optional[str] = None,
|
||||
mcp_auth_header: Optional[str] = None,
|
||||
mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None,
|
||||
oauth2_headers: Optional[Dict[str, str]] = None,
|
||||
raw_headers: Optional[Dict[str, str]] = None,
|
||||
) -> tuple[List[Any], dict[str, str]]:
|
||||
"""
|
||||
Process MCP tools through filtering and deduplication pipeline without OpenAI transformation.
|
||||
|
|
@ -286,6 +312,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
Args:
|
||||
user_api_key_auth: User authentication info for access control
|
||||
mcp_tools_with_litellm_proxy: ToolParam objects with server_url starting with "litellm_proxy"
|
||||
mcp_auth_header: Legacy auth header for MCP server (from x-mcp-auth)
|
||||
mcp_server_auth_headers: Server-specific auth headers (from x-mcp-<server>-* headers)
|
||||
oauth2_headers: OAuth2 headers for MCP server
|
||||
raw_headers: Raw HTTP headers from the original request
|
||||
|
||||
Returns:
|
||||
List of filtered and deduplicated MCP tools in their original format
|
||||
|
|
@ -301,6 +331,10 @@ class LiteLLM_Proxy_MCP_Handler:
|
|||
user_api_key_auth=user_api_key_auth,
|
||||
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy,
|
||||
litellm_trace_id=litellm_trace_id,
|
||||
mcp_auth_header=mcp_auth_header,
|
||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||
oauth2_headers=oauth2_headers,
|
||||
raw_headers=raw_headers,
|
||||
)
|
||||
|
||||
# Step 2: Filter tools based on allowed_tools parameter
|
||||
|
|
|
|||
|
|
@ -402,3 +402,87 @@ async def test_get_mcp_tools_from_manager_enables_list_tools_logging(monkeypatch
|
|||
assert mock_get_tools.await_args is not None
|
||||
assert mock_get_tools.await_args.kwargs["log_list_tools_to_spendlogs"] is True
|
||||
assert mock_get_tools.await_args.kwargs["list_tools_log_source"] == "responses"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_mcp_tools_from_manager_forwards_auth_headers(monkeypatch):
|
||||
"""
|
||||
Verify that _get_mcp_tools_from_manager forwards x-mcp-auth and
|
||||
x-mcp-<server>-* auth headers to _get_tools_from_mcp_servers.
|
||||
|
||||
Previously these were hardcoded to None, meaning MCP servers that
|
||||
require authentication would fail during tool list retrieval even
|
||||
though the caller supplied the correct headers.
|
||||
"""
|
||||
mock_get_tools = AsyncMock(return_value=[])
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy._experimental.mcp_server.server._get_tools_from_mcp_servers",
|
||||
mock_get_tools,
|
||||
)
|
||||
|
||||
fake_manager = types.SimpleNamespace(
|
||||
get_allowed_mcp_servers=AsyncMock(return_value=[]),
|
||||
get_mcp_servers_from_ids=MagicMock(return_value=[]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager",
|
||||
fake_manager,
|
||||
)
|
||||
|
||||
user_auth = types.SimpleNamespace(api_key="test_key", user_id="test_user")
|
||||
|
||||
test_mcp_auth_header = "Bearer my-mcp-token"
|
||||
test_server_auth_headers = {"deepwiki": {"Authorization": "Bearer deepwiki-token"}}
|
||||
test_oauth2_headers = {"Authorization": "Bearer oauth2-token"}
|
||||
test_raw_headers = {"x-mcp-auth": "Bearer my-mcp-token"}
|
||||
|
||||
await LiteLLM_Proxy_MCP_Handler._get_mcp_tools_from_manager(
|
||||
user_api_key_auth=user_auth,
|
||||
mcp_tools_with_litellm_proxy=[
|
||||
{"type": "mcp", "server_url": "litellm_proxy/mcp/deepwiki"}
|
||||
],
|
||||
mcp_auth_header=test_mcp_auth_header,
|
||||
mcp_server_auth_headers=test_server_auth_headers,
|
||||
oauth2_headers=test_oauth2_headers,
|
||||
raw_headers=test_raw_headers,
|
||||
)
|
||||
|
||||
assert mock_get_tools.await_count == 1
|
||||
call_kwargs = mock_get_tools.await_args.kwargs
|
||||
assert call_kwargs["mcp_auth_header"] == test_mcp_auth_header
|
||||
assert call_kwargs["mcp_server_auth_headers"] == test_server_auth_headers
|
||||
assert call_kwargs["oauth2_headers"] == test_oauth2_headers
|
||||
assert call_kwargs["raw_headers"] == test_raw_headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_mcp_tools_without_openai_transform_forwards_auth_headers(
|
||||
monkeypatch,
|
||||
):
|
||||
"""
|
||||
Verify that _process_mcp_tools_without_openai_transform passes auth
|
||||
headers through to _get_mcp_tools_from_manager.
|
||||
"""
|
||||
mock_get_manager = AsyncMock(return_value=([], []))
|
||||
monkeypatch.setattr(
|
||||
LiteLLM_Proxy_MCP_Handler,
|
||||
"_get_mcp_tools_from_manager",
|
||||
mock_get_manager,
|
||||
)
|
||||
|
||||
test_mcp_auth = "Bearer token-abc"
|
||||
test_server_headers = {"myserver": {"x-api-key": "key123"}}
|
||||
|
||||
await LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform(
|
||||
user_api_key_auth=None,
|
||||
mcp_tools_with_litellm_proxy=[
|
||||
{"type": "mcp", "server_url": "litellm_proxy/mcp/myserver"}
|
||||
],
|
||||
mcp_auth_header=test_mcp_auth,
|
||||
mcp_server_auth_headers=test_server_headers,
|
||||
)
|
||||
|
||||
assert mock_get_manager.await_count == 1
|
||||
call_kwargs = mock_get_manager.await_args.kwargs
|
||||
assert call_kwargs["mcp_auth_header"] == test_mcp_auth
|
||||
assert call_kwargs["mcp_server_auth_headers"] == test_server_headers
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue