mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
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:
parent
d1df4e838b
commit
f25ce7437c
2 changed files with 57 additions and 1 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue