From 222cb293ca61a57d6771bbaca43307f2e15f11fd Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:14:18 -0700 Subject: [PATCH] fix(mcp): name the access group that resolved when an agent vetoes a mixed scope --- .../proxy/_experimental/mcp_server/server.py | 12 ++++--- .../mcp_server/test_mcp_server.py | 31 +++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 9f530ddf862..b3b574673db 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -1489,10 +1489,14 @@ if MCP_AVAILABLE: ) } raise HTTPException(status_code=403, detail=agent_denial) - vetoed_group: Final = ( - next((name for name in requested_names if _registered_server_id(name) is None), None) - if resolved_ids - else None + vetoed_group: Final = next( + ( + name + for name in requested_names + if _registered_server_id(name) is None + and any(name in (server.access_groups or ()) for server in resolved_without_agent) + ), + None, ) if vetoed_group is not None: group_denial: Final[_McpDeniedDetail] = { diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index fb69ad3b1c4..ea64fdf2a5d 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -1336,11 +1336,13 @@ def _denied_scope_manager(known_server_names_to_ids: dict[str, str]) -> MagicMoc return manager -def _scope_resolver(resolved_without_agent: list[str]) -> AsyncMock: +def _scope_resolver(resolved_without_agent: list[str], access_groups: tuple[str, ...] = ()) -> AsyncMock: async def resolve(user_api_key_auth, mcp_servers, client_ip=None): if user_api_key_auth is not None and user_api_key_auth.agent_id: return [] - return [MagicMock(server_id=server_id) for server_id in resolved_without_agent] + return [ + MagicMock(server_id=server_id, access_groups=list(access_groups)) for server_id in resolved_without_agent + ] return AsyncMock(side_effect=resolve) @@ -1450,7 +1452,7 @@ async def test_scoped_list_access_group_vetoed_by_agent_names_agent_and_group(): user_api_key_auth, ["prod-group"], _denied_scope_manager({}), - _scope_resolver(resolved_without_agent=["srv-github"]), + _scope_resolver(resolved_without_agent=["srv-github"], access_groups=("prod-group",)), ) assert denial.status_code == 403 @@ -1460,6 +1462,29 @@ async def test_scoped_list_access_group_vetoed_by_agent_names_agent_and_group(): assert "mcp_access_groups" in message +@pytest.mark.asyncio +async def test_scoped_list_mixed_unknown_and_vetoed_group_names_the_group_that_resolved(): + """With an unknown name ahead of the agent-vetoed group in the scope, the 403 must name the group + whose servers the key can reach, never the unknown name, or the admin is told to grant a group + that does not exist.""" + pytest.importorskip("litellm.proxy._experimental.mcp_server.server") + + user_api_key_auth = UserAPIKeyAuth(api_key="test_key", user_id="test_user", agent_id="agent-123") + + denial = await _denied_scoped_list( + user_api_key_auth, + ["no-such-group", "prod-group"], + _denied_scope_manager({}), + _scope_resolver(resolved_without_agent=["srv-github"], access_groups=("prod-group",)), + ) + + assert denial.status_code == 403 + message = denial.detail["error"] + assert "access group 'prod-group'" in message + assert "no-such-group" not in message + assert "agent 'agent-123'" in message + + @pytest.mark.asyncio async def test_scoped_list_agent_key_denied_by_key_grants_raises_generic_403(): """When the agent-stripped rerun still resolves nothing, the 403 stays generic instead of