diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 51e2ea63329..88a61f955da 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3119,6 +3119,7 @@ if MCP_AVAILABLE: server=mcp_server, raw_headers=raw_headers, litellm_logging_obj=litellm_logging_obj, + tool=global_mcp_server_manager.get_listed_tool(mcp_server, original_tool_name), ) # `pre_call_tool_check` may return guardrail-modified # arguments; honor them on the local path too. @@ -3225,6 +3226,7 @@ if MCP_AVAILABLE: server=prefix_server, raw_headers=raw_headers, litellm_logging_obj=litellm_logging_obj, + tool=global_mcp_server_manager.get_listed_tool(prefix_server, original_tool_name), ) if "arguments" in hook_result: arguments = hook_result["arguments"] # pyright: ignore[reportAny] # hook returns untyped args diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index 3db642bcc12..f8955754618 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -17,6 +17,7 @@ from mcp.types import ( TextContent, TextResourceContents, ) +from mcp.types import Tool as MCPTool from litellm.proxy._types import ( LiteLLM_MCPServerTable, @@ -7159,6 +7160,58 @@ 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_hands_openapi_listed_tool_metadata_to_pre_call_hooks(): + """OpenAPI-generated tools dispatch through the local registry, so the pre-call hooks must get the + listed description and input schema on that path too, not only on the managed-server path.""" + from litellm.proxy._experimental.mcp_server import server as mcp_module + + petstore = MCPServer( + server_id="petstore-id", + name="petstore", + server_name="petstore", + transport=MCPTransport.http, + url=None, + spec_path="https://example.com/petstore.yaml", + ) + schema = {"type": "object", "properties": {"limit": {"type": "integer"}}} + mcp_module.global_mcp_tool_registry.register_tool( + name="petstore-list_pets", description="List the pets", input_schema=schema, handler=lambda: None + ) + manager = mcp_module.global_mcp_server_manager + manager._create_prefixed_tools( + [MCPTool(name="list_pets", description="List the pets", inputSchema=schema)], petstore + ) + pre_call_tool_check = AsyncMock(return_value={}) + + try: + with ( + patch.object(manager, "_get_mcp_server_from_tool_name", return_value=petstore), + patch.object(manager, "pre_call_tool_check", new=pre_call_tool_check), + patch( + "litellm.proxy._experimental.mcp_server.server._handle_local_mcp_tool", + new=AsyncMock(return_value=[]), + ), + patch( + "litellm.proxy._experimental.mcp_server.server.MCPRequestHandler.is_tool_allowed", + return_value=True, + ), + ): + await mcp_module.execute_mcp_tool( + name="petstore-list_pets", + arguments={"limit": 10}, + allowed_mcp_servers=[petstore], + start_time=datetime.now(), + user_api_key_auth=UserAPIKeyAuth(api_key="sk-user", user_id="alice"), + ) + finally: + mcp_module.global_mcp_tool_registry.unregister_tools_with_prefix("petstore-") + manager._listed_tools_by_server_id.pop(petstore.server_id, None) + + handed_tool = pre_call_tool_check.call_args.kwargs["tool"] + assert handed_tool is not None and (handed_tool.description, handed_tool.inputSchema) == ("List the pets", schema) + + @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.