From ff2f96d09e6f2f99c56a168eeed70eecab54a9ea Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 11 Mar 2026 00:23:40 -0700 Subject: [PATCH] Fix mcp_tool_permissions JSON string deserialization in _resolve_team_allowed_mcp_servers Co-Authored-By: Claude Opus 4.6 --- .../object_permission_utils.py | 7 ++-- .../test_object_permission_utils.py | 38 +++++++++++++++++++ .../hooks/mcpServers/useMCPServers.ts | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/management_helpers/object_permission_utils.py b/litellm/proxy/management_helpers/object_permission_utils.py index 194c64448e..319a0b5eb7 100644 --- a/litellm/proxy/management_helpers/object_permission_utils.py +++ b/litellm/proxy/management_helpers/object_permission_utils.py @@ -203,9 +203,10 @@ async def _resolve_team_allowed_mcp_servers( team_object_permission.mcp_access_groups or [] ) ) - tool_perm_servers: List[str] = list( - (team_object_permission.mcp_tool_permissions or {}).keys() - ) + raw_tool_perms = team_object_permission.mcp_tool_permissions or {} + if isinstance(raw_tool_perms, str): + raw_tool_perms = json.loads(raw_tool_perms) + tool_perm_servers: List[str] = list(raw_tool_perms.keys()) return set(direct_servers + access_group_servers + tool_perm_servers) diff --git a/tests/test_litellm/proxy/management_helpers/test_object_permission_utils.py b/tests/test_litellm/proxy/management_helpers/test_object_permission_utils.py index 5f09708a4e..202b95b319 100644 --- a/tests/test_litellm/proxy/management_helpers/test_object_permission_utils.py +++ b/tests/test_litellm/proxy/management_helpers/test_object_permission_utils.py @@ -15,6 +15,7 @@ from litellm.proxy._types import LiteLLM_ObjectPermissionTable from litellm.proxy.management_helpers.object_permission_utils import ( _extract_requested_mcp_access_groups, _extract_requested_mcp_server_ids, + _resolve_team_allowed_mcp_servers, _set_object_permission, validate_key_mcp_servers_against_team, ) @@ -395,3 +396,40 @@ async def test_validate_team_access_groups_resolve_to_servers(mock_access_groups team_obj=team_obj, ) + +# ---- Tests for _resolve_team_allowed_mcp_servers with JSON string mcp_tool_permissions ---- + + +@pytest.mark.asyncio +@patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.MCPRequestHandler._get_mcp_servers_from_access_groups", + new_callable=AsyncMock, + return_value=[], +) +async def test_resolve_team_allowed_mcp_servers_string_tool_permissions(mock_access_groups): + """mcp_tool_permissions stored as a JSON string (via safe_dumps) should be deserialized correctly.""" + mock_perm = MagicMock(spec=LiteLLM_ObjectPermissionTable) + mock_perm.mcp_servers = ["server-1"] + mock_perm.mcp_access_groups = [] + mock_perm.mcp_tool_permissions = json.dumps({"server-2": ["tool1"]}) + + result = await _resolve_team_allowed_mcp_servers(mock_perm) + assert result == {"server-1", "server-2"} + + +@pytest.mark.asyncio +@patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.MCPRequestHandler._get_mcp_servers_from_access_groups", + new_callable=AsyncMock, + return_value=[], +) +async def test_resolve_team_allowed_mcp_servers_dict_tool_permissions(mock_access_groups): + """mcp_tool_permissions as a dict should work without deserialization.""" + mock_perm = MagicMock(spec=LiteLLM_ObjectPermissionTable) + mock_perm.mcp_servers = [] + mock_perm.mcp_access_groups = [] + mock_perm.mcp_tool_permissions = {"server-a": ["tool1"]} + + result = await _resolve_team_allowed_mcp_servers(mock_perm) + assert result == {"server-a"} + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.ts index a00a16cbee..9210e25e1a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/mcpServers/useMCPServers.ts @@ -9,7 +9,7 @@ const mcpServersKeys = createQueryKeys("mcpServers"); export const useMCPServers = (teamId?: string | null) => { const { accessToken } = useAuthorized(); return useQuery({ - queryKey: mcpServersKeys.list({ teamId: teamId ?? undefined }), + queryKey: mcpServersKeys.list(teamId ? { filters: { teamId } } : undefined), queryFn: async () => await fetchMCPServers(accessToken!, teamId), enabled: !!accessToken, });