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.
This commit is contained in:
Lance Hsu 2026-04-02 18:33:44 +08:00
parent d1df4e838b
commit f25ce7437c
No known key found for this signature in database
GPG key ID: CFF816BB8560B8B8
2 changed files with 57 additions and 1 deletions

View file

@ -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)

View file

@ -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