fix(mcp): grant MCP server access via unified key/team access_group_ids (LIT-3399) - #29102
Conversation
…ds (LIT-3399) Update litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py
…ds (LIT-3399) Update tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py
|
|
Greptile SummaryThis PR fixes LIT-3399: a virtual key or team whose only MCP grant was through a unified access group (
Confidence Score: 5/5Safe to merge — the change is purely additive, expanding MCP access in the same way model access already works, and is backed by a focused set of regression tests. The logic change is narrow and consistent with the existing unified-access-group pattern used for model checks. Both new helpers guard against None inputs and swallow exceptions, keeping the auth path non-fatal. The early-return removal is safe because the unified-group result is computed before the guard, so the no-object-permission case now returns that result instead of a hardcoded empty list. No pre-existing tests were weakened. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py | Adds two static helpers and wires unified access_group_ids resolution into both the key and team MCP permission paths, mirroring the model-access semantics already used elsewhere in the auth layer. |
| tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py | Appends 9 focused regression tests for LIT-3399 (key and team unified access group paths) plus cosmetic unpacking cleanups; all tests use mocks, no real network calls. |
Reviews (2): Last reviewed commit: "fix(mcp): keep separate _get_team_object..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…h path (Greptile P2 LIT-3399)
…h path (Greptile P2 LIT-3399)
… dropped by team-fn refactor (LIT-3399)
…ed_access_group_ids helpers (LIT-3399) Reverts the Greptile P2 'collapse' refactor that broke 3 existing JWT MCP tests which mock _get_team_object_permission. Both helpers share the same cached get_team_object() call, so the second invocation is served from cache; the P2 was non-blocking.
…ed_access_group_ids helpers (LIT-3399) Reverts the Greptile P2 'collapse' refactor that broke 3 existing JWT MCP tests which mock _get_team_object_permission. Both helpers share the same cached get_team_object() call, so the second invocation is served from cache; the P2 was non-blocking.
|
Greptile P2 follow-up: the redundant I tried collapsing the two calls into one @greptileai please re-review on |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Summary
Fixes LIT-3399: assigning a unified access group to a virtual key (
key.access_group_ids) — or to a team (team.access_group_ids) — did not grant access to the MCP servers listed on that access group. The proxy returned HTTP 403access_deniedfrom/v1/mcp/serverand the MCP tool routes unless the same key/team was ALSO listed on the access group'sassigned_key_ids/assigned_team_ids. From the operator's perspective the attachment looked complete but enforcement denied access.The fix adds an inclusive (un-gated) resolution path that mirrors the model-side semantics already used by
can_token_call_modelandcan_team_access_model: when a key/team has an access group attached viaaccess_group_ids, the group'saccess_mcp_server_idsare granted, independently ofassigned_*.Root cause
_get_allowed_mcp_servers_for_keyand_get_allowed_mcp_servers_for_teaminlitellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.pyonly read MCP grants from the legacyobject_permissionrow (mcp_servers,mcp_access_groupsMCP-name tags,mcp_tool_permissions). They never consulted the unifiedaccess_group_idsfield that the Admin UI writes when an admin attaches an access group via "Access Groups" on the key edit screen.auth_checks._get_mcp_server_ids_from_access_groups()already exists for this purpose but was never wired into the MCP path. Additionally, the existing_get_allowed_mcp_servers_for_keyshort-circuited and returned[]wheneverkey_object_permission is None, so a key whose ONLY MCP grant was through a unified access group got nothing.Fix
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py:_get_unified_access_group_mcp_servers_for_object(access_group_ids)→ callsauth_checks._get_mcp_server_ids_from_access_groupsthen runs each result throughglobal_mcp_server_manager.expand_permission_listfor name/alias → server_id normalization. Errors swallowed and return[](auth path must not 500)._get_team_unified_access_group_ids(user_api_key_auth)→ fetches the team object via the same cachedget_team_object()call already used by_get_team_object_permission, returnsteam.access_group_ids or []._get_allowed_mcp_servers_for_key: removed the short-circuit-on-None forkey_object_permission; the unified access-group resolution now runs regardless. Whenkey_object_permission is Nonethe unified-only set is returned; otherwise both sets are unioned._get_allowed_mcp_servers_for_team: same shape — unioned the unified-access-group set with the legacyobject_permissionset.The existing gated
_get_key_access_group_mcp_server_extraspath (added in PR #28890 / #28997 againstlitellm_internal_staging) is intentionally untouched. That path serves a different purpose (allowing a key's access group to bypass the team-MCP intersection ceiling when the key is explicitly assigned) and is not present in this branch yet anyway. This patch is the inclusive owner-style path that mirrors model semantics.Evidence
Driving
MCPRequestHandler.get_allowed_mcp_servers()end-to-end against the exact ticket scenario (key K hasaccess_group_ids = [G], G hasaccess_mcp_server_ids = [S]and emptyassigned_*, no team, noobject_permission):BEFORE (clean
litellm_oss_agent_shin_daily_branch, HEAD1fe911d):This is what the bug reporter sees as HTTP 403 / empty discovery.
AFTER (this PR):
Server
Sis now granted via the unifiedaccess_group_idsattachment alone, with noassigned_key_idschange.Tests
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py— appended 9 regression tests under theLIT-3399header:test_lit3399_key_access_group_grants_mcp_without_assigned_key_ids— top-level repro throughMCPRequestHandler.get_allowed_mcp_serverstest_lit3399_key_no_unified_groups_returns_empty_when_no_object_perm— negative case unchangedtest_lit3399_get_unified_helper_resolves_servers— helper resolves via_get_mcp_server_ids_from_access_groups+expand_permission_listtest_lit3399_get_unified_helper_empty_input_returns_empty— short-circuit for empty inputtest_lit3399_get_unified_helper_swallows_exceptions— resolver errors must not break authtest_lit3399_key_with_object_perm_and_unified_groups_unions_both— union semanticstest_lit3399_team_unified_access_group_grants_mcp— team-side equivalenttest_lit3399_get_team_unified_access_group_ids_returns_empty_for_no_team— guard clausestest_lit3399_get_team_unified_access_group_ids_reads_team_field— happy path + None-tolerantFull mcp_server/auth test directory: 149 passed, 0 failed.
Branch / scope
litellm_oss_agent_shin_daily_branch(per Shin fork policy)oss-agent-shin:shin/lit-3399-mcp-unified-access-groupsGITHUB_TOKENlacksrepo+workflowscopes forgit push. Reviewers should expect a noisier merge-base diff than a normal fast-forward; the actual change is the two files listed above.Files changed: 2
Verification (ship-pr)
litellm_oss_agent_shin_daily_branch(HEAD1fe911d):MCPRequestHandler.get_allowed_mcp_servers()returns[]for a key withaccess_group_ids=[G], G grantsaccess_mcp_server_ids=[S], no team, emptyassigned_*. Matches the 403 the bug reporter sees.['server-S-uuid']— server S is granted via the unified access-group attachment alone, with noassigned_key_idschange. Inline in the Evidence section above.tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.pycover positive, negative, union, exception-swallow, and team/key paths.136 passedacross the auth tests + the three pre-existing JWT MCP enforcement tests.ad5f61b, including all lint / mypy / proxy-* test suites. Remaining check is Veria AI - PR Review, still running.get_team_objectcall addressed in a PR comment (see thread): the two calls hit the sameuser_api_key_cachekey, so per-request behavior is 1 DB round-trip + 1 cache hit, not 2 DB queries. Collapsing them into one helper broke 3 existing JWT MCP tests that mock_get_team_object_permission, so the two-helper structure was kept.litellm_oss_agent_shin_daily_branch(Shin fork policy).GITHUB_TOKENlacksrepo+workflowscopes. Final diff: 2 files, +500/−4 (the test additions account for ~480 of the +500).