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 <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-03-10 23:09:14 +00:00
parent cf331c4fc5
commit 66d77cfa22
4 changed files with 148 additions and 25 deletions

View file

@ -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"
}
}
}
}

View file

@ -415,7 +415,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
const handleCancel = () => {
form.resetFields();
setCostConfig({});
setTools([]);
clearTools();
setAllowedTools([]);
setAliasManuallyEdited(false);
setModalVisible(false);

View file

@ -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(
<QueryClientProvider client={queryClient}>
<MCPServers {...defaultProps} />
</QueryClientProvider>,
);
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();
});
});
});

View file

@ -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<MCPServerProps> = ({ accessToken, userRole, userID })
const [editServer, setEditServer] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<string>("all");
const [selectedMcpAccessGroup, setSelectedMcpAccessGroup] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>("");
const [filteredServers, setFilteredServers] = useState<MCPServer[]>([]);
const [isModalVisible, setModalVisible] = useState(false);
const [isDiscoveryVisible, setDiscoveryVisible] = useState(false);
@ -113,8 +114,8 @@ const MCPServers: React.FC<MCPServerProps> = ({ 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<MCPServerProps> = ({ 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<HTMLInputElement>) => {
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<MCPServerProps> = ({ accessToken, userRole, userID })
<div className="w-full h-full">
<div className="w-full">
<div className="flex flex-col space-y-4">
<div className="flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-4 bg-white rounded-lg px-4 py-3 border border-gray-200">
<div className="flex items-center gap-2 flex-1">
<Input
placeholder="Search servers by name, alias, URL..."
prefix={<SearchOutlined className="text-gray-400" />}
value={searchTerm}
onChange={handleSearchChange}
allowClear
className="rounded-lg"
size="middle"
/>
</div>
<div className="h-6 w-px bg-gray-200"></div>
<div className="flex items-center gap-2">
<Text className="text-sm font-medium text-gray-600 whitespace-nowrap">Team</Text>
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 220 }} size="middle">