import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Fragment } from "react"; import { CodeEditor } from "@/components/ui/codeEditor"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { HeadersTable } from "@/components/ui/headersTable"; import { Input } from "@/components/ui/input"; import { MultiSelect } from "@/components/ui/multiSelect"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { TriStateCheckbox } from "@/components/ui/tristateCheckbox"; import { useToast } from "@/hooks/use-toast"; import { useDebouncedValue } from "@/hooks/useDebounce"; import { MCP_STATUS_COLORS } from "@/lib/constants/config"; import { getErrorMessage, useGetCoreConfigQuery, useGetVirtualKeysQuery, useUpdateMCPClientMutation } from "@/lib/store"; import { MCPClient, MCPVKConfig } from "@/lib/types/mcp"; import { mcpClientUpdateSchema, type MCPClientUpdateSchema } from "@/lib/types/schemas"; import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib"; import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronDown, ChevronRight, Info, Plus, Trash2 } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; interface MCPClientSheetProps { mcpClient: MCPClient; onClose: () => void; onSubmitSuccess: () => void; } /** API sends tool_sync_interval as nanoseconds (Go time.Duration). Normalize to minutes for form/store. */ function toolSyncIntervalToMinutes(v: number | undefined | null): number { if (v === undefined || v === null) return 0; const n = Number(v); if (Number.isNaN(n)) return 0; if (Math.abs(n) >= 1e9) return Math.round(n / 6e10); return n; } export default function MCPClientSheet({ mcpClient, onClose, onSubmitSuccess }: MCPClientSheetProps) { const hasUpdateMCPClientAccess = useRbac(RbacResource.MCPGateway, RbacOperation.Update); const [updateMCPClient, { isLoading: isUpdating }] = useUpdateMCPClientMutation(); const { data: bifrostConfig } = useGetCoreConfigQuery({ fromDB: true }); const globalToolSyncInterval = bifrostConfig?.client_config?.mcp_tool_sync_interval ?? 10; const { toast } = useToast(); const [expandedTools, setExpandedTools] = useState>(new Set()); // VK access management — search-based dropdown (limit 20), no pagination issue const [vkSearch, setVKSearch] = useState(""); const [vkPopoverOpen, setVKPopoverOpen] = useState(false); const debouncedVkSearch = useDebouncedValue(vkSearch, 300); const { data: vksData } = useGetVirtualKeysQuery({ limit: 20, search: debouncedVkSearch || undefined }); const allToolNames = useMemo(() => mcpClient.tools?.map((t) => t.name) ?? [], [mcpClient.tools]); // Initial VK configs come directly from the MCP client response — always complete, no pagination issue. const initialVKConfigs = useMemo( () => (mcpClient.vk_configs ?? []).map((vc) => ({ virtual_key_id: vc.virtual_key_id, tools_to_execute: vc.tools_to_execute })), [mcpClient.vk_configs], ); const [vkConfigs, setVKConfigs] = useState([]); const [vkConfigsDirty, setVKConfigsDirty] = useState(false); const [allowedExtraHeadersRaw, setAllowedExtraHeadersRaw] = useState((mcpClient.config.allowed_extra_headers || []).join(", ")); // Persists names for newly added VKs so they survive search result changes const [localVKNames, setLocalVKNames] = useState>({}); // Sync vkConfigs when mcpClient changes useEffect(() => { setVKConfigs(initialVKConfigs); setVKConfigsDirty(false); setLocalVKNames({}); }, [initialVKConfigs]); // Sync allowedExtraHeadersRaw when mcpClient changes useEffect(() => { setAllowedExtraHeadersRaw((mcpClient.config.allowed_extra_headers || []).join(", ")); }, [mcpClient.config.allowed_extra_headers]); // Name lookup: server response names → search results → locally cached names (highest priority) const vkNameByID = useMemo>(() => { const m: Record = {}; for (const vc of mcpClient.vk_configs ?? []) m[vc.virtual_key_id] = vc.virtual_key_name; for (const vk of vksData?.virtual_keys ?? []) m[vk.id] = vk.name; Object.assign(m, localVKNames); return m; }, [mcpClient.vk_configs, vksData, localVKNames]); const vkOptions = useMemo( () => (vksData?.virtual_keys ?? []) .filter((vk) => !vkConfigs.some((vc) => vc.virtual_key_id === vk.id)) .map((vk) => ({ value: vk.id, label: vk.name })), [vksData, vkConfigs], ); const toolOptions = useMemo( () => [ { value: "*", label: "Allow All Tools", description: "Allow all current and future tools" }, ...allToolNames.map((n) => ({ value: n, label: n })), ], [allToolNames], ); const addVKConfig = (vkId: string) => { const name = vksData?.virtual_keys?.find((vk) => vk.id === vkId)?.name; if (name) setLocalVKNames((prev) => ({ ...prev, [vkId]: name })); setVKConfigs((prev) => [...prev, { virtual_key_id: vkId, tools_to_execute: ["*"] }]); setVKConfigsDirty(true); }; const removeVKConfig = (vkId: string) => { setVKConfigs((prev) => prev.filter((vc) => vc.virtual_key_id !== vkId)); setVKConfigsDirty(true); }; const updateVKConfigTools = (vkId: string, tools: string[]) => { setVKConfigs((prev) => prev.map((vc) => (vc.virtual_key_id === vkId ? { ...vc, tools_to_execute: tools } : vc))); setVKConfigsDirty(true); }; const toggleToolExpanded = (toolName: string) => { setExpandedTools((prev) => { const next = new Set(prev); if (next.has(toolName)) { next.delete(toolName); } else { next.add(toolName); } return next; }); }; const form = useForm({ resolver: zodResolver(mcpClientUpdateSchema), mode: "onBlur", defaultValues: { name: mcpClient.config.name, is_code_mode_client: mcpClient.config.is_code_mode_client || false, is_ping_available: mcpClient.config.is_ping_available === true || mcpClient.config.is_ping_available === undefined, allow_on_all_virtual_keys: mcpClient.config.allow_on_all_virtual_keys || false, headers: mcpClient.config.headers, tools_to_execute: mcpClient.config.tools_to_execute || [], tools_to_auto_execute: mcpClient.config.tools_to_auto_execute || [], tool_pricing: mcpClient.config.tool_pricing || {}, tool_sync_interval: toolSyncIntervalToMinutes(mcpClient.config.tool_sync_interval), allowed_extra_headers: mcpClient.config.allowed_extra_headers || [], }, }); // Reset form when mcpClient changes useEffect(() => { form.reset({ name: mcpClient.config.name, is_code_mode_client: mcpClient.config.is_code_mode_client || false, is_ping_available: mcpClient.config.is_ping_available === true || mcpClient.config.is_ping_available === undefined, allow_on_all_virtual_keys: mcpClient.config.allow_on_all_virtual_keys || false, headers: mcpClient.config.headers, tools_to_execute: mcpClient.config.tools_to_execute || [], tools_to_auto_execute: mcpClient.config.tools_to_auto_execute || [], tool_pricing: mcpClient.config.tool_pricing || {}, tool_sync_interval: toolSyncIntervalToMinutes(mcpClient.config.tool_sync_interval), allowed_extra_headers: mcpClient.config.allowed_extra_headers || [], }); }, [form, mcpClient]); const onSubmit = async (data: MCPClientUpdateSchema) => { try { await updateMCPClient({ id: mcpClient.config.client_id, data: { name: data.name, is_code_mode_client: data.is_code_mode_client, is_ping_available: data.is_ping_available, allow_on_all_virtual_keys: data.allow_on_all_virtual_keys, headers: data.headers ?? {}, tools_to_execute: data.tools_to_execute, tools_to_auto_execute: data.tools_to_auto_execute, tool_pricing: data.tool_pricing, tool_sync_interval: data.tool_sync_interval ?? 0, allowed_extra_headers: data.allowed_extra_headers, vk_configs: vkConfigsDirty ? vkConfigs : undefined, }, }).unwrap(); toast({ title: "Success", description: "MCP client updated successfully", }); onSubmitSuccess(); } catch (error) { toast({ title: "Error", description: getErrorMessage(error), variant: "destructive", }); } }; const handleToolToggle = (toolName: string, checked: boolean) => { const currentTools = form.getValues("tools_to_execute") || []; let newTools: string[]; const allToolNames = mcpClient.tools?.map((tool) => tool.name) || []; // Check if we're in "all tools" mode (wildcard) const isAllToolsMode = currentTools.includes("*"); if (isAllToolsMode) { if (checked) { // Already all selected, keep wildcard newTools = ["*"]; } else { // Unchecking a tool when all are selected - switch to explicit list without this tool newTools = allToolNames.filter((name) => name !== toolName); } } else { // We're in explicit tool selection mode if (checked) { // Add tool to selection newTools = currentTools.includes(toolName) ? currentTools : [...currentTools, toolName]; // If we now have all tools selected, switch to wildcard mode if (newTools.length === allToolNames.length) { newTools = ["*"]; } } else { // Remove tool from selection newTools = currentTools.filter((tool) => tool !== toolName); } } form.setValue("tools_to_execute", newTools, { shouldDirty: true }); // If tool is being removed from tools_to_execute, also remove it from tools_to_auto_execute if (!checked) { const currentAutoExecute = form.getValues("tools_to_auto_execute") || []; if (currentAutoExecute.includes(toolName) || currentAutoExecute.includes("*")) { const newAutoExecute = currentAutoExecute.filter((tool) => tool !== toolName); // If we had "*" and removed a tool, we need to recalculate if (currentAutoExecute.includes("*")) { // If all tools mode, keep "*" only if tool is still in tools_to_execute if (newTools.includes("*")) { form.setValue("tools_to_auto_execute", ["*"], { shouldDirty: true }); } else { // Switch to explicit list - when in wildcard mode, all remaining tools should be auto-execute form.setValue("tools_to_auto_execute", newTools, { shouldDirty: true }); } } else { form.setValue("tools_to_auto_execute", newAutoExecute, { shouldDirty: true }); } } } }; const handleAutoExecuteToggle = (toolName: string, checked: boolean) => { const currentAutoExecute = form.getValues("tools_to_auto_execute") || []; const currentTools = form.getValues("tools_to_execute") || []; const allToolNames = mcpClient.tools?.map((tool) => tool.name) || []; // Check if we're in "all tools" mode (wildcard) const isAllToolsMode = currentTools.includes("*"); const isAllAutoExecuteMode = currentAutoExecute.includes("*"); let newAutoExecute: string[]; if (isAllAutoExecuteMode) { if (checked) { // Already all selected, keep wildcard newAutoExecute = ["*"]; } else { // Unchecking a tool when all are selected - switch to explicit list without this tool if (isAllToolsMode) { newAutoExecute = allToolNames.filter((name) => name !== toolName); } else { newAutoExecute = currentTools.filter((name) => name !== toolName); } } } else { // We're in explicit tool selection mode if (checked) { // Add tool to selection newAutoExecute = currentAutoExecute.includes(toolName) ? currentAutoExecute : [...currentAutoExecute, toolName]; // Only switch to wildcard if ALL tools are enabled (tools_to_execute is "*") // and all of those tools are now auto-executed. When specific tools are // explicitly listed, keep the explicit list to avoid sending "*" when only // a subset of tools is enabled. if ( isAllToolsMode && newAutoExecute.length === allToolNames.length && allToolNames.every((tool) => newAutoExecute.includes(tool)) ) { newAutoExecute = ["*"]; } } else { // Remove tool from selection newAutoExecute = currentAutoExecute.filter((tool) => tool !== toolName); } } form.setValue("tools_to_auto_execute", newAutoExecute, { shouldDirty: true }); }; return (
{mcpClient.config.name} {mcpClient.state} MCP server configuration and available tools
{/* Name and Header Section */}

Basic Information

(
Name

Use a descriptive, meaningful name that clearly identifies the server. For example, use "google_drive" instead of "gdrive", or "hacker_news" instead of "hn". This name is used as the Python module name in code mode.

)} /> ( Code Mode Client )} /> (
Ping Available for Health Check

Enable to use lightweight ping method for health checks. Disable if your MCP server doesn't support ping - will use listTools instead.

)} /> (
Allow on All Virtual Keys

When enabled, this MCP server is accessible to all virtual keys without requiring explicit per-key assignment. All tools are allowed by default. If a virtual key has an explicit MCP config for this server, that config takes precedence and overrides this behaviour.

)} /> { const isUsingGlobal = field.value === undefined || field.value === null || field.value === 0; return (
Tool Sync Interval (minutes)

Override the global tool sync interval for this server. Leave empty to use global setting. Set to -1 to disable sync for this server.

{isUsingGlobal &&

Using global setting

}
{ const val = e.target.value === "" ? undefined : parseInt(e.target.value); field.onChange(val); }} min="-1" />
); }} /> ( )} /> (
Allowed Extra Headers

Allowlist of headers that callers can forward to this MCP server at request time.

{ setAllowedExtraHeadersRaw(e.target.value); }} onBlur={() => { const parsed = allowedExtraHeadersRaw.trim() ? allowedExtraHeadersRaw .split(",") .map((h) => h.trim()) .filter(Boolean) : []; field.onChange(parsed); field.onBlur(); }} />

Comma-separated header names, or * to allow all. Leave empty to block all extra headers.

)} />
{/* Client Configuration */}

Configuration

Client ConnectionConfig
{ const { client_id: _client_id, name: _name, tools_to_execute: _tools_to_execute, headers: _headers, ...rest } = mcpClient.config; return rest; })(), null, 2, )} lang="json" readonly={true} options={{ scrollBeyondLastLine: false, collapsibleBlocks: true, lineNumbers: "off", alwaysConsumeMouseWheel: false, }} />
{/* Tools Section */}

Available Tools ({mcpClient.tools?.length || 0})

{mcpClient.tools && mcpClient.tools.length > 0 && (
{/* Enable All */} { const currentTools = form.watch("tools_to_execute") || []; const allToolNames = mcpClient.tools?.map((tool) => tool.name) || []; const isAllEnabled = currentTools.includes("*"); const isNoneEnabled = currentTools.length === 0; const selectedIds = isAllEnabled ? allToolNames : currentTools; return (
{isAllEnabled ? "All enabled" : isNoneEnabled ? "None enabled" : `${currentTools.length} enabled`} { if (nextSelectedIds.length === 0) { form.setValue("tools_to_execute", [], { shouldDirty: true }); // Also clear auto-execute when disabling all form.setValue("tools_to_auto_execute", [], { shouldDirty: true }); } else if (nextSelectedIds.length === allToolNames.length) { form.setValue("tools_to_execute", ["*"], { shouldDirty: true }); } else { form.setValue("tools_to_execute", nextSelectedIds, { shouldDirty: true }); } }} />
); }} /> {/* Auto-execute All */} { const currentTools = form.watch("tools_to_execute") || []; const currentAutoExecute = form.watch("tools_to_auto_execute") || []; const allToolNames = mcpClient.tools?.map((tool) => tool.name) || []; // Get the list of enabled tools const enabledToolNames = currentTools.includes("*") ? allToolNames : currentTools; const isAllAutoExecute = currentAutoExecute.includes("*"); const isNoneAutoExecute = currentAutoExecute.length === 0; // For TriStateCheckbox, we need the selected auto-execute tools that are also enabled const selectedAutoExecuteIds = isAllAutoExecute ? enabledToolNames : currentAutoExecute.filter((t) => enabledToolNames.includes(t)); const autoExecuteCount = isAllAutoExecute ? enabledToolNames.length : selectedAutoExecuteIds.length; return (
{isAllAutoExecute ? "All auto-execute" : isNoneAutoExecute ? "None auto-execute" : `${autoExecuteCount} auto-execute`} { if (nextSelectedIds.length === 0) { form.setValue("tools_to_auto_execute", [], { shouldDirty: true }); } else if (nextSelectedIds.length === enabledToolNames.length) { form.setValue("tools_to_auto_execute", ["*"], { shouldDirty: true }); } else { form.setValue("tools_to_auto_execute", nextSelectedIds, { shouldDirty: true }); } }} />
); }} />
)}
{mcpClient.tools && mcpClient.tools.length > 0 ? (
Tool Name Enabled Auto-execute Cost (USD) {mcpClient.tools.map((tool, index) => { const currentTools = form.watch("tools_to_execute") || []; const currentAutoExecute = form.watch("tools_to_auto_execute") || []; const isToolEnabled = currentTools?.includes("*") || currentTools?.includes(tool.name); const isAutoExecuteEnabled = (currentAutoExecute?.includes("*") && isToolEnabled) || (currentAutoExecute?.includes(tool.name) && isToolEnabled); const isExpanded = expandedTools.has(tool.name); return (
{tool.name}
{tool.description && (

{tool.description}

)}
( handleToolToggle(tool.name, checked)} /> )} /> ( handleAutoExecuteToggle(tool.name, checked)} /> )} /> ( { const value = e.target.value === "" ? undefined : parseFloat(e.target.value); const newPricing = { ...field.value }; if (value === undefined || isNaN(value)) { delete newPricing[tool.name]; } else { newPricing[tool.name] = value; } field.onChange(newPricing); }} /> )} />
{isExpanded && (
)} ); })}
Parameters Schema
{tool.parameters ? ( ) : (
No parameters defined
)}
) : (

No tools available

)} {mcpClient.tools && mcpClient.tools.length > 0 && (
Virtual Key Access

Control which virtual keys can use this MCP server and which specific tools they can call.

{ setVKPopoverOpen(open); if (!open) setVKSearch(""); }} >
setVKSearch(e.target.value)} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter") e.preventDefault(); }} className="h-7 rounded-b-none border-0 border-b text-sm focus-visible:ring-0" autoFocus />
{vkOptions.length > 0 ? ( vkOptions.map((opt) => ( )) ) : (
No virtual keys found
)}
{form.watch("allow_on_all_virtual_keys") && (

Configuring access for a virtual key here overrides the{" "} Allow on All Virtual Keys setting for that key.

)}
{vkConfigs.length > 0 ? (
Virtual Key Allowed Tools {vkConfigs.map((vc) => ( {vkNameByID[vc.virtual_key_id] ?? vc.virtual_key_id} { const hadStar = vc.tools_to_execute.includes("*"); const hasStar = tools.includes("*"); let next: string[]; if (!hadStar && hasStar) { next = ["*"]; } else if (hadStar && hasStar && tools.length > 1) { next = tools.filter((t) => t !== "*"); } else { next = tools; } updateVKConfigTools(vc.virtual_key_id, next); }} placeholder={ vc.tools_to_execute.includes("*") ? "All tools allowed" : vc.tools_to_execute.length === 0 ? "No tools allowed" : "Select tools..." } maxCount={3} className="bg-background dark:bg-input/30 border-input text-foreground hover:bg-accent hover:text-accent-foreground rounded-sm font-normal" /> ))}
) : form.watch("allow_on_all_virtual_keys") ? (

All virtual keys can access this MCP server unless a key has an explicit override.

) : (

No virtual keys have access to this MCP server

)}
)}
); }