import React, { useState, useEffect } from 'react'; import type { McpServerConfig, StdioServerConfig, SseServerConfig, HttpServerConfig, } from '@dexto/core'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose, } from './ui/dialog'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { KeyValueEditor } from './ui/key-value-editor'; import { Checkbox } from './ui/checkbox'; import { useAddServer } from './hooks/useServers'; interface ConnectServerModalProps { isOpen: boolean; onClose: () => void; onServerConnected?: () => void; initialName?: string; initialConfig?: | Partial | Partial | Partial; lockName?: boolean; } export default function ConnectServerModal({ isOpen, onClose, onServerConnected, initialName, initialConfig, lockName, }: ConnectServerModalProps) { const addServerMutation = useAddServer(); const [serverName, setServerName] = useState(''); const [serverType, setServerType] = useState<'stdio' | 'sse' | 'http'>('stdio'); const [command, setCommand] = useState(''); const [args, setArgs] = useState(''); const [url, setUrl] = useState(''); const [headerPairs, setHeaderPairs] = useState< Array<{ key: string; value: string; id: string }> >([]); const [envPairs, setEnvPairs] = useState>([]); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [persistToAgent, setPersistToAgent] = useState(false); // Helper function to convert header pairs to record const headersToRecord = ( pairs: Array<{ key: string; value: string; id: string }> ): Record => { const headers: Record = {}; pairs.forEach((pair) => { if (pair.key.trim() && pair.value.trim()) { headers[pair.key.trim()] = pair.value.trim(); } }); return headers; }; // Helper function to convert env pairs to record const envToRecord = ( pairs: Array<{ key: string; value: string; id: string }> ): Record => { const env: Record = {}; for (const { key, value } of pairs) { const k = key.trim(); const v = value.trim(); if (k && v) env[k] = v; } return env; }; // Helper function to mask sensitive environment values for logging const maskSensitiveEnv = (env: Record): Record => { const sensitiveKeys = ['api_key', 'secret', 'token', 'password', 'key']; const masked: Record = {}; for (const [key, value] of Object.entries(env)) { const isSensitive = sensitiveKeys.some((sk) => key.toLowerCase().includes(sk)); masked[key] = isSensitive ? '***masked***' : value; } return masked; }; // Helper to mask sensitive headers for logging const maskSensitiveHeaders = (headers: Record): Record => { const sensitive = [ 'authorization', 'proxy-authorization', 'api-key', 'x-api-key', 'token', 'cookie', 'set-cookie', ]; const masked: Record = {}; for (const [k, v] of Object.entries(headers)) { const key = k.toLowerCase(); const isSensitive = sensitive.some((s) => key === s || key.includes(s)); masked[k] = isSensitive ? '***masked***' : v; } return masked; }; useEffect(() => { if (!isOpen) { const timer = setTimeout(() => { setServerName(''); setServerType('stdio'); setCommand(''); setArgs(''); setUrl(''); setHeaderPairs([]); setEnvPairs([]); setError(null); setIsSubmitting(false); }, 300); return () => clearTimeout(timer); } }, [isOpen]); // Apply initialName/initialConfig when they change and modal opens useEffect(() => { if (!isOpen) return; setServerName(initialName ?? ''); const type = initialConfig?.type ?? 'stdio'; setServerType(type); if (type === 'stdio') { const std = (initialConfig ?? {}) as Partial; setCommand(typeof std.command === 'string' ? std.command : ''); setArgs(Array.isArray(std.args) ? std.args.join(', ') : ''); const envEntries = Object.entries(std.env ?? {}); setEnvPairs( envEntries.map(([key, value], idx) => ({ key, value: String(value ?? ''), id: `env-${idx}`, })) ); // clear URL/header state setUrl(''); setHeaderPairs([]); } else { const net = (initialConfig ?? {}) as Partial; setUrl(typeof net.url === 'string' ? net.url : ''); const hdrEntries = Object.entries(net.headers ?? {}); setHeaderPairs( hdrEntries.map(([key, value], idx) => ({ key, value: String(value ?? ''), id: `hdr-${idx}`, })) ); // clear stdio state setCommand(''); setArgs(''); setEnvPairs([]); } }, [isOpen, initialName, initialConfig]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsSubmitting(true); // Validate server name if (!serverName.trim()) { setError('Server name is required.'); setIsSubmitting(false); return; } // Validate required fields based on server type if (serverType === 'stdio') { if (!command.trim()) { setError('Command is required for stdio servers.'); setIsSubmitting(false); return; } // Validate environment variables const requiredKeys = envPairs.map((p) => p.key.trim()).filter(Boolean); if (requiredKeys.length) { const dupes = requiredKeys.filter((k, i) => requiredKeys.indexOf(k) !== i); if (dupes.length) { setError( `Duplicate environment variables: ${Array.from(new Set(dupes)).join(', ')}` ); setIsSubmitting(false); return; } const missing = envPairs .filter((p) => p.key.trim() && !p.value.trim()) .map((p) => p.key.trim()); if (missing.length) { setError(`Please set required environment variables: ${missing.join(', ')}`); setIsSubmitting(false); return; } } } else { if (!url.trim()) { setError(`URL is required for ${serverType.toUpperCase()} servers.`); setIsSubmitting(false); return; } try { new URL(url.trim()); } catch (_) { setError(`Invalid URL format for ${serverType.toUpperCase()} server.`); setIsSubmitting(false); return; } } // Create config after all validation passes let config: McpServerConfig; if (serverType === 'stdio') { config = { type: 'stdio', command: command.trim(), args: args .split(',') .map((s) => s.trim()) .filter(Boolean), env: envToRecord(envPairs), timeout: 30000, connectionMode: 'lenient', }; } else if (serverType === 'sse') { config = { type: 'sse', url: url.trim(), headers: headerPairs.length ? headersToRecord(headerPairs) : {}, timeout: 30000, connectionMode: 'lenient', }; } else { config = { type: 'http', url: url.trim(), headers: headerPairs.length ? headersToRecord(headerPairs) : {}, timeout: 30000, connectionMode: 'lenient', }; } try { await addServerMutation.mutateAsync({ name: serverName.trim(), config, persistToAgent, }); if (import.meta.env.DEV) { // Create a safe version for logging with masked sensitive values const safeConfig = { ...config }; if (safeConfig.type === 'stdio' && safeConfig.env) { safeConfig.env = maskSensitiveEnv(safeConfig.env); } else if ( (safeConfig.type === 'sse' || safeConfig.type === 'http') && safeConfig.headers ) { safeConfig.headers = maskSensitiveHeaders(safeConfig.headers); } console.debug( `[ConnectServerModal.handleSubmit] Connected server with config: ${JSON.stringify(safeConfig)}` ); } onServerConnected?.(); onClose(); } catch (err: unknown) { let message = 'Failed to connect server'; if (err instanceof Error) { message = err.message || message; } else if (typeof err === 'string') { message = err; } addServerMutation.reset(); setError(message); } finally { setIsSubmitting(false); } }; return ( !open && onClose()}> Connect New MCP Server Configure connection details for a new MCP server (stdio, SSE, or HTTP).
{error && ( {error} )}
setServerName(e.target.value)} className="col-span-3" placeholder="e.g., My Local Tools" required disabled={isSubmitting || !!lockName} />
{serverType === 'stdio' ? ( <>
setCommand(e.target.value)} className="col-span-3" placeholder="e.g., /path/to/executable or python" required disabled={isSubmitting} />
setArgs(e.target.value)} className="col-span-3" placeholder="Comma-separated, e.g., -m,script.py,--port,8080" disabled={isSubmitting} />
) : serverType === 'sse' ? ( <>
setUrl(e.target.value)} className="col-span-3" placeholder="e.g., http://localhost:8000/events" required disabled={isSubmitting} />
) : ( <>
setUrl(e.target.value)} className="col-span-3" placeholder="e.g., http://localhost:8080" required disabled={isSubmitting} />
)} {/* Persist to Agent Checkbox */}
setPersistToAgent(checked === true)} disabled={isSubmitting} />
); }