From 2441b0570048d965026edcc01def2ee09bf4ae90 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 20 Jan 2026 21:38:58 -0800 Subject: [PATCH 1/4] Fix MCP Server resetting back to Overview --- .../components/mcp_tools/mcp_server_view.tsx | 31 ++- .../src/components/mcp_tools/mcp_servers.tsx | 200 +++++++++--------- .../src/components/mcp_tools/mcp_tools.tsx | 5 +- 3 files changed, 119 insertions(+), 117 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx index 6a4e9c105ff..809095c048c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx @@ -36,6 +36,8 @@ export const MCPServerView: React.FC = ({ const [editing, setEditing] = useState(isEditing); const [showFullUrl, setShowFullUrl] = useState(false); const [copiedStates, setCopiedStates] = useState>({}); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const handleSuccess = (updated: MCPServer) => { setEditing(false); onBack(); @@ -72,11 +74,10 @@ export const MCPServerView: React.FC = ({ size="small" icon={copiedStates["mcp-server_name"] ? : } onClick={() => copyToClipboard(mcpServer.server_name, "mcp-server_name")} - className={`left-2 z-10 transition-all duration-200 ${ - copiedStates["mcp-server_name"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} + className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-server_name"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} /> {mcpServer.alias && ( <> @@ -87,11 +88,10 @@ export const MCPServerView: React.FC = ({ size="small" icon={copiedStates["mcp-alias"] ? : } onClick={() => copyToClipboard(mcpServer.alias, "mcp-alias")} - className={`left-2 z-10 transition-all duration-200 ${ - copiedStates["mcp-alias"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} + className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-alias"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} /> )} @@ -103,18 +103,17 @@ export const MCPServerView: React.FC = ({ size="small" icon={copiedStates["mcp-server-id"] ? : } onClick={() => copyToClipboard(mcpServer.server_id, "mcp-server-id")} - className={`left-2 z-10 transition-all duration-200 ${ - copiedStates["mcp-server-id"] - ? "text-green-600 bg-green-50 border-green-200" - : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" - }`} + className={`left-2 z-10 transition-all duration-200 ${copiedStates["mcp-server-id"] + ? "text-green-600 bg-green-50 border-green-200" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-100" + }`} /> {/* TODO: magic number for index */} - + {[ Overview, diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index f6669fb2829..a0cd1720c88 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -212,103 +212,28 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) return
Missing required authentication parameters.
; } - const ServersTab = () => - selectedServerId ? ( - server.server_id === selectedServerId) || { - server_id: "", - server_name: "", - alias: "", - url: "", - transport: "", - auth_type: "", - created_at: "", - created_by: "", - updated_at: "", - updated_by: "", - } - } - onBack={() => { - setEditServer(false); - setSelectedServerId(null); - refetch(); - }} - isProxyAdmin={isAdminRole(userRole)} - isEditing={editServer} - accessToken={accessToken} - userID={userID} - userRole={userRole} - availableAccessGroups={uniqueMcpAccessGroups} - /> - ) : ( -
-
-
-
-
- Current Team: - - - Access Group: - - - - - -
-
-
-
-
-
} - getRowCanExpand={() => false} - isLoading={isLoadingServers} - noDataMessage="No MCP servers configured" - loadingMessage="🚅 Loading MCP servers..." - /> -
-
- ); + // Memoize the selected server to prevent unnecessary re-renders + const selectedServer = React.useMemo(() => { + return filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || { + server_id: "", + server_name: "", + alias: "", + url: "", + transport: "", + auth_type: "", + created_at: "", + created_by: "", + updated_at: "", + updated_by: "", + }; + }, [filteredServers, selectedServerId]); + + // Memoize the onBack callback to prevent unnecessary re-renders + const handleBack = React.useCallback(() => { + setEditServer(false); + setSelectedServerId(null); + refetch(); + }, [refetch]); return (
@@ -381,7 +306,86 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) - + {selectedServerId ? ( + + ) : ( +
+
+
+
+
+ Current Team: + + + Access Group: + + + + + +
+
+
+
+
+
} + getRowCanExpand={() => false} + isLoading={isLoadingServers} + noDataMessage="No MCP servers configured" + loadingMessage="🚅 Loading MCP servers..." + /> +
+
+ )}
diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx index 37c61023f23..7ee1e64a228 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx @@ -127,11 +127,10 @@ const MCPToolsViewer = ({ {toolsData.map((tool: MCPTool) => (
{ setSelectedTool(tool); setToolResult(null); From 90bd80a4c2ad1afca94b43eed546c2b09335e947 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 20 Jan 2026 21:46:21 -0800 Subject: [PATCH 2/4] fixing build --- .../src/components/mcp_tools/mcp_servers.tsx | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx index a0cd1720c88..77034309c91 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -2,7 +2,7 @@ import { isAdminRole } from "@/utils/roles"; import { QuestionCircleOutlined } from "@ant-design/icons"; import { Button, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; import { Descriptions, Modal, Select, Tooltip, Typography } from "antd"; -import React, { useEffect, useState, useMemo } from "react"; +import React, { useEffect, useState, useMemo, useCallback } from "react"; import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useMCPServerHealth } from "../../app/(dashboard)/hooks/mcpServers/useMCPServerHealth"; import NotificationsManager from "../molecules/notifications_manager"; @@ -115,20 +115,8 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) ); }, [serversWithHealth]); - // Handle team filter change - const handleTeamChange = (teamId: string) => { - setSelectedTeam(teamId); - filterServers(teamId, selectedMcpAccessGroup); - }; - - // Handle MCP access group filter change - const handleMcpAccessGroupChange = (group: string) => { - setSelectedMcpAccessGroup(group); - filterServers(selectedTeam, group); - }; - // Filtering logic for both team and access group - const filterServers = (teamId: string, group: string) => { + const filterServers = useCallback((teamId: string, group: string) => { if (!serversWithHealth) return setFilteredServers([]); let filtered = serversWithHealth; if (teamId === "personal") { @@ -144,12 +132,24 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) ); } setFilteredServers(filtered); + }, [serversWithHealth]); + + // Handle team filter change + const handleTeamChange = (teamId: string) => { + setSelectedTeam(teamId); + filterServers(teamId, selectedMcpAccessGroup); + }; + + // Handle MCP access group filter change + const handleMcpAccessGroupChange = (group: string) => { + setSelectedMcpAccessGroup(group); + filterServers(selectedTeam, group); }; // Initial and effect-based filtering (trigger on query data updates and health data updates) useEffect(() => { filterServers(selectedTeam, selectedMcpAccessGroup); - }, [serversWithHealth, selectedTeam, selectedMcpAccessGroup]); + }, [serversWithHealth, selectedTeam, selectedMcpAccessGroup, filterServers]); const columns = React.useMemo( () => @@ -207,11 +207,6 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) setModalVisible(false); }; - if (!accessToken || !userRole || !userID) { - console.log("Missing required authentication parameters", { accessToken, userRole, userID }); - return
Missing required authentication parameters.
; - } - // Memoize the selected server to prevent unnecessary re-renders const selectedServer = React.useMemo(() => { return filteredServers.find((server: MCPServer) => server.server_id === selectedServerId) || { @@ -235,6 +230,11 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) refetch(); }, [refetch]); + if (!accessToken || !userRole || !userID) { + console.log("Missing required authentication parameters", { accessToken, userRole, userID }); + return
Missing required authentication parameters.
; + } + return (
Date: Tue, 20 Jan 2026 21:50:28 -0800 Subject: [PATCH 3/4] Adding tests --- .../components/mcp_tools/mcp_servers.test.tsx | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx index 4b8698b9762..b77e962f5f3 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { render, waitFor } from "@testing-library/react"; +import { render, waitFor, screen, fireEvent, act } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import MCPServers from "./mcp_servers"; @@ -228,4 +228,120 @@ describe("MCPServers", () => { expect(networking.fetchMCPServerHealth).toHaveBeenCalled(); }); }); + + it("should filter servers by team when a team is selected", async () => { + // Mock MCP servers with different teams + const mockServers = [ + { + server_id: "server-1", + server_name: "Team A Server", + alias: "team-a-server", + url: "https://example.com/mcp", + transport: "http", + auth_type: "none", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + teams: [{ team_id: "team-a", team_alias: "Team A" }], + mcp_access_groups: [], + }, + { + server_id: "server-2", + server_name: "Team B Server", + alias: "team-b-server", + url: "https://example2.com/mcp", + transport: "sse", + auth_type: "api_key", + created_at: "2024-01-02T00:00:00Z", + created_by: "user-2", + updated_at: "2024-01-02T00:00:00Z", + updated_by: "user-2", + teams: [{ team_id: "team-b", team_alias: "Team B" }], + mcp_access_groups: [], + }, + { + server_id: "server-3", + server_name: "Team A Server 2", + alias: "team-a-server-2", + url: "https://example3.com/mcp", + transport: "http", + auth_type: "none", + created_at: "2024-01-03T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-03T00:00:00Z", + updated_by: "user-1", + teams: [{ team_id: "team-a", team_alias: "Team A" }], + mcp_access_groups: [], + }, + ]; + + vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers); + vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]); + + const queryClient = createQueryClient(); + render( + + + , + ); + + // Wait for the component to load + await waitFor(() => { + expect(screen.getByText("MCP Servers")).toBeInTheDocument(); + }); + + // Wait for servers to be rendered + await waitFor(() => { + expect(screen.getByText("Team A Server")).toBeInTheDocument(); + }); + + // Verify all servers are initially displayed + expect(screen.getByText("Team A Server")).toBeInTheDocument(); + expect(screen.getByText("Team B Server")).toBeInTheDocument(); + expect(screen.getByText("Team A Server 2")).toBeInTheDocument(); + + // Find the team select dropdown by looking for the "Current Team:" label + const teamLabel = screen.getByText("Current Team:"); + const teamSelectContainer = teamLabel.closest("div")?.querySelector(".ant-select"); + expect(teamSelectContainer).toBeTruthy(); + + // Open the dropdown by clicking on the selector + const selectSelector = teamSelectContainer?.querySelector(".ant-select-selector"); + expect(selectSelector).toBeTruthy(); + + act(() => { + fireEvent.mouseDown(selectSelector!); + }); + + // Wait for dropdown to open + await waitFor( + () => { + const dropdownOptions = document.querySelectorAll(".ant-select-item-option"); + expect(dropdownOptions.length).toBeGreaterThan(0); + }, + { timeout: 5000 }, + ); + + // Find and click on "Team A" option + const dropdownOptions = document.querySelectorAll(".ant-select-item-option"); + const teamAOption = Array.from(dropdownOptions).find((option) => + option.textContent?.includes("Team A"), + ); + expect(teamAOption).toBeTruthy(); + + act(() => { + fireEvent.click(teamAOption!); + }); + + // Wait for filtering to complete + await waitFor(() => { + // Team A servers should still be visible + expect(screen.getByText("Team A Server")).toBeInTheDocument(); + expect(screen.getByText("Team A Server 2")).toBeInTheDocument(); + }); + + // Team B server should not be visible + expect(screen.queryByText("Team B Server")).not.toBeInTheDocument(); + }); }); From 032b1a8cd2d9c2742443a1e067a780a302024d2d Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 20 Jan 2026 21:51:13 -0800 Subject: [PATCH 4/4] Fixing tests --- .../src/components/mcp_tools/mcp_servers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx index b77e962f5f3..4d80b383703 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx @@ -208,7 +208,7 @@ describe("MCPServers", () => { vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers); // Mock health check to never resolve (to test loading state) vi.mocked(networking.fetchMCPServerHealth).mockImplementation( - () => new Promise(() => {}), // Never resolves + () => new Promise(() => { }), // Never resolves ); const queryClient = createQueryClient();