import { PlusCircle, Loader2, Check } from 'lucide-react'; import { CircleAlert } from 'lucide-react'; import { useState, useCallback } from 'react'; import type { ReactNode } from 'react'; import type { MCPClient, StdioMCPConfig, HttpMCPConfig } from '@/api/types'; import { Alert, AlertDescription } from '@/components/ui/alert.tsx'; import { Button } from '@/components/ui/button.tsx'; import { Checkbox } from '@/components/ui/checkbox.tsx'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog.tsx'; import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldSet, } from '@/components/ui/field.tsx'; import { InputGroup, InputGroupTextarea } from '@/components/ui/input-group.tsx'; import { useTranslation } from '@/i18n/useI18n.ts'; type Status = 'idle' | 'loading' | 'success' | 'error'; interface Props { children: ReactNode; onAdd: (mcps: MCPClient[]) => Promise; } function parseMcpConfig( raw: string, keepAlive: boolean, t: (key: string, opts?: Record) => string, ): MCPClient[] { let parsed: unknown; try { parsed = JSON.parse(raw); } catch (e) { throw new Error(t('dialog-mcp-create.parseError', { message: (e as Error).message })); } const obj = parsed as Record; const servers = obj.mcpServers as Record> | undefined; if (!servers || typeof servers !== 'object') { throw new Error(t('dialog-mcp-create.missingMcpServers')); } const entries = Object.entries(servers); if (entries.length === 0) { throw new Error(t('dialog-mcp-create.emptyMcpServers')); } return entries.map(([name, config]) => { let mcp_config: StdioMCPConfig | HttpMCPConfig; if ('url' in config) { mcp_config = { type: 'http_mcp', url: config.url as string, headers: (config.headers as Record | undefined) ?? null, timeout: (config.timeout as number | undefined) ?? null, }; } else { mcp_config = { type: 'stdio_mcp', command: config.command as string, args: (config.args as string[] | undefined) ?? null, env: (config.env as Record | undefined) ?? null, cwd: (config.cwd as string | undefined) ?? null, }; } return { name, is_stateful: keepAlive, mcp_config }; }); } export const CreateMCPDialog = ({ children, onAdd }: Props) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [configValue, setConfigValue] = useState(''); const [keepAlive, setKeepAlive] = useState(true); const [status, setStatus] = useState('idle'); const [errorMsg, setErrorMsg] = useState(''); const reset = useCallback(() => { setConfigValue(''); setKeepAlive(true); setStatus('idle'); setErrorMsg(''); }, []); const handleOpenChange = useCallback( (next: boolean) => { if (!next) reset(); setOpen(next); }, [reset], ); const handleAdd = useCallback(async () => { setErrorMsg(''); let mcpClients: MCPClient[]; try { mcpClients = parseMcpConfig(configValue, keepAlive, t); } catch (e) { setErrorMsg((e as Error).message); setStatus('error'); return; } setStatus('loading'); try { await onAdd(mcpClients); setStatus('success'); setTimeout(() => { handleOpenChange(false); }, 1500); } catch (e) { // ApiErrors are already shown via the global toast in client.ts. // Show only local validation errors (e.g. duplicate name) inline. const isApiError = e instanceof Error && e.name === 'ApiError'; if (!isApiError) { setErrorMsg(e instanceof Error ? e.message : String(e)); } setStatus('idle'); } }, [configValue, keepAlive, t, onAdd, handleOpenChange]); return ( {children} {t('dialog-mcp-create.title')} {t('dialog-mcp-create.description')}
{t('dialog-mcp-create.configLabel')} setConfigValue(e.target.value)} placeholder={ '{\n "mcpServers": {\n "playwright": {\n "command": "npx",\n "args": ["@playwright/mcp@latest"]\n }\n }\n}' } /> setKeepAlive(v === true)} /> {t('dialog-mcp-create.keepAlive')} {t('dialog-mcp-create.keepAliveDesc')}
{errorMsg && ( {errorMsg} )}
); };