import { useState, useEffect } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { IconPlus, IconTrash, IconGripVertical, IconCodeDots, } from '@tabler/icons-react' import { MCPServerConfig } from '@/hooks/useMCPServers' import { useTranslation } from '@/i18n/react-i18next-compat' import { DndContext, closestCenter, useSensor, useSensors, PointerSensor, KeyboardSensor, } from '@dnd-kit/core' import { SortableContext, verticalListSortingStrategy, arrayMove, useSortable, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { cn } from '@/lib/utils' import CodeEditor from '@uiw/react-textarea-code-editor' import '@uiw/react-textarea-code-editor/dist.css' // Sortable argument item component function SortableArgItem({ id, value, onChange, onRemove, canRemove, placeholder, }: { id: number value: string onChange: (value: string) => void onRemove: () => void canRemove: boolean placeholder: string }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }) const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, } return (
onChange(e.target.value)} placeholder={placeholder} className="flex-1" /> {canRemove && (
)}
) } interface AddEditMCPServerProps { open: boolean onOpenChange: (open: boolean) => void editingKey: string | null initialData?: MCPServerConfig onSave: (name: string, config: MCPServerConfig) => void } export default function AddEditMCPServer({ open, onOpenChange, editingKey, initialData, onSave, }: AddEditMCPServerProps) { const { t } = useTranslation() const [serverName, setServerName] = useState('') const [command, setCommand] = useState('') const [args, setArgs] = useState(['']) const [envKeys, setEnvKeys] = useState(['']) const [envValues, setEnvValues] = useState(['']) const [transportType, setTransportType] = useState<'stdio' | 'http' | 'sse'>( 'stdio' ) const [url, setUrl] = useState('') const [headerKeys, setHeaderKeys] = useState(['']) const [headerValues, setHeaderValues] = useState(['']) const [timeout, setTimeout] = useState('') const [isToggled, setIsToggled] = useState(false) const [jsonContent, setJsonContent] = useState('') const [error, setError] = useState(null) // Reset form when modal opens/closes or editing key changes useEffect(() => { if (open && editingKey && initialData) { setServerName(editingKey) setCommand(initialData.command || '') setUrl(initialData.url || '') setTimeout(initialData.timeout ? initialData.timeout.toString() : '') setArgs(initialData.args?.length > 0 ? initialData.args : ['']) setTransportType(initialData?.type || 'stdio') // Initialize JSON content for toggle mode try { const jsonData = { [editingKey]: initialData } setJsonContent(JSON.stringify(jsonData, null, 2)) } catch { setJsonContent('') } if (initialData.env) { // Convert env object to arrays of keys and values const keys = Object.keys(initialData.env) const values = keys.map((key) => initialData.env[key]) setEnvKeys(keys.length > 0 ? keys : ['']) setEnvValues(values.length > 0 ? values : ['']) } if (initialData.headers) { // Convert headers object to arrays of keys and values const headerKeysList = Object.keys(initialData.headers) const headerValuesList = headerKeysList.map( (key) => initialData.headers![key] ) setHeaderKeys(headerKeysList.length > 0 ? headerKeysList : ['']) setHeaderValues(headerValuesList.length > 0 ? headerValuesList : ['']) } } else if (open) { // Add mode - reset form resetForm() } }, [open, editingKey, initialData]) const resetForm = () => { setServerName('') setCommand('') setUrl('') setTimeout('') setArgs(['']) setEnvKeys(['']) setEnvValues(['']) setHeaderKeys(['']) setHeaderValues(['']) setTransportType('stdio') setIsToggled(false) setJsonContent('') setError(null) } const handleAddArg = () => { setArgs([...args, '']) } const handleRemoveArg = (index: number) => { const newArgs = [...args] newArgs.splice(index, 1) setArgs(newArgs.length > 0 ? newArgs : ['']) } const handleArgChange = (index: number, value: string) => { const newArgs = [...args] newArgs[index] = value setArgs(newArgs) } const handleReorderArgs = (oldIndex: number, newIndex: number) => { setArgs(arrayMove(args, oldIndex, newIndex)) } // Sensors for drag and drop const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { delay: 100, tolerance: 5, }, }), useSensor(KeyboardSensor) ) const handleAddEnv = () => { setEnvKeys([...envKeys, '']) setEnvValues([...envValues, '']) } const handleRemoveEnv = (index: number) => { const newKeys = [...envKeys] const newValues = [...envValues] newKeys.splice(index, 1) newValues.splice(index, 1) setEnvKeys(newKeys.length > 0 ? newKeys : ['']) setEnvValues(newValues.length > 0 ? newValues : ['']) } const handleEnvKeyChange = (index: number, value: string) => { const newKeys = [...envKeys] newKeys[index] = value setEnvKeys(newKeys) } const handleEnvValueChange = (index: number, value: string) => { const newValues = [...envValues] newValues[index] = value setEnvValues(newValues) } const handleAddHeader = () => { setHeaderKeys([...headerKeys, '']) setHeaderValues([...headerValues, '']) } const handleRemoveHeader = (index: number) => { const newKeys = [...headerKeys] const newValues = [...headerValues] newKeys.splice(index, 1) newValues.splice(index, 1) setHeaderKeys(newKeys.length > 0 ? newKeys : ['']) setHeaderValues(newValues.length > 0 ? newValues : ['']) } const handleHeaderKeyChange = (index: number, value: string) => { const newKeys = [...headerKeys] newKeys[index] = value setHeaderKeys(newKeys) } const handleHeaderValueChange = (index: number, value: string) => { const newValues = [...headerValues] newValues[index] = value setHeaderValues(newValues) } const handleSave = () => { // Handle JSON mode if (isToggled) { try { const parsedData = JSON.parse(jsonContent) // Validate that it's an object with server configurations if (typeof parsedData !== 'object' || parsedData === null) { setError(t('mcp-servers:editJson.errorFormat')) return } // Check if this looks like a server config object instead of the expected format if (parsedData.command || parsedData.url) { setError(t('mcp-servers:editJson.errorMissingServerNameKey')) return } // For each server in the JSON, validate serverName and config for (const [serverName, config] of Object.entries(parsedData)) { const trimmedServerName = serverName.trim() if (!trimmedServerName) { setError(t('mcp-servers:editJson.errorServerName')) return } // Validate the config object const serverConfig = config as MCPServerConfig // Validate type field if present if ( serverConfig.type && !['stdio', 'http', 'sse'].includes(serverConfig.type) ) { setError( t('mcp-servers:editJson.errorInvalidType', { serverName: trimmedServerName, type: serverConfig.type, }) ) return } onSave(trimmedServerName, serverConfig as MCPServerConfig) } onOpenChange(false) resetForm() setError(null) return } catch { setError(t('mcp-servers:editJson.errorFormat')) return } } // Handle form mode // Convert env arrays to object const envObj: Record = {} envKeys.forEach((key, index) => { const keyName = key.trim() if (keyName !== '') { envObj[keyName] = envValues[index]?.trim() || '' } }) // Convert headers arrays to object const headersObj: Record = {} headerKeys.forEach((key, index) => { const keyName = key.trim() if (keyName !== '') { headersObj[keyName] = headerValues[index]?.trim() || '' } }) // Filter out empty args const filteredArgs = args.map((arg) => arg.trim()).filter((arg) => arg) const config: MCPServerConfig = { command: transportType === 'stdio' ? command.trim() : '', args: transportType === 'stdio' ? filteredArgs : [], env: transportType === 'stdio' ? envObj : {}, type: transportType, ...(transportType !== 'stdio' && { url: url.trim(), headers: Object.keys(headersObj).length > 0 ? headersObj : undefined, timeout: timeout.trim() !== '' ? parseInt(timeout) : undefined, }), } if (serverName.trim() !== '') { onSave(serverName.trim(), config) onOpenChange(false) resetForm() } } return ( { e.preventDefault() }} > {editingKey ? t('mcp-servers:editServer') : t('mcp-servers:addServer')}
setIsToggled(!isToggled)} >
{isToggled ? (
{ setJsonContent(e.target.value) setError(null) }} onPaste={() => setError(null)} style={{ fontFamily: 'ui-monospace', backgroundColor: 'transparent', wordBreak: 'break-all', overflowWrap: 'anywhere', whiteSpace: 'pre-wrap', }} className="w-full !text-sm min-h-[300px]" />
{error &&
{error}
}
) : (
setServerName(e.target.value)} placeholder={t('mcp-servers:enterServerName')} autoFocus />
setTransportType(value as 'http' | 'sse') } className="flex gap-6" >
{transportType === 'stdio' ? (
setCommand(e.target.value)} placeholder={t('mcp-servers:enterCommand')} />
) : (
setUrl(e.target.value)} placeholder="Enter URL" />
)} {transportType === 'stdio' && (
{ const { active, over } = event if (active.id !== over?.id) { const oldIndex = parseInt(active.id.toString()) const newIndex = parseInt(over?.id.toString() || '0') handleReorderArgs(oldIndex, newIndex) } }} > index)} strategy={verticalListSortingStrategy} > {args.map((arg, index) => ( handleArgChange(index, value)} onRemove={() => handleRemoveArg(index)} canRemove={args.length > 1} placeholder={t('mcp-servers:argument', { index: index + 1, })} /> ))}
)} {transportType === 'stdio' && (
{envKeys.map((key, index) => (
handleEnvKeyChange(index, e.target.value) } placeholder={t('mcp-servers:key')} className="flex-1" /> handleEnvValueChange(index, e.target.value) } placeholder={t('mcp-servers:value')} className="flex-1" /> {envKeys.length > 1 && (
handleRemoveEnv(index)} >
)}
))}
)} {(transportType === 'http' || transportType === 'sse') && ( <>
{headerKeys.map((key, index) => (
handleHeaderKeyChange(index, e.target.value) } placeholder="Header name" className="flex-1" /> handleHeaderValueChange(index, e.target.value) } placeholder="Header value" className="flex-1" /> {headerKeys.length > 1 && (
handleRemoveHeader(index)} >
)}
))}
setTimeout(e.target.value)} placeholder="Enter timeout in seconds" type="number" />
)}
)}
) }