fix(mcp): is_tool_name_prefixed validates against known server prefixes#25085
Conversation
Fixes BerriAI#25081. is_tool_name_prefixed() checked for the presence of MCP_TOOL_PREFIX_SEPARATOR (default '-') anywhere in the tool name. Any non-MCP tool whose name contains a hyphen (e.g. 'text-to-speech', 'code-review') was silently misclassified as an MCP-prefixed tool. When the semantic tool filter is enabled, these tools would be routed through semantic matching and potentially dropped. Fix: accept an optional known_server_prefixes set. When supplied, the function extracts the candidate prefix (text before the first separator) and checks it against the normalised set of registered server prefixes. Only a genuine match returns True. Without the set, legacy behaviour is preserved for backward compatibility. Updated _get_mcp_server_from_tool_name() to build the prefix set from the live registry and pass it through. 9 new tests.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a silent false-positive bug in Key changes:
Confidence Score: 5/5Safe to merge — focused, backward-compatible bug fix with full test coverage and no regressions. All remaining findings are P2 style suggestions (bare No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/utils.py | Adds optional known_server_prefixes param to is_tool_name_prefixed; logic is correct and backward-compatible; minor: bare set type annotation instead of Set[str]. |
| litellm/proxy/_experimental/mcp_server/mcp_server_manager.py | Builds registry-derived prefix set before calling is_tool_name_prefixed, correctly scoping the check to known servers; minor: get_server_prefix invoked twice per server in the set comprehension. |
| tests/test_litellm/proxy/_experimental/mcp_server/test_is_tool_name_prefixed.py | 9 new unit tests covering legacy heuristic, known-prefix matching, empty-prefix-set, normalisation, and non-MCP hyphenated names; no real network calls; covers all key branches. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["is_tool_name_prefixed(tool_name, known_server_prefixes)"] --> B{MCP_TOOL_PREFIX_SEPARATOR\nin tool_name?}
B -- No --> C["return False"]
B -- Yes --> D{known_server_prefixes\nprovided?}
D -- No --> E["Legacy: return True\n(heuristic — any hyphen)"]
D -- Yes --> F["candidate_prefix = tool_name.split(sep, 1)[0]"]
F --> G["normalize_server_name(candidate_prefix)\nin known_server_prefixes?"]
G -- Yes --> H["return True ✅\n(genuine MCP prefix)"]
G -- No --> I["return False ✅\n(e.g. 'text-to-speech')"]
Reviews (1): Last reviewed commit: "fix(mcp): is_tool_name_prefixed validate..." | Re-trigger Greptile
| def is_tool_name_prefixed(tool_name: str) -> bool: | ||
| def is_tool_name_prefixed( | ||
| tool_name: str, | ||
| known_server_prefixes: Optional[set] = None, |
There was a problem hiding this comment.
Unparameterized
set type annotation
known_server_prefixes: Optional[set] uses a bare, unparameterized set. The codebase uses Optional[Set[str]] elsewhere (e.g. with from typing import Set). Bare set loses the element-type information for mypy and IDEs.
| known_server_prefixes: Optional[set] = None, | |
| known_server_prefixes: Optional[Set[str]] = None, |
You'll also need to add Set to the existing from typing import line.
| known_prefixes = { | ||
| normalize_server_name(get_server_prefix(s)) | ||
| for s in self.get_registry().values() | ||
| if get_server_prefix(s) | ||
| } |
There was a problem hiding this comment.
get_server_prefix called twice per server in set comprehension
get_server_prefix(s) is invoked once in the if guard and once to produce the value, so it's called twice for every server in the registry. For a small registry this is negligible, but it's cleaner to call it once:
| known_prefixes = { | |
| normalize_server_name(get_server_prefix(s)) | |
| for s in self.get_registry().values() | |
| if get_server_prefix(s) | |
| } | |
| known_prefixes = { | |
| normalized | |
| for s in self.get_registry().values() | |
| for prefix in (get_server_prefix(s),) | |
| if prefix | |
| for normalized in (normalize_server_name(prefix),) | |
| } |
Alternatively a simple loop:
known_prefixes: Set[str] = set()
for s in self.get_registry().values():
prefix = get_server_prefix(s)
if prefix:
known_prefixes.add(normalize_server_name(prefix))There was a problem hiding this comment.
Good catch on the double call. However, get_server_prefix is a pure O(1) string split — no I/O, no allocation beyond the prefix string itself. The set comprehension runs once at call time (not in a hot loop), and the number of MCP servers is typically single-digit. The readability win of the guard + value pattern outweighs caching here IMO, but happy to add a walrus operator (if (p := get_server_prefix(s)) is not None) if the team prefers.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
cf94f4d
into
BerriAI:litellm_oss_staging_04_04_2026
|
The merge commit Happy to open a replacement PR that reapplies this change (with cc @krrish-berri-2 (original merger), @voidborne-d (original author) |
Summary
Fixes #25081.
is_tool_name_prefixed()checked for the presence ofMCP_TOOL_PREFIX_SEPARATOR(default"-") anywhere in the tool name. Any non-MCP tool whose name contains a hyphen (e.g.text-to-speech,code-review) was silently misclassified as an MCP-prefixed tool.When
mcp_semantic_tool_filteris enabled, these tools get routed through the semantic filter and potentially dropped — a silent data loss bug.Root Cause
Fix
Added an optional
known_server_prefixesparameter. When supplied, the function extracts the candidate prefix (text before the first separator) and checks it against the normalised set of registered server prefixes. Only a genuine match returnsTrue.Without the set, legacy behaviour is preserved for backward compatibility.
Updated
_get_mcp_server_from_tool_name()to build the prefix set from the live registry and pass it through.Changes
litellm/proxy/_experimental/mcp_server/utils.py—is_tool_name_prefixed()now acceptsknown_server_prefixes: Optional[set]litellm/proxy/_experimental/mcp_server/mcp_server_manager.py—_get_mcp_server_from_tool_name()builds the prefix set from the registryTesting