From f25ce7437c28e726fd48cc0db498fd1684b313a8 Mon Sep 17 00:00:00 2001 From: Lance Hsu Date: Thu, 2 Apr 2026 18:33:44 +0800 Subject: [PATCH] fix(mcp): return only non-MCP tools on zero semantic matches When the semantic tool filter finds no matches for a user query, it previously returned all available tools. With many MCP servers this can exceed provider tool limits. Now it drops MCP tools (prefixed) and returns only non-MCP tools on zero matches. --- .../mcp_server/semantic_tool_filter.py | 8 ++- .../mcp_server/test_semantic_tool_filter.py | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py b/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py index 0bafd7da265..29bf029c1e0 100644 --- a/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py +++ b/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py @@ -6,6 +6,7 @@ Filters MCP tools semantically for /chat/completions and /responses endpoints. from typing import TYPE_CHECKING, Any, Dict, List, Optional from litellm._logging import verbose_logger +from litellm.proxy._experimental.mcp_server.utils import is_tool_name_prefixed if TYPE_CHECKING: from semantic_router.routers import SemanticRouter @@ -190,7 +191,12 @@ class SemanticMCPToolFilter: matched_tool_names = self._extract_tool_names_from_matches(matches) if not matched_tool_names: - return available_tools + # No semantic matches — drop MCP tools (prefixed) and keep only + # non-MCP tools to avoid exceeding provider tool limits. + return [ + t for t in available_tools + if not is_tool_name_prefixed(self._extract_tool_info(t)[0]) + ] return self._get_tools_by_names(matched_tool_names, available_tools) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py index 87c597c659b..73f9e6c7ca3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py @@ -392,3 +392,53 @@ async def test_semantic_filter_hook_skips_no_tools(): assert result is None, "Hook should skip requests without tools" print("✅ Hook correctly skips requests without tools") + +@pytest.mark.asyncio +async def test_semantic_filter_zero_matches_returns_only_non_mcp_tools(): + """ + Test that when zero semantic matches are found, only non-MCP tools are returned. + + MCP tools have server-name prefixes (e.g., "weather-get_forecast"). + Non-MCP tools don't (e.g., "web_search"). On zero matches, the filter + should drop all MCP tools to avoid exceeding the 128 tool limit. + """ + from litellm.proxy._experimental.mcp_server.semantic_tool_filter import ( + SemanticMCPToolFilter, + ) + + mock_router = Mock() + filter_instance = SemanticMCPToolFilter( + embedding_model="text-embedding-3-small", + litellm_router_instance=mock_router, + top_k=5, + similarity_threshold=0.3, + enabled=True, + ) + + # Mock the semantic router to return no matches + filter_instance.tool_router = Mock(return_value=[]) + + # Mix of MCP tools (prefixed) and non-MCP tools (no prefix) + available_tools = [ + MCPTool(name="weather-get_forecast", description="Get forecast", inputSchema={"type": "object"}), + MCPTool(name="email-send_email", description="Send email", inputSchema={"type": "object"}), + MCPTool(name="docs-read_document", description="Read doc", inputSchema={"type": "object"}), + MCPTool(name="web_search", description="Search the web", inputSchema={"type": "object"}), + MCPTool(name="code_interpreter", description="Run code", inputSchema={"type": "object"}), + ] + + filtered = await filter_instance.filter_tools( + query="hello", + available_tools=available_tools, + ) + + # Should return only non-MCP tools + filtered_names = [t.name for t in filtered] + assert "web_search" in filtered_names + assert "code_interpreter" in filtered_names + assert len(filtered) == 2, f"Expected 2 non-MCP tools, got {len(filtered)}: {filtered_names}" + # MCP tools should be excluded + assert "weather-get_forecast" not in filtered_names + assert "email-send_email" not in filtered_names + assert "docs-read_document" not in filtered_names +