From 742038bb5e01d6f660cc5db701ee39141abd0a21 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 16:52:03 -0700 Subject: [PATCH 01/17] build: deploy/ push new prisma migration --- .../20251003165142_add_allowed_tools_to_mcp/migration.sql | 3 +++ litellm-proxy-extras/litellm_proxy_extras/schema.prisma | 1 + 2 files changed, 4 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20251003165142_add_allowed_tools_to_mcp/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20251003165142_add_allowed_tools_to_mcp/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251003165142_add_allowed_tools_to_mcp/migration.sql new file mode 100644 index 0000000000..bdac1e42bc --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20251003165142_add_allowed_tools_to_mcp/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "LiteLLM_MCPServerTable" ADD COLUMN "allowed_tools" TEXT[] DEFAULT ARRAY[]::TEXT[]; + diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 766625145f..5a79e17143 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -178,6 +178,7 @@ model LiteLLM_MCPServerTable { updated_by String? mcp_info Json? @default("{}") mcp_access_groups String[] + allowed_tools String[] @default([]) // Health check status status String? @default("unknown") last_health_check DateTime? From 69edc546c044eb190ffa4bcf4e77ffb979182a84 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:15:15 -0700 Subject: [PATCH 02/17] fix(mcp_server_manager.py): don't return an invalid server id on list servers Prevents user from hitting 'server not found' error, even when they see it being listed on litellm ui --- litellm/proxy/_experimental/mcp_server/db.py | 4 +- .../mcp_server/mcp_server_manager.py | 20 ++++-- .../mcp_server/test_mcp_server_manager.py | 66 +++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 79e3b0f762..2269548574 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -1,7 +1,7 @@ -from litellm._uuid import uuid from typing import Any, Dict, Iterable, List, Optional, Set, Union from litellm._logging import verbose_proxy_logger +from litellm._uuid import uuid from litellm.proxy._types import ( LiteLLM_MCPServerTable, LiteLLM_ObjectPermissionTable, @@ -30,7 +30,7 @@ def _prepare_mcp_server_data( from litellm.litellm_core_utils.safe_json_dumps import safe_dumps # Convert model to dict - data_dict = data.model_dump() + data_dict = data.model_dump(exclude_none=True) # Ensure alias is always present in the dict (even if None) if "alias" not in data_dict: data_dict["alias"] = getattr(data, "alias", None) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 9172568f30..e16b5192e5 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -285,6 +285,13 @@ class MCPServerManager: self.registry[mcp_server.server_id] = new_server verbose_logger.debug(f"Added MCP Server: {name_for_prefix}") + def get_all_mcp_server_ids(self) -> List[str]: + """ + Get all MCP server IDs + """ + all_servers = self.get_registry().values() + return [server.server_id for server in all_servers] + async def get_allowed_mcp_servers( self, user_api_key_auth: Optional[UserAPIKeyAuth] = None ) -> List[str]: @@ -1176,6 +1183,13 @@ class MCPServerManager: } ) + ## filter out invalid servers + valid_server_ids = self.get_all_mcp_server_ids() + filtered_list_mcp_servers = [] + for server in list_mcp_servers: + if server.server_id not in valid_server_ids: + filtered_list_mcp_servers.append(server) + # Map servers to their teams and return with health data from typing import cast @@ -1198,9 +1212,7 @@ class MCPServerManager: else [] ), allowed_tools=( - server.allowed_tools - if server.allowed_tools is not None - else [] + server.allowed_tools if server.allowed_tools is not None else [] ), mcp_info=server.mcp_info, teams=cast( @@ -1212,7 +1224,7 @@ class MCPServerManager: args=getattr(server, "args", None) or [], env=getattr(server, "env", None) or {}, ) - for server in list_mcp_servers + for server in filtered_list_mcp_servers ] async def reload_servers_from_database(self): diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 146f9434b8..a5eaa0f6f5 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -909,6 +909,72 @@ class TestMCPServerManager: assert "tool_1" in tool_names assert "tool_2" in tool_names + @pytest.mark.asyncio + async def test_get_all_mcp_servers_filters_invalid_server_ids(self): + """Test that get_all_mcp_servers_with_health_and_teams filters out servers with invalid server_ids""" + manager = MCPServerManager() + + # Mock get_allowed_mcp_servers to return some server IDs + manager.get_allowed_mcp_servers = AsyncMock(return_value=["valid-server-1", "invalid-server-1"]) + + # Mock get_all_mcp_server_ids to return only valid server IDs + manager.get_all_mcp_server_ids = MagicMock(return_value=["valid-server-1", "another-valid-server"]) + + # Mock config_mcp_servers to be empty for this test + manager.config_mcp_servers = {} + + # Create mock servers - one valid, one invalid + valid_server = LiteLLM_MCPServerTable( + server_id="valid-server-1", + server_name="Valid Server", + alias="valid", + description="A valid server", + url="http://valid.com", + transport=MCPTransport.http, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + invalid_server = LiteLLM_MCPServerTable( + server_id="invalid-server-1", + server_name="Invalid Server", + alias="invalid", + description="An invalid server", + url="http://invalid.com", + transport=MCPTransport.http, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + # Mock the database call to return both servers + with patch('litellm.proxy._experimental.mcp_server.db.get_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = [valid_server, invalid_server] + + # Mock prisma_client to not be None + with patch('litellm.proxy.proxy_server.prisma_client', new=MagicMock()): + # Mock _user_has_admin_view to return False (non-admin user) + with patch('litellm.proxy.management_endpoints.common_utils._user_has_admin_view') as mock_admin_view: + mock_admin_view.return_value = False + + # Create mock user auth + user_auth = MagicMock() + user_auth.user_id = "test-user" + + # Call the method + result = await manager.get_all_mcp_servers_with_health_and_teams( + user_api_key_auth=user_auth, + include_health=False + ) + + # Verify that only the valid server is returned + assert len(result) == 1 + assert result[0].server_id == "valid-server-1" + assert result[0].server_name == "Valid Server" + + # Verify invalid server is not in results + server_ids = [server.server_id for server in result] + assert "invalid-server-1" not in server_ids + if __name__ == "__main__": pytest.main([__file__]) From c043ad6c2bdf3dddb3336a04c2c1aee5f2e0f56a Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:17:20 -0700 Subject: [PATCH 03/17] fix: replace list in for set in faster --- .../mcp_server/mcp_server_manager.py | 10 ++-- .../mcp_server/test_mcp_server_manager.py | 59 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index e16b5192e5..820824f333 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -10,7 +10,7 @@ import asyncio import datetime import hashlib import json -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Set, Union, cast from fastapi import HTTPException from mcp.types import CallToolRequestParams as MCPCallToolRequestParams @@ -285,12 +285,12 @@ class MCPServerManager: self.registry[mcp_server.server_id] = new_server verbose_logger.debug(f"Added MCP Server: {name_for_prefix}") - def get_all_mcp_server_ids(self) -> List[str]: + def get_all_mcp_server_ids(self) -> Set[str]: """ Get all MCP server IDs """ - all_servers = self.get_registry().values() - return [server.server_id for server in all_servers] + all_servers = list(self.get_registry().values()) + return {server.server_id for server in all_servers} async def get_allowed_mcp_servers( self, user_api_key_auth: Optional[UserAPIKeyAuth] = None @@ -1187,7 +1187,7 @@ class MCPServerManager: valid_server_ids = self.get_all_mcp_server_ids() filtered_list_mcp_servers = [] for server in list_mcp_servers: - if server.server_id not in valid_server_ids: + if server.server_id in valid_server_ids: filtered_list_mcp_servers.append(server) # Map servers to their teams and return with health data diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index a5eaa0f6f5..3dd2ca272a 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -654,6 +654,7 @@ class TestMCPServerManager: "Tool tool3 is not allowed for server test-server" in exc_info.value.detail["error"] ) + async def test_get_tools_from_server_add_prefix(self): """Verify _get_tools_from_server respects add_prefix True/False.""" manager = MCPServerManager() @@ -913,16 +914,20 @@ class TestMCPServerManager: async def test_get_all_mcp_servers_filters_invalid_server_ids(self): """Test that get_all_mcp_servers_with_health_and_teams filters out servers with invalid server_ids""" manager = MCPServerManager() - + # Mock get_allowed_mcp_servers to return some server IDs - manager.get_allowed_mcp_servers = AsyncMock(return_value=["valid-server-1", "invalid-server-1"]) - + manager.get_allowed_mcp_servers = AsyncMock( + return_value=["valid-server-1", "invalid-server-1"] + ) + # Mock get_all_mcp_server_ids to return only valid server IDs - manager.get_all_mcp_server_ids = MagicMock(return_value=["valid-server-1", "another-valid-server"]) - + manager.get_all_mcp_server_ids = MagicMock( + return_value=["valid-server-1", "another-valid-server"] + ) + # Mock config_mcp_servers to be empty for this test manager.config_mcp_servers = {} - + # Create mock servers - one valid, one invalid valid_server = LiteLLM_MCPServerTable( server_id="valid-server-1", @@ -934,43 +939,53 @@ class TestMCPServerManager: created_at=datetime.now(), updated_at=datetime.now(), ) - + invalid_server = LiteLLM_MCPServerTable( - server_id="invalid-server-1", + server_id="invalid-server-1", server_name="Invalid Server", alias="invalid", description="An invalid server", - url="http://invalid.com", + url="http://invalid.com", transport=MCPTransport.http, created_at=datetime.now(), updated_at=datetime.now(), ) - + # Mock the database call to return both servers - with patch('litellm.proxy._experimental.mcp_server.db.get_mcp_servers') as mock_get_servers: - mock_get_servers.return_value = [valid_server, invalid_server] - - # Mock prisma_client to not be None - with patch('litellm.proxy.proxy_server.prisma_client', new=MagicMock()): + with patch( + "litellm.proxy._experimental.mcp_server.db.get_mcp_servers", + new=AsyncMock(return_value=[valid_server, invalid_server]), + ) as mock_get_servers: + + # Mock prisma_client with proper async methods + mock_prisma = MagicMock() + mock_db = MagicMock() + mock_teamtable = MagicMock() + mock_teamtable.find_many = AsyncMock(return_value=[]) + mock_db.litellm_teamtable = mock_teamtable + mock_prisma.db = mock_db + + with patch("litellm.proxy.proxy_server.prisma_client", new=mock_prisma): # Mock _user_has_admin_view to return False (non-admin user) - with patch('litellm.proxy.management_endpoints.common_utils._user_has_admin_view') as mock_admin_view: + with patch( + "litellm.proxy.management_endpoints.common_utils._user_has_admin_view" + ) as mock_admin_view: mock_admin_view.return_value = False - + # Create mock user auth user_auth = MagicMock() user_auth.user_id = "test-user" - + # Call the method result = await manager.get_all_mcp_servers_with_health_and_teams( - user_api_key_auth=user_auth, - include_health=False + user_api_key_auth=user_auth, include_health=False ) - + # Verify that only the valid server is returned assert len(result) == 1 assert result[0].server_id == "valid-server-1" assert result[0].server_name == "Valid Server" - + # Verify invalid server is not in results server_ids = [server.server_id for server in result] assert "invalid-server-1" not in server_ids From 6ccb20d69d7074c52093cae07d8cb94bbbf6d79e Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:25:08 -0700 Subject: [PATCH 04/17] fix(mcp_server_manager.py): mark mcp server as unhealthy and show reason for not being able to serve it on list servers on ui make it easy for user to debug why server can't be added --- .../mcp_server/mcp_server_manager.py | 141 ++++++++---------- litellm/proxy/_types.py | 2 +- 2 files changed, 60 insertions(+), 83 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 820824f333..6ede5d3cf1 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -240,50 +240,57 @@ class MCPServerManager: ) def add_update_server(self, mcp_server: LiteLLM_MCPServerTable): - if mcp_server.server_id not in self.get_registry(): - _mcp_info: MCPInfo = mcp_server.mcp_info or {} - # Use helper to deserialize environment dictionary - # Safely access env field which may not exist on Prisma model objects - env_data = getattr(mcp_server, "env", None) - env_dict = _deserialize_env_dict(env_data) - # Use alias for name if present, else server_name - name_for_prefix = ( - mcp_server.alias or mcp_server.server_name or mcp_server.server_id - ) - # Preserve all custom fields from database while setting defaults for core fields - mcp_info: MCPInfo = _mcp_info.copy() - # Set default values for core fields if not present - if "server_name" not in mcp_info: - mcp_info["server_name"] = mcp_server.server_name or mcp_server.server_id - if "description" not in mcp_info and mcp_server.description: - mcp_info["description"] = mcp_server.description + try: + if mcp_server.server_id not in self.get_registry(): + _mcp_info: MCPInfo = mcp_server.mcp_info or {} + # Use helper to deserialize environment dictionary + # Safely access env field which may not exist on Prisma model objects + env_data = getattr(mcp_server, "env", None) + env_dict = _deserialize_env_dict(env_data) + # Use alias for name if present, else server_name + name_for_prefix = ( + mcp_server.alias or mcp_server.server_name or mcp_server.server_id + ) + # Preserve all custom fields from database while setting defaults for core fields + mcp_info: MCPInfo = _mcp_info.copy() + # Set default values for core fields if not present + if "server_name" not in mcp_info: + mcp_info["server_name"] = ( + mcp_server.server_name or mcp_server.server_id + ) + if "description" not in mcp_info and mcp_server.description: + mcp_info["description"] = mcp_server.description - new_server = MCPServer( - server_id=mcp_server.server_id, - name=name_for_prefix, - alias=getattr(mcp_server, "alias", None), - server_name=getattr(mcp_server, "server_name", None), - url=mcp_server.url, - transport=cast(MCPTransportType, mcp_server.transport), - auth_type=cast(MCPAuthType, mcp_server.auth_type), - mcp_info=mcp_info, - extra_headers=getattr(mcp_server, "extra_headers", None), - # oauth specific fields - client_id=getattr(mcp_server, "client_id", None), - client_secret=getattr(mcp_server, "client_secret", None), - scopes=getattr(mcp_server, "scopes", None), - authorization_url=getattr(mcp_server, "authorization_url", None), - token_url=getattr(mcp_server, "token_url", None), - # Stdio-specific fields - command=getattr(mcp_server, "command", None), - args=getattr(mcp_server, "args", None) or [], - env=env_dict, - access_groups=getattr(mcp_server, "mcp_access_groups", None), - allowed_tools=getattr(mcp_server, "allowed_tools", None), - disallowed_tools=getattr(mcp_server, "disallowed_tools", None), - ) - self.registry[mcp_server.server_id] = new_server - verbose_logger.debug(f"Added MCP Server: {name_for_prefix}") + new_server = MCPServer( + server_id=mcp_server.server_id, + name=name_for_prefix, + alias=getattr(mcp_server, "alias", None), + server_name=getattr(mcp_server, "server_name", None), + url=mcp_server.url, + transport=cast(MCPTransportType, mcp_server.transport), + auth_type=cast(MCPAuthType, mcp_server.auth_type), + mcp_info=mcp_info, + extra_headers=getattr(mcp_server, "extra_headers", None), + # oauth specific fields + client_id=getattr(mcp_server, "client_id", None), + client_secret=getattr(mcp_server, "client_secret", None), + scopes=getattr(mcp_server, "scopes", None), + authorization_url=getattr(mcp_server, "authorization_url", None), + token_url=getattr(mcp_server, "token_url", None), + # Stdio-specific fields + command=getattr(mcp_server, "command", None), + args=getattr(mcp_server, "args", None) or [], + env=env_dict, + access_groups=getattr(mcp_server, "mcp_access_groups", None), + allowed_tools=getattr(mcp_server, "allowed_tools", None), + disallowed_tools=getattr(mcp_server, "disallowed_tools", None), + ) + self.registry[mcp_server.server_id] = new_server + verbose_logger.debug(f"Added MCP Server: {name_for_prefix}") + + except Exception as e: + verbose_logger.debug(f"Failed to add MCP server: {str(e)}") + raise e def get_all_mcp_server_ids(self) -> Set[str]: """ @@ -1183,49 +1190,19 @@ class MCPServerManager: } ) - ## filter out invalid servers + ## mark invalid servers w/ reason for being invalid valid_server_ids = self.get_all_mcp_server_ids() - filtered_list_mcp_servers = [] for server in list_mcp_servers: if server.server_id in valid_server_ids: - filtered_list_mcp_servers.append(server) + server.status = "unhealthy" + ## try adding server to registry to get error + try: + self.add_update_server(server) + except Exception as e: + server.health_check_error = str(e) + server.health_check_error = "Server is not in in memory registry yet. This could be a temporary sync issue." - # Map servers to their teams and return with health data - from typing import cast - - return [ - LiteLLM_MCPServerTable( - server_id=server.server_id, - server_name=server.server_name, - alias=server.alias, - description=server.description, - url=server.url, - transport=server.transport, - auth_type=server.auth_type, - created_at=server.created_at, - created_by=server.created_by, - updated_at=server.updated_at, - updated_by=server.updated_by, - mcp_access_groups=( - server.mcp_access_groups - if server.mcp_access_groups is not None - else [] - ), - allowed_tools=( - server.allowed_tools if server.allowed_tools is not None else [] - ), - mcp_info=server.mcp_info, - teams=cast( - List[Dict[str, str | None]], - server_to_teams_map.get(server.server_id, []), - ), - # Stdio-specific fields - command=getattr(server, "command", None), - args=getattr(server, "args", None) or [], - env=getattr(server, "env", None) or {}, - ) - for server in filtered_list_mcp_servers - ] + return list_mcp_servers async def reload_servers_from_database(self): """ diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index efe7ff9097..12f2b7b297 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -989,7 +989,7 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase): allowed_tools: List[str] = Field(default_factory=list) mcp_info: Optional[MCPInfo] = None # Health check status - status: Optional[str] = Field( + status: Optional[Literal["healthy", "unhealthy", "unknown"]] = Field( default="unknown", description="Health status: 'healthy', 'unhealthy', 'unknown'", ) From 3816d3b36f67a1d3a8551f15bacfce6bbfddf8d8 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:27:49 -0700 Subject: [PATCH 05/17] test: add unit testing --- .../mcp_server/test_mcp_server_manager.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 3dd2ca272a..796ec47cca 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -990,6 +990,39 @@ class TestMCPServerManager: server_ids = [server.server_id for server in result] assert "invalid-server-1" not in server_ids + def test_add_db_mcp_server_to_registry(self): + """Test that add_db_mcp_server_to_registry adds a MCP server to the registry""" + manager = MCPServerManager() + server = LiteLLM_MCPServerTable( + **{ + "server_id": "4c679a81-acd9-4954-9f84-30b739362498", + "server_name": "edc_mcp_server", + "alias": "edc_mcp_server", + "description": None, + "url": "fake_mcp_url", + "transport": "http", + "auth_type": "none", + "created_at": "2025-09-30T08:28:31.353000Z", + "created_by": "a1248959", + "updated_at": "2025-09-30T08:28:31.353000Z", + "updated_by": "a1248959", + "teams": [], + "mcp_access_groups": [], + "mcp_info": { + "server_name": "edc_mcp_server", + "mcp_server_cost_info": None, + }, + "status": "unknown", + "last_health_check": None, + "health_check_error": None, + "command": None, + "args": [], + "env": {}, + }, + ) + manager.add_update_server(server) + assert server.server_id in manager.get_registry() + if __name__ == "__main__": pytest.main([__file__]) From 45f7f864800e8def21385a2e609856f86aa1de45 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:42:06 -0700 Subject: [PATCH 06/17] feat(mcp_servers.tsx): show health of mcp servers on UI allow user to know if mcp server is in invalid state --- .../mcp_tools/mcp_server_columns.tsx | 61 +++++++++++++++++++ .../src/components/mcp_tools/types.tsx | 3 + 2 files changed, 64 insertions(+) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx index b4369f0a45..20259eef3d 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_columns.tsx @@ -61,6 +61,67 @@ export const mcpServerColumns = ( ), }, + { + id: "health_status", + header: "Health Status", + cell: ({ row }) => { + const server = row.original; + const status = server.status || "unknown"; + const lastCheck = server.last_health_check; + const error = server.health_check_error; + + const getStatusColor = (status: string) => { + switch (status) { + case "healthy": + return "text-green-500 bg-green-50 hover:bg-green-100"; + case "unhealthy": + return "text-red-500 bg-red-50 hover:bg-red-100"; + default: + return "text-gray-500 bg-gray-50 hover:bg-gray-100"; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case "healthy": + return "●"; + case "unhealthy": + return "●"; + default: + return "●"; + } + }; + + const tooltipContent = ( +
+
Health Status: {status}
+ {lastCheck && ( +
+ Last Check: {new Date(lastCheck).toLocaleString()} +
+ )} + {error && ( +
+
Error:
+
{error}
+
+ )} + {!lastCheck && !error && ( +
No health check data available
+ )} +
+ ); + + return ( + + + + ); + }, + }, { id: "mcp_access_groups", header: "Access Groups", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx index 9bf5725ac0..5973a43865 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/types.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/types.tsx @@ -138,6 +138,9 @@ export interface MCPServer { created_by: string updated_at: string updated_by: string + status?: "healthy" | "unhealthy" | "unknown" + last_health_check?: string | null + health_check_error?: string | null teams?: Team[] mcp_access_groups?: string[] allowed_tools?: string[] From 72b5cf2b71d1c7978edc13acd96d393d0d39d6f9 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 17:57:25 -0700 Subject: [PATCH 07/17] fix(schema.prisma): add new db columns --- litellm/proxy/_experimental/mcp_server/mcp_server_manager.py | 2 +- litellm/proxy/schema.prisma | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 6ede5d3cf1..a3f6a83ff3 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -1193,7 +1193,7 @@ class MCPServerManager: ## mark invalid servers w/ reason for being invalid valid_server_ids = self.get_all_mcp_server_ids() for server in list_mcp_servers: - if server.server_id in valid_server_ids: + if server.server_id not in valid_server_ids: server.status = "unhealthy" ## try adding server to registry to get error try: diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 5a79e17143..b37bd13f7b 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -179,6 +179,8 @@ model LiteLLM_MCPServerTable { mcp_info Json? @default("{}") mcp_access_groups String[] allowed_tools String[] @default([]) + disallowed_tools String[] @default([]) + extra_headers String[] @default([]) // Health check status status String? @default("unknown") last_health_check DateTime? From 950210790b529216bb75dfcbfa09f2f89769c8fe Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 3 Oct 2025 18:12:31 -0700 Subject: [PATCH 08/17] feat(create_mcp_server.tsx): initial ui component for adding extra headers, allowed tools, disallowed tools to litellm ui Closes LIT-1183 --- .../mcp_tools/MCPPermissionManagement.tsx | 139 ++++++++++++++++++ .../mcp_tools/create_mcp_server.tsx | 35 ++--- 2 files changed, 149 insertions(+), 25 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx new file mode 100644 index 0000000000..b66f095006 --- /dev/null +++ b/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react" +import { Form, Select, Tooltip, Collapse } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" + +const { Panel } = Collapse + +interface MCPPermissionManagementProps { + availableAccessGroups: string[] + searchValue: string + setSearchValue: (value: string) => void + getAccessGroupOptions: () => Array<{ + value: string + label: React.ReactNode + }> +} + +const MCPPermissionManagement: React.FC = ({ + availableAccessGroups, + searchValue, + setSearchValue, + getAccessGroupOptions, +}) => { + return ( + + +
+
+

Permission Management / Access Control

+
+

+ Configure access permissions and security settings (Optional) +

+ + } + key="permissions" + className="border-0" + > +
+ + MCP Access Groups + + + + + } + name="mcp_access_groups" + className="mb-4" + > + + + + + Allowed Tools + + + + + } + name="allowed_tools" + > + + +
+
+
+ ) +} + +export default MCPPermissionManagement diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index d2ba0fbfca..c088108860 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -8,6 +8,7 @@ import MCPServerCostConfig from "./mcp_server_cost_config" import MCPConnectionStatus from "./mcp_connection_status" import MCPToolConfiguration from "./mcp_tool_configuration" import StdioConfiguration from "./StdioConfiguration" +import MCPPermissionManagement from "./MCPPermissionManagement" import { isAdminRole } from "@/utils/roles" import { validateMCPServerUrl, validateMCPServerName } from "./utils" import NotificationsManager from "../molecules/notifications_manager" @@ -381,32 +382,16 @@ const CreateMCPServer: React.FC = ({ {/* Stdio Configuration - only show for stdio transport */} + - - MCP Access Groups - - - - - } - name="mcp_access_groups" - className="mb-4" - > - - - - - Disallowed Tools - - - - - } - name="disallowed_tools" - > - 0 + ? `Currently: ${mcpServer.extra_headers.join(', ')}` + : "Enter header names (e.g., Authorization, X-Custom-Header)" + } className="rounded-lg" size="large" tokenSeparators={[","]} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index c088108860..3a21a10be6 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -388,6 +388,7 @@ const CreateMCPServer: React.FC = ({
= ({ mcpServer, accessToken, o // Ensure access groups is always a string array const accessGroups = (values.mcp_access_groups || []).map((g: any) => typeof g === 'string' ? g : g.name || String(g)); - // Prepare the payload with cost configuration + // Prepare the payload with cost configuration and permission fields const payload = { ...values, server_id: mcpServer.server_id, @@ -125,6 +126,10 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o }, mcp_access_groups: accessGroups, alias: values.alias, + // Include permission management fields + extra_headers: values.extra_headers || [], + allowed_tools: values.allowed_tools || [], + disallowed_tools: values.disallowed_tools || [], }; const updated = await updateMCPServer(accessToken, payload); @@ -179,34 +184,15 @@ const MCPServerEdit: React.FC = ({ mcpServer, accessToken, o - - MCP Access Groups - - - - - } - name="mcp_access_groups" - getValueFromEvent={value => value} - > -