Skip to content

fix(mcp): resolve key.access_group_ids → MCP servers (ungated)#29195

Merged
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_mcp_key_access_group_servers
May 29, 2026
Merged

fix(mcp): resolve key.access_group_ids → MCP servers (ungated)#29195
yuneng-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_mcp_key_access_group_servers

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

A teamless virtual key whose unified access_group_ids reference an MCP-granting access group now discovers and can call that server, instead of getting an empty GET /v1/mcp/server list and 403 access_denied on tool routes. Previously the key path read only the legacy object_permission; the only place that resolved key.access_group_ids for MCP was the gated extras path, which requires the group to list the key in assigned_key_ids/assigned_team_ids.

This adds the missing ungated base resolution: _get_allowed_mcp_servers_for_key now folds key.access_group_ids → access_group.access_mcp_server_ids into the key's own scope, mirroring can_key_call_model's ungated fallback on the model side. The gated, team-ceiling-busting override (_get_key_access_group_mcp_server_extras) is unchanged, so the anti-escalation guard still applies where it should. Net semantics now match models and the team path.

screenshots

  1. created key for an internal user
  2. gave that key an access group with an mcp
  3. tested the users able to use that mcp
Screenshot 2026-05-29 at 11 52 23 AM

before - wasnt able to use it

Screenshot 2026-05-29 at 12 06 35 PM

after - was able to use it

Screenshot 2026-05-29 at 12 06 45 PM

Test plan

  • tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py::test_key_access_group_ids_resolves_mcp_servers_ungated — teamless key, group grants server with empty assigned_* → server returned
  • ...::test_key_access_group_ids_union_with_object_permission — legacy object_permission ∪ access-group servers
  • ...::test_key_access_group_ids_empty_returns_no_extras — empty access_group_ids short-circuits to []
  • ...::test_get_allowed_mcp_servers_key_access_group_base_end_to_end — gated extras still [], but base path grants the server
  • Full file: 131 passed (uv run pytest .../test_user_api_key_auth_mcp.py)
  • Manual end-to-end against a DB-backed proxy: same key/group/server shape, GET /v1/mcp/server returns [] + tools/list 403 before, returns the server + 200 after

Resolves LIT-3399

A teamless virtual key whose unified access_group_ids grant an
MCP-granting access group now sees and can call that server instead of
getting an empty list / 403. The key path previously read only the
legacy object_permission; this folds key.access_group_ids into the
key's base scope (ungated), mirroring can_key_call_model's fallback.
The gated assigned_*-checked override is unchanged.
@codecov

codecov Bot commented May 28, 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 May 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the missing ungated resolution of key.access_group_ids → MCP servers in _get_allowed_mcp_servers_for_key, so a teamless virtual key whose unified access_group_ids reference an MCP-granting group now discovers those servers. It mirrors the existing ungated fallback already present for the model (can_key_call_model) and agent paths, making MCP consistent with them.

  • _get_allowed_mcp_servers_for_key now eagerly resolves key.access_group_ids via _get_mcp_server_ids_from_access_groups + expand_permission_list before the legacy object_permission lookup, and unions both sources — with expand_permission_list applied at the source so both the early-return and full-union branches surface resolved IDs.
  • Four new focused unit/integration tests cover the ungated resolve, the union with object_permission, the empty access_group_ids short-circuit, and an end-to-end repro that verifies the gated extras path still returns [] while the new base path grants the server.

Confidence Score: 5/5

The change is a read-time permission expansion scoped to a single helper function, with no writes, schema changes, or modifications to existing auth paths.

The fix is a focused one-file change that makes the key's access_group_ids → MCP resolution consistent with already-approved patterns used by models and agents. The previously raised concerns about missing expand_permission_list at the early-return and full-union sites have been addressed at the source. All four new tests are properly mocked, no existing tests are weakened, and the gated extras path is unchanged.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Adds ungated key.access_group_ids → MCP server resolution to _get_allowed_mcp_servers_for_key; previously flagged expand_permission_list gap is now addressed at the source; refactors import hoisting and the prisma_client is not None guard cleanly.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Adds 4 new unit/integration tests (ungated resolve, union with object_permission, empty short-circuit, end-to-end repro); all use mocks — no real network calls; existing tests are untouched.

Reviews (2): Last reviewed commit: "fix(mcp): expand name/alias entries in k..." | Re-trigger Greptile

Comment thread litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Outdated
@veria-ai

veria-ai Bot commented May 28, 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: 1 · PR risk: 0/10

access_mcp_server_ids may hold server names/aliases, not just ids. The
new ungated key path now runs them through expand_permission_list at the
source, so the early-return and union branches both surface resolved ids
— matching the legacy object_permission path and the gated extras path.
@ryan-crabbe-berri

ryan-crabbe-berri commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@greptileai re-review

Addressed the alias-expansion feedback: key.access_group_ids → access_mcp_server_ids is now run through expand_permission_list at the source (user_api_key_auth_mcp.py:960), so both the key_object_permission is None early return and the full-union path surface resolved server IDs, matching the gated extras path. No duplicated expansion at the two call sites.

@yuneng-berri yuneng-berri merged commit 909a5f5 into litellm_internal_staging May 29, 2026
115 of 118 checks passed
ryan-crabbe-berri added a commit that referenced this pull request May 30, 2026
The previous commit moved key.access_group_ids resolution out of the
intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the
ungated additive grant path (_get_key_access_group_mcp_server_extras),
unioned on top of the team ceiling. Five tests from #28890/#29195 still
asserted the old gated / in-key-scope contract and failed:

- _get_allowed_mcp_servers_for_key now returns the object_permission
  ceiling only and never resolves access_group_ids; two tests now assert
  the group resolver is not called from that path (with and without an
  object_permission present).
- The extras path is ungated, so a group whose assigned_team_ids /
  assigned_key_ids exclude the caller still contributes its servers.
- The end-to-end test asserts the grant surfaces via the extras path
  rather than the base key path.
- Dropped test_key_access_group_ids_empty_returns_no_extras; the empty
  case is already covered by the extras family's no-groups test.
ryan-crabbe-berri added a commit that referenced this pull request May 31, 2026
…gnment (#29313)

* fix(mcp): make key.access_group_ids grants additive over team ceiling

A key whose unified access_group_ids grant a private MCP server was
having that grant intersected against its team's MCP ceiling, so a key
in a team scoped to other servers (or with no own scope) lost the
granted server entirely. Resolve access_group_ids once as ungated
additive grants and union them on top of the key/team ceiling instead
of folding them into the key scope that gets intersected.

* test(mcp): align key access-group tests with additive-grant model

The previous commit moved key.access_group_ids resolution out of the
intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the
ungated additive grant path (_get_key_access_group_mcp_server_extras),
unioned on top of the team ceiling. Five tests from #28890/#29195 still
asserted the old gated / in-key-scope contract and failed:

- _get_allowed_mcp_servers_for_key now returns the object_permission
  ceiling only and never resolves access_group_ids; two tests now assert
  the group resolver is not called from that path (with and without an
  object_permission present).
- The extras path is ungated, so a group whose assigned_team_ids /
  assigned_key_ids exclude the caller still contributes its servers.
- The end-to-end test asserts the grant surfaces via the extras path
  rather than the base key path.
- Dropped test_key_access_group_ids_empty_returns_no_extras; the empty
  case is already covered by the extras family's no-groups test.

* feat(auth): gate member access-group assignment on keys behind opt-in

Non-admin team members could attach access_group_ids to keys they create
or update, letting them self-grant resources (MCP servers/models) the team
admin never intended. Add an opt-in KEY_ACCESS_GROUP_ASSIGNMENT team-member
permission (default-deny) enforced at /key/generate and /key/update; proxy
and team admins bypass. Surfaces automatically as a checkbox in the team
Member Permissions UI.

* fix(auth): gate access-group assignment on /key/regenerate too

RegenerateKeyRequest inherits access_group_ids and prepare_key_update_data
persists it, so a non-admin key owner could self-grant access groups by
regenerating. Apply the same opt-in member gate using the existing key's
team.

* test(auth): cover member access-group gate and additive MCP grants

Add unit tests for enforce_member_can_assign_access_groups (deny without
opt-in, allow with opt-in, and proxy-admin / team-admin / non-team-key
bypasses) and for _get_key_access_group_mcp_server_extras (no-auth and
no-resolved-servers return empty, resolved ids are expanded, errors
degrade to no grants).
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…AI#29195)

* fix(mcp): resolve key.access_group_ids → MCP servers (ungated)

A teamless virtual key whose unified access_group_ids grant an
MCP-granting access group now sees and can call that server instead of
getting an empty list / 403. The key path previously read only the
legacy object_permission; this folds key.access_group_ids into the
key's base scope (ungated), mirroring can_key_call_model's fallback.
The gated assigned_*-checked override is unchanged.

* fix(mcp): expand name/alias entries in key access-group servers

access_mcp_server_ids may hold server names/aliases, not just ids. The
new ungated key path now runs them through expand_permission_list at the
source, so the early-return and union branches both surface resolved ids
— matching the legacy object_permission path and the gated extras path.
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…gnment (BerriAI#29313)

* fix(mcp): make key.access_group_ids grants additive over team ceiling

A key whose unified access_group_ids grant a private MCP server was
having that grant intersected against its team's MCP ceiling, so a key
in a team scoped to other servers (or with no own scope) lost the
granted server entirely. Resolve access_group_ids once as ungated
additive grants and union them on top of the key/team ceiling instead
of folding them into the key scope that gets intersected.

* test(mcp): align key access-group tests with additive-grant model

The previous commit moved key.access_group_ids resolution out of the
intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the
ungated additive grant path (_get_key_access_group_mcp_server_extras),
unioned on top of the team ceiling. Five tests from BerriAI#28890/BerriAI#29195 still
asserted the old gated / in-key-scope contract and failed:

- _get_allowed_mcp_servers_for_key now returns the object_permission
  ceiling only and never resolves access_group_ids; two tests now assert
  the group resolver is not called from that path (with and without an
  object_permission present).
- The extras path is ungated, so a group whose assigned_team_ids /
  assigned_key_ids exclude the caller still contributes its servers.
- The end-to-end test asserts the grant surfaces via the extras path
  rather than the base key path.
- Dropped test_key_access_group_ids_empty_returns_no_extras; the empty
  case is already covered by the extras family's no-groups test.

* feat(auth): gate member access-group assignment on keys behind opt-in

Non-admin team members could attach access_group_ids to keys they create
or update, letting them self-grant resources (MCP servers/models) the team
admin never intended. Add an opt-in KEY_ACCESS_GROUP_ASSIGNMENT team-member
permission (default-deny) enforced at /key/generate and /key/update; proxy
and team admins bypass. Surfaces automatically as a checkbox in the team
Member Permissions UI.

* fix(auth): gate access-group assignment on /key/regenerate too

RegenerateKeyRequest inherits access_group_ids and prepare_key_update_data
persists it, so a non-admin key owner could self-grant access groups by
regenerating. Apply the same opt-in member gate using the existing key's
team.

* test(auth): cover member access-group gate and additive MCP grants

Add unit tests for enforce_member_can_assign_access_groups (deny without
opt-in, allow with opt-in, and proxy-admin / team-admin / non-team-key
bypasses) and for _get_key_access_group_mcp_server_extras (no-auth and
no-resolved-servers return empty, resolved ids are expanded, errors
degrade to no grants).
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