From 66d77cfa22660273060663d604a260178a88319b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 10 Mar 2026 23:09:14 +0000 Subject: [PATCH] Add search functionality to MCP Servers page - Added search input field that filters servers by name, alias, URL, server ID, and transport - Search bar placed prominently in the filter section alongside Team and Access Group filters - Updated filtering logic to support text search in addition to existing filters - Added comprehensive unit test for search functionality - Fixed bug in create_mcp_server.tsx where setTools was called instead of clearTools The search feature allows users to quickly find MCP servers by typing in any relevant text, filtering the displayed servers in real-time. Co-authored-by: Ishaan Jaff --- ui/litellm-dashboard/package-lock.json | 15 --- .../mcp_tools/create_mcp_server.tsx | 2 +- .../components/mcp_tools/mcp_servers.test.tsx | 106 ++++++++++++++++++ .../src/components/mcp_tools/mcp_servers.tsx | 50 +++++++-- 4 files changed, 148 insertions(+), 25 deletions(-) diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index d4f60ff0d7f..69efbf19c38 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -13279,21 +13279,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.33", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", - "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } 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 de60ff2b782..1d4a7728ef9 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 @@ -415,7 +415,7 @@ const CreateMCPServer: React.FC = ({ const handleCancel = () => { form.resetFields(); setCostConfig({}); - setTools([]); + clearTools(); setAllowedTools([]); setAliasManuallyEdited(false); setModalVisible(false); 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 8385fc7ac70..584d41b98ae 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 @@ -348,4 +348,110 @@ describe("MCPServers", () => { // Team B server should not be visible expect(screen.queryByText("Team B Server")).not.toBeInTheDocument(); }); + + it("should filter servers by search term", async () => { + const mockServers = [ + { + server_id: "server-1", + server_name: "GitHub Server", + alias: "github", + url: "https://api.github.com/mcp", + transport: "http", + auth_type: "oauth2", + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + teams: [], + mcp_access_groups: [], + }, + { + server_id: "server-2", + server_name: "Notion Server", + alias: "notion", + url: "https://api.notion.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: [], + mcp_access_groups: [], + }, + { + server_id: "server-3", + server_name: "Slack Server", + alias: "slack", + url: "https://slack.com/api/mcp", + transport: "http", + auth_type: "oauth2", + created_at: "2024-01-03T00:00:00Z", + created_by: "user-3", + updated_at: "2024-01-03T00:00:00Z", + updated_by: "user-3", + teams: [], + mcp_access_groups: [], + }, + ]; + + vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers); + vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText("MCP Servers")).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText("GitHub Server")).toBeInTheDocument(); + }); + + expect(screen.getByText("GitHub Server")).toBeInTheDocument(); + expect(screen.getByText("Notion Server")).toBeInTheDocument(); + expect(screen.getByText("Slack Server")).toBeInTheDocument(); + + const searchInput = screen.getByPlaceholderText("Search servers by name, alias, URL..."); + expect(searchInput).toBeInTheDocument(); + + act(() => { + fireEvent.change(searchInput, { target: { value: "github" } }); + }); + + await waitFor(() => { + expect(screen.getByText("GitHub Server")).toBeInTheDocument(); + }); + + expect(screen.getByText("GitHub Server")).toBeInTheDocument(); + expect(screen.queryByText("Notion Server")).not.toBeInTheDocument(); + expect(screen.queryByText("Slack Server")).not.toBeInTheDocument(); + + act(() => { + fireEvent.change(searchInput, { target: { value: "notion.com" } }); + }); + + await waitFor(() => { + expect(screen.getByText("Notion Server")).toBeInTheDocument(); + }); + + expect(screen.queryByText("GitHub Server")).not.toBeInTheDocument(); + expect(screen.getByText("Notion Server")).toBeInTheDocument(); + expect(screen.queryByText("Slack Server")).not.toBeInTheDocument(); + + act(() => { + fireEvent.change(searchInput, { target: { value: "" } }); + }); + + await waitFor(() => { + expect(screen.getByText("GitHub Server")).toBeInTheDocument(); + expect(screen.getByText("Notion Server")).toBeInTheDocument(); + expect(screen.getByText("Slack Server")).toBeInTheDocument(); + }); + }); }); 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 0edf8c2a4c6..f200c278467 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx @@ -1,8 +1,8 @@ import { isAdminRole } from "@/utils/roles"; -import { QuestionCircleOutlined } from "@ant-design/icons"; +import { QuestionCircleOutlined, SearchOutlined } from "@ant-design/icons"; import { Button, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; import NewBadge from "../common_components/NewBadge"; -import { Descriptions, Modal, Select, Tooltip, Typography } from "antd"; +import { Descriptions, Input, Modal, Select, Tooltip, Typography } from "antd"; import React, { useEffect, useState, useMemo, useCallback } from "react"; import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useMCPServerHealth } from "../../app/(dashboard)/hooks/mcpServers/useMCPServerHealth"; @@ -57,6 +57,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) const [editServer, setEditServer] = useState(false); const [selectedTeam, setSelectedTeam] = useState("all"); const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState("all"); + const [searchTerm, setSearchTerm] = useState(""); const [filteredServers, setFilteredServers] = useState([]); const [isModalVisible, setModalVisible] = useState(false); const [isDiscoveryVisible, setDiscoveryVisible] = useState(false); @@ -113,8 +114,8 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) ); }, [serversWithHealth]); - // Filtering logic for both team and access group - const filterServers = useCallback((teamId: string, group: string) => { + // Filtering logic for team, access group, and search + const filterServers = useCallback((teamId: string, group: string, search: string) => { if (!serversWithHealth) return setFilteredServers([]); let filtered = serversWithHealth; if (teamId === "personal") { @@ -129,25 +130,44 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID }) server.mcp_access_groups?.some((g: any) => (typeof g === "string" ? g === group : g && g.name === group)), ); } + if (search.trim()) { + const searchLower = search.toLowerCase(); + filtered = filtered.filter((server) => { + return ( + (server.server_name && server.server_name.toLowerCase().includes(searchLower)) || + (server.alias && server.alias.toLowerCase().includes(searchLower)) || + (server.url && server.url.toLowerCase().includes(searchLower)) || + (server.server_id && server.server_id.toLowerCase().includes(searchLower)) || + (server.transport && server.transport.toLowerCase().includes(searchLower)) + ); + }); + } setFilteredServers(filtered); }, [serversWithHealth]); // Handle team filter change const handleTeamChange = (teamId: string) => { setSelectedTeam(teamId); - filterServers(teamId, selectedMcpAccessGroup); + filterServers(teamId, selectedMcpAccessGroup, searchTerm); }; // Handle MCP access group filter change const handleMcpAccessGroupChange = (group: string) => { setSelectedMcpAccessGroup(group); - filterServers(selectedTeam, group); + filterServers(selectedTeam, group, searchTerm); + }; + + // Handle search term change + const handleSearchChange = (e: React.ChangeEvent) => { + const term = e.target.value; + setSearchTerm(term); + filterServers(selectedTeam, selectedMcpAccessGroup, term); }; // Initial and effect-based filtering (trigger on query data updates and health data updates) useEffect(() => { - filterServers(selectedTeam, selectedMcpAccessGroup); - }, [serversWithHealth, selectedTeam, selectedMcpAccessGroup, filterServers]); + filterServers(selectedTeam, selectedMcpAccessGroup, searchTerm); + }, [serversWithHealth, selectedTeam, selectedMcpAccessGroup, searchTerm, filterServers]); const columns = React.useMemo( () => @@ -364,7 +384,19 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID })
-
+
+
+ } + value={searchTerm} + onChange={handleSearchChange} + allowClear + className="rounded-lg" + size="middle" + /> +
+
Team