Skip to content

fix(mcp): resolve team.access_group_ids → MCP servers - #28997

Merged
ryan-crabbe-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_mcp_team_access_group_resolution
May 27, 2026
Merged

fix(mcp): resolve team.access_group_ids → MCP servers#28997
ryan-crabbe-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_mcp_team_access_group_resolution

Conversation

@ryan-crabbe-berri

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

Copy link
Copy Markdown
Contributor

Summary

A virtual key whose team has an MCP-granting access group attached via /v1/access_group now sees that server through /v1/mcp/server and can call tools on it. Previously the runtime only resolved the key's own access_group_idsteam.access_group_ids was ignored, so virtual keys in such teams got HTTP 200 [] from discovery and 403 from tool calls. The fix mirrors the existing model-side pattern in can_team_access_model: the group being attached to the team is itself the gate, no assigned_team_ids re-check needed.

Fix is in _get_allowed_mcp_servers_for_team (user_api_key_auth_mcp.py) — fetches the full team object via get_team_object and unions in MCP servers from team.access_group_ids using the existing _get_mcp_server_ids_from_access_groups helper.

Screenshots

before
Screenshot 2026-05-27 at 9 56 16 AM

after
Screenshot 2026-05-27 at 9 56 25 AM

Test plan

  • test_team_access_group_ids_resolve_to_mcp_servers — direct repro of the bug
  • test_team_access_group_ids_union_with_object_permission — legacy object_permission + unified access_group_ids union
  • test_team_access_group_ids_empty_returns_no_extras — null guard, no resolver call when empty
  • test_get_allowed_mcp_servers_includes_team_access_group_extras_end_to_end — top-level helper path
  • Refreshed mocks in existing helper-coverage tests (test_user_api_key_auth_mcp, test_jwt_mcp_simple, test_jwt_mcp_enforcement) for the new internal call site — 134 tests pass across MCP auth + JWT MCP suites
  • Manual repro on a live proxy + Neon DB: created an access group with access_mcp_server_ids, attached to a team, generated a virtual key in that team — GET /v1/mcp/server returns [] before, returns the server after

Resolves #27657
Resolves LIT-3181

A virtual key whose team has an MCP-granting access group attached
via /v1/access_group now sees that server through /v1/mcp/server (and
can call tools on it) instead of getting an empty list. The runtime
already resolves the key's unified access_group_ids; this adds the
symmetric resolution on the team side, mirroring the model-side
pattern in can_team_access_model — the group being on the team is
itself the gate, so no assigned_team_ids re-check is needed.

Resolves #27657
@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...erimental/mcp_server/auth/user_api_key_auth_mcp.py 92.30% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where virtual keys in teams with access_group_ids set saw empty MCP server lists. The fix updates _get_allowed_mcp_servers_for_team to fetch the full team object via get_team_object and union in MCP servers from team.access_group_ids using the existing _get_mcp_server_ids_from_access_groups helper, mirroring the model-side can_team_access_model pattern.

  • Core fix (user_api_key_auth_mcp.py): replaces the pre-loaded object_permission-only read with a get_team_object call followed by a union of legacy object_permission servers and unified access_group_ids-sourced servers; early returns if prisma_client is None or team not found.
  • Test updates (3 test files): existing helper-coverage tests are reworked to mock at the get_team_object level instead of _get_team_object_permission, and four new targeted regression tests for the specific bug are added.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the MCP team permission lookup, correctly mirrors the established model-side pattern, and all modified tests preserve their original behavioral assertions.

The fix is a well-contained addition to _get_allowed_mcp_servers_for_team: it fetches the team via the cache-first get_team_object helper (consistent with the existing key-level access group path) and unions in servers from team.access_group_ids using the same _get_mcp_server_ids_from_access_groups helper used elsewhere. The existing try/except guard ensures any lookup failure degrades gracefully to an empty list rather than an unhandled exception. Four new regression tests directly reproduce the reported bug, and the modified JWT tests preserve their intersection/inheritance assertions while correctly updating mock targets to match the new code path.

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_team now fetches team via get_team_object (cache-first) and unions legacy object_permission servers with unified access_group_ids servers. Logic is correct and mirrors the existing model-side pattern.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Existing tests updated to mock at get_team_object level; four new regression tests added covering the team access_group_ids bug path, including null guard, union with object_permission, and end-to-end helper flow.
tests/test_litellm/proxy/_experimental/mcp_server/test_jwt_mcp_enforcement.py test_e2e_jwt_team_mcp_key_intersection updated to patch auth_checks.get_team_object instead of _get_team_object_permission; intersection assertion is preserved and still exercises the same boundary condition.
tests/test_litellm/proxy/_experimental/mcp_server/test_jwt_mcp_simple.py Simple JWT tests updated to mock get_team_object with a full LiteLLM_TeamTable (including object_permission); behavioral assertions unchanged, coverage is maintained.

Reviews (2): Last reviewed commit: "chore(mcp): address greptile review on t..." | Re-trigger Greptile

if team_obj is None:
return []

team_access_group_servers = await _get_mcp_server_ids_from_access_groups(

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.

Medium: Missing access-group assignment check

A team admin can write access_group_ids on their team through /team/update; this now grants every MCP server in those groups without confirming the access group’s assigned_team_ids includes user_api_key_auth.team_id. Filter these groups through the access-group assignment metadata before adding access_mcp_server_ids, or restrict team-level access_group_ids changes to proxy-admin-managed assignment paths.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-existing. model-side already reads team.access_group_ids without an assigned_team_ids check (auth_checks.py:3146-3162). root cause is /team/update permitting the write.

@veria-ai

veria-ai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

PR overview

The PR still has an open authorization gap around team-level MCP server resolution. A team admin can set access group IDs on their team and inherit MCP server access from those groups without verifying that the groups are actually assigned to that team, which could allow unauthorized access to MCP servers. No issues have been addressed yet, so the current security posture still depends on closing this access-control bypass.

Open issues (1)

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

Forward already-imported prisma_client / user_api_key_cache /
proxy_logging_obj to _get_mcp_server_ids_from_access_groups so it
skips its lazy re-import path. Update test docstring + assertions to
reflect that the resolver is invoked with [] (and short-circuits
without DB access) rather than skipped entirely.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri
ryan-crabbe-berri merged commit ea01533 into litellm_internal_staging May 27, 2026
114 of 118 checks passed
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
* fix(mcp): resolve team.access_group_ids → MCP servers

A virtual key whose team has an MCP-granting access group attached
via /v1/access_group now sees that server through /v1/mcp/server (and
can call tools on it) instead of getting an empty list. The runtime
already resolves the key's unified access_group_ids; this adds the
symmetric resolution on the team side, mirroring the model-side
pattern in can_team_access_model — the group being on the team is
itself the gate, so no assigned_team_ids re-check is needed.

Resolves BerriAI#27657

* chore(mcp): address greptile review on team access-group resolver

Forward already-imported prisma_client / user_api_key_cache /
proxy_logging_obj to _get_mcp_server_ids_from_access_groups so it
skips its lazy re-import path. Update test docstring + assertions to
reflect that the resolver is invoked with [] (and short-circuits
without DB access) rather than skipped entirely.
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.

[Bug]: Virtual key MCP access ignores team access groups (discovery AND enforcement)

2 participants