diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 4dda665f70..2eea28f6cc 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, @@ -626,6 +638,9 @@ 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 9d4e0aeded..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 @@ -1,9 +1,34 @@ +import sys +import types +from unittest.mock import AsyncMock + import pytest 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: + 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(): @@ -142,3 +167,93 @@ def test_transform_mcp_tools_to_openai_uses_chat_format(monkeypatch): assert resp_tools == [{"responses": True}] assert captured["chat"] == ["tool"] 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) + 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