From 0c3cd9ec6e856ae0d3cc1653c5c62ee8d9128697 Mon Sep 17 00:00:00 2001 From: akhilc08 Date: Fri, 3 Apr 2026 13:46:31 -0400 Subject: [PATCH] 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) --- .../mcp_server/semantic_tool_filter.py | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py b/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py index 0bafd7da265..6906e582d8e 100644 --- a/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py +++ b/litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py @@ -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)