Skip to content

fix(mcp): resolve toolset tools by the server's known prefix - #31254

Merged
tin-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_toolset_tool_prefix_resolution
Jun 25, 2026
Merged

fix(mcp): resolve toolset tools by the server's known prefix#31254
tin-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_toolset_tool_prefix_resolution

Conversation

@tin-berri

@tin-berri tin-berri commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-3419

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Toolsets persist each tool as {server_id, bare tool_name}. At list time the bare name is reconciled against the live tool name, which the gateway serves prefixed as {server-prefix}-{tool}. The old reconciliation chopped the live name at the first -, so a server whose prefix contains a - had its tools silently dropped from the toolset. A server gets such a prefix when it has no alias and falls back to its UUID server_id, or when it carries a legacy hyphenated alias. Servers with a clean prefix (no separator) were unaffected, which is why some toolsets worked and others returned nothing.

Reproduced against a live proxy on localhost:4099 backed by real Postgres, talking to the public DeepWiki MCP server. The server is registered with no alias, so its prefix is its hyphenated UUID.

Setup (identical for both runs):

# DeepWiki MCP server, no alias -> prefix is the hyphenated UUID server_id
SID=$(curl -s -X POST http://localhost:4099/v1/mcp/server \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d '{"url":"https://mcp.deepwiki.com/mcp","transport":"http"}' | jq -r .server_id)
# -> 20976bf7-f206-4638-a499-94cc4c242346

# toolset stores BARE tool names
curl -s -X POST http://localhost:4099/v1/mcp/toolset \
  -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" \
  -d "{\"toolset_name\":\"wikiset\",\"tools\":[
        {\"server_id\":\"$SID\",\"tool_name\":\"read_wiki_contents\"},
        {\"server_id\":\"$SID\",\"tool_name\":\"read_wiki_structure\"}]}"

Listing tools through the toolset URL, before and after the change. The global /mcp/ count stays at 3 in both runs, so the server and its tools are present throughout; only the toolset filter differs.

list() { curl -s "$1" -H "Authorization: Bearer sk-1234" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'; }
list http://localhost:4099/mcp/                 # global, sanity
list http://localhost:4099/toolset/wikiset/mcp  # the toolset

Before (current litellm_internal_staging):

global tools available: 3            <- server present, tools served
/toolset/wikiset/mcp tools/list: {"result":{"tools":[]}}   <- dropped

After (this PR):

global tools available: 3
/toolset/wikiset/mcp tools/list:
   20976bf7-f206-4638-a499-94cc4c242346-read_wiki_structure
   20976bf7-f206-4638-a499-94cc4c242346-read_wiki_contents

The dashboard change is display only. On the MCP Toolsets tab the "Your Toolset" chips and the toolset list "Tools" column now render each tool as {server-prefix}-{tool} (for example deepwiki-read_wiki_contents) so the same tool name on different servers is distinguishable; the persisted record is unchanged. To see it, open http://localhost:4000/ui/?page=mcp-servers, go to the Toolsets tab, create or edit a toolset, and pick a couple of tools.

Type

🐛 Bug Fix

Changes

The reconciliation between a toolset's stored bare tool name and the live prefixed tool name used split_server_prefix_from_name, which removes everything up to the first MCP_TOOL_PREFIX_SEPARATOR with no knowledge of the server. When the server's prefix itself contains the separator the cut lands inside the prefix, so the stripped name never matches the stored bare name and the tool is filtered out.

The toolset already stores the server_id, so the prefix is knowable. This adds strip_known_server_prefix(name, server) in utils.py, which removes exactly {known_prefix}{separator} for one of the server's registered prefixes (via iter_known_server_prefixes) and leaves the name untouched when none match. filter_tools_by_key_team_permissions (the list/filter side) and resolve_toolset_tool_permissions (the stored-name side) both use it now, keyed by the server_id they already hold, so the two sides reduce to the same true bare name for hyphenated aliases, UUID prefixes, and the short-prefix mode alike. Storage format is unchanged and nothing is prefixed on write.

The same first-separator weakness exists in the server-level allowed_tools / display-override paths (_tool_name_matches, apply_tool_overrides); those are a separate feature gated on optional per-server config and are left for a follow-up to keep this change scoped to the toolset bug.

Tests extend test_mcp_toolset_scope.py with a parametrized regression over a clean alias, a hyphenated alias, and a no-alias UUID prefix, asserting the granted tools survive the filter and that resolution reduces an already-prefixed stored name back to bare. The clean-alias case passes on the old code; the hyphenated and UUID cases fail before this change and pass after.

A follow-up commit adds focused unit tests for strip_known_server_prefix in test_short_mcp_tool_prefix.py, exercising the clean, hyphenated-alias, and UUID server_id prefixes plus the unprefixed-passthrough and server=None fallback paths directly on the helper with real MCPServer objects

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes toolset tool filtering for MCP servers whose prefix contains a hyphen — such as a hyphenated alias or a UUID server_id used as the fallback prefix. The root cause was that split_server_prefix_from_name always cut at the first separator, which lands inside the prefix itself when the prefix contains hyphens, so the stripped name never matched the stored bare tool name and all tools were silently dropped.

  • Core fix (utils.py): Adds strip_known_server_prefix(name, server), which iterates every registered prefix form for the server via iter_known_server_prefixes and removes exactly {known_prefix}{separator} — cleanly handling hyphenated aliases, UUID server_id fallbacks, and the existing short-prefix mode. Falls back to the legacy split only when server is None.
  • Call-site updates (server.py, mcp_server_manager.py): Both filter_tools_by_key_team_permissions and resolve_toolset_tool_permissions now call strip_known_server_prefix with the server object fetched from the in-memory registry.
  • Tests: Direct unit tests in test_short_mcp_tool_prefix.py use real MCPServer objects with the _reset_env autouse fixture, faithfully reproducing the hyphenated-alias and UUID-fallback failure cases regardless of environment. UI changes are display-only, rendering chips as {server-prefix}-{tool} for disambiguation.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped to prefix stripping, storage format is unchanged, and the fallback to legacy behavior when the server is not found in the registry preserves existing behavior for any unregistered server.

The change replaces a single heuristic split with an exact-prefix lookup keyed on the server object the caller already holds. The new helper is fully invertible with add_server_prefix_to_name, the registry lookup is in-memory and cached at the resolution layer, and direct unit tests using real MCPServer objects confirm round-trip correctness for all three prefix forms.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/utils.py Adds strip_known_server_prefix that iterates all known server prefixes via iter_known_server_prefixes and strips the exact {prefix}{separator} match — correctly handling hyphenated aliases and UUID server_id fallback prefixes where the old first-separator split failed.
litellm/proxy/_experimental/mcp_server/server.py Replaces split_server_prefix_from_name with strip_known_server_prefix in filter_tools_by_key_team_permissions; also slightly refactors the function to use an early return for the no-restriction case. Change is correct and the fallback when get_mcp_server_by_id returns None is safe.
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Switches resolve_toolset_tool_permissions to use strip_known_server_prefix; get_mcp_server_by_id is an in-memory registry lookup, and results are cached after the first resolution so the per-tool call overhead is negligible.
tests/test_litellm/proxy/_experimental/mcp_server/test_short_mcp_tool_prefix.py Adds TestStripKnownServerPrefix with direct unit tests using real MCPServer objects (not SimpleNamespace). The _reset_env autouse fixture ensures short-prefix mode is off, so hyphenated-alias and UUID-fallback cases are faithfully reproduced regardless of environment.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_toolset_scope.py Adds TestToolsetPrefixResolution regression tests using SimpleNamespace with short_prefix=None. These correctly reproduce the failing cases when LITELLM_USE_SHORT_MCP_TOOL_PREFIX is unset (default), but if that env var is set in CI the SimpleNamespace will compute a clean 3-char short prefix and the hyphenated/UUID cases won't exercise the real bug path — the direct unit tests in test_short_mcp_tool_prefix.py close this gap.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py Adds short_prefix=None to existing server mocks to align with the new prefix resolution path, and corrects server.alias from "gitmcp" to "GITMCP" to match mock tool names — necessary because strip_known_server_prefix is case-sensitive where the old first-separator split was not.
ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx Display-only: renders tool chips as {server-prefix}-{tool} so the same bare tool name on different servers is distinguishable. serverPrefixById memos are independent between MCPToolsetsTab and CreateToolsetModal but both consume the same React Query cache entry so no extra network calls occur.

Reviews (5): Last reviewed commit: "test(mcp): add focused unit tests for st..." | Re-trigger Greptile

Comment thread ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a toolset tool-filtering bug where tools on MCP servers whose prefix contains the separator character (-) were silently dropped. The root cause was split_server_prefix_from_name cutting at the first separator, which lands inside the prefix for hyphenated aliases or UUID-based fallback prefixes; the fix introduces strip_known_server_prefix, which resolves the exact prefix from the server's registry entry and strips it precisely.

  • utils.py: Adds strip_known_server_prefix(name, server), iterating all registered prefix forms via iter_known_server_prefixes and stripping {normalized_prefix}{separator} via startswith; falls back to the legacy split only when server is None.
  • server.py / mcp_server_manager.py: Both toolset-filtering callsites (filter_tools_by_key_team_permissions and resolve_toolset_tool_permissions) now use the new function instead of the first-separator heuristic.
  • MCPToolsetsTab.tsx: Display-only update — tool chips in the toolset list and create/edit modal now render {server-prefix}-{tool_name} so tools from different servers with identical bare names are distinguishable.

Confidence Score: 4/5

The production fix is correct and well-scoped; the only concern is that the new regression tests don't actually exercise the hyphenated-alias and UUID-prefix failure paths they document.

The core logic change — replacing first-separator guessing with prefix-aware stripping — is sound and handles all described edge cases correctly. The graceful None-server fallback preserves backward compatibility. The gap is entirely in the test layer: _server sets short_prefix=None, which routes get_server_prefix through compute_short_server_prefix (a 3-char alphanumeric, no hyphens), so both pre- and post-fix code pass the parametrized suite. The tests would need to omit the short_prefix attribute to force the alias/server_id fallbacks that reproduce the original bug.

tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_toolset_scope.py — the _server factory needs adjustment to reproduce the actual failing scenarios

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/utils.py Adds strip_known_server_prefix, which iterates all known prefixes via iter_known_server_prefixes and uses startswith to strip exactly {normalized_prefix}{separator}. Logic is correct and consistent with how add_server_prefix_to_name builds names.
litellm/proxy/_experimental/mcp_server/server.py Replaces the first-separator split in filter_tools_by_key_team_permissions with strip_known_server_prefix; early-returns on None permissions. Change is correct and equivalent for clean-prefix servers, superior for hyphenated/UUID prefix servers.
litellm/proxy/_experimental/mcp_server/mcp_server_manager.py Replaces the first-separator split in resolve_toolset_tool_permissions with strip_known_server_prefix; gracefully falls back to legacy behaviour when server lookup returns None. Change is correct.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_toolset_scope.py Adds TestToolsetPrefixResolution with parametrized cases for clean alias, hyphenated alias, and UUID server_id. The server factory sets short_prefix=None, causing all three cases to use a 3-char computed prefix (no hyphens), so the pre-fix code also passes all three cases — the tests don't guard against regression.
ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx Display-only change: adds displayToolName helper and serverPrefixById map so tool chips show {server-prefix}-{tool} in both the toolset list column and the create/edit modal. No functional impact on stored data.

Reviews (2): Last reviewed commit: "fix(mcp): resolve toolset tools by the s..." | Re-trigger Greptile

@tin-berri
tin-berri force-pushed the litellm_fix_toolset_tool_prefix_resolution branch from df2ea64 to 054fe90 Compare June 24, 2026 23:56
@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri
tin-berri force-pushed the litellm_fix_toolset_tool_prefix_resolution branch from 054fe90 to 0f58194 Compare June 24, 2026 23:58
Toolsets store {server_id, bare tool_name} and reconcile that against the
live prefixed tool name at list time. The reconciliation chopped the live
name at the first MCP_TOOL_PREFIX_SEPARATOR with no server context, so a
server whose prefix contains the separator (a hyphenated alias, or the
UUID server_id used as the prefix when a server has no alias) had its
tools silently dropped from /toolset/<name>/mcp while listing fine
everywhere else. Strip the exact known prefix for the tool's server_id
instead of guessing the boundary, on both the resolve and filter sides

Also render toolset tools as {server-prefix}-{tool} in the dashboard
picker result and chips; this is display only, the persisted record
stays {server_id, bare tool_name}

Resolves LIT-3419
@tin-berri
tin-berri force-pushed the litellm_fix_toolset_tool_prefix_resolution branch from 0f58194 to 1daaa2c Compare June 25, 2026 00:16
@tin-berri

Copy link
Copy Markdown
Contributor Author

Addressed the one concern from the review: the dashboard MCP_TOOL_PREFIX_SEPARATOR is now documented as mirroring the backend default and being display-only, so overriding the backend env var only changes the cosmetic label and never what is stored or matched. The backend logic is unchanged from the 4/5 review; later commits only add that doc comment, a lint-driven param refactor in the toolset table, and test-mock corrections.

@greptileai

Cover the LIT-3419 cases directly on the helper with real MCPServer
objects: clean prefix round-trip, hyphenated alias, UUID server_id
fallback, unprefixed passthrough, and the server=None legacy fallback
@tin-berri

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit with direct unit tests for the new helper. @greptileai

@mateo-berri mateo-berri left a comment

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.

LGTM; thanks!

@tin-berri
tin-berri merged commit f426912 into litellm_internal_staging Jun 25, 2026
124 checks passed
@tin-berri
tin-berri deleted the litellm_fix_toolset_tool_prefix_resolution branch June 25, 2026 03:50
ishaan-berri pushed a commit to ishaan-berri/litellm that referenced this pull request Jun 25, 2026
…#31254)

* fix(mcp): resolve toolset tools by the server's known prefix

Toolsets store {server_id, bare tool_name} and reconcile that against the
live prefixed tool name at list time. The reconciliation chopped the live
name at the first MCP_TOOL_PREFIX_SEPARATOR with no server context, so a
server whose prefix contains the separator (a hyphenated alias, or the
UUID server_id used as the prefix when a server has no alias) had its
tools silently dropped from /toolset/<name>/mcp while listing fine
everywhere else. Strip the exact known prefix for the tool's server_id
instead of guessing the boundary, on both the resolve and filter sides

Also render toolset tools as {server-prefix}-{tool} in the dashboard
picker result and chips; this is display only, the persisted record
stays {server_id, bare tool_name}

Resolves LIT-3419

* test(mcp): add focused unit tests for strip_known_server_prefix

Cover the LIT-3419 cases directly on the helper with real MCPServer
objects: clean prefix round-trip, hyphenated alias, UUID server_id
fallback, unprefixed passthrough, and the server=None legacy fallback
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.

2 participants