tryaii-mcp-server
Version:
TryAII MCP Server - 15+ AI models with comparison, cost tracking, and collective intelligence
327 lines • 11.9 kB
JavaScript
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