Skip to content

fix(ui): show indirectly granted and name-keyed MCP servers in the tool matrix - #35154

Merged
yassin-berriai merged 10 commits into
litellm_internal_stagingfrom
litellm_mcp_perm_ui_inherited_servers
Sep 5, 2026
Merged

yassin-berriai merged 10 commits into
litellm_internal_stagingfrom
litellm_mcp_perm_ui_inherited_servers

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Access-group and toolset servers never render in the tool matrix
  • So a save either wipes their allowlist or leaves it stale
  • Name-keyed grants render as a server with zero tools
  • Admins read that as "this server has no tools"
  • Editing one of several equivalent keys leaves the others granting
  • Saving a team drops the allowlist of any server granted indirectly
  • A server named after another's id joins the matrix and gets granted on open
  • A toolset-restricted server shows every tool, so a deselect grants the rest

How it solves it:

  • Resolve groups and toolsets to servers; render them badged as inherited
  • Say so loudly when that resolution fails, instead of showing none
  • Match server id, name and alias in both components
  • Show the union of equivalent keys; collapse onto one key on write
  • Resolve an identifier with the backend's exact-id precedence
  • Fold a toolset's tools into what the card shows, and lock them
  • Never write through a key that names more than one server
  • Team save keeps allowlists for the effective set, refuses while unresolved
  • A grant that was tool-permission-only at load stays; only grants removed in this edit are dropped
  • A shared name key is split per still-granted server when one duplicate is deselected
  • Tool fetch waits for toolsets so the delete-blocked default cannot outrun a toolset restriction
  • Flat list reads a missing entry as allow-all, like the CRUD panel
  • Team and agent create write mcp_toolsets, so a toolset picked at create time is stored

User Flow

Before: an admin saving an unrelated team setting silently removes the tool allowlist of a server the team reaches through an access group

  1. They open http://localhost:3000/teams/?team=86c0bc2f-6e5f-43b2-a682-88cc13408167, Settings tab, Edit Settings, and scroll to MCP Servers
  2. The selector shows deploy_tracker and the access group ops_readonly, but the tool matrix has one card, deploy_tracker; issue_tracker and its list_issues allowlist are not on screen
  3. They change nothing and click Save Changes
  4. GET http://localhost:4000/team/info?team_id=86c0bc2f-6e5f-43b2-a682-88cc13408167 now returns mcp_tool_permissions with only deploy_tracker; the issue_tracker entry is gone
  5. A key on that team can now call delete_repo on issue_tracker, which the allowlist used to block

After: the same save keeps the allowlist, and the matrix shows the inherited server

  1. They open the same page, Settings tab, Edit Settings, and scroll to MCP Servers
  2. The tool matrix shows two cards, deploy_tracker and issue_tracker badged "Via access group: ops_readonly", with list_issues the only tool checked on the second
  3. They change nothing and click Save Changes
  4. GET http://localhost:4000/team/info?team_id=86c0bc2f-6e5f-43b2-a682-88cc13408167 still returns both entries, deploy_tracker: [create_issue] and issue_tracker: [list_issues]
  5. A key on that team still gets 403 on delete_repo for issue_tracker

Relevant issues

Linear ticket

Resolves LIT-4963
Resolves LIT-4961
Resolves LIT-4958

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. npx vitest run src/components/team/TeamInfo.test.tsx src/components/mcp_server_management/MCPToolPermissions.test.tsx in ui/litellm-dashboard
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • 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 http://localhost:4000 (python litellm/proxy/proxy_cli.py --config litellm/proxy/dev_config.yaml --detailed_debug --reload --use_v2_migration_resolver), dashboard via npm run dev in ui/litellm-dashboard, and a real streamable-HTTP MCP server on http://localhost:8765/mcp exposing list_issues, create_issue and delete_repo. Two MCP servers registered against it: deploy_tracker and issue_tracker, the latter in access group ops_readonly. Team mcp-demo-team (id 86c0bc2f-6e5f-43b2-a682-88cc13408167) has deploy_tracker directly, ops_readonly as an access group, and mcp_tool_permissions of {"deploy_tracker": ["create_issue"], "issue_tracker": ["list_issues"]}, so issue_tracker is reachable only through the group and its allowlist is keyed by name

Shared check, run before each save

$ curl -s "http://localhost:4000/team/info?team_id=86c0bc2f-6e5f-43b2-a682-88cc13408167" -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
    | jq '.team_info.object_permission | {mcp_servers, mcp_access_groups, mcp_tool_permissions}'
{"mcp_servers": ["deploy_tracker"], "mcp_access_groups": ["ops_readonly"],
 "mcp_tool_permissions": {"issue_tracker": ["list_issues"], "deploy_tracker": ["create_issue"]}}

Before (bd10977)

Team editor shows the inherited server

  1. Open http://localhost:3000/teams/?team=86c0bc2f-6e5f-43b2-a682-88cc13408167, Settings tab, Edit Settings, scroll to MCP Servers / Access Groups
  2. The selector lists deploy_tracker and ops_readonly, but the tool matrix below it has a single card, deploy_tracker. issue_tracker and its list_issues allowlist are nowhere on screen

before: only the direct server has a card

Saving the team keeps the inherited allowlist

  1. Click Save Changes without touching anything
  2. Re-run the shared check: the issue_tracker entry is gone, so the team now reaches every tool on that server, including delete_repo
{"mcp_servers": ["deploy_tracker"], "mcp_access_groups": ["ops_readonly"],
 "mcp_tool_permissions": {"deploy_tracker": ["create_issue"]}}

After (94cc52e; the later commits change the save filter, the toolset load guard, the flat view and the create payloads, each covered by unit tests, and this run has not been repeated at 05ccfb2)

Team editor shows the inherited server

  1. Same page, same clicks
  2. The matrix has two cards. issue_tracker carries the badge VIA ACCESS GROUP: OPS_READONLY and reads 1/3 allowed

after: inherited server card with access group badge

  1. Expanding its Other group shows list_issues on and create_issue, delete_repo off, matching the stored name-keyed entry

after: inherited server tools expanded

Saving the team keeps the inherited allowlist

  1. Click Save Changes without touching anything
  2. Re-run the shared check: both entries are still there
{"mcp_servers": ["deploy_tracker"], "mcp_access_groups": ["ops_readonly"],
 "mcp_tool_permissions": {"issue_tracker": ["list_issues"], "deploy_tracker": ["create_issue"]}}

The virtual key surface renders the same inherited card. A key granted ops_readonly shows issue_tracker via the group with list_issues on at http://localhost:3000/api-keys/ (key row, Settings, Edit Settings)

after: key editor inherited server

Unit test and mutation evidence

$ npx vitest run src/components/team src/components/mcp_server_management src/components/permissions src/components/mcp_tools
 Test Files  26 passed (26)
      Tests  524 passed (524)

Mutation check on the resolver and editor, reverting MCPToolPermissions.tsx and MCPServerPermissions.tsx to the parent commit and leaving the tests in place; every new test fails and every pre-existing one passes (16 failed | 15 passed). Further single-line mutations, each killed by the tests that pin it: badge always null kills 3; dropping the inherited default-write gate kills 1; removing both failure banners kills 2; id-only permission key kills 4; id-only identifier match kills 6; dropping access-group resolution kills 5; dropping toolset resolution kills 2; dropping tool-permission rows kills 2; removing the opt-out short-circuit kills 1; never superseding an equivalent key kills 3; dropping the shared-name guard kills 1; reading one key instead of the union kills 4; writing one key instead of collapsing kills 1; never appending an entry for a server that had none kills 3; dropping the shared-name notice kills 1; calling every extra key ambiguous kills 2; no exact-id precedence kills 10; permission keys not checked against the catalog kills 4; toolset tools left out of the allowed set kills 4; write copies toolset tools into the entry kills 3; toolset tools not locked kills 3; panel ignores locked tools kills 1; group toggle ignores locked tools kills 1; shared key allowed as the written key kills 6. Collision cases run over both catalog orders so a first-match regression cannot hide behind fixture order

Lint and format

$ npx eslint <changed files>
✖ 0 errors
$ npx prettier --check <changed files>
All matched files use Prettier code style!
$ node scripts/check-lint-budgets.mjs ...
all budgets within limits

Type

🐛 Bug Fix

Changes

effectiveMcpServers.ts is new and holds the resolution as a pure function: it mirrors the backend's direct + access_group + tool_perm + toolset union, resolves an access group from each server's own mcp_access_groups and a toolset from the toolset's tool list (both already loaded by MCPServerSelector, so no new endpoint and no extra request), matches a server by id, name or alias, and returns the mcp_tool_permissions key each server's allowlist actually lives under. Exported so a save handler can filter against the same effective set rather than guessing

Known limit of resolving membership on the client: the warning fires when the server list cannot be fetched, not when it is fetched successfully and a selected group turns out to have no member servers, so a group left empty by a misconfiguration still presents as a group with no servers. A group-membership endpoint would close that gap and is worth considering on its own

MCPToolPermissions.tsx renders that set. A non-direct server carries a badge naming its source (Via access group: <name>, Via toolset: <name>, Via tool permissions) and a dashed border, and its tools are editable and clearable like any other server's. Reads and writes go through the resolved permission key, so a name-keyed entry round-trips instead of forking into a second id-keyed one. When the server list or toolset list fails to load, a warning renders in place of the missing rows; failing to render an inherited server is safer than an empty list that reads as "there are none". Opening the editor no longer applies the delete-blocked-by-default allowlist to an inherited server, which would have narrowed a grant nobody edited; direct servers keep that default. The no-mcp-servers opt-out still renders nothing, since it short-circuits the backend resolver to zero servers

A server can be named by several mcp_tool_permissions keys at once, its id and its name for instance, and the backend unions every key's list. So the resolver reports, per server, the key an edit keeps, the equivalent keys it supersedes, and the union those keys currently allow; the editor renders that union, and every write goes through one function that writes the kept key and drops the superseded ones. After an edit there is exactly one entry per server and it says what the admin sees. The kept key is the one already in the map, preferring the id when both are present, so a name-keyed grant round-trips to its own key rather than being rewritten to an id nobody chose. One exception, because collapsing blindly would repeat the bug in the other direction: a key that also names a DIFFERENT server, which happens when two servers share a name, is never dropped, since dropping it would strip that other server's restriction. Retaining is the direction that cannot widen anyone's access, so it is the one taken, and it has a cost worth naming: the retained key keeps granting its tools to the server being edited, so an admin cannot revoke those tools from this screen. Rather than leave that to be inferred from an edit that bounces back, the card names the ambiguous key and says its tools stay allowed until the servers no longer share the name, which points at the fix an admin can apply. A key that names a server the fetched list does not include cannot be detected at all, which is the residual limit of resolving this on the client. Worth stating plainly about the whole case: because a name-keyed entry is unioned into every server that answers to that name, such a key is ambiguous by construction, so the notice mitigates a symptom and the shape of a real fix is a separate question, tracked on LIT-4982; the notice does not block on it

