fix: preserve non-MCP tools in mcp_semantic_tool_filter

Before this fix, the semantic tool filter treated all tools equally
without distinguishing MCP tools from non-MCP built-in tools. This
caused two failure modes:

1. When matches found: only matched MCP tools were returned, silently
   dropping all non-MCP built-in tools from the result.
2. When zero matches: the filter fell back to returning ALL tools
   unfiltered, defeating the purpose and exceeding provider limits.

The fix separates tools into MCP tools (those registered in the semantic
router's _tool_map) and non-MCP tools before filtering. Only MCP tools
are subjected to semantic filtering. Non-MCP tools are always preserved
in the result. On zero matches, only non-MCP tools are returned instead
of the full unfiltered list.

Closes #24984

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
akhilc08 2026-04-03 13:46:31 -04:00
parent d4a3a5e530
commit 0c3cd9ec6e

View file

@ -149,6 +149,29 @@ class SemanticMCPToolFilter:
self.tool_router = None
raise
def _is_mcp_tool(self, tool: Any) -> bool:
"""Check if a tool is an MCP tool (registered in the semantic router)."""
name, _ = self._extract_tool_info(tool)
return name in self._tool_map
def _separate_mcp_and_non_mcp_tools(
self, tools: List[Any]
) -> tuple[List[Any], List[Any]]:
"""
Separate tools into MCP tools and non-MCP tools.
Returns:
Tuple of (mcp_tools, non_mcp_tools)
"""
mcp_tools = []
non_mcp_tools = []
for tool in tools:
if self._is_mcp_tool(tool):
mcp_tools.append(tool)
else:
non_mcp_tools.append(tool)
return mcp_tools, non_mcp_tools
async def filter_tools(
self,
query: str,
@ -158,13 +181,16 @@ class SemanticMCPToolFilter:
"""
Filter tools semantically based on query.
Only MCP tools (those registered in the semantic router) are filtered.
Non-MCP tools are always preserved in the result.
Args:
query: User query to match against tools
available_tools: Full list of available MCP tools
available_tools: Full list of available tools (MCP and non-MCP)
top_k: Override default top_k (optional)
Returns:
Filtered and ordered list of tools (up to top_k)
Non-MCP tools plus filtered and ordered MCP tools (up to top_k)
"""
# Early returns for cases where we can't/shouldn't filter
if not self.enabled:
@ -183,16 +209,32 @@ class SemanticMCPToolFilter:
)
return available_tools
# Run semantic filtering
# Separate MCP tools from non-MCP tools before filtering
mcp_tools, non_mcp_tools = self._separate_mcp_and_non_mcp_tools(
available_tools
)
# If there are no MCP tools to filter, return everything as-is
if not mcp_tools:
return available_tools
# Run semantic filtering on MCP tools only
try:
limit = top_k or self.top_k
matches = self.tool_router(text=query, limit=limit)
matched_tool_names = self._extract_tool_names_from_matches(matches)
if not matched_tool_names:
return available_tools
# No MCP tools matched the query - return only non-MCP tools
verbose_logger.debug(
"No MCP tools matched query, returning only non-MCP tools"
)
return non_mcp_tools
return self._get_tools_by_names(matched_tool_names, available_tools)
filtered_mcp_tools = self._get_tools_by_names(
matched_tool_names, mcp_tools
)
return non_mcp_tools + filtered_mcp_tools
except Exception as e:
verbose_logger.error(f"Semantic tool filter failed: {e}", exc_info=True)