Skip to content

Fix MCP semantic filter native tool passthrough#26956

Open
Genmin wants to merge 3 commits intoBerriAI:litellm_internal_stagingfrom
Genmin:fix/mcp-semantic-filter-preserve-native-tools
Open

Fix MCP semantic filter native tool passthrough#26956
Genmin wants to merge 3 commits intoBerriAI:litellm_internal_stagingfrom
Genmin:fix/mcp-semantic-filter-preserve-native-tools

Conversation

@Genmin
Copy link
Copy Markdown

@Genmin Genmin commented May 1, 2026

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_filter no longer strips normal OpenAI-format tools from /chat/completions or /responses requests.

This covers three cases:

  • all-native tool requests are left untouched and do not emit semantic-filter stats
  • mixed native + MCP-router tools filter only the MCP tools and merge native tools back unchanged
  • LiteLLM-proxy MCP reference expansion preserves non-MCP tools that arrived in the same tools array

The 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 -q
  • uv run ruff check litellm/proxy/hooks/mcp_semantic_filter/hook.py tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py
  • git diff --check

The semantic filter test module passes: 19 passed, with existing semantic_router Pydantic deprecation warnings from the dependency.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

❌ Patch coverage is 92.95775% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/hooks/mcp_semantic_filter/hook.py 92.95% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR fixes the MCP semantic filter stripping caller-owned native tools by capturing other_tools from _parse_mcp_tools (previously discarded) and introducing a _partition_mcp_router_tools helper that uses _tool_map + canonical-name matching to classify each tool before filtering — so only MCP router tools are passed to filter.filter_tools and native tools bypass it unchanged. Three new mock-only tests cover all three described cases.

Confidence Score: 4/5

Safe 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 _partition_mcp_router_tools and _is_mcp_router_tool.

Important Files Changed

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines +160 to +163

return any(
name_matches_canonical(tool_name, canonical_name)
for canonical_name in tool_map
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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).

Comment on lines +148 to +164
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
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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?

Bojun-Vvibe added a commit to Bojun-Vvibe/oss-contributions that referenced this pull request May 1, 2026
…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)
@Genmin Genmin force-pushed the fix/mcp-semantic-filter-preserve-native-tools branch from 91b038d to b860ef5 Compare May 1, 2026 21:56
@Genmin Genmin force-pushed the fix/mcp-semantic-filter-preserve-native-tools branch from b860ef5 to 631b414 Compare May 2, 2026 01:39
@Genmin
Copy link
Copy Markdown
Author

Genmin commented May 2, 2026

Rebased this branch onto the latest litellm_internal_staging to clear the dirty merge state; the only remaining diff is the semantic-filter hook and its focused tests. Local validation after the rebase: uv run ruff check litellm/proxy/hooks/mcp_semantic_filter/hook.py tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py and uv run --extra proxy --with semantic-router pytest tests/test_litellm/proxy/_experimental/mcp_server/test_semantic_tool_filter.py -q (22 passed).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: mcp_semantic_tool_filter silently drops all non-MCP ("native") tools

2 participants