Identifier resolution follows the backend's precedence rather than comparing three fields at once. expand_permission_list treats a string that is a registry server id as naming exactly that server and stops there; only a string that is no server's id falls through to name and alias, which can name several. Matching all three at once meant a server whose name or alias happened to equal another server's id joined the matrix as if it had been selected, and since it landed there as a direct server it also received the delete-blocked default write on open. An mcp_tool_permissions key is itself a grant source, so saving then handed out a server nobody granted, with no admin gesture involved. The same precedence decides which keys belong to a server: a key is read into a server's allowlist only when it resolves back to that server, so an entry that belongs to the id's owner is neither counted as this server's grant nor overwritten by an edit made here

A toolset grant is now part of what the card shows. The backend unions a toolset's tools with whatever mcp_tool_permissions allows (key_direct_tools | key_toolset_tools), so a toolset-only grant restricts the server to that toolset's tools; reading the map alone found no entry and rendered every tool on the server as allowed. Deselecting one from that state wrote all the others as a permission entry, and the union turned a revocation gesture into a grant of every tool the toolset never included. The resolved entry now carries toolsetTools alongside keyedTools, so the matrix opens on what is in force; the delete-blocked default is withheld from a server a toolset restricts, for the same reason it is withheld from an inherited one but in the opposite direction, since there it would widen rather than narrow; and a write withholds the tools only the toolset accounts for, so a grant that ends with the toolset does not quietly become a standing one while a tool the entry already held is not dropped either. Those tools cannot be revoked from this screen at all, so they render allowed and disabled and the card names them and points at the toolset. Scope worth stating: this union is key-scoped in the backend (mcp_toolsets is read only on the key path), so the team surface never had the escalation, only the display

The shared-name guard covers the key an edit writes, not only the ones it supersedes. It was written on the assumption that the kept key names one server, which fails when a shared key is a server's only entry: it becomes the key an edit writes, ambiguousKeys is derived from the secondary keys and comes back empty, and allowing a tool on one card writes the shared key, which the backend unions into the other server too. The key an edit writes is now the first that names this server and no other, falling back to the server's own id, so a shared key is never written through and an edit creates an id-keyed entry instead of moving the neighbour's allowlist. Splitting the shared key into per-server entries was the alternative and was not taken, since it edits a server the admin is not looking at, which is the direction this PR refuses everywhere else. allowedTools still reports what the shared key contributes, because that is what the backend enforces here, and both cards carry the notice, since neither can revoke it

MCPServerPermissions.tsx looks up the server by id, name or alias, unions the allowlist the same way, and its display name falls back through alias, server name and id

The five call sites (keys create and edit, teams create and edit, agents) pass the access groups and toolsets they already hold

TeamInfo.tsx no longer filters the submitted mcp_tool_permissions down to the directly selected server ids, which dropped an inherited server's allowlist on every team save whether or not the admin touched it (LIT-4961). The save handler now resolves the same effective set the editor renders (direct servers, access groups, toolsets, plus the servers behind the team's unified access groups) and keeps every entry that resolves to one of those servers, by id, name or alias. An entry whose key matches no known server is kept as well, since dropping what cannot be resolved is the direction that widens access. If the server list, toolset list or access group list failed to load or is still loading, the save refuses with a toast instead of guessing, because a filter run against an empty inventory would drop everything. Tests cover the indirect grant, alias and name keys, a unified access group, an unknown identifier, and the unresolved-inventory refusal

Two cases sharpen that filter. A server the team reached only through an mcp_tool_permissions entry when the editor loaded is a standing grant the backend honours on purpose, so standingToolPermissionServerIds resolves the loaded object permission the same way and adds those servers to the granted set; a server that is tool-permission-only after the edit because the admin just removed the group, toolset or direct grant that supplied it is still dropped, which is the stale-entry case LIT-4963 describes. And a key that names several servers (duplicate names) used to be kept whole as long as one match stayed granted, which left a deselected duplicate reachable through the shared key. retainedMcpToolPermissions now splits such a key into id-keyed entries for the servers that remain granted, unioning with any entry already under that id, and drops the shared key; a shared key whose matches are all still granted is left as it was. Tests cover both the standing grant on an unrelated save and the split

This branch also carries the LIT-4961 change from #35153 (commit 389ef42). When the team's unified access group selection is unchanged but the group list did not resolve, the save reloads the team's access_group_mcp_server_ids instead of refusing, and only refuses when the selection changed and cannot be resolved. standingToolPermissionServerIds takes those loaded team group server ids into account as well

Three follow-ups from Bugbot on MCPToolPermissions.tsx and the create forms. The tool fetch effect now waits for useMCPToolsets to settle, and the delete-blocked default is skipped when toolsets failed to load while some are selected, so the default can no longer be written against an empty toolset list and widen a toolset-restricted server. The flat list view read a missing entry as [] while the CRUD panel read it as allow-all, so a single click there replaced an unrestricted grant with a one-tool allowlist; it now starts from every tool checked. Team create and agent create passed selectedToolsets to the matrix but never wrote object_permission.mcp_toolsets, so the toolset the matrix locked tools for was gone after create; both payloads now carry it

QA runbook

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 permission editing and team/key/agent save payloads for MCP tool allowlists; mistakes could drop or widen grants, though the PR adds extensive tests and refuses saves when resolution data is incomplete.

Overview
Fixes MCP tool permission editing so the UI matches how the backend resolves grants (direct servers, access groups, toolsets, and existing mcp_tool_permissions keys).

effectiveMcpServers is new shared logic that resolves the effective server set, unions equivalent id/name/alias keys, applies backend-style id-first identifier matching, and centralizes writes so edits collapse onto one key without widening access via shared names or toolset-only tools.

MCPToolPermissions now lists inherited servers with source badges, shows load warnings when server or toolset data is missing, locks toolset-granted tools, skips inappropriate delete-by-default allowlists for inherited or toolset-restricted servers, and handles the no-MCP-servers opt-out internally. Call sites for teams, keys, and agents pass access groups and toolsets and persist mcp_toolsets on create where applicable.

Team save no longer drops allowlists for indirectly granted servers: it retains permissions for the resolved grant set (including standing tool-permission-only grants), splits ambiguous shared-name keys when a duplicate is removed, and blocks saves with tool permissions while MCP inventory is still loading or failed.

MCPServerPermissions (read-only view) resolves display names and tool lists the same way. McpCrudPermissionPanel gains lockedTools for non-editable grants.

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

Link to Devin session: https://app.devin.ai/sessions/a2bbed1daa344692b1bb861a909a45e1
Open in Devin Desktop: https://app.devin.ai/desktop/session/a2bbed1daa344692b1bb861a909a45e1?variant=devin
Requested by: @yassin-berriai

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

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

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes MCP permission editing reflect the backend’s effective grants rather than only directly selected server IDs.

  • Resolves servers inherited through MCP access groups, toolsets, and existing tool-permission entries.
  • Normalizes equivalent ID, name, and alias permission keys while guarding ambiguous shared identifiers.
  • Preserves effective team allowlists during saves and refuses saves when required grant inventories cannot be resolved safely.
  • Adds MCP toolset support to team and agent creation payloads.
  • Adds focused tests for inherited grants, identifier collisions, toolset restrictions, save retention, and loading failures.

Confidence Score: 5/5

The PR appears safe to merge; no actionable new defects or outstanding previous findings remain.

The effective-grant resolver, editor write path, and team-save retention logic consistently account for direct, inherited, toolset, name-keyed, and ambiguous grants. The prior equivalent-key finding is fixed, the backend-name finding was correctly withdrawn, and the team-retention threads were resolved or explicitly confirmed as intended behavior.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts Centralizes effective-server resolution, identifier precedence, equivalent-key normalization, and toolset-derived permissions.
ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx Renders direct and inherited MCP grants accurately, locks toolset-owned tools, and routes edits through normalized permission writes.
ui/litellm-dashboard/src/components/team/TeamInfo.tsx Retains tool allowlists for the effective team grant set and safely reloads unified access-group grants when local inventory is incomplete.
ui/litellm-dashboard/src/components/permissions/MCPServerPermissions.tsx Aligns server lookup and displayed allowlists with ID, name, and alias resolution.
ui/litellm-dashboard/src/components/Teams.tsx Includes selected MCP toolsets in team creation and passes complete selections to the tool matrix.
ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx Persists selected MCP toolsets in agent object permissions.
ui/litellm-dashboard/src/components/mcp_tools/McpCrudPermissionPanel.tsx Prevents locked toolset-derived grants from being changed through individual or group controls.

Reviews (9): Last reviewed commit: "fix(ui): guard MCP default write on tool..." | Re-trigger Greptile

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Fixed the P1 on equivalent permission keys in dac8138

You were right that a write was authoritative for one key rather than for the server. A server can be named by several mcp_tool_permissions keys at once (its id and its name, say), the backend unions every key list, so reading one key understated what was in force and writing one key left the others granting.

The resolver now reports, per server, the key an edit keeps, the equivalent keys it supersedes, and the union those keys currently allow. The editor renders the union, so a card shows what the backend actually enforces, and every write goes through one function that writes the kept key and drops the superseded ones; after an edit there is exactly one entry per server and it matches what the admin sees. The kept key is the one already in the map, preferring the id when both are present, so a name-keyed grant still round-trips to its own key instead of being silently rewritten to an id the operator never chose.

One deliberate exception, because collapsing blindly repeats the same bug in the other direction: a key that also names a DIFFERENT server, which happens when two servers share a name, is never dropped, since dropping it would strip that other server no one was editing of its restriction. Such a key stays and the card keeps showing the union it contributes, so the state stays visible rather than silently altered. That case has its own test.

New tests, each mutation-checked: the union read; superseded keys reported; a write collapsing onto the kept key; a single-key server and every other server left untouched; the shared-name key never dropped; an entry added for a server that had none; one card rendering the union of both keys; a deselect removing the tool from every equivalent key and leaving one entry; and one card badged by its strongest grant when a key and an access group both name the server. Five single-line mutations confirm they bite: never superseding kills 3, dropping the shared-name guard kills 1, reading one key instead of the union kills 4, writing one key instead of collapsing kills 1, never appending an entry kills 3. Reverting both components to the parent commit fails 15 of 15 new tests while all 14 pre-existing ones pass. Suites: 54 pass in the two component directories, 484 across every directory this PR touches. Lint 0 errors, all six budgets keep headroom, build compiles.

On the badge question implicit in your review: dedup is by server_id across all four grant sources, so a server named by a key AND resolved through a group renders one card attributed to the stronger source, never two. Pinned by a test now.

On the TeamInfo.tsx save filter you flagged outside the diff: agreed it is a real gap, and it is deliberately not in this PR. That handler is LIT-4961, owned by a separate change already in flight, and this PR was scoped to leave every owner save path untouched so the two can merge in any order. With the effective set now exported, that handler can filter against it instead of the direct list.

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The implementation is thorough and addresses everything correctly. Walking through the key pieces:

Equivalent-key union/collapse (effectiveMcpServers.ts):

  • mcpAllowedToolsFor unions across id/name/alias keys — reads now reflect what the backend actually enforces
  • applyToolPermissionWrite drops superseded keys and writes the kept one — no more phantom grants from other spellings
  • The shared-name guard (namesOneServerOnly) correctly excludes a key from supersededKeys when it also names a different server, preventing a silent strip of a server nobody was editing

Inherited-server default-allowlist gate (MCPToolPermissions.tsx):

const isDirect = entry.source.kind === "direct";
const unrestricted = mcpAllowedToolsFor(entry.server, latestPermissions) === undefined;
if (isDirect && unrestricted && fetchedTools.length > 0) {

Correct — inherited servers no longer get a narrowing default written on open.

Dedup by strengthdirect entries come first in candidates, so findIndex keeps the direct entry when a server appears both directly and via a group. One card, right badge, no duplicates.

MCPServerPermissions.tsx — the fallback chain alias || server_name || server_id also fixes the pre-existing bug where alias was always used even when undefined, which would have rendered undefined (abc...1234).

The known limitation on empty access groups (group selected, no member servers in the fetched list → silent gap until a membership endpoint exists) is correctly scoped and documented. The warning banners on fetch failure cover the more dangerous case where the server list itself couldn't load.

This all looks correct. The mutation evidence and test coverage are convincing. The one thing I'd have the maintainer confirm before merge is the TeamInfo.tsx save-filter gap scoped to LIT-4961 — as long as that change can land in any order relative to this one without a regression window in between, the sequencing call makes sense.

LGTM on the logic.

@veria-ai

veria-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 6 · PR risk: 0/10

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

UI captures

Driven against a live proxy on a real MCP server, one restart per bundle. Both bundles were built from this branch's worktree; the before bundle is the whole ui/litellm-dashboard/src at 551e5d097c rather than a partial revert, so it is literal staging rather than a hybrid. Build identity was verified before each shot by grepping the served directory: the string Via access group appears in 0 files in the before bundle and 8 in the after one.

Fixture: a config-defined MCP server issue_tracker (three tools of escalating blast radius) carrying access_groups: ["ops_readonly"], a team whose only path to it is that group, and a key whose allowlist is keyed by the server NAME while a second entry is keyed by its id.

A server reached only through an access group now renders, badged and editable. This is the case the editor could not show at all: the team holds no direct server, so the matrix was empty and an admin had no way to see or clear the allowlist that was in force.

inherited server rendered with a Via access group badge

Note the tool state is the real one: list_issues on under Read, create_issue and delete_repo off under Create and Delete, matching the stored allowlist rather than a default.

A name-keyed grant resolves. Same key, same frame, before and after. A config-defined server has a null alias, so the label interpolated it literally; the lookup was also id-only, so a name-keyed allowlist could not be found.

before, at 551e5d097c

key info card showing null as the server label

after

key info card showing issue_tracker as the server label

The failure-path banner is deliberately not screenshotted; it is pinned by two tests that fail without it, which is tighter evidence than a frame of a blocked request would be.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Both findings addressed in 71f3978

Finding 1, shared-name keys. Correct, and I agree it is not resolvable by any filter: retaining the key is right for the neighbouring server and wrong for the server being edited. Retaining stays the default because it is the only direction that cannot widen anyone access; dropping the key would silently strip a restriction from a server nobody was editing. The cost of that choice was invisible, which was the actual defect, so the editor now states it: the resolver reports ambiguous keys separately from superseded ones, and the card names them and says their tools stay allowed until the servers no longer share the name, which points at the fix an admin can apply (rename or split) rather than leaving them to infer it from an edit that bounces back. The PR body now states which direction was chosen and why.

One residual I want on the record rather than buried: a key naming a server that the fetched server list does not include cannot be detected at all, so the notice cannot fire for it. That is inherent to resolving membership on the client and is documented in the body next to the empty-group limit.

Two tests, both mutation-checked: the notice renders naming the ambiguous key when two servers share a name; and it does not render when every key names one server. Removing the notice kills the first; calling every extra key ambiguous kills the second plus the superseded-keys test.

Finding 2, the TeamInfo save filter. Verified rather than assumed, and it is a pre-existing gap, not a regression from this PR. The filter at TeamInfo.tsx is byte-identical at this branch parent 551e5d0: git show 551e5d097c:...TeamInfo.tsx has the same serverIds.has(serverId) predicate, and this PR diff for that file is two render props and nothing else. It already drops an inherited server allowlist entry on any team save today, whether or not an admin touches it, so the stored outcome is unchanged by this PR. What does change is reachability: an admin can now edit an inherited card on the team surface and have that edit dropped from the request. Replacing the filter is LIT-4961, in flight separately, so this PR leaves it alone by design and the body now says so, including that landing the two together (or that one first) is the tidier order.

Test and check state at this SHA: 56 pass in the two component directories, 486 across every directory this PR touches, 0 eslint errors, all six lint budgets with headroom, build compiles. Reverting both components to the parent fails 16 of the 17 new tests, with the one exception noted in the body being the guard that has to pass when the notice is correctly absent.

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Both findings are addressed correctly.

Finding 1 — shared-name keys: The resolver cleanly separates supersededKeys (unique to this server, safe to collapse) from ambiguousKeys (names another server too, must be kept) via the namesOneServerOnly predicate. applyToolPermissionWrite only drops supersededKeys, never ambiguousKeys, so the neighboring server's restriction is always preserved. The amber notice on the card surfaces the cost to the admin clearly and actionably. The two mutation tests (notice renders when ambiguous; does not render when all keys are unambiguous) pin both sides of that branch. This is the right approach — retaining is the only safe direction when the alternative silently widens access on a server nobody was editing.

Finding 2 — TeamInfo filter: Verified pre-existing, not a regression. The delta makes the gap reachable from a new control but does not change what the handler stores, and the LIT-4961 sequencing note in the PR body is the right way to handle it. Agreed that landing these together (or that one first) is the cleaner order.

The implementation is solid. LGTM.

Comment thread ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts Outdated
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Filed the underlying data-layer issue as LIT-4982 so this PR is not asked to carry a backend argument: a name- or alias-keyed mcp_tool_permissions entry is ambiguous by construction, because names are not unique and expand_tool_permissions unions such a key into every matching server. That is what makes the shared-name case unresolvable by any save filter, and it is why surfacing it here is the right immediate move rather than picking a direction. The notice in this PR should not block on that ticket.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Test-only hardening in 2c5600e, no production diff

The shared-name tests were coin flips and I measured it rather than assuming otherwise. An identifier can resolve to several servers, so a first-match implementation is right in one catalog order and wrong in the other, which means a single fixture only catches it when it happens to be written in the wrong-answer order:

mutant: allServers.find(matches)?.server_id === server.server_id
 fixtures as written (edited server first):  2 failed | 54 passed
 same mutant, other server first:            0 failed | 56 passed

Both shared-name tests now run over both catalog orders, so the mutant fails the same named case every time instead of depending on how a later fixture happens to be ordered.

The mcp_tool_permissions map is a second collection of non-unique identifiers for one server, and the key lookup picks a winner from it, so that lookup asserts both write orders too. A winner taken in map order rather than by identifier precedence fails the name-key-first case and passes the other, so one order alone would have been green against it:

mutant: keys taken in map order rather than [id, name, alias]
 name key first: 1 failed | 58 passed

Worth stating as the general rule, since it is the root cause one level below the grant-inference bugs this PR is about: using a single-answer primitive for a multi-answer question is itself an inference that the first match is the only one. Any lookup resolving an identifier against a collection that can hold several matches needs its tests parameterized over every ordering, otherwise the suite reads as coverage while checking nothing.

State at this SHA: 59 pass in the two component directories, 489 across every directory this PR touches, 0 eslint errors, all six lint budgets with headroom, build compiles. Nothing else changed; the production code is identical to 71f3978.

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Confirmed — the commit touches only the two test files (48 changes total, zero production lines), and the hardening is exactly right.

The underlying principle you've stated is worth internalizing as a general rule: any lookup resolving an identifier against a collection where multiple matches are possible is implicitly a first-match decision, and a single fixture only detects a regression in first-match logic when the fixture happens to be ordered so the wrong answer comes first. Parameterizing over both orders makes the kill deterministic by construction rather than by accident.

The mcp_tool_permissions map case is a clean illustration of the same issue one layer down — map iteration order is implementation-defined, so a test that only exercises one insertion order is in the same position as the single-catalog-order case. Asserting both write orders closes that.

The production logic at 71f3978 is unchanged. This hardening commit is a straightforward sign-off.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Same note as on #35153, since the Confidence Score at the top of this PR is also a stale artifact rather than a verdict on the current code.

That 3/5 was posted against dac8138c28. Two commits landed after it:

71f3978a9a  say on the card when a permission key names more than one server
2c5600ea96  assert MCP identifier lookups over every collection order   (test-only)

Greptile has reviewed twice since without re-emitting a number, which it only prints on a summary comment. Its most recent review, against the current head, independently verified the test-only claim ("the commit touches only the two test files, zero production lines") and signs off: "This hardening commit is a straightforward sign-off."

I verified that claim separately too: git diff --name-only 71f3978a9a 2c5600ea96 filtered for non-test paths returns nothing.

So the score is lower than the code it is attached to. Worth stating because the usual reason to distrust a low score is under-reviewed code, and this is the reverse. Re-triggering from the Greptile app would produce a current number; neither a re-request nor a push does, and both have been tried, the test-only push being a clean isolation of that variable.

CI is green: 74 checks pass, zero fail. #35153 is stacked on this one.

@yassin-berriai
yassin-berriai force-pushed the litellm_mcp_perm_ui_inherited_servers branch 2 times, most recently from e719019 to 04a1b26 Compare August 1, 2026 17:21
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head 04a1b26

Both open veria findings are fixed in that commit, and both were real; I verified each against the backend rather than arguing the layer below rejects it.

Identifier collisions: expand_permission_list short-circuits on an exact registry id and only falls back to name and alias for a string that is no server's id, so matching all three fields at once put a server merely named after another's id into the matrix as a directly selected one. That also qualified it for the delete-blocked default write on open, and since a mcp_tool_permissions key is itself a grant source, opening the editor and saving granted a server nobody selected. Resolution now mirrors that precedence in both directions, including which keys count as a given server's.

Toolset grants: the backend unions a toolset's tools with the map (key_direct_tools | key_toolset_tools), so a toolset-only grant restricts the server to that toolset's tools while the editor showed every tool as allowed. Deselecting one wrote the rest and net-granted them. The entry now carries the toolset's tools, the default write is withheld from a toolset-restricted server, a write withholds the tools only the toolset accounts for, and those tools render locked with a note naming them, since nothing written here can revoke them.

Eight single-line mutations pin the round (baseline 81 passed; each mutant fails between 1 and 10 tests), the mutation script asserts each edit really applied, and both new lookups are asserted over every collection order. 138 pass across the three component directories, 701 across every directory this PR touches, 0 eslint errors, all six budgets with headroom, build compiles.

Note for anyone reading the Confidence Score at the top: it was posted against dac8138c28, three commits back, and Greptile's later reviews signed off in prose without emitting a new number.

Comment thread ui/litellm-dashboard/src/components/team/TeamInfo.tsx Outdated
Comment thread ui/litellm-dashboard/src/components/mcp_server_management/effectiveMcpServers.ts Outdated
@yassin-berriai
yassin-berriai force-pushed the litellm_mcp_perm_ui_inherited_servers branch from 04a1b26 to f84c7fe Compare August 1, 2026 17:36
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai please review the current head f84c7fe

Second security round. veria marked the two identifier and toolset findings addressed and opened two more; one is fixed here, one is deliberately elsewhere.

The Medium was real and was a gap in this PR's own shared-name guard, not a limit of it. The guard inspected the keys an edit supersedes, assuming the key it keeps names one server. When a shared key is a server's only entry it becomes the key an edit writes and ambiguousKeys comes back empty, so allowing a tool on one card wrote the shared key and the backend unioned it into the other server. The key an edit writes is now the first that names this server and no other, falling back to the server's own id, so a shared key is never written through. I did not take the suggested alternative of splitting the shared key into per-server entries, since that edits a server the admin is not looking at.

The Low is the TeamInfo save filter, which is byte-identical at this branch's merge-base and is removed by #35153, whose base branch is this one. Stacked rather than parallel, so they land in order; doing it here too would leave the child inheriting a conflict for nothing.

Nine mutations now, each killed, baseline 85. 705 pass across every directory this PR touches, 0 eslint errors, all six budgets with headroom, build compiles.

One existing test changed as a consequence and is stronger for it: the twin's card correctly carries the shared-key notice now, so it asserts both cards say it rather than exactly one.

@yassin-berriai
yassin-berriai force-pushed the litellm_mcp_perm_ui_inherited_servers branch from f84c7fe to bdb07af Compare August 13, 2026 20:33
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai rebased onto current staging, no scope change. Please re-review at head bdb07af; the earlier score is orphaned.

…ol matrix

The MCP tool permission editor was fed the direct server list only, so a server a
principal reaches through an access group or a toolset never appeared in the matrix.
That single blind spot produced two opposite bugs depending on how a save handler
filtered mcp_tool_permissions: filtering by the selected servers deletes an indirect
server's allowlist, and because a missing entry means "no restriction from this
level", the principal silently gains every tool on it; not filtering leaves a stale
entry that keeps a removed access group's server reachable, since a server named
under mcp_tool_permissions is entitled on purpose.

The editor now resolves the selected access groups and toolsets to their servers and
renders them alongside the direct ones, badged with where the grant comes from, so an
admin can see and clear an inherited server's tools like any other. Resolution reuses
the data the selector already loads: access groups resolve from each server's
mcp_access_groups, toolsets from the toolset's own tool list. When that data cannot be
loaded the editor says so instead of rendering an empty list, because an absent
inherited server reads as "there are none". Servers named only by an
mcp_tool_permissions key are listed too, which is what makes a leftover entry
visible; the opt-out sentinel still renders nothing, since it short-circuits the
backend resolver to zero servers.

Opening the editor no longer applies the delete-blocked-by-default allowlist to an
inherited server. Writing an entry for one would narrow a grant the admin never
touched just by opening the form; direct servers keep that default.

Both components also matched on server_id alone, while the backend accepts a server
id, name or alias interchangeably. A grant or allowlist written by API or config with
a name rendered as a selected server with no tools under it, which reads as "this
server has no tools". Matching now covers all three identifiers, and an edit writes
back to the key the entry already uses rather than forking a second id-keyed entry.

The same mismatch could also put one server under several keys at once, its id and
its name for instance. The backend unions every key's list, so reading one key
understated what was in force and writing one key left the others granting. The
resolver now reports, per server, the key an edit keeps, the equivalent keys it
supersedes, and the union those keys allow; the card renders the union and every
write goes through one function that writes the kept key and drops the superseded
ones. A key that also names a DIFFERENT server, which happens when two servers share
a name, is never dropped, because dropping it would strip the neighbouring server's
restriction; the card names such a key and says its tools stay allowed until the
servers no longer share the name, so an admin is told rather than left to infer it
from an edit that bounces back.

A third divergence from the backend sat in the same matching. The backend resolves an
identifier with exact-id precedence: a string that is a registry server id names that
server and stops, and only a string that is no server's id falls back to name and alias,
which can name several. Matching all three fields at once meant a server merely named
after another server's id joined the matrix as if it had been selected, and because it
landed there as a directly selected server it also received the delete-blocked default
write on open. Since an mcp_tool_permissions key is itself a grant source, saving then
handed out a server nobody granted, with no admin gesture involved. Identifier
resolution now mirrors the backend's precedence, and a key is read as this server's only
when it resolves back to it, so an entry that belongs to the id's owner is neither read
into this server's allowlist nor overwritten by an edit made against it.

A toolset grant was also invisible to the tool matrix. The backend unions a toolset's
tools with whatever mcp_tool_permissions allows, so a toolset-only grant restricts the
server to that toolset's tools; the editor read the map alone, found no entry and
rendered every tool on the server as allowed. Deselecting one from that state wrote all
the others as a permission entry, and the union turned a revocation into a grant of
every tool the toolset never included. The resolved entry now carries the toolset's
tools, so the matrix opens on what is actually in force, the delete-blocked default is
withheld from a server a toolset restricts, and a write keeps out the tools only the
toolset accounts for so a grant that ends with the toolset does not become a standing
one. Those tools cannot be revoked from this screen at all, since the backend unions
them in; they render allowed and locked and the card says which of them a toolset holds
open and where to go to revoke them.

That guard originally covered only the keys an edit supersedes, on the assumption that
the key it keeps names one server. It does not when a shared key is a server's only
entry: it then becomes the key an edit writes, and writing it moves the other server's
allowlist too, which is the widening the guard exists to prevent. The key an edit writes
is now the first one naming this server and no other, falling back to the server's own
id, so a shared key is never written through and an edit against one card cannot reach
the server behind the other. Both cards say the shared key holds tools open, since
neither can revoke them.

No owner's save handler changes here. With the full effective set now available to
the editor, the key and team handlers can filter against it instead of guessing,
which makes the internal-user surface's unfiltered save redundant

Resolves LIT-4963
Resolves LIT-4958
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@greptileai conflicts with litellm_internal_staging resolved, no scope change. Please re-review the current head

toolPermissions: Readonly<Record<string, readonly string[]>>,
allServers: readonly MCPServer[],
): readonly string[] =>
[server.server_id, server.server_name, server.alias].filter(

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.

P1 Backend name keys remain active

When short MCP tool prefixes are enabled, the backend accepts the server's generated name as a permission key, but this resolver checks only server_id, server_name, and alias. The editor therefore neither displays nor removes tools granted through that key, causing a deselected tool to remain allowed.

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.

In default mode name is alias, server_name or server_id, all matched here. The short-prefix value is never returned by the server list API, so it needs a backend change first

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.

That’s correct. In the default server-list response, the resolver covers all identifiers exposed by the API (server_id, server_name, and alias), while the short-prefix value is not available to the client. Supporting that key requires a backend/API change first, so this is not actionable for this PR. I’m withdrawing the comment; the UI can handle it once the backend exposes the generated name.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Pre-existing backend semantics; out of scope here. The editor now surfaces such stale entries as a visible card so admins can revoke them

yassin-berriai and others added 3 commits September 2, 2026 15:47
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…rvers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@greptileai the TeamInfo save filter now keeps allowlists for the effective server set, with before and after UI captures added

Comment thread ui/litellm-dashboard/src/components/team/TeamInfo.tsx Outdated
Comment thread ui/litellm-dashboard/src/components/team/TeamInfo.tsx
@yassin-berriai
yassin-berriai enabled auto-merge (squash) September 5, 2026 20:32
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team September 5, 2026 20:41
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@greptileai please review head ffd8dff: standing tool-permission grants survive team saves and partially granted shared keys are split

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

Comment thread ui/litellm-dashboard/src/components/team/TeamInfo.tsx

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

Stale Bugbot comment from a previous run.

Comment thread ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx Outdated
yassin-berriai and others added 3 commits September 5, 2026 20:59
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…#35153)

* fix(ui): filter team MCP tool allowlists against the effective server set

Saving a team filtered mcp_tool_permissions down to the directly selected servers. A server reached
through an access group or a toolset is never in that list, so any save dropped its entry, including
a save that only changed the team alias. Because the resolver unions tool-permission keys into the
entitled server set and treats a missing entry as "no restriction from this level", the team kept
the server and lost the tool allowlist on it

Filtering on the direct list alone cannot get this right in either direction. Keeping every entry a
level did not directly select leaves a removed access group's server reachable through its own stale
entry, which breaks revocation. Dropping on deselection alone widens a server that an access group
still supplies

The save handler now resolves the effective server set with resolveEffectiveMcpServers and keeps an
entry only when something other than the entry itself still grants that server: a direct selection, a
selected access group, or a selected toolset. Unified access group ids are added when that selection
is untouched, since the loaded server list is then still accurate

When the server or toolset list cannot be resolved, every entry is kept and the admin is told the
allowlists were saved unchanged. Pruning on incomplete knowledge is the direction that silently
widens, so it only happens when the editor can show the server became unreachable. A failed lookup
and a changed access group selection are separate cases in a tagged union, so the notice names what
actually happened instead of describing the intentional one as a failure, and both hooks gate the
filter symmetrically so a save fired before toolsets settle cannot resolve against an empty toolset
list

Resolves LIT-4961

* fix(ui): resolve team MCP grants from access group metadata and refuse unsafe saves

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): resolve team access group grants from team info when the access group list is role-gated

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): match every selected access group by id instead of by count

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): reload team access group grants at save time

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): keep frontend lint budget within limit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ui): cover a standing allowlist no group grant covers at load or save

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(ui): keep MCP grant inputs in named variables for the lint budget

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…s, fix flat view

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@greptileai please review head 05ccfb2, which folds in #35153 and the three Bugbot fixes

@mateo-berri

Copy link
Copy Markdown
Contributor

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 05ccfb2. Configure here.

@yassin-berriai
yassin-berriai enabled auto-merge (squash) September 5, 2026 21:28
@yassin-berriai
yassin-berriai merged commit 0cb7597 into litellm_internal_staging Sep 5, 2026
184 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_mcp_perm_ui_inherited_servers branch September 5, 2026 21:49
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.

4 participants