import React, { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { FiServer, FiDatabase, FiGithub, FiHardDrive, FiSettings, FiInfo, } from 'react-icons/fi'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../ui/dialog'; import { Button } from '../ui'; import { Input } from '../ui'; import { Textarea } from '../ui/textarea'; import Typography from '../ui/Typography'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../ui/select'; import { cn } from '../../lib/utils'; import { AddMCPServerProps, MCPServerType, MCPServerFormData, MCPServerConfigType, } from '../../types/mcp'; import { MCPServerFormValidator, FieldError } from '../../lib/mcp-validation'; import { FiAlertCircle } from 'react-icons/fi'; interface FormData { name: string; type: MCPServerType; rootPath?: string; allowedPaths?: string; excludePaths?: string; readOnly?: boolean; token?: string; owner?: string; repo?: string; branch?: string; host?: string; port?: number; database?: string; username?: string; password?: string; ssl?: boolean; path?: string; command?: string; args?: string; env?: string; cwd?: string; } const serverTypes = [ { type: 'filesystem' as MCPServerType, name: 'Filesystem', description: 'Access local files and directories', icon: , }, { type: 'github' as MCPServerType, name: 'GitHub', description: 'Interact with GitHub repositories', icon: , }, { type: 'postgres' as MCPServerType, name: 'PostgreSQL', description: 'Connect to PostgreSQL databases', icon: , }, { type: 'sqlite' as MCPServerType, name: 'SQLite', description: 'Access SQLite database files', icon: , }, { type: 'custom' as MCPServerType, name: 'Custom', description: 'Run custom MCP server commands', icon: , }, ]; /** * Helper component to display field requirements */ const FieldRequirement: React.FC<{ requirement: string }> = ({ requirement, }) => { if (!requirement) return null; return (
{requirement}
); }; /** * Modal component for adding/editing MCP servers * @param props - Add server props including modal state and handlers * @returns Modal with form for server configuration */ export interface AddMCPServerPropsExtended extends AddMCPServerProps { template?: { name: string; type: MCPServerType; config: Record; requirements?: string[]; }; } export const AddMCPServer: React.FC = ({ isOpen, onClose, onSubmit, editingServer, template, }) => { const [selectedType, setSelectedType] = useState('filesystem'); const [isSubmitting, setIsSubmitting] = useState(false); const [validationErrors, setValidationErrors] = useState< Record >({}); const [fieldRequirements, setFieldRequirements] = useState< Record >({}); const { register, handleSubmit, watch, setValue, reset, formState: { errors }, trigger, getValues, } = useForm({ defaultValues: { name: '', type: 'filesystem', readOnly: false, ssl: false, port: 5432, }, }); const watchedType = watch('type'); useEffect(() => { setSelectedType(watchedType); const requirements = MCPServerFormValidator.getAllFieldRequirements(watchedType); setFieldRequirements(requirements); setValidationErrors({}); }, [watchedType]); useEffect(() => { if (editingServer) { const formData: any = { name: editingServer.name, type: editingServer.type, ...editingServer.config, }; if ( editingServer.type === 'custom' && editingServer.config.type === 'custom' && editingServer.config.env ) { formData.env = Object.entries(editingServer.config.env) .map(([key, value]) => `${key}=${value}`) .join('\n'); } if ( editingServer.type === 'custom' && editingServer.config.type === 'custom' && editingServer.config.args ) { formData.args = editingServer.config.args.join(' '); } reset(formData); setSelectedType(editingServer.type); } else if (template) { const templateData: any = { name: template.name, type: template.type, ...template.config, }; if (template.type === 'custom' && template.config?.env) { templateData.env = Object.entries(template.config.env) .map(([key, value]) => `${key}=${value}`) .join('\n'); } if (template.type === 'custom' && template.config?.args) { templateData.args = template.config.args.join(' '); } if (templateData.rootPath === '$HOME') { templateData.rootPath = process.env.HOME || process.env.USERPROFILE || ''; } reset(templateData); setSelectedType(template.type); } else { reset({ name: '', type: 'filesystem', readOnly: false, ssl: false, port: 5432, }); setSelectedType('filesystem'); } }, [editingServer, template, reset]); /** * Validate a single field in real-time */ const validateField = (field: string, value: unknown) => { const validators = MCPServerFormValidator.getValidationHelpers(selectedType); const defaultValidators = MCPServerFormValidator.getValidationHelpers('filesystem'); let error: FieldError | null = null; if (field === 'name') { error = defaultValidators.validateField(field, value); } else { error = validators.validateField(field, value); } setValidationErrors((prev) => { const newErrors = { ...prev }; if (error) { newErrors[field] = error.message; } else { delete newErrors[field]; } return newErrors; }); }; /** * Handle field blur to trigger validation */ const handleFieldBlur = (field: string) => { const value = getValues(field as keyof FormData); validateField(field, value); }; const handleFormSubmit = async (data: FormData) => { setIsSubmitting(true); try { const allErrors = MCPServerFormValidator.validateAllFields( data.type, data ); if (allErrors.length > 0) { const errorMap: Record = {}; allErrors.forEach((error) => { errorMap[error.field] = error.message; }); setValidationErrors(errorMap); setIsSubmitting(false); return; } const config: Record = { type: data.type }; switch (data.type) { case 'filesystem': config.rootPath = data.rootPath; if (data.allowedPaths) { config.allowedPaths = data.allowedPaths .split(',') .map((p) => p.trim()); } if (data.excludePaths) { config.excludePaths = data.excludePaths .split(',') .map((p) => p.trim()); } config.readOnly = data.readOnly; break; case 'github': config.token = data.token; config.owner = data.owner; config.repo = data.repo; config.branch = data.branch || 'main'; break; case 'postgres': config.host = data.host; config.port = data.port || 5432; config.database = data.database; config.username = data.username; config.password = data.password; config.ssl = data.ssl; break; case 'sqlite': config.path = data.path; config.readOnly = data.readOnly; break; case 'custom': config.command = data.command; if (data.args) { config.args = data.args.split(' ').filter((arg) => arg.trim()); } if (data.env) { config.env = data.env.split('\n').reduce( (acc, line) => { const [key, value] = line.split('=', 2); if (key && value) { acc[key.trim()] = value.trim(); } return acc; }, {} as Record ); } config.cwd = data.cwd; break; } const formData: MCPServerFormData = { name: data.name, type: data.type, config: config as MCPServerConfigType, }; await onSubmit(formData); onClose(); } catch (error) { } finally { setIsSubmitting(false); } }; /** * Filesystem configuration fields component */ const FilesystemConfigFields: React.FC<{ register: any; errors: any; validationErrors: Record; fieldRequirements: Record; handleFieldBlur: (field: string) => void; }> = ({ register, errors, validationErrors, fieldRequirements, handleFieldBlur }) => (
handleFieldBlur('rootPath')} className={cn( validationErrors.rootPath && 'border-red-500 focus:border-red-500' )} /> {(errors.rootPath || validationErrors.rootPath) && ( {errors.rootPath?.message || validationErrors.rootPath} )}
); /** * GitHub configuration fields component */ const GitHubConfigFields: React.FC<{ register: any; errors: any; validationErrors: Record; fieldRequirements: Record; handleFieldBlur: (field: string) => void; }> = ({ register, errors, validationErrors, fieldRequirements, handleFieldBlur }) => (
handleFieldBlur('token')} className={cn( validationErrors.token && 'border-red-500 focus:border-red-500' )} /> {(errors.token || validationErrors.token) && ( {errors.token?.message || validationErrors.token} )}
{errors.owner && ( {errors.owner.message} )}
{errors.repo && ( {errors.repo.message} )}
); /** * PostgreSQL configuration fields component */ const PostgresConfigFields: React.FC<{ register: any; errors: any; }> = ({ register, errors }) => (
{errors.host && ( {errors.host.message} )}
{errors.port && ( {errors.port.message} )}
{errors.database && ( {errors.database.message} )}
{errors.username && ( {errors.username.message} )}
{errors.password && ( {errors.password.message} )}
); /** * SQLite configuration fields component */ const SqliteConfigFields: React.FC<{ register: any; errors: any; }> = ({ register, errors }) => (
{errors.path && ( {errors.path.message} )}
); /** * Custom configuration fields component */ const CustomConfigFields: React.FC<{ register: any; errors: any; }> = ({ register, errors }) => (
{errors.command && ( {errors.command.message} )}
Enter an npm package name (e.g., @your-org/mcp-server) to run with npx, or a full path to a local executable