Fix MCP semantic filter native tool passthrough#26956
Fix MCP semantic filter native tool passthrough#26956Genmin wants to merge 3 commits intoBerriAI:litellm_internal_stagingfrom
Conversation
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes the MCP semantic filter stripping caller-owned native tools by capturing Confidence Score: 4/5Safe to merge; the fix is logically correct and well-covered by new tests. Only P2 concerns remain. All findings are P2: a tool-ordering change (native tools always placed before MCP tools regardless of original interleaving), a name-collision edge case for Responses-API tools sharing a name with MCP canonicals, and a minor O(n) scan per tool for prefix matching. No P0/P1 regressions introduced. litellm/proxy/hooks/mcp_semantic_filter/hook.py — tool ordering and name-collision logic in
|
| Filename | Overview |
|---|---|
| litellm/proxy/hooks/mcp_semantic_filter/hook.py | Core fix: captures other_tools from _parse_mcp_tools, adds _get_tool_name/_is_mcp_router_tool/_partition_mcp_router_tools helpers to separate native from MCP tools before semantic filtering; tool reordering and name-collision edge cases are P2. |
| tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py | Adds three new targeted tests covering all three cases from the PR description; only removes unused imports and debug print statements from existing tests — no weakening of assertions. |
Reviews (1): Last reviewed commit: "fix mcp semantic filter native tool pass..." | Re-trigger Greptile
| available_tools=tools, # type: ignore | ||
| available_tools=mcp_tools, # type: ignore | ||
| ) | ||
| filtered_tools = native_tools + filtered_mcp_tools |
There was a problem hiding this comment.
Tool order silently changed for mixed-tool requests
filtered_tools = native_tools + filtered_mcp_tools always places native tools first, then MCP tools. If the original request interleaved them (e.g. [mcp_tool, native_tool, mcp_tool]), the reordering could influence model behavior since some LLMs are sensitive to the order tools are presented. The original relative positions of native tools among MCP tools are not preserved.
|
|
||
| return any( | ||
| name_matches_canonical(tool_name, canonical_name) | ||
| for canonical_name in tool_map |
There was a problem hiding this comment.
O(n) prefix scan on every tool per request
When tool_name is not an exact hit in _tool_map, the fallback any(name_matches_canonical(tool_name, c) for c in tool_map) scans every canonical name for each tool in the request. For a large MCP registry with thousands of tools, this becomes O(tools_in_request × registry_size) per call. Consider pre-computing a reverse index from suffix patterns to canonical names, or accepting that any() short-circuits on the first match (so cost is only worst-case when no match exists).
| def _is_mcp_router_tool(self, tool: Any) -> bool: | ||
| tool_name = self._get_tool_name(tool) | ||
| if not tool_name: | ||
| return False | ||
|
|
||
| tool_map = getattr(self.filter, "_tool_map", {}) | ||
| if tool_name in tool_map: | ||
| return True | ||
|
|
||
| name_matches_canonical = getattr(self.filter, "_name_matches_canonical", None) | ||
| if name_matches_canonical is None: | ||
| return False | ||
|
|
||
| return any( | ||
| name_matches_canonical(tool_name, canonical_name) | ||
| for canonical_name in tool_map | ||
| ) |
There was a problem hiding this comment.
Native Responses-API tool name can collide with MCP canonical
_is_mcp_router_tool classifies tools purely by name. A Responses-API native tool with a top-level "name" key matching an MCP router canonical (e.g. {"type": "function", "name": "github-search", ...}) will be sent to filter.filter_tools as if it were an MCP tool and may be dropped when the query doesn't match. MCP canonicals are server-prefixed in practice, so collisions are unlikely — but there is no way for the caller to prevent one. Is there an explicit contract that Responses-API native tools will never share names with MCP router canonicals, or should a discriminator (e.g. the tool's type field or presence of server metadata) be used instead of name-only classification?
…ini-cli) - openai/codex#20564 — Enforce animations=false for screen readers (merge-as-is) - openai/codex#20530 — Support multi-env filesystem tools (merge-after-nits) - BerriAI/litellm#26956 — Fix MCP semantic filter native tool passthrough (merge-after-nits) - google-gemini/gemini-cli#26277 — docs(sdk): add JSDoc to all exports (merge-as-is)
91b038d to
b860ef5
Compare
b860ef5 to
631b414
Compare
|
Rebased this branch onto the latest |
Relevant issues
Fixes #26212
Summary
The MCP semantic filter now applies only to tools that are known to the MCP router. Caller-owned native tools pass through unchanged, so enabling
litellm_settings.mcp_semantic_tool_filterno longer strips normal OpenAI-format tools from/chat/completionsor/responsesrequests.This covers three cases:
toolsarrayThe hook also extracts tool names from both Chat Completions-style nested function tools (
function.name) and Responses-style top-level function tools (name), so headers remain accurate after native tools are preserved.Validation
uv run --extra proxy --extra semantic-router pytest tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py -quv run ruff check litellm/proxy/hooks/mcp_semantic_filter/hook.py tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.pygit diff --checkThe semantic filter test module passes: 19 passed, with existing
semantic_routerPydantic deprecation warnings from the dependency.