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 }) => (
);
/**
* 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 }) => (
);
/**
* PostgreSQL configuration fields component
*/
const PostgresConfigFields: React.FC<{
register: any;
errors: any;
}> = ({ register, errors }) => (
);
/**
* SQLite configuration fields component
*/
const SqliteConfigFields: React.FC<{
register: any;
errors: any;
}> = ({ register, errors }) => (
);
/**
* Custom configuration fields component
*/
const CustomConfigFields: React.FC<{
register: any;
errors: any;
}> = ({ register, errors }) => (
);
/**
* Configuration fields container component
*/
const ConfigFields: React.FC<{
selectedType: MCPServerType;
register: any;
errors: any;
validationErrors: Record;
fieldRequirements: Record;
handleFieldBlur: (field: string) => void;
}> = ({ selectedType, register, errors, validationErrors, fieldRequirements, handleFieldBlur }) => {
switch (selectedType) {
case 'filesystem':
return ;
case 'github':
return ;
case 'postgres':
return ;
case 'sqlite':
return ;
case 'custom':
return ;
default:
return null;
}
};
return (
);
};