From 3e7121706be80a471da7184b08b21298e4e1861f Mon Sep 17 00:00:00 2001 From: Praising Harris Ratnam Date: Sun, 18 Oct 2026 21:12:36 +0530 Subject: [PATCH 1/3] fix(mcp): scope /v1/mcp/access_groups to caller accessible servers (#46912) - Restrict GET /v1/mcp/access_groups to accessible servers for non-admin callers - Preserve full catalog view for admin callers - Add unit tests verifying admin vs non-admin access group scoping --- .../mcp_management_endpoints.py | 41 ++++++----- .../test_mcp_management_endpoints.py | 68 +++++++++++++++++++ 2 files changed, 93 insertions(+), 16 deletions(-) diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index d014ff6cf07..ee59590cd92 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1112,7 +1112,8 @@ if MCP_AVAILABLE: user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Get all available MCP access groups from the database AND config + Get available MCP access groups from the database AND config. + Admins receive the full catalog; non-admin callers are scoped to servers they have access to. """ from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, @@ -1121,22 +1122,30 @@ if MCP_AVAILABLE: access_groups: Final = set() - # Get from config-loaded servers - for server in global_mcp_server_manager.config_mcp_servers.values(): - if server.access_groups: - access_groups.update(server.access_groups) + if user_api_key_has_admin_view(user_api_key_dict): + # Get from config-loaded servers + for server in global_mcp_server_manager.config_mcp_servers.values(): + if server.access_groups: + access_groups.update(server.access_groups) - # Get from DB - if prisma_client is not None: - try: - mcp_servers: Final[Sequence[prisma_models.LiteLLM_MCPServerTable]] = await MCPServerRepository( - prisma_client - ).table.find_many() - for server in mcp_servers: - if hasattr(server, "mcp_access_groups") and server.mcp_access_groups: - access_groups.update(server.mcp_access_groups) - except Exception as e: - verbose_proxy_logger.debug("Error getting MCP access groups: %s", e) + # Get from DB + if prisma_client is not None: + try: + mcp_servers: Final[Sequence[prisma_models.LiteLLM_MCPServerTable]] = await MCPServerRepository( + prisma_client + ).table.find_many() + for server in mcp_servers: + if hasattr(server, "mcp_access_groups") and server.mcp_access_groups: + access_groups.update(server.mcp_access_groups) + except Exception as e: + verbose_proxy_logger.debug("Error getting MCP access groups: %s", e) + else: + accessible_servers: Final = await _resolve_accessible_mcp_servers(user_api_key_dict) + for server in accessible_servers: + if hasattr(server, "mcp_access_groups") and server.mcp_access_groups: + access_groups.update(server.mcp_access_groups) + if hasattr(server, "access_groups") and server.access_groups: + access_groups.update(server.access_groups) # Convert to sorted list access_groups_list: Final = sorted(list(access_groups)) diff --git a/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py index 0d59c655835..496b472e324 100644 --- a/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -11704,3 +11704,71 @@ async def test_mcp_register_application_type_reaches_upstream_or_is_rejected( assert posted["application_type"] == application_type finally: mgmt_endpoints._temporary_mcp_servers.pop(server.server_id, None) + + +@pytest.mark.asyncio +async def test_get_mcp_access_groups_admin_sees_all_groups() -> None: + admin_auth = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.PROXY_ADMIN) + + config_server = MagicMock() + config_server.access_groups = ["config-group-1"] + + db_server = MagicMock() + db_server.mcp_access_groups = ["db-group-1"] + + mock_manager = MagicMock() + mock_manager.config_mcp_servers = {"server-1": config_server} + + mock_repo = MagicMock() + mock_repo.table.find_many = AsyncMock(return_value=[db_server]) + + with ( + patch("litellm.proxy.management_endpoints.mcp_management_endpoints.user_api_key_has_admin_view", return_value=True), + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager", mock_manager), + patch("litellm.proxy.proxy_server.prisma_client", MagicMock()), + patch( + "litellm.proxy.management_endpoints.mcp_management_endpoints.MCPServerRepository", + return_value=mock_repo, + ), + ): + result = await mgmt_endpoints.get_mcp_access_groups(user_api_key_dict=admin_auth) + + assert result == {"access_groups": ["config-group-1", "db-group-1"]} + + +@pytest.mark.asyncio +async def test_get_mcp_access_groups_scoped_for_non_admin() -> None: + non_admin_auth = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.INTERNAL_USER) + + accessible_server = MagicMock() + accessible_server.mcp_access_groups = ["allowed-group"] + accessible_server.access_groups = None + + with ( + patch("litellm.proxy.management_endpoints.mcp_management_endpoints.user_api_key_has_admin_view", return_value=False), + patch( + "litellm.proxy.management_endpoints.mcp_management_endpoints._resolve_accessible_mcp_servers", + AsyncMock(return_value=[accessible_server]), + ) as mock_resolve, + ): + result = await mgmt_endpoints.get_mcp_access_groups(user_api_key_dict=non_admin_auth) + + mock_resolve.assert_awaited_once_with(non_admin_auth) + assert result == {"access_groups": ["allowed-group"]} + + +@pytest.mark.asyncio +async def test_get_mcp_access_groups_non_admin_empty_when_no_accessible_servers() -> None: + non_admin_auth = generate_mock_user_api_key_auth(user_role=LitellmUserRoles.INTERNAL_USER) + + with ( + patch("litellm.proxy.management_endpoints.mcp_management_endpoints.user_api_key_has_admin_view", return_value=False), + patch( + "litellm.proxy.management_endpoints.mcp_management_endpoints._resolve_accessible_mcp_servers", + AsyncMock(return_value=[]), + ), + ): + result = await mgmt_endpoints.get_mcp_access_groups(user_api_key_dict=non_admin_auth) + + assert result == {"access_groups": []} + From 91ac9a5b7de30074f84f8e7b5bd8b598c3a0d689 Mon Sep 17 00:00:00 2001 From: Pyhroff <215876416+Pyhroff@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:17:54 +0530 Subject: [PATCH 2/3] fix(mcp): resolve basedpyright attribute access and unknown type warnings --- .../mcp_management_endpoints.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index ee59590cd92..70a5c16a7d5 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1120,13 +1120,16 @@ if MCP_AVAILABLE: ) from litellm.proxy.proxy_server import prisma_client - access_groups: Final = set() + access_groups: Final[set[str]] = set() if user_api_key_has_admin_view(user_api_key_dict): # Get from config-loaded servers for server in global_mcp_server_manager.config_mcp_servers.values(): - if server.access_groups: - access_groups.update(server.access_groups) + cfg_groups = getattr(server, "access_groups", None) + if cfg_groups and isinstance(cfg_groups, (list, set, tuple)): + for g in cfg_groups: + if g: + access_groups.add(str(g)) # Get from DB if prisma_client is not None: @@ -1135,20 +1138,29 @@ if MCP_AVAILABLE: prisma_client ).table.find_many() for server in mcp_servers: - if hasattr(server, "mcp_access_groups") and server.mcp_access_groups: - access_groups.update(server.mcp_access_groups) + mcp_groups = getattr(server, "mcp_access_groups", None) + if mcp_groups and isinstance(mcp_groups, (list, set, tuple)): + for g in mcp_groups: + if g: + access_groups.add(str(g)) except Exception as e: verbose_proxy_logger.debug("Error getting MCP access groups: %s", e) else: accessible_servers: Final = await _resolve_accessible_mcp_servers(user_api_key_dict) for server in accessible_servers: - if hasattr(server, "mcp_access_groups") and server.mcp_access_groups: - access_groups.update(server.mcp_access_groups) - if hasattr(server, "access_groups") and server.access_groups: - access_groups.update(server.access_groups) + mcp_groups = getattr(server, "mcp_access_groups", None) + if mcp_groups and isinstance(mcp_groups, (list, set, tuple)): + for g in mcp_groups: + if g: + access_groups.add(str(g)) + cfg_groups = getattr(server, "access_groups", None) + if cfg_groups and isinstance(cfg_groups, (list, set, tuple)): + for g in cfg_groups: + if g: + access_groups.add(str(g)) # Convert to sorted list - access_groups_list: Final = sorted(list(access_groups)) + access_groups_list: Final[list[str]] = sorted(list(access_groups)) return {"access_groups": access_groups_list} @router.get( From 86beb0403a29e62a065e521c827c12e82afaeecc Mon Sep 17 00:00:00 2001 From: Pyhroff <215876416+Pyhroff@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:42:55 +0530 Subject: [PATCH 3/3] fix(mcp): update lazy openapi snapshot and reduce get_mcp_access_groups complexity --- litellm/proxy/_lazy_openapi_snapshot.json | 2 +- .../mcp_management_endpoints.py | 39 +++++++------------ 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 961d32ab664..5bb0ee47d4c 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -42714,7 +42714,7 @@ "paths": { "/v1/mcp/access_groups": { "get": { - "description": "Get all available MCP access groups from the database AND config", + "description": "Get available MCP access groups from the database AND config.\nAdmins receive the full catalog; non-admin callers are scoped to servers they have access to.", "operationId": "get_mcp_access_groups_v1_mcp_access_groups_get", "responses": { "200": { diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 70a5c16a7d5..eff5bf638ac 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1122,42 +1122,31 @@ if MCP_AVAILABLE: access_groups: Final[set[str]] = set() - if user_api_key_has_admin_view(user_api_key_dict): - # Get from config-loaded servers - for server in global_mcp_server_manager.config_mcp_servers.values(): - cfg_groups = getattr(server, "access_groups", None) - if cfg_groups and isinstance(cfg_groups, (list, set, tuple)): - for g in cfg_groups: + def _collect_groups(server_obj: Any) -> None: + for attr in ("mcp_access_groups", "access_groups"): + raw_groups = getattr(server_obj, attr, None) + if raw_groups and isinstance(raw_groups, (list, set, tuple)): + for g in raw_groups: if g: access_groups.add(str(g)) - # Get from DB + if user_api_key_has_admin_view(user_api_key_dict): + for cfg_server in global_mcp_server_manager.config_mcp_servers.values(): + _collect_groups(cfg_server) + if prisma_client is not None: try: - mcp_servers: Final[Sequence[prisma_models.LiteLLM_MCPServerTable]] = await MCPServerRepository( + db_servers: Final[Sequence[prisma_models.LiteLLM_MCPServerTable]] = await MCPServerRepository( prisma_client ).table.find_many() - for server in mcp_servers: - mcp_groups = getattr(server, "mcp_access_groups", None) - if mcp_groups and isinstance(mcp_groups, (list, set, tuple)): - for g in mcp_groups: - if g: - access_groups.add(str(g)) + for db_server in db_servers: + _collect_groups(db_server) except Exception as e: verbose_proxy_logger.debug("Error getting MCP access groups: %s", e) else: accessible_servers: Final = await _resolve_accessible_mcp_servers(user_api_key_dict) - for server in accessible_servers: - mcp_groups = getattr(server, "mcp_access_groups", None) - if mcp_groups and isinstance(mcp_groups, (list, set, tuple)): - for g in mcp_groups: - if g: - access_groups.add(str(g)) - cfg_groups = getattr(server, "access_groups", None) - if cfg_groups and isinstance(cfg_groups, (list, set, tuple)): - for g in cfg_groups: - if g: - access_groups.add(str(g)) + for acc_server in accessible_servers: + _collect_groups(acc_server) # Convert to sorted list access_groups_list: Final[list[str]] = sorted(list(access_groups))