import React, { useState } from 'react'; import { Input } from '../../ui/input'; import { LabelWithTooltip } from '../../ui/label-with-tooltip'; import { Button } from '../../ui/button'; import { Collapsible } from '../../ui/collapsible'; import { Plus, Trash2, ChevronDown, ChevronUp } from 'lucide-react'; import type { AgentConfig } from '@dexto/core'; import { MCP_SERVER_TYPES, MCP_CONNECTION_MODES, DEFAULT_MCP_CONNECTION_MODE } from '@dexto/core'; type McpServersConfig = NonNullable; interface McpServersSectionProps { value: McpServersConfig; onChange: (value: McpServersConfig) => void; errors?: Record; open?: boolean; onOpenChange?: (open: boolean) => void; errorCount?: number; sectionErrors?: string[]; } export function McpServersSection({ value, onChange, errors = {}, open, onOpenChange, errorCount = 0, sectionErrors = [], }: McpServersSectionProps) { const [expandedServers, setExpandedServers] = useState>(new Set()); // Local state for text fields that need special parsing (args, env, headers) // Key is "serverName:fieldName", value is the raw string being edited const [editingFields, setEditingFields] = useState>({}); const servers = Object.entries(value || {}); const toggleServer = (name: string) => { setExpandedServers((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }; const addServer = () => { const newName = `server-${Object.keys(value || {}).length + 1}`; onChange({ ...value, [newName]: { type: 'stdio', command: '', connectionMode: 'strict', }, }); setExpandedServers((prev) => new Set(prev).add(newName)); }; const removeServer = (name: string) => { const newValue = { ...value }; delete newValue[name]; onChange(newValue); setExpandedServers((prev) => { const next = new Set(prev); next.delete(name); return next; }); }; const updateServer = ( oldName: string, updates: Partial & { name?: string }> ) => { const server = value[oldName]; // Extract name from updates if present (it's not part of the server config, just used for the key) const { name: newName, ...serverUpdates } = updates; const newServer = { ...server, ...serverUpdates } as McpServersConfig[string]; // If name changed via updates, handle the name change if (newName && typeof newName === 'string' && newName !== oldName) { // Guard against collision: prevent overwriting an existing server if (value[newName]) { // TODO: Surface a user-facing error via onChange/errors map or toast notification return; // No-op to avoid overwriting an existing server } const newValue = { ...value }; delete newValue[oldName]; newValue[newName] = newServer; onChange(newValue); // Update expanded state setExpandedServers((prev) => { const next = new Set(prev); if (next.has(oldName)) { next.delete(oldName); next.add(newName); } return next; }); } else { onChange({ ...value, [oldName]: newServer }); } }; // Get the current value for a field (either from editing state or from config) const getFieldValue = (serverName: string, fieldName: string, fallback: string): string => { const key = `${serverName}:${fieldName}`; return editingFields[key] ?? fallback; }; // Update local editing state while typing const setFieldValue = (serverName: string, fieldName: string, value: string) => { const key = `${serverName}:${fieldName}`; setEditingFields((prev) => ({ ...prev, [key]: value })); }; // Clear editing state for a field const clearFieldValue = (serverName: string, fieldName: string) => { const key = `${serverName}:${fieldName}`; setEditingFields((prev) => { const next = { ...prev }; delete next[key]; return next; }); }; // Parse and commit args on blur const commitArgs = (serverName: string, argsString: string) => { clearFieldValue(serverName, 'args'); if (!argsString.trim()) { updateServer(serverName, { args: undefined }); return; } const args = argsString .split(',') .map((arg) => arg.trim()) .filter(Boolean); updateServer(serverName, { args: args.length > 0 ? args : undefined }); }; // Parse and commit env on blur const commitEnv = (serverName: string, envString: string) => { clearFieldValue(serverName, 'env'); if (!envString.trim()) { updateServer(serverName, { env: undefined }); return; } const env: Record = {}; envString .split('\n') .map((line) => line.trim()) .filter(Boolean) .forEach((line) => { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { env[key.trim()] = valueParts.join('=').trim(); } }); updateServer(serverName, { env: Object.keys(env).length > 0 ? env : undefined }); }; // Parse and commit headers on blur const commitHeaders = (serverName: string, headersString: string) => { clearFieldValue(serverName, 'headers'); if (!headersString.trim()) { updateServer(serverName, { headers: undefined }); return; } const headers: Record = {}; headersString .split('\n') .map((line) => line.trim()) .filter(Boolean) .forEach((line) => { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { headers[key.trim()] = valueParts.join('=').trim(); } }); updateServer(serverName, { headers: Object.keys(headers).length > 0 ? headers : undefined, }); }; return (
{servers.length === 0 ? (

No MCP servers configured

) : ( servers.map(([name, server]) => { const isExpanded = expandedServers.has(name); return (
{/* Server Header */}
{/* Server Details */} {isExpanded && (
{/* Server Name */}
Server Name updateServer(name, { name: e.target.value }) } placeholder="e.g., filesystem" />
{/* Server Type */}
Connection Type *
{/* stdio-specific fields */} {server.type === 'stdio' && ( <> {/* Command */}
Command * updateServer(name, { command: e.target.value, }) } placeholder="e.g., npx, node, python" aria-invalid={ !!errors[`mcpServers.${name}.command`] } /> {errors[`mcpServers.${name}.command`] && (

{errors[`mcpServers.${name}.command`]}

)}
{/* Arguments */}
Arguments setFieldValue( name, 'args', e.target.value ) } onBlur={(e) => commitArgs(name, e.target.value) } placeholder="--port, 3000, --host, localhost" className="font-mono" />
{/* Environment Variables */}
Environment Variables