From 276b1682e2e71dc2876f00ad4c6bda0d29b8d21d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Apr 2026 10:02:19 +0000 Subject: [PATCH] feat(ui): migrate mcp_server_management to shadcn - MCPServerSelector: antd Select mode=multiple \u2192 shadcn Popover + chip-style multi-select (same pattern as VectorStoreSelector / GuardrailSelector). Merges three lists (access groups / servers / toolsets) into a single searchable popover with a categorical emerald / blue / purple dot + label per item. Added to .eslintrc.json overrides. - MCPToolPermissions: @tremor Text + antd Spin/Radio.Group \u2192 shadcn Checkbox/Skeleton/ToggleGroup + plain Tailwind. View-mode picker uses a ToggleGroup with 'Risk Groups' / 'Flat List' options. Flat-list tool rows use shadcn Checkbox. Gates: TS \u2713 | Vitest 3/3 (existing MCPToolPermissions.test) \u2713 | Build \u2713. Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 3 +- .../MCPServerSelector.tsx | 281 ++++++++++++------ .../MCPToolPermissions.tsx | 109 ++++--- 3 files changed, 255 insertions(+), 138 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 78891527ad7..ec5b085ba42 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -37,7 +37,8 @@ "src/components/cache_dashboard.tsx", "src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx", "src/components/mcp_hub_table_columns.tsx", - "src/components/model_hub_table_columns.tsx" + "src/components/model_hub_table_columns.tsx", + "src/components/mcp_server_management/MCPServerSelector.tsx" ], "rules": { "litellm-ui/no-banned-ui-imports": "off", diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx index fc4b20517cf..3b5fa6ab185 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPServerSelector.tsx @@ -1,11 +1,23 @@ import { useMCPAccessGroups } from "@/app/(dashboard)/hooks/mcpServers/useMCPAccessGroups"; import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolsets"; -import { Select } from "antd"; -import React from "react"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import React, { useMemo, useState } from "react"; interface MCPServerSelectorProps { - onChange: (selected: { servers: string[]; accessGroups: string[]; toolsets: string[] }) => void; + onChange: (selected: { + servers: string[]; + accessGroups: string[]; + toolsets: string[]; + }) => void; value?: { servers: string[]; accessGroups: string[]; @@ -20,121 +32,212 @@ interface MCPServerSelectorProps { const TOOLSET_PREFIX = "toolset:"; +/** + * Multi-select that combines three lists: + * - MCP Access Groups (emerald dot) + * - MCP Servers (blue dot) + * - Toolsets (purple dot) + * + * Selected items become chips; popover shows the three categories together + * with an inline search input. + */ const MCPServerSelector: React.FC = ({ onChange, value, className, - accessToken, placeholder = "Select MCP servers", disabled = false, teamId, }) => { - const { data: mcpServers = [], isLoading: serversLoading } = useMCPServers(teamId); - const { data: accessGroups = [], isLoading: groupsLoading } = useMCPAccessGroups(); - const { data: toolsets = [], isLoading: toolsetsLoading } = useMCPToolsets(); + const { data: mcpServers = [] } = useMCPServers(teamId); + const { data: accessGroups = [] } = useMCPAccessGroups(); + const { data: toolsets = [] } = useMCPToolsets(); - const loading = serversLoading || groupsLoading || toolsetsLoading; + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); - const accessGroupSet = new Set(accessGroups); + const accessGroupSet = useMemo( + () => new Set(accessGroups), + [accessGroups], + ); - // Combine options: access groups (green) + servers (blue) + toolsets (purple) - const options = [ - ...accessGroups.map((group) => ({ - label: group, - value: group, - type: "accessGroup" as const, - searchText: `${group} Access Group`, - })), - ...mcpServers.map((server) => ({ - label: `${server.server_name || server.server_id} (${server.server_id})`, - value: server.server_id, - type: "server" as const, - searchText: `${server.server_name || server.server_id} ${server.server_id} MCP Server`, - })), - ...toolsets.map((toolset) => ({ - label: toolset.toolset_name, - value: `${TOOLSET_PREFIX}${toolset.toolset_id}`, - type: "toolset" as const, - searchText: `${toolset.toolset_name} ${toolset.toolset_id} Toolset`, - })), - ]; + const options = useMemo( + () => [ + ...accessGroups.map((group) => ({ + label: group, + value: group, + type: "accessGroup" as const, + searchText: `${group} Access Group`, + })), + ...mcpServers.map((server) => ({ + label: `${server.server_name || server.server_id} (${server.server_id})`, + value: server.server_id, + type: "server" as const, + searchText: `${server.server_name || server.server_id} ${server.server_id} MCP Server`, + })), + ...toolsets.map((toolset) => ({ + label: toolset.toolset_name, + value: `${TOOLSET_PREFIX}${toolset.toolset_id}`, + type: "toolset" as const, + searchText: `${toolset.toolset_name} ${toolset.toolset_id} Toolset`, + })), + ], + [accessGroups, mcpServers, toolsets], + ); - const colorByType: Record = { - accessGroup: "#52c41a", - server: "#1890ff", - toolset: "#722ed1", + const selectedValues = useMemo( + () => [ + ...(value?.servers || []), + ...(value?.accessGroups || []), + ...(value?.toolsets || []).map((id) => `${TOOLSET_PREFIX}${id}`), + ], + [value], + ); + + const filteredOptions = useMemo( + () => + options + .filter((o) => !selectedValues.includes(o.value)) + .filter((o) => + query + ? o.searchText.toLowerCase().includes(query.toLowerCase()) + : true, + ), + [options, selectedValues, query], + ); + + const dotByType: Record = { + accessGroup: "bg-emerald-500", + server: "bg-blue-500", + toolset: "bg-purple-500", }; + const labelByType: Record = { accessGroup: "Access Group", server: "MCP Server", toolset: "Toolset", }; - // Flatten value for Select — prefix toolset IDs - const selectedValues = [ - ...(value?.servers || []), - ...(value?.accessGroups || []), - ...(value?.toolsets || []).map((id) => `${TOOLSET_PREFIX}${id}`), - ]; + const textByType: Record = { + accessGroup: "text-emerald-700 dark:text-emerald-400", + server: "text-blue-700 dark:text-blue-400", + toolset: "text-purple-700 dark:text-purple-400", + }; - // Handle selection - const handleChange = (selected: string[]) => { - const toolsetsSelected = selected + const applyChange = (next: string[]) => { + const toolsetsSelected = next .filter((v) => v.startsWith(TOOLSET_PREFIX)) .map((v) => v.slice(TOOLSET_PREFIX.length)); - const rest = selected.filter((v) => !v.startsWith(TOOLSET_PREFIX)); + const rest = next.filter((v) => !v.startsWith(TOOLSET_PREFIX)); const servers = rest.filter((v) => !accessGroupSet.has(v)); const accessGroupsSelected = rest.filter((v) => accessGroupSet.has(v)); - onChange({ servers, accessGroups: accessGroupsSelected, toolsets: toolsetsSelected }); + onChange({ + servers, + accessGroups: accessGroupsSelected, + toolsets: toolsetsSelected, + }); + }; + + const labelFor = (v: string) => { + const opt = options.find((o) => o.value === v); + return opt ? opt.label : v; + }; + const typeFor = (v: string) => { + const opt = options.find((o) => o.value === v); + return opt?.type ?? "server"; }; return ( -
- setQuery(e.target.value)} + className="h-8 mb-2" + /> +
+ {filteredOptions.length === 0 ? ( +
+ No matches
- - ))} - -
+ ) : ( + filteredOptions.map((opt) => ( + + )) + )} +
+ + ); }; diff --git a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx index 5e16a5c4b84..bbd65234f95 100644 --- a/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx +++ b/ui/litellm-dashboard/src/components/mcp_server_management/MCPToolPermissions.tsx @@ -1,8 +1,12 @@ import React, { useEffect, useRef, useState, useMemo } from "react"; import { listMCPTools } from "../networking"; import { MCPTool, MCPServer } from "../mcp_tools/types"; -import { Text } from "@tremor/react"; -import { Spin, Radio } from "antd"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + ToggleGroup, + ToggleGroupItem, +} from "@/components/ui/toggle-group"; import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers"; import McpCrudPermissionPanel from "../mcp_tools/McpCrudPermissionPanel"; import { classifyToolOp } from "../../utils/mcpToolCrudClassification"; @@ -117,34 +121,42 @@ const MCPToolPermissions: React.FC = ({ const viewMode = viewModes[server.server_id] ?? "crud"; return ( -
- {/* Header */} -
+
+
- {serverName} - {server.description && {server.description}} +

{serverName}

+ {server.description && ( +

+ {server.description} +

+ )}
{!disabled && tools.length > 0 && ( - - setViewModes((prev) => ({ ...prev, [server.server_id]: e.target.value })) - } - size="small" - optionType="button" - buttonStyle="solid" - options={[ - { label: "Risk Groups", value: "crud" }, - { label: "Flat List", value: "flat" }, - ]} - /> + onValueChange={(v) => { + if (!v) return; + setViewModes((prev) => ({ + ...prev, + [server.server_id]: v as "crud" | "flat", + })); + }} + > + Risk Groups + Flat List + )} {!disabled && ( <>
- {/* Tools */}
- {/* Loading */} {isLoading && ( -
- - Loading tools... +
+ +

Loading tools...

)} - {/* Error */} {error && !isLoading && ( -
- Unable to load tools - {error} +
+

+ Unable to load tools +

+

{error}

)} - {/* CRUD grouped view */} {!isLoading && !error && tools.length > 0 && viewMode === "crud" && ( handleCrudPanelChange(server.server_id, allowed)} + value={ + !toolPermissions[server.server_id] ? undefined : selectedTools + } + onChange={(allowed) => + handleCrudPanelChange(server.server_id, allowed) + } readOnly={disabled} /> )} - {/* Flat list view */} {!isLoading && !error && tools.length > 0 && viewMode === "flat" && (
{tools.map((tool) => { const isSelected = selectedTools.includes(tool.name); return (
- { - if (disabled) return; - const next = isSelected - ? selectedTools.filter((n) => n !== tool.name) - : [...selectedTools, tool.name]; - handleCrudPanelChange(server.server_id, next); - }} disabled={disabled} className="mt-0.5" + onCheckedChange={(next) => { + if (disabled) return; + const nextList = next + ? [...selectedTools, tool.name] + : selectedTools.filter((n) => n !== tool.name); + handleCrudPanelChange(server.server_id, nextList); + }} />
- {tool.name} - + + {tool.name} + + - {tool.description || "No description"} - +
@@ -225,10 +239,9 @@ const MCPToolPermissions: React.FC = ({
)} - {/* Empty State */} {!isLoading && !error && tools.length === 0 && (
- No tools available +

No tools available

)}