feat(ui): MCPServerPermissions to shadcn (expandable list)

- @tremor Text/Badge + @heroicons Server/Chevron + antd Tooltip \u2192
  shadcn Badge/Tooltip + lucide Server/ChevronDown/ChevronRight.
- Preserves the categorical blue/emerald/purple palette for
  servers/access-groups/toolsets respectively. File added to
  .eslintrc.json overrides.
- Expand/collapse logic for tool lists under each server/toolset
  retained; chevron direction tied to expansion state.

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 10:57:48 +00:00
parent 1fe796cebe
commit 6b42172e99
No known key found for this signature in database
2 changed files with 144 additions and 110 deletions

View file

@ -52,6 +52,7 @@
"src/components/agents/agent_card.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx"
],

View file

@ -1,7 +1,12 @@
import React, { useState, useEffect } from "react";
import { Text, Badge } from "@tremor/react";
import { ServerIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
import { Tooltip } from "antd";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ChevronDown, ChevronRight, Server } from "lucide-react";
import { fetchMCPServers, fetchMCPToolsets } from "../networking";
import { MCPServer, MCPToolset } from "../mcp_tools/types";
@ -18,38 +23,29 @@ export function MCPServerPermissions({
mcpAccessGroups = [],
mcpToolPermissions = {},
mcpToolsets = [],
accessToken
accessToken,
}: MCPServerPermissionsProps) {
const [mcpServerDetails, setMCPServerDetails] = useState<MCPServer[]>([]);
const [toolsetDetails, setToolsetDetails] = useState<MCPToolset[]>([]);
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
const [expandedToolsets, setExpandedToolsets] = useState<Set<string>>(new Set());
const [expandedServers, setExpandedServers] = useState<Set<string>>(
new Set(),
);
const [expandedToolsets, setExpandedToolsets] = useState<Set<string>>(
new Set(),
);
const toggleServerExpansion = (serverId: string) => {
setExpandedServers((prev) => {
const newSet = new Set(prev);
if (newSet.has(serverId)) {
newSet.delete(serverId);
} else {
newSet.add(serverId);
}
return newSet;
const toggle = (
setter: React.Dispatch<React.SetStateAction<Set<string>>>,
id: string,
) => {
setter((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const toggleToolsetExpansion = (toolsetId: string) => {
setExpandedToolsets((prev) => {
const newSet = new Set(prev);
if (newSet.has(toolsetId)) {
newSet.delete(toolsetId);
} else {
newSet.add(toolsetId);
}
return newSet;
});
};
// Fetch MCP server details when component mounts
useEffect(() => {
const fetchMCPServerDetails = async () => {
if (accessToken && mcpServers.length > 0) {
@ -68,7 +64,6 @@ export function MCPServerPermissions({
fetchMCPServerDetails();
}, [accessToken, mcpServers.length]);
// Fetch toolset details
useEffect(() => {
const fetchToolsets = async () => {
if (accessToken && mcpToolsets.length > 0) {
@ -86,17 +81,20 @@ export function MCPServerPermissions({
fetchToolsets();
}, [accessToken, mcpToolsets.length]);
// Function to get display name for MCP server
const getMCPServerDisplayName = (serverId: string) => {
const serverDetail = mcpServerDetails.find((server) => server.server_id === serverId);
const serverDetail = mcpServerDetails.find(
(s) => s.server_id === serverId,
);
if (serverDetail) {
const truncatedId = serverId.length > 7 ? `${serverId.slice(0, 3)}...${serverId.slice(-4)}` : serverId;
const truncatedId =
serverId.length > 7
? `${serverId.slice(0, 3)}...${serverId.slice(-4)}`
: serverId;
return `${serverDetail.alias} (${truncatedId})`;
}
return serverId;
};
// Merge servers and access groups into one list
const mergedItems = [
...mcpServers.map((server) => ({ type: "server", value: server })),
...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
@ -106,9 +104,9 @@ export function MCPServerPermissions({
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<ServerIcon className="h-4 w-4 text-blue-600" />
<Text className="font-semibold text-gray-900">MCP Servers</Text>
<Badge color="blue" size="xs">
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="font-semibold text-foreground">MCP Servers</span>
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs">
{totalCount}
</Badge>
</div>
@ -116,33 +114,51 @@ export function MCPServerPermissions({
{totalCount > 0 ? (
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-1">
{mergedItems.map((item, index) => {
const toolsForServer = item.type === "server" ? mcpToolPermissions[item.value] : undefined;
const hasToolRestrictions = toolsForServer && toolsForServer.length > 0;
const toolsForServer =
item.type === "server"
? mcpToolPermissions[item.value]
: undefined;
const hasToolRestrictions =
toolsForServer && toolsForServer.length > 0;
const isExpanded = expandedServers.has(item.value);
return (
<div key={index} className="space-y-2">
<div
onClick={() => hasToolRestrictions && toggleServerExpansion(item.value)}
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-gray-200 transition-all ${
onClick={() =>
hasToolRestrictions &&
toggle(setExpandedServers, item.value)
}
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-border transition-all ${
hasToolRestrictions
? 'cursor-pointer hover:bg-gray-50 hover:border-gray-300'
: 'bg-white'
? "cursor-pointer hover:bg-muted hover:border-foreground/30"
: "bg-background"
}`}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{item.type === "server" ? (
<Tooltip title={`Full ID: ${item.value}`} placement="top">
<div className="inline-flex items-center gap-2 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0"></span>
<span className="text-sm font-medium text-gray-900 truncate">{getMCPServerDisplayName(item.value)}</span>
</div>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex items-center gap-2 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0" />
<span className="text-sm font-medium text-foreground truncate">
{getMCPServerDisplayName(item.value)}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
Full ID: {item.value}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div className="inline-flex items-center gap-2 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></span>
<span className="text-sm font-medium text-gray-900 truncate">{item.value}</span>
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-green-600 bg-green-50 border border-green-200 rounded uppercase tracking-wide flex-shrink-0">
<span className="inline-block w-1.5 h-1.5 bg-emerald-500 rounded-full flex-shrink-0" />
<span className="text-sm font-medium text-foreground truncate">
{item.value}
</span>
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900 dark:text-emerald-300 rounded uppercase tracking-wide flex-shrink-0">
Group
</span>
</div>
@ -151,25 +167,28 @@ export function MCPServerPermissions({
{hasToolRestrictions && (
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
<span className="text-xs font-medium text-gray-600">{toolsForServer.length}</span>
<span className="text-xs text-gray-500">{toolsForServer.length === 1 ? "tool" : "tools"}</span>
<span className="text-xs font-medium text-foreground">
{toolsForServer.length}
</span>
<span className="text-xs text-muted-foreground">
{toolsForServer.length === 1 ? "tool" : "tools"}
</span>
{isExpanded ? (
<ChevronDownIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
) : (
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
)}
</div>
)}
</div>
{/* Show tool permissions if expanded */}
{hasToolRestrictions && isExpanded && (
<div className="ml-4 pl-4 border-l-2 border-blue-200 pb-1">
<div className="ml-4 pl-4 border-l-2 border-blue-200 dark:border-blue-900 pb-1">
<div className="flex flex-wrap gap-1.5">
{toolsForServer.map((tool, toolIndex) => (
<span
key={toolIndex}
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-xs font-medium"
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 dark:bg-blue-950/30 dark:border-blue-900 dark:text-blue-200 text-xs font-medium"
>
{tool}
</span>
@ -181,65 +200,79 @@ export function MCPServerPermissions({
);
})}
{/* Toolsets section */}
{mcpToolsets.length > 0 && mcpToolsets.map((toolsetId, index) => {
const detail = toolsetDetails.find((t) => t.toolset_id === toolsetId);
const isExpanded = expandedToolsets.has(toolsetId);
const toolCount = detail?.tools.length ?? 0;
{mcpToolsets.length > 0 &&
mcpToolsets.map((toolsetId, index) => {
const detail = toolsetDetails.find(
(t) => t.toolset_id === toolsetId,
);
const isExpanded = expandedToolsets.has(toolsetId);
const toolCount = detail?.tools.length ?? 0;
return (
<div key={`toolset-${index}`} className="space-y-2">
<div
onClick={() => toolCount > 0 && toggleToolsetExpansion(toolsetId)}
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-purple-200 transition-all ${
toolCount > 0 ? 'cursor-pointer hover:bg-purple-50 hover:border-purple-300' : 'bg-white'
}`}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0"></span>
<span className="text-sm font-medium text-gray-900 truncate">
{detail?.toolset_name ?? toolsetId}
</span>
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-purple-600 bg-purple-50 border border-purple-200 rounded uppercase tracking-wide flex-shrink-0">
Toolset
</span>
return (
<div key={`toolset-${index}`} className="space-y-2">
<div
onClick={() =>
toolCount > 0 && toggle(setExpandedToolsets, toolsetId)
}
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-purple-200 dark:border-purple-900 transition-all ${
toolCount > 0
? "cursor-pointer hover:bg-purple-50 dark:hover:bg-purple-950/30 hover:border-purple-300 dark:hover:border-purple-800"
: "bg-background"
}`}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0" />
<span className="text-sm font-medium text-foreground truncate">
{detail?.toolset_name ?? toolsetId}
</span>
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-purple-600 bg-purple-50 border border-purple-200 dark:bg-purple-950/30 dark:border-purple-900 dark:text-purple-300 rounded uppercase tracking-wide flex-shrink-0">
Toolset
</span>
</div>
{toolCount > 0 && (
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
<span className="text-xs font-medium text-foreground">
{toolCount}
</span>
<span className="text-xs text-muted-foreground">
{toolCount === 1 ? "tool" : "tools"}
</span>
{isExpanded ? (
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
)}
</div>
)}
</div>
{toolCount > 0 && (
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
<span className="text-xs font-medium text-gray-600">{toolCount}</span>
<span className="text-xs text-gray-500">{toolCount === 1 ? "tool" : "tools"}</span>
{isExpanded ? (
<ChevronDownIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
) : (
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
)}
{toolCount > 0 && isExpanded && detail && (
<div className="ml-4 pl-4 border-l-2 border-purple-200 dark:border-purple-900 pb-1">
<div className="flex flex-wrap gap-1.5">
{detail.tools.map((tool, toolIndex) => (
<span
key={toolIndex}
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 dark:bg-purple-950/30 dark:border-purple-900 dark:text-purple-200 text-xs font-medium"
>
<span className="text-purple-400 dark:text-purple-500 mr-1 text-[10px]">
{tool.server_id.slice(0, 6)}…
</span>
{tool.tool_name}
</span>
))}
</div>
</div>
)}
</div>
{toolCount > 0 && isExpanded && detail && (
<div className="ml-4 pl-4 border-l-2 border-purple-200 pb-1">
<div className="flex flex-wrap gap-1.5">
{detail.tools.map((tool, toolIndex) => (
<span
key={toolIndex}
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 text-xs font-medium"
>
<span className="text-purple-400 mr-1 text-[10px]">{tool.server_id.slice(0, 6)}…</span>
{tool.tool_name}
</span>
))}
</div>
</div>
)}
</div>
);
})}
);
})}
</div>
) : (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
<ServerIcon className="h-4 w-4 text-gray-400" />
<Text className="text-gray-500 text-sm">No MCP servers, access groups, or toolsets configured</Text>
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted border border-border">
<Server className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground text-sm">
No MCP servers, access groups, or toolsets configured
</span>
</div>
)}
</div>