UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

1,164 lines (1,016 loc) 33.2 kB
import { spawn, ChildProcess } from 'child_process'; import { join, resolve, normalize } from 'path'; import * as path from 'path'; import { logger } from '../core/logger.js'; import chalk from 'chalk'; import { readFile, writeFile, access, stat, readdir } from 'fs/promises'; import { existsSync } from 'fs'; import axios from 'axios'; import { SmitheryMCPServer, SmitheryMCPConfig } from './smithery-mcp-server.js'; import { AdvancedSecurityValidator, ValidationResult, } from '../core/security/advanced-security-validator.js'; import { InputSanitizer } from '../core/security/input-sanitizer.js'; export interface MCPServerConfig { filesystem: { enabled: boolean; restrictedPaths: string[]; allowedPaths: string[]; }; git: { enabled: boolean; autoCommitMessages: boolean; safeModeEnabled: boolean; }; terminal: { enabled: boolean; allowedCommands: string[]; blockedCommands: string[]; }; packageManager: { enabled: boolean; autoInstall: boolean; securityScan: boolean; }; smithery?: { enabled: boolean; apiKey?: string; enabledServers?: string[]; autoDiscovery?: boolean; }; } export interface MCPServer { id: string; name: string; process?: ChildProcess; enabled: boolean; status: 'stopped' | 'starting' | 'running' | 'error'; lastError?: string; capabilities?: ServerCapabilities; performance?: PerformanceMetrics; } export interface ServerCapabilities { tools: ToolInfo[]; resources: ResourceInfo[]; prompts: PromptInfo[]; lastDiscovered: Date; } export interface ToolInfo { name: string; description: string; inputSchema: any; } export interface ResourceInfo { uri: string; name: string; description?: string; mimeType?: string; } export interface PromptInfo { name: string; description?: string; arguments?: any[]; } export interface PerformanceMetrics { avgResponseTime: number; successRate: number; lastHealthCheck: Date; availability: number; } /** * MCP Server Manager * * Manages Model Context Protocol servers for extended functionality * Provides safe, sandboxed access to file system, git, terminal, and package management */ export class MCPServerManager { private config: MCPServerConfig; private servers: Map<string, MCPServer>; private smitheryServer?: SmitheryMCPServer; private isInitialized = false; private securityValidator: AdvancedSecurityValidator; constructor(config: MCPServerConfig) { this.config = config; this.servers = new Map(); this.securityValidator = new AdvancedSecurityValidator({ allowCodeExecution: false, allowFileAccess: true, allowNetworkAccess: false, requireSandbox: true, maxInputLength: 10000, }); this.initializeServers(); } /** * Initialize all enabled MCP servers */ private initializeServers(): void { const serverConfigs = [ { name: 'filesystem', enabled: this.config.filesystem.enabled }, { name: 'git', enabled: this.config.git.enabled }, { name: 'terminal', enabled: this.config.terminal.enabled }, { name: 'packageManager', enabled: this.config.packageManager.enabled }, { name: 'smithery', enabled: this.config.smithery?.enabled ?? false }, ]; serverConfigs.forEach(({ name, enabled }) => { this.servers.set(name, { id: name, name, enabled, status: 'stopped', }); }); logger.info(`Initialized ${this.servers.size} MCP servers`); } /** * Start all enabled MCP servers with timeout optimization */ async startServers(): Promise<void> { if (this.isInitialized) { logger.warn('MCP servers already initialized'); return; } console.log(chalk.blue('🔧 Starting MCP servers with timeout optimization...')); const startTime = Date.now(); // Create timeout-optimized promises for each server const enabledServers = Array.from(this.servers.values()).filter(server => server.enabled); const serverPromises = enabledServers.map(async server => { const serverStartTime = Date.now(); try { // Race between server startup and timeout await Promise.race([ this.startServer(server.name), new Promise((_, reject) => setTimeout(() => reject(new Error(`Server ${server.name} startup timeout`)), 5000) ) ]); const duration = Date.now() - serverStartTime; logger.info(`MCP server ${server.name} started in ${duration}ms`); } catch (error) { const duration = Date.now() - serverStartTime; logger.warn(`MCP server ${server.name} failed or timed out after ${duration}ms:`, error); server.status = 'error'; server.lastError = error instanceof Error ? error.message : 'Startup timeout'; } }); await Promise.allSettled(serverPromises); this.isInitialized = true; const totalTime = Date.now() - startTime; const runningServers = Array.from(this.servers.values()).filter(s => s.status === 'running'); const failedServers = Array.from(this.servers.values()).filter(s => s.status === 'error'); console.log(chalk.green(`✅ Started ${runningServers.length} MCP servers in ${totalTime}ms`)); if (failedServers.length > 0) { console.log(chalk.yellow(`⚠️ ${failedServers.length} servers failed or timed out`)); } } /** * Start a specific MCP server */ async startServer(serverName: string): Promise<void> { const server = this.servers.get(serverName); if (!server || !server.enabled) { return; } try { server.status = 'starting'; console.log(chalk.gray(` Starting ${serverName} server...`)); // For now, we'll implement built-in MCP functionality // rather than spawning external processes await this.initializeBuiltinServer(serverName); server.status = 'running'; logger.info(`MCP server started: ${serverName}`); } catch (error) { server.status = 'error'; server.lastError = error instanceof Error ? error.message : 'Unknown error'; logger.error(`Failed to start MCP server ${serverName}:`, error); } } /** * Initialize built-in MCP server functionality */ private async initializeBuiltinServer(serverName: string): Promise<void> { // For this implementation, we're creating built-in MCP-like functionality // In a full implementation, you might spawn actual MCP server processes switch (serverName) { case 'filesystem': await this.initializeFilesystemServer(); break; case 'git': await this.initializeGitServer(); break; case 'terminal': await this.initializeTerminalServer(); break; case 'packageManager': await this.initializePackageManagerServer(); break; case 'smithery': await this.initializeSmitheryServer(); break; } } /** * Stop all MCP servers */ async stopServers(): Promise<void> { console.log(chalk.yellow('🛑 Stopping MCP servers...')); const stopPromises = Array.from(this.servers.values()) .filter(server => server.status === 'running') .map(server => this.stopServer(server.name)); await Promise.allSettled(stopPromises); this.isInitialized = false; console.log(chalk.green('✅ All MCP servers stopped')); } /** * Stop a specific MCP server */ async stopServer(serverName: string): Promise<void> { const server = this.servers.get(serverName); if (!server) return; try { if (server.process) { server.process.kill(); server.process = undefined; } server.status = 'stopped'; logger.info(`MCP server stopped: ${serverName}`); } catch (error) { logger.error(`Failed to stop MCP server ${serverName}:`, error); } } /** * Get server status */ getServerStatus(serverName?: string): MCPServer | MCPServer[] { if (serverName) { const server = this.servers.get(serverName); if (!server) { throw new Error(`Server ${serverName} not found`); } return server; } return Array.from(this.servers.values()); } // Built-in MCP Server Implementations /** * Initialize filesystem server functionality */ private async initializeFilesystemServer(): Promise<void> { // Validate restricted paths for (const path of this.config.filesystem.restrictedPaths) { if (existsSync(path)) { logger.info(`Filesystem: Restricting access to ${path}`); } } } /** * Initialize git server functionality */ private async initializeGitServer(): Promise<void> { // Check if we're in a git repository try { await access('.git'); logger.info('Git: Repository detected'); } catch { logger.warn('Git: No repository found in current directory'); } } /** * Initialize terminal server functionality */ private async initializeTerminalServer(): Promise<void> { logger.info(`Terminal: Allowed commands: ${this.config.terminal.allowedCommands.join(', ')}`); logger.info(`Terminal: Blocked commands: ${this.config.terminal.blockedCommands.join(', ')}`); } /** * Initialize package manager server functionality */ private async initializePackageManagerServer(): Promise<void> { // Check for package.json try { await access('package.json'); logger.info('PackageManager: package.json found'); } catch { logger.warn('PackageManager: No package.json found'); } } /** * Initialize Smithery MCP server functionality */ private async initializeSmitheryServer(): Promise<void> { if (!this.config.smithery?.apiKey) { logger.warn('Smithery: API key not configured, skipping initialization'); return; } try { const smitheryConfig: SmitheryMCPConfig = { apiKey: this.config.smithery.apiKey, enabledServers: this.config.smithery.enabledServers || [], autoDiscovery: this.config.smithery.autoDiscovery ?? true, }; this.smitheryServer = new SmitheryMCPServer(smitheryConfig); await this.smitheryServer.getServer(); // Initialize the server logger.info('Smithery: MCP server initialized successfully'); // Log discovered servers for debugging const availableServers = this.smitheryServer.getAvailableServers(); const availableTools = this.smitheryServer.getAvailableTools(); logger.info(`Smithery: Discovered ${availableServers.length} servers with ${availableTools.length} tools`); if (availableServers.length > 0) { logger.debug('Smithery servers:', availableServers.map(s => s.qualifiedName)); } if (availableTools.length > 0) { logger.debug('Smithery tools:', availableTools.map(t => t.name)); } } catch (error) { logger.error('Failed to initialize Smithery server:', error); throw error; } } // MCP Server Operations /** * Safe file system operations - SECURITY FIX for path traversal */ async readFileSecure(filePath: string): Promise<string> { // SECURITY FIX: Additional validation using InputSanitizer const pathValidation = InputSanitizer.validateFilePath(filePath); if (!pathValidation.isValid) { const securityError = InputSanitizer.createSecurityError(pathValidation, 'file-read'); logger.error('Path traversal attempt blocked', securityError); throw new Error(`Security violation: ${pathValidation.violations.join(', ')}`); } if (!this.isPathAllowed(filePath)) { logger.warn('File access denied by path policy', { filePath }); throw new Error(`Access denied: ${filePath}`); } try { return await readFile(filePath, 'utf8'); } catch (error) { logger.error('File read operation failed', { filePath, error }); throw new Error( `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } async writeFileSecure(filePath: string, content: string): Promise<void> { if (!this.isPathAllowed(filePath)) { throw new Error(`Access denied: ${filePath}`); } try { await writeFile(filePath, content, 'utf8'); logger.info(`File written: ${filePath}`); } catch (error) { throw new Error( `Failed to write file: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } async listDirectorySecure(dirPath: string): Promise<string[]> { if (!this.isPathAllowed(dirPath)) { throw new Error(`Access denied: ${dirPath}`); } try { return await readdir(dirPath); } catch (error) { throw new Error( `Failed to list directory: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } async getFileStats(filePath: string): Promise<{ exists: boolean; isFile: boolean; isDirectory: boolean; size: number; modified: string; }> { if (!this.isPathAllowed(filePath)) { throw new Error(`Access denied: ${filePath}`); } try { const stats = await stat(filePath); return { exists: true, isFile: stats.isFile(), isDirectory: stats.isDirectory(), size: stats.size, modified: stats.mtime.toISOString(), }; } catch (error) { return { exists: false, isFile: false, isDirectory: false, size: 0, modified: '', }; } } /** * Safe command execution with enhanced security */ async executeCommandSecure(command: string, args: string[] = []): Promise<string> { // Input validation and sanitization const sanitizedCommand = await this.sanitizeCommandInput(command); const sanitizedArgs = await Promise.all(args.map(arg => this.sanitizeCommandInput(arg))); if (!this.isCommandAllowed(sanitizedCommand)) { throw new Error(`Command not allowed: ${sanitizedCommand}`); } // Additional security checks this.validateCommandSecurity(sanitizedCommand, sanitizedArgs); return new Promise((resolve, reject) => { const child = spawn(sanitizedCommand, sanitizedArgs, { cwd: this.getSafeCwd(), stdio: 'pipe', shell: false, // Prevent shell injection env: this.getSafeEnvironment(), timeout: 30000, // Built-in timeout }); let stdout = ''; let stderr = ''; child.stdout?.on('data', data => { const chunk = data.toString(); // Limit output size to prevent memory exhaustion if (stdout.length + chunk.length > 1024 * 1024) { // 1MB limit child.kill(); reject(new Error('Command output too large')); return; } stdout += chunk; }); child.stderr?.on('data', data => { const chunk = data.toString(); if (stderr.length + chunk.length > 1024 * 1024) { // 1MB limit child.kill(); reject(new Error('Command error output too large')); return; } stderr += chunk; }); child.on('close', code => { if (code === 0) { resolve(stdout); } else { reject(new Error(`Command failed with code ${code}: ${stderr.substring(0, 500)}`)); } }); child.on('error', error => { reject(new Error(`Command execution error: ${error.message}`)); }); // Additional timeout safeguard setTimeout(() => { if (!child.killed) { child.kill('SIGKILL'); reject(new Error('Command timeout - forcefully terminated')); } }, 35000); }); } /** * Sanitize command input to prevent injection */ private async sanitizeCommandInput(input: string): Promise<string> { if (typeof input !== 'string') { throw new Error('Command input must be a string'); } // Use the advanced security validator const validationResult: ValidationResult = await this.securityValidator.validateInput( input, 'command' ); if (!validationResult.isValid || validationResult.riskLevel === 'critical') { logger.warn('Command blocked due to security violations:', { input: input.substring(0, 100) + '...', violations: validationResult.violations, riskLevel: validationResult.riskLevel, }); throw new Error( `Command blocked: ${validationResult.violations[0]?.description || 'Security violation detected'}` ); } if (validationResult.riskLevel === 'high') { logger.warn('High-risk command detected but allowed:', { input: input.substring(0, 100) + '...', violations: validationResult.violations, }); } // Use sanitized input from validator const sanitized = validationResult.sanitizedInput || input; if (sanitized.trim().length === 0) { throw new Error('Command cannot be empty after sanitization'); } if (sanitized.length > 255) { throw new Error('Command too long'); } return sanitized.trim(); } /** * Additional security validation for commands */ private validateCommandSecurity(command: string, args: string[]): void { // Check for path traversal attempts if (command.includes('..') || args.some(arg => arg.includes('..'))) { throw new Error('Path traversal attempt detected'); } // Enhanced Guardian-level security patterns from OWASP Top 10 const suspiciousPatterns = [ // File system attacks /rm\s+-rf/i, /rmdir\s+/i, /del\s+/i, /format\s+/i, // Privilege escalation /sudo/i, /su\s+/i, /runas/i, /chmod\s+\+[sx]/i, /chown/i, // Remote code execution /curl.*\|.*sh/i, /wget.*\|.*sh/i, /powershell.*-c/i, /cmd.*\/c/i, /nc\s+.*-e/i, // netcat backdoor /python.*-c/i, // python one-liners can be dangerous /eval/i, /exec/i, /system/i, // Network attacks /nmap/i, /telnet/i, /ssh.*@/i, // Data exfiltration /scp\s+/i, /ftp/i, /sftp/i, // Encoding/obfuscation attempts /base64\s+-d/i, /xxd\s+-r/i, /uuencode/i, // Shell metacharacters in dangerous contexts /;\s*rm/i, /&&\s*rm/i, /\|\s*sh/i, /\$\(/i, // command substitution /`[^`]*`/i, // backtick command substitution ]; const fullCommand = [command, ...args].join(' '); for (const pattern of suspiciousPatterns) { if (pattern.test(fullCommand)) { logger.error('SECURITY VIOLATION: Dangerous command blocked', { command: command, args: args.map(arg => (arg.length > 50 ? arg.substring(0, 50) + '...' : arg)), pattern: pattern.source, timestamp: new Date().toISOString(), }); throw new Error(`Suspicious command pattern detected: ${pattern.source}`); } } // Enhanced argument validation if (args.length > 20) { throw new Error('Too many command arguments'); } // Check for argument length limits to prevent buffer overflow attacks for (const arg of args) { if (arg.length > 4096) { throw new Error('Argument too long - possible buffer overflow attempt'); } } // Check for null bytes (common in injection attacks) if (fullCommand.includes('\0')) { throw new Error('Null byte detected - possible injection attempt'); } } /** * Get safe current working directory */ private getSafeCwd(): string { try { const cwd = process.cwd(); // Ensure we're not in a sensitive directory const sensitivePaths = ['/etc', '/usr/bin', '/bin', '/sbin', '/root']; if (sensitivePaths.some(path => cwd.startsWith(path))) { return process.env.HOME || '/tmp'; } return cwd; } catch { return '/tmp'; } } /** * Get safe environment variables */ private getSafeEnvironment(): Record<string, string> { const safeVars = ['PATH', 'HOME', 'USER', 'PWD', 'LANG', 'LC_ALL']; const safeEnv: Record<string, string> = {}; for (const varName of safeVars) { if (process.env[varName]) { safeEnv[varName] = process.env[varName]!; } } return safeEnv; } /** * Git operations */ async gitStatus(): Promise<string> { if (!this.config.git.enabled) { throw new Error('Git server not enabled'); } return await this.executeCommandSecure('git', ['status', '--porcelain']); } async gitAdd(files: string[]): Promise<string> { if (!this.config.git.enabled) { throw new Error('Git server not enabled'); } return await this.executeCommandSecure('git', ['add', ...files]); } async gitCommit(message: string): Promise<string> { if (!this.config.git.enabled) { throw new Error('Git server not enabled'); } if (this.config.git.safeModeEnabled && !message.trim()) { throw new Error('Commit message required in safe mode'); } return await this.executeCommandSecure('git', ['commit', '-m', message]); } /** * Package manager operations */ async installPackage(packageName: string, dev = false): Promise<string> { if (!this.config.packageManager.enabled) { throw new Error('Package manager server not enabled'); } if (!this.config.packageManager.autoInstall) { throw new Error('Auto-install disabled. Manual confirmation required.'); } const args = ['install', packageName]; if (dev) args.push('--save-dev'); return await this.executeCommandSecure('npm', args); } async runScript(scriptName: string): Promise<string> { if (!this.config.packageManager.enabled) { throw new Error('Package manager server not enabled'); } return await this.executeCommandSecure('npm', ['run', scriptName]); } /** * Get Smithery registry health and available tools */ async getSmitheryStatus(): Promise<any> { if (!this.smitheryServer) { return { enabled: false, error: 'Smithery server not initialized' }; } try { const health = await this.smitheryServer.getRegistryHealth(); const servers = this.smitheryServer.getAvailableServers(); const tools = this.smitheryServer.getAvailableTools(); return { enabled: true, health, servers: servers.length, tools: tools.length, serversList: servers.map(s => ({ name: s.qualifiedName, displayName: s.displayName, toolCount: s.tools?.length || 0 })), toolsList: tools.map(t => ({ name: t.name, description: t.description })) }; } catch (error) { return { enabled: true, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Refresh Smithery servers and tools */ async refreshSmitheryServers(): Promise<void> { if (!this.smitheryServer) { throw new Error('Smithery server not initialized'); } await this.smitheryServer.refreshServers(); logger.info('Smithery servers refreshed'); } // Security and validation methods /** * Check if a file path is allowed - SECURITY FIX for path traversal vulnerability */ private isPathAllowed(filePath: string): boolean { try { // SECURITY FIX: Prevent path traversal attacks if (!filePath || typeof filePath !== 'string') { return false; } // Normalize and resolve the path to prevent traversal const resolvedPath = path.resolve(filePath); const cwd = process.cwd(); // Check for directory traversal attempts if (filePath.includes('..') || filePath.includes('~')) { return false; } // Ensure the resolved path is within the current working directory or explicitly allowed paths const normalizedPath = path.normalize(resolvedPath); // Additional security checks if (normalizedPath.includes('\0')) { // Null byte injection return false; } // Check if path is outside project directory (unless explicitly allowed) if (!normalizedPath.startsWith(cwd)) { // Only allow if explicitly in allowed paths if (this.config.filesystem.allowedPaths.length === 0) { return false; } } // Check restricted paths first for (const restrictedPath of this.config.filesystem.restrictedPaths) { const resolvedRestricted = path.resolve(restrictedPath); if (normalizedPath.startsWith(resolvedRestricted)) { return false; } } // Check allowed paths if they exist if (this.config.filesystem.allowedPaths.length > 0) { for (const allowedPath of this.config.filesystem.allowedPaths) { const expandedPath = allowedPath.replace('~/', (process.env.HOME || '') + '/'); const resolvedAllowed = path.resolve(expandedPath); if (normalizedPath.startsWith(resolvedAllowed)) { return true; } } return false; // Not in any allowed path } // Default: only allow paths within current working directory return normalizedPath.startsWith(cwd); } catch (error) { // If path resolution fails, deny access return false; } } /** * Check if a command is allowed */ private isCommandAllowed(command: string): boolean { // Default deny list for critical system commands const defaultBlockedCommands = [ 'rm', 'rmdir', 'del', 'format', 'fdisk', 'sudo', 'su', 'runas', 'passwd', 'mount', 'umount', 'systemctl', 'service', 'reboot', 'shutdown', 'halt', 'poweroff', 'iptables', 'netsh', 'route', 'crontab', 'at', 'schtasks', 'nc', 'netcat', 'ncat', 'socat', 'curl', 'wget', 'fetch', // if not explicitly allowed 'python', 'python3', 'node', 'ruby', 'perl', // interpreters without whitelist ]; // Check blocked commands (config + defaults) const allBlockedCommands = [...this.config.terminal.blockedCommands, ...defaultBlockedCommands]; for (const blockedCommand of allBlockedCommands) { if (command === blockedCommand || command.startsWith(blockedCommand + ' ')) { return false; } } // Check allowed commands - use whitelist approach for security if (this.config.terminal.allowedCommands.length > 0) { return this.config.terminal.allowedCommands.some( allowedCommand => command === allowedCommand || command.startsWith(allowedCommand + ' ') ); } // Default safe commands if no explicit allow list const defaultSafeCommands = [ 'ls', 'dir', 'pwd', 'cd', 'echo', 'cat', 'head', 'tail', 'grep', 'find', 'which', 'whoami', 'id', 'uname', 'git', 'npm', 'yarn', 'node', 'python', // only basic usage ]; return defaultSafeCommands.some( safeCommand => command === safeCommand || command.startsWith(safeCommand + ' ') ); } /** * Enhanced MCP capability discovery following Claude Code patterns */ async listServers(): Promise<MCPServer[]> { return Array.from(this.servers.values()); } async discoverServerCapabilities(serverId: string): Promise<ServerCapabilities | null> { const server = this.servers.get(serverId); if (!server || server.status !== 'running') { return null; } try { // Discover capabilities based on server type const capabilities = await this.performCapabilityDiscovery(serverId); // Cache capabilities server.capabilities = capabilities; return capabilities; } catch (error) { logger.error(`Capability discovery failed for ${serverId}:`, error); return null; } } private async performCapabilityDiscovery(serverId: string): Promise<ServerCapabilities> { // Implement capability discovery based on server type const tools: ToolInfo[] = []; const resources: ResourceInfo[] = []; const prompts: PromptInfo[] = []; switch (serverId) { case 'filesystem': tools.push( { name: 'read_file', description: 'Read file contents securely', inputSchema: { path: 'string' }, }, { name: 'write_file', description: 'Write file contents securely', inputSchema: { path: 'string', content: 'string' }, }, { name: 'list_directory', description: 'List directory contents', inputSchema: { path: 'string' }, } ); break; case 'git': tools.push( { name: 'git_status', description: 'Get git repository status', inputSchema: {} }, { name: 'git_add', description: 'Stage files for commit', inputSchema: { files: 'string[]' }, }, { name: 'git_commit', description: 'Commit staged changes', inputSchema: { message: 'string' }, } ); break; case 'terminal': tools.push({ name: 'execute_command', description: 'Execute terminal command securely', inputSchema: { command: 'string', args: 'string[]' }, }); break; case 'packageManager': tools.push( { name: 'install_package', description: 'Install npm package', inputSchema: { packageName: 'string', dev: 'boolean' }, }, { name: 'run_script', description: 'Run npm script', inputSchema: { scriptName: 'string' }, } ); break; case 'smithery': if (this.smitheryServer) { const smitheryTools = this.smitheryServer.getAvailableTools(); tools.push(...smitheryTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema || {} }))); } break; } return { tools, resources, prompts, lastDiscovered: new Date(), }; } /** * Health check for all servers with performance metrics */ async healthCheck(): Promise<{ [key: string]: any }> { const health: { [key: string]: any } = {}; for (const [name, server] of this.servers) { const startTime = Date.now(); let isHealthy = false; try { // Perform health check based on server type await this.performHealthCheck(server); isHealthy = true; } catch (error) { // Health check failed } const responseTime = Date.now() - startTime; // Update performance metrics if (!server.performance) { server.performance = { avgResponseTime: responseTime, successRate: isHealthy ? 1 : 0, lastHealthCheck: new Date(), availability: isHealthy ? 1 : 0, }; } else { // Update running averages server.performance.avgResponseTime = server.performance.avgResponseTime * 0.9 + responseTime * 0.1; server.performance.successRate = server.performance.successRate * 0.9 + (isHealthy ? 0.1 : 0); server.performance.lastHealthCheck = new Date(); server.performance.availability = server.performance.availability * 0.95 + (isHealthy ? 0.05 : 0); } health[name] = { enabled: server.enabled, status: server.status, lastError: server.lastError, performance: server.performance, capabilities: server.capabilities ? { toolCount: server.capabilities.tools.length, resourceCount: server.capabilities.resources.length, promptCount: server.capabilities.prompts.length, lastDiscovered: server.capabilities.lastDiscovered, } : null, }; } return health; } private async performHealthCheck(server: MCPServer): Promise<void> { // Simple health check - in production would be more sophisticated if (server.status !== 'running') { throw new Error('Server not running'); } // Server-specific health checks could be added here switch (server.id) { case 'filesystem': // Check if we can access current directory await this.listDirectorySecure('.'); break; case 'git': // Check if git is available (if in a git repo) try { await this.gitStatus(); } catch (error) { // Git might not be available, which is OK } break; // Add other server-specific health checks as needed } } }