import React, { useState } from 'react'; import type { ServerRegistryEntry } from '@dexto/registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from './ui/dialog'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Label } from './ui/label'; import { Textarea } from './ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { Alert, AlertDescription } from './ui/alert'; import { Plus, Save } from 'lucide-react'; import { KeyValueEditor } from './ui/key-value-editor'; interface HeaderPair { key: string; value: string; id: string; } interface AddCustomServerModalProps { isOpen: boolean; onClose: () => void; onAddServer: ( entry: Omit ) => Promise; } export default function AddCustomServerModal({ isOpen, onClose, onAddServer, }: AddCustomServerModalProps) { const [formData, setFormData] = useState<{ name: string; description: string; category: | 'productivity' | 'development' | 'research' | 'creative' | 'data' | 'communication' | 'custom'; icon: string; version: string; author: string; homepage: string; config: { type: 'stdio' | 'sse' | 'http'; command: string; args: string[]; url: string; env: Record; headers: Record; timeout: number; }; tags: string[]; isInstalled: boolean; requirements: { platform: 'win32' | 'darwin' | 'linux' | 'all'; node: string; python: string; dependencies: string[]; }; }>({ name: '', description: '', category: 'custom', icon: '', version: '', author: '', homepage: '', config: { type: 'stdio', command: '', args: [], url: '', env: {}, headers: {}, timeout: 30000, }, tags: [], isInstalled: false, requirements: { platform: 'all', node: '', python: '', dependencies: [], }, }); const [argsInput, setArgsInput] = useState(''); const [tagsInput, setTagsInput] = useState(''); const [envInput, setEnvInput] = useState(''); const [headerPairs, setHeaderPairs] = useState([]); const [dependenciesInput, setDependenciesInput] = useState(''); const [error, setError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const categories = [ { value: 'productivity', label: 'Productivity' }, { value: 'development', label: 'Development' }, { value: 'research', label: 'Research' }, { value: 'creative', label: 'Creative' }, { value: 'data', label: 'Data' }, { value: 'communication', label: 'Communication' }, { value: 'custom', label: 'Custom' }, ]; const platforms = [ { value: 'all', label: 'All Platforms' }, { value: 'win32', label: 'Windows' }, { value: 'darwin', label: 'macOS' }, { value: 'linux', label: 'Linux' }, ]; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setIsSubmitting(true); try { // Parse inputs const args = argsInput .split(',') .map((s) => s.trim()) .filter(Boolean); const tags = tagsInput .split(',') .map((s) => s.trim()) .filter(Boolean); const dependencies = dependenciesInput .split(',') .map((s) => s.trim()) .filter(Boolean); // Parse environment variables const env: Record = {}; if (envInput.trim()) { const envLines = envInput.split('\n'); for (const line of envLines) { // Skip empty lines const trimmedLine = line.trim(); if (!trimmedLine) { continue; } // Split only at the first '=' character const equalIndex = trimmedLine.indexOf('='); if (equalIndex > 0) { // Key must exist (equalIndex > 0, not >= 0) const key = trimmedLine.substring(0, equalIndex).trim(); const value = trimmedLine.substring(equalIndex + 1).trim(); // Only add if key is not empty if (key) { env[key] = value; // Value can be empty string } } } } // Convert header pairs to record const headers: Record = {}; headerPairs.forEach((pair) => { if (pair.key.trim() && pair.value.trim()) { headers[pair.key.trim()] = pair.value.trim(); } }); // Validate required fields if (!formData.name.trim()) { throw new Error('Server name is required'); } if (!formData.description.trim()) { throw new Error('Description is required'); } if (formData.config.type === 'stdio' && !formData.config.command.trim()) { throw new Error('Command is required for stdio servers'); } if (formData.config.type === 'sse' && !formData.config.url.trim()) { throw new Error('URL is required for SSE servers'); } if (formData.config.type === 'http' && !formData.config.url.trim()) { throw new Error('URL is required for HTTP servers'); } const entry: Omit = { ...formData, config: { ...formData.config, args, env, headers, }, tags, requirements: { ...formData.requirements, dependencies, }, }; await onAddServer(entry); onClose(); // Reset form setFormData({ name: '', description: '', category: 'custom', icon: '', version: '', author: '', homepage: '', config: { type: 'stdio', command: '', args: [], url: '', env: {}, headers: {}, timeout: 30000, }, tags: [], isInstalled: false, requirements: { platform: 'all', node: '', python: '', dependencies: [], }, }); setArgsInput(''); setTagsInput(''); setEnvInput(''); setHeaderPairs([]); setDependenciesInput(''); } catch (err: any) { setError(err.message || 'Failed to add custom server'); } finally { setIsSubmitting(false); } }; const handleConfigChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, config: { ...prev.config, [name]: value, }, })); }; return ( !open && onClose()}> Add Custom Server to Registry Add your own custom MCP server configuration to the registry for easy reuse.
{error && ( {error} )} {/* Basic Information */}
setFormData((prev) => ({ ...prev, name: e.target.value })) } placeholder="My Custom Server" required />