Skip to content

fix(mcp): expand toolset grants in shared permission primitives so tools/call honors them - #33612

Merged
tin-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_lit4448_toolset_call_grants
Jul 17, 2026
Merged

fix(mcp): expand toolset grants in shared permission primitives so tools/call honors them#33612
tin-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_lit4448_toolset_call_grants

Conversation

@tin-berri

@tin-berri tin-berri commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Customer report (Pylon #4335): a key granted access through a toolset can list the toolset's tools but calling any of them returns 403

Linear ticket

Part of LIT-4448 (prerequisite for the entitlement enforcement point; does not resolve the ticket)

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 CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Live proxy on localhost:4012, fresh Postgres, stub streamable-HTTP MCP server ("stubtools") exposing two tools: lookup_status and delete_everything. Fixture: a toolset containing ONLY {stubtools, lookup_status}, and a key whose object_permission grants ONLY that toolset (mcp_toolsets: [<toolset_id>], no mcp_servers, no tool permissions)

Before (unfixed, captured at 669ef389b9): the key can list the tool but not call it, on both the MCP protocol path and the REST path

# streamable /mcp with x-litellm-api-key: Bearer <toolset-only key>
tools/list  -> tools: ['stubtools-lookup_status']
tools/call stubtools-lookup_status
            -> {"result":{"content":[{"type":"text","text":"Error: User not allowed to call this tool."}],"isError":true}}

# REST call path
POST /mcp-rest/tools/call {"server_id":"<id>","name":"lookup_status",...}
            -> {"detail":{"error":"access_denied","message":"The key is not allowed to access server <id>"}}

After (fixed, captured at e25cab6ed5): same fixture, same curls

tools/list  -> tools: ['stubtools-lookup_status']                      # list unchanged
tools/call stubtools-lookup_status
            -> {"content":[{"type":"text","text":"status of abc: OK"}],"isError":false}

# negative control: tool on the SAME server but NOT in the toolset stays blocked
tools/call stubtools-delete_everything
            -> {"content":[{"type":"text","text":"Error: {'error': \"Tool 'delete_everything' is not allowed for your key/team on server 'stubtools'...\"}"}],"isError":true}

# REST call path now matches
POST /mcp-rest/tools/call name=lookup_status      -> {"content":[{"text":"status of rest: OK"}],"isError":false}
POST /mcp-rest/tools/call name=delete_everything  -> {"detail":{"error":"Tool 'delete_everything' is not allowed for your key/team on server 'stubtools'..."}}

# admin sanity: master key still calls anything
POST /mcp-rest/tools/call name=delete_everything (master key) -> {"content":[{"text":"deleted everything (yes)"}],"isError":false}

Type

🐛 Bug Fix

Changes

A key granted MCP access only through toolsets could list the granted tools but not call them. tools/list ran a toolset expansion step (_merge_toolset_permissions) before filtering, but call_mcp_tool computed allowed servers from the raw auth object, so the server-level check saw an empty server list and returned 403 before execution. The REST /mcp-rest/tools/call endpoint had the same gap through its own resolver

Rather than bolting the expansion onto each remaining entry point (there were already four near-duplicate toolset expansion wrappers, and any new entry point would re-introduce the bug), this PR moves toolset awareness into the two shared permission primitives every path already consults: _get_allowed_mcp_servers_for_key now unions the servers referenced by the key's toolsets into the key's scope (subject to the same team/org ceilings as any other key-level grant), and get_allowed_tools_for_server unions the toolset's tool list into the key's per-server tool restrictions. The second half is what prevents over-granting: the call path's tool-level check defaults to allow-all when a server has no restrictions, so expanding servers without tools would have exposed every tool on a toolset-referenced server. With both in place, a toolset key reaches exactly the toolset's tools and nothing else

The list path's _merge_toolset_permissions wrapper is deleted; tools/list now gets identical behavior from the shared primitives, so list and call can no longer drift. A review round surfaced the one remaining path still reading raw object_permission for tool filtering: the REST tools list helper, which would have listed every tool on a toolset server; it now consults the same get_allowed_tools_for_server primitive, which also applies the team/agent/org tool ceilings the raw read ignored and fails closed on an empty allowed list, matching the protocol path. /toolset/{name}/mcp scoping (_apply_toolset_scope) replaces the object_permission and empties mcp_toolsets before the primitives run, so toolset-scoped routes are unaffected; the Responses API MCP handler does its own equivalent merge and is likewise unaffected. Team-level mcp_toolsets remains a key-creation ceiling only, matching its behavior on every path before this change

Tests: six new cases in the mapped auth test file covering server expansion for a toolset-only key, the no-toolsets fast path (no resolver round-trip), end-to-end inheritance through get_allowed_mcp_servers, direct+toolset tool union, the over-grant negative control (granted tool allowed, sibling tool on the same server denied), and preservation of allow-all when no restrictions exist. Mutation-checked: dropping the server union, the tool union, or the union gate each fails at least one test. A follow-up commit pins the scope semantics explicitly: toolset grants expand the KEY scope (union within a level) and the team ceiling still intersects it (intersection across levels), killing the mutant where toolset servers escape into the additive grant path. Full MCP suite (393 pre-existing tests) passes with the wrapper removed

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Note

Medium Risk
Changes MCP authorization for keys using toolsets across list, call, and REST; incorrect union/intersection logic could over-grant tools or block valid access, but behavior is heavily regression-tested.

Overview
Fixes toolset-only API keys that could list granted MCP tools but got 403 on call (and inconsistent REST behavior) because toolset expansion lived only on the list path.

mcp_toolsets is now resolved inside shared auth primitives instead of a one-off merge before tools/list. _get_allowed_mcp_servers_for_key unions servers from the key’s toolsets into allowed servers (still capped by team/org ceilings). get_allowed_tools_for_server unions toolset tools with direct mcp_tool_permissions so tool-level checks see the full key scope and toolset-only keys are restricted to toolset tools, not allow-all on the server.

_merge_toolset_permissions is removed from the protocol list path; list and call both use the same primitives. REST _get_tools_for_single_server stops reading raw object_permission and calls get_allowed_tools_for_server so REST listing matches protocol paths and team ceilings.

Tests cover toolset-only server access, team intersection, tool union/over-grant guards, REST list filtering, and toolset resolution caching.

Reviewed by Cursor Bugbot for commit b5d38b8. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a 403 error on tools/call (and REST /mcp-rest/tools/call) for keys whose only MCP grant is through mcp_toolsets: the list path used a one-off _merge_toolset_permissions wrapper while the call paths read raw auth with no toolset awareness, so the server-level check saw an empty server list and denied the call.

  • Toolset expansion is moved into the two shared permission primitives: _get_allowed_mcp_servers_for_key now unions toolset-referenced servers into the key scope (still capped by the team/org ceiling), and get_allowed_tools_for_server unions toolset tools with direct mcp_tool_permissions so the tool-level check sees the full effective grant rather than defaulting to allow-all.
  • The _merge_toolset_permissions list-only wrapper is deleted; the REST tool filter is updated to call get_allowed_tools_for_server (the same shared primitive) instead of reading object_permission.mcp_tool_permissions directly.
  • Six new unit tests cover server expansion, the no-toolsets fast path, team ceiling intersection, direct+toolset tool union, the over-grant negative control, and the allow-all preservation case.

Confidence Score: 5/5

Safe to merge; the change is well-scoped and the new behavior is fully covered by mutation-checked tests.

The fix centralizes toolset expansion in the two primitives every entry point already consults, so list and call paths are structurally in sync. Keys with no mcp_toolsets see no behavior change (the early-exit guard in each primitive is tested). The team-ceiling intersection is preserved because toolset servers land in key_set, which is intersected with team_set before access-group grants are unioned in. The REST filter tightening from len > 0 to is not None is a correct fix: an empty intersection now blocks all tools rather than silently falling through to allow-all. No pre-existing tests were weakened.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Core fix: _get_allowed_mcp_servers_for_key now unions toolset-referenced servers into the key scope (capped by team ceiling), and get_allowed_tools_for_server now unions toolset tools with direct tool grants — both guards that were missing on the call path.
litellm/proxy/_experimental/mcp_server/rest_endpoints.py REST tool-listing filter now delegates to MCPRequestHandler.get_allowed_tools_for_server (the same toolset-aware shared primitive) instead of reading raw mcp_tool_permissions directly; also tightens the filter guard from len > 0 to is not None which correctly blocks empty-intersection cases.
litellm/proxy/_experimental/mcp_server/server.py Removes the _merge_toolset_permissions wrapper and its single call site in _list_mcp_tools; the list path's filter_tools_by_key_team_permissions already delegates to get_allowed_tools_for_server which now handles toolset expansion natively.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Six new tests covering server expansion for toolset-only keys, the no-toolsets fast path (asserts no DB round-trip), end-to-end inheritance through get_allowed_mcp_servers, direct+toolset tool union, over-grant negative control, and allow-all preservation; mutation-safe against dropping either union.
tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py Adds test_resolve_toolset_tool_permissions_single_db_fetch_across_checks which pins the within-request dedup contract: two sequential calls to resolve_toolset_tool_permissions with the same IDs result in exactly one list_mcp_toolsets await.
tests/test_litellm/proxy/_experimental/mcp_server/test_rest_endpoints.py Adds TestRestListToolsetFiltering covering the REST list path for a toolset-only key: raw server catalog contains two tools, toolset grants only one, and the test asserts only the granted tool appears in the result.

Reviews (3): Last reviewed commit: "fix(mcp): route REST tools list filterin..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_lit4448_toolset_call_grants (b5d38b8) with litellm_internal_staging (68f0fb0)

Open in CodSpeed

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@tin-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b5d38b8. Configure here.

@tin-berri
tin-berri merged commit ea48ded into litellm_internal_staging Jul 17, 2026
131 of 132 checks passed
@tin-berri
tin-berri deleted the litellm_lit4448_toolset_call_grants branch July 17, 2026 17:16
tin-berri added a commit that referenced this pull request Jul 17, 2026
…omes

Conflict in _list_mcp_tools: staging (#33612) moved toolset-grant expansion into the shared
permission primitives and removed the _merge_toolset_permissions call; resolution applies that
removal to this branch's AggregateToolListing structure
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