UNPKG

tryaii-mcp-server

Version:

TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence

327 lines 11.9 kB
import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import { logger, loggers } from '../utils/logger.js'; import { config } from '../utils/config.js'; export class MCPClient extends EventEmitter { process = null; pendingRequests = new Map(); status = { isRunning: false, restartCount: 0 }; metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, processRestarts: 0, uptime: 0, lastHealthCheck: new Date() }; buffer = ''; isConnected = false; startTime = 0; constructor() { super(); this.setupEventHandlers(); } setupEventHandlers() { this.on('error', (error) => { loggers.error('MCP Client error', error); }); this.on('connected', () => { logger.info('MCP Client connected successfully'); this.isConnected = true; }); this.on('disconnected', () => { logger.warn('MCP Client disconnected'); this.isConnected = false; }); } async start() { if (this.status.isRunning) { logger.warn('MCP process is already running'); return; } try { this.startTime = Date.now(); await this.spawnProcess(); this.status.isRunning = true; this.status.startTime = new Date(); this.emit('connected'); logger.info('MCP Client started successfully', { executablePath: config.mcp.executablePath, pid: this.process?.pid }); } catch (error) { this.status.lastError = error instanceof Error ? error.message : 'Unknown error'; this.status.isRunning = false; loggers.error('Failed to start MCP process', error instanceof Error ? error : new Error(String(error))); throw error; } } async spawnProcess() { return new Promise((resolve, reject) => { logger.info('Spawning MCP process', { executablePath: config.mcp.executablePath, env: Object.keys(config.mcp.env || {}) }); this.process = spawn('node', [config.mcp.executablePath], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...config.mcp.env } }); if (!this.process.stdout || !this.process.stderr || !this.process.stdin) { reject(new Error('Failed to establish stdio streams')); return; } this.process.stdout.setEncoding('utf8'); this.process.stderr.setEncoding('utf8'); // Handle stdout data (JSON-RPC messages) this.process.stdout.on('data', (data) => { this.handleStdoutData(data); }); // Handle stderr for debugging this.process.stderr.on('data', (data) => { logger.debug('MCP process stderr', { data: data.toString().trim() }); }); // Handle process exit this.process.on('exit', (code, signal) => { this.handleProcessExit(code, signal); }); // Handle process errors this.process.on('error', (error) => { loggers.error('MCP process error', error); reject(error); }); // Wait for initial connection confirmation setTimeout(() => { if (this.process && this.process.pid) { resolve(); } else { reject(new Error('MCP process failed to start within timeout')); } }, 2000); }); } handleStdoutData(data) { this.buffer += data; // Process complete JSON-RPC messages const lines = this.buffer.split('\n'); this.buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const message = JSON.parse(line); this.handleMessage(message); } catch (error) { logger.warn('Failed to parse MCP message', { line, error }); } } } } handleMessage(message) { const requestId = String(message.id); const pendingRequest = this.pendingRequests.get(requestId); if (!pendingRequest) { logger.warn('Received response for unknown request', { requestId, message }); return; } const responseTime = Date.now() - pendingRequest.startTime; // Clear timeout clearTimeout(pendingRequest.timeout); this.pendingRequests.delete(requestId); // Update metrics this.updateMetrics(true, responseTime); if (message.error) { loggers.mcpResponse(requestId, false, responseTime, { error: message.error }); pendingRequest.reject(new Error(`MCP Error ${message.error.code}: ${message.error.message}`)); } else { loggers.mcpResponse(requestId, true, responseTime); pendingRequest.resolve(message.result); } } handleProcessExit(code, signal) { logger.warn('MCP process exited', { code, signal, restartCount: this.status.restartCount }); this.status.isRunning = false; this.isConnected = false; this.emit('disconnected'); // Reject all pending requests for (const [requestId, request] of this.pendingRequests) { clearTimeout(request.timeout); request.reject(new Error('MCP process exited unexpectedly')); } this.pendingRequests.clear(); // Attempt restart if under limit if (this.status.restartCount < config.mcp.maxRestarts) { this.attemptRestart(); } else { logger.error('Max restart attempts reached. MCP process will not be restarted.'); this.emit('error', new Error('MCP process failed to restart after maximum attempts')); } } async attemptRestart() { this.status.restartCount++; this.status.lastRestart = new Date(); this.metrics.processRestarts++; const delay = Math.min(1000 * Math.pow(2, this.status.restartCount), 30000); // Exponential backoff, max 30s logger.info(`Attempting to restart MCP process in ${delay}ms (attempt ${this.status.restartCount}/${config.mcp.maxRestarts})`); setTimeout(async () => { try { await this.start(); } catch (error) { loggers.error('Failed to restart MCP process', error instanceof Error ? error : new Error(String(error))); } }, delay); } async sendRequest(method, params = {}) { if (!this.isConnected || !this.process?.stdin) { throw new Error('MCP client is not connected'); } const requestId = uuidv4(); const request = { jsonrpc: '2.0', id: requestId, method, params }; return new Promise((resolve, reject) => { const startTime = Date.now(); // Set up timeout const timeout = setTimeout(() => { this.pendingRequests.delete(requestId); this.updateMetrics(false, Date.now() - startTime); reject(new Error(`Request timeout after ${config.mcp.timeout}ms`)); }, config.mcp.timeout); // Store pending request this.pendingRequests.set(requestId, { resolve, reject, timeout, startTime }); // Send request try { const message = JSON.stringify(request) + '\n'; this.process.stdin.write(message); this.metrics.totalRequests++; loggers.mcpRequest(requestId, method, params); } catch (error) { clearTimeout(timeout); this.pendingRequests.delete(requestId); this.updateMetrics(false, Date.now() - startTime); reject(error); } }); } // High-level MCP tool methods async listTools() { return this.sendRequest('tools/list'); } async callTool(name, arguments_) { const params = { name, arguments: arguments_ }; return this.sendRequest('tools/call', params); } // Convenience methods for specific tools async listAvailableModels(provider) { return this.callTool('list_available_models', { provider }); } async chatWithModel(params) { return this.callTool('chat_with_model', params); } async compareModels(params) { return this.callTool('compare_models', params); } async getModelInfo(modelId) { return this.callTool('get_model_info', { modelId }); } async brains(params) { return this.callTool('brains', params); } updateMetrics(success, responseTime) { if (success) { this.metrics.successfulRequests++; } else { this.metrics.failedRequests++; } // Update average response time const totalCompleted = this.metrics.successfulRequests + this.metrics.failedRequests; this.metrics.averageResponseTime = (this.metrics.averageResponseTime * (totalCompleted - 1) + responseTime) / totalCompleted; this.metrics.uptime = this.startTime ? Date.now() - this.startTime : 0; this.metrics.lastHealthCheck = new Date(); } async stop() { logger.info('Stopping MCP client...'); if (this.process) { // Graceful shutdown this.process.stdin?.end(); // Wait for process to exit, or force kill after timeout await new Promise((resolve) => { if (!this.process) { resolve(); return; } const forceKillTimeout = setTimeout(() => { if (this.process) { logger.warn('Force killing MCP process'); this.process.kill('SIGKILL'); } resolve(); }, 5000); this.process.on('exit', () => { clearTimeout(forceKillTimeout); resolve(); }); this.process.kill('SIGTERM'); }); this.process = null; } this.status.isRunning = false; this.isConnected = false; this.emit('disconnected'); logger.info('MCP client stopped'); } // Status and metrics getStatus() { return { ...this.status }; } getMetrics() { return { ...this.metrics }; } isHealthy() { return this.isConnected && this.status.isRunning && this.process !== null; } async healthCheck() { if (!this.isHealthy()) { return false; } try { // Simple health check by listing tools await this.listTools(); this.metrics.lastHealthCheck = new Date(); return true; } catch (error) { loggers.error('Health check failed', error instanceof Error ? error : new Error(String(error))); return false; } } } // Create singleton instance export const mcpClient = new MCPClient(); //# sourceMappingURL=mcpClient.js.map