From 48579eafe3098daac1ccd04eedfd631454f131d0 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 13 Dec 2025 07:17:50 +0900 Subject: [PATCH 1/3] fix: strip mcp server prefixes in responses --- .../mcp/litellm_proxy_mcp_handler.py | 14 +++- .../mcp/test_litellm_proxy_mcp_handler.py | 84 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 4dda665f70..40f185a27b 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -494,9 +494,21 @@ class LiteLLM_Proxy_MCP_Handler: server_name = tool_server_map[tool_name] + # Remove the server name prefix if the tool name includes it. + sanitized_tool_name = tool_name + unprefixed_name, prefixed_server_name = split_server_prefix_from_name( + tool_name + ) + if ( + prefixed_server_name + and prefixed_server_name == server_name + and unprefixed_name + ): + sanitized_tool_name = unprefixed_name + result = await global_mcp_server_manager.call_tool( server_name=server_name, - name=tool_name, + name=sanitized_tool_name, arguments=parsed_arguments, user_api_key_auth=user_api_key_auth, mcp_auth_header=mcp_auth_header, diff --git a/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py b/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py index 9d4e0aeded..ab84294b35 100644 --- a/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py +++ b/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py @@ -1,3 +1,7 @@ +import sys +import types +from unittest.mock import AsyncMock + import pytest from litellm.responses.mcp.litellm_proxy_mcp_handler import ( @@ -6,6 +10,26 @@ from litellm.responses.mcp.litellm_proxy_mcp_handler import ( from litellm.types.utils import ModelResponse +class _DummyMCPResult: + def __init__(self): + self.content = [] + + +def _setup_mcp_call_environment(monkeypatch: pytest.MonkeyPatch) -> AsyncMock: + """Patch MCP globals so _execute_tool_calls can run in tests.""" + proxy_module = types.SimpleNamespace(proxy_logging_obj=object()) + monkeypatch.setitem(sys.modules, "litellm.proxy.proxy_server", proxy_module) + + fake_manager = types.SimpleNamespace( + call_tool=AsyncMock(return_value=_DummyMCPResult()) + ) + monkeypatch.setattr( + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager", + fake_manager, + ) + return fake_manager.call_tool + + def test_deduplicate_mcp_tools_single_allowed_server(): tools = [{"name": "search"}, {"name": "search"}] # duplicate on purpose @@ -142,3 +166,63 @@ def test_transform_mcp_tools_to_openai_uses_chat_format(monkeypatch): assert resp_tools == [{"responses": True}] assert captured["chat"] == ["tool"] assert captured["responses"] == ["tool"] + + +@pytest.mark.asyncio +async def test_execute_tool_calls_strips_server_prefix(monkeypatch): + call_tool_mock = _setup_mcp_call_environment(monkeypatch) + tool_name = "deepwiki-read_wiki_structure" + tool_calls = [ + { + "id": "call-1", + "function": {"name": tool_name, "arguments": "{}"}, + } + ] + + await LiteLLM_Proxy_MCP_Handler._execute_tool_calls( + tool_server_map={tool_name: "deepwiki"}, + tool_calls=tool_calls, + user_api_key_auth=None, + ) + + assert call_tool_mock.await_args.kwargs["name"] == "read_wiki_structure" + + +@pytest.mark.asyncio +async def test_execute_tool_calls_keeps_tool_name_without_prefix(monkeypatch): + call_tool_mock = _setup_mcp_call_environment(monkeypatch) + tool_name = "read_wiki_structure" + tool_calls = [ + { + "id": "call-2", + "function": {"name": tool_name, "arguments": "{}"}, + } + ] + + await LiteLLM_Proxy_MCP_Handler._execute_tool_calls( + tool_server_map={tool_name: "deepwiki"}, + tool_calls=tool_calls, + user_api_key_auth=None, + ) + + assert call_tool_mock.await_args.kwargs["name"] == tool_name + + +@pytest.mark.asyncio +async def test_execute_tool_calls_keeps_tool_name_when_equal_to_server(monkeypatch): + call_tool_mock = _setup_mcp_call_environment(monkeypatch) + tool_name = "echo" + tool_calls = [ + { + "id": "call-3", + "function": {"name": tool_name, "arguments": "{}"}, + } + ] + + await LiteLLM_Proxy_MCP_Handler._execute_tool_calls( + tool_server_map={tool_name: "echo"}, + tool_calls=tool_calls, + user_api_key_auth=None, + ) + + assert call_tool_mock.await_args.kwargs["name"] == tool_name From 864b61b43327cc3ab9e8fcc1274f0615178c058b Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 13 Dec 2025 07:55:13 +0900 Subject: [PATCH 2/3] fix: support ResponseFunctionToolCall in follow-up input --- .../mcp/litellm_proxy_mcp_handler.py | 5 +++ .../mcp/test_litellm_proxy_mcp_handler.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 40f185a27b..2a63789383 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -638,6 +638,11 @@ class LiteLLM_Proxy_MCP_Handler: function_calls: List[Dict[str, Any]] = [] for output_item in response.output: + if not isinstance(output_item, dict) and hasattr( + output_item, "model_dump" + ): + output_item = output_item.model_dump() + if isinstance(output_item, dict): if output_item.get("type") == "function_call": call_id = output_item.get("call_id") or output_item.get("id") diff --git a/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py b/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py index ab84294b35..b632e72f56 100644 --- a/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py +++ b/tests/test_litellm/responses/mcp/test_litellm_proxy_mcp_handler.py @@ -8,6 +8,7 @@ from litellm.responses.mcp.litellm_proxy_mcp_handler import ( LiteLLM_Proxy_MCP_Handler, ) from litellm.types.utils import ModelResponse +from litellm.types.responses.main import OutputFunctionToolCall class _DummyMCPResult: @@ -168,6 +169,36 @@ def test_transform_mcp_tools_to_openai_uses_chat_format(monkeypatch): assert captured["responses"] == ["tool"] +def test_create_follow_up_input_handles_response_function_tool_call(): + response = types.SimpleNamespace( + output=[ + OutputFunctionToolCall( + id="id", + type="function_call", + call_id="call-1", + name="foo", + arguments="{}", + status="completed", + ) + ] + ) + + follow_up = LiteLLM_Proxy_MCP_Handler._create_follow_up_input( + response=response, + tool_results=[], + original_input=None, + ) + + assert follow_up == [ + { + "type": "function_call", + "call_id": "call-1", + "name": "foo", + "arguments": "{}", + } + ] + + @pytest.mark.asyncio async def test_execute_tool_calls_strips_server_prefix(monkeypatch): call_tool_mock = _setup_mcp_call_environment(monkeypatch) From bcb26cc55aee599cbd50c2d49fefd67177809040 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sat, 13 Dec 2025 07:56:05 +0900 Subject: [PATCH 3/3] fix: format --- litellm/responses/mcp/litellm_proxy_mcp_handler.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 2a63789383..2eea28f6cc 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -638,9 +638,7 @@ class LiteLLM_Proxy_MCP_Handler: function_calls: List[Dict[str, Any]] = [] for output_item in response.output: - if not isinstance(output_item, dict) and hasattr( - output_item, "model_dump" - ): + if not isinstance(output_item, dict) and hasattr(output_item, "model_dump"): output_item = output_item.model_dump() if isinstance(output_item, dict):