UNPKG

tryaii-mcp-server

Version:

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

271 lines 9.83 kB
import axios from 'axios'; import { EventEmitter } from 'events'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '../utils/logger.js'; import { config } from '../utils/config.js'; export class HttpClient extends EventEmitter { client; config; metrics = { totalRequests: 0, successfulRequests: 0, failedRequests: 0, averageResponseTime: 0, lastHealthCheck: new Date(), connectionErrors: 0 }; semaphore = 0; isHealthy = false; healthCheckInterval; constructor(config) { super(); this.config = config; this.client = this.createAxiosInstance(); this.setupEventHandlers(); } createAxiosInstance() { const instance = axios.create({ baseURL: this.config.baseURL, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'User-Agent': 'TryAII-MCP-Client/1.0.0' } }); // Request interceptor instance.interceptors.request.use((config) => { config.metadata = { startTime: Date.now() }; this.metrics.totalRequests++; return config; }, (error) => { this.metrics.failedRequests++; return Promise.reject(error); }); // Response interceptor instance.interceptors.response.use((response) => { const duration = Date.now() - response.config.metadata.startTime; this.updateMetrics(true, duration); return response; }, (error) => { const duration = error.config?.metadata ? Date.now() - error.config.metadata.startTime : 0; this.updateMetrics(false, duration); this.metrics.connectionErrors++; return Promise.reject(error); }); return instance; } setupEventHandlers() { this.on('healthy', () => { logger.info('HTTP Client is healthy'); const wasUnhealthy = !this.isHealthy; this.isHealthy = true; // Restart periodic health check with longer interval when healthy if (wasUnhealthy && this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.startPeriodicHealthCheck(); } }); this.on('unhealthy', () => { logger.warn('HTTP Client is unhealthy'); const wasHealthy = this.isHealthy; this.isHealthy = false; // Restart periodic health check with shorter interval when unhealthy if (wasHealthy && this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.startPeriodicHealthCheck(); } }); } async start() { try { const isHealthy = await this.healthCheck(); if (!isHealthy) { logger.warn('Initial health check failed, will retry periodically', { baseURL: this.config.baseURL }); } // Start periodic health checks this.startPeriodicHealthCheck(); logger.info('HTTP Client started successfully', { baseURL: this.config.baseURL, timeout: this.config.timeout, healthy: this.isHealthy }); } catch (error) { logger.error('Failed to start HTTP Client', { error: error.message, baseURL: this.config.baseURL }); // Don't throw - allow server to start but mark as unhealthy // The health check will be retried periodically this.isHealthy = false; this.startPeriodicHealthCheck(); } } startPeriodicHealthCheck() { // Check health every 30 seconds if unhealthy, every 5 minutes if healthy const interval = this.isHealthy ? 5 * 60 * 1000 : 30 * 1000; this.healthCheckInterval = setInterval(async () => { try { await this.healthCheck(); } catch (error) { logger.debug('Periodic health check failed', { error: error.message }); } }, interval); } async stop() { this.isHealthy = false; if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); this.healthCheckInterval = undefined; } logger.info('HTTP Client stopped'); } async acquireSemaphore() { while (this.semaphore >= this.config.maxConcurrentRequests) { await new Promise(resolve => setTimeout(resolve, 10)); } this.semaphore++; } releaseSemaphore() { this.semaphore = Math.max(0, this.semaphore - 1); } async makeRequest(method, endpoint, data, retryCount = 0) { await this.acquireSemaphore(); try { const requestId = uuidv4(); logger.debug('Making HTTP request', { requestId, method, endpoint, attempt: retryCount + 1 }); const response = await this.client.request({ method, url: endpoint, data, headers: { 'X-Request-ID': requestId } }); if (!response.data.success) { throw new Error(response.data.error || 'Request failed'); } return response.data.data; } catch (error) { if (retryCount < this.config.maxRetries) { logger.warn('Request failed, retrying...', { endpoint, attempt: retryCount + 1, maxRetries: this.config.maxRetries, error: error.message }); await new Promise(resolve => setTimeout(resolve, this.config.retryDelay * (retryCount + 1))); return this.makeRequest(method, endpoint, data, retryCount + 1); } logger.error('Request failed after all retries', { endpoint, attempts: retryCount + 1, error: error.message }); throw error; } finally { this.releaseSemaphore(); } } // API Methods matching MCP interface async listAvailableModels(provider) { const params = provider ? `?provider=${encodeURIComponent(provider)}` : ''; return this.makeRequest('GET', `/api/models${params}`); } async getModelInfo(modelId) { return this.makeRequest('GET', `/api/models/${encodeURIComponent(modelId)}`); } async chatWithModel(params) { return this.makeRequest('POST', '/api/chat', params); } async compareModels(params) { const result = await this.makeRequest('POST', '/api/compare', params); return result; // The HTTP endpoint returns { results: [...] } } async brains(params) { return this.makeRequest('POST', '/api/brains', params); } updateMetrics(success, responseTime) { if (success) { this.metrics.successfulRequests++; } else { this.metrics.failedRequests++; } const totalCompleted = this.metrics.successfulRequests + this.metrics.failedRequests; this.metrics.averageResponseTime = (this.metrics.averageResponseTime * (totalCompleted - 1) + responseTime) / totalCompleted; } async healthCheck() { try { // Use direct axios call to avoid the makeRequest wrapper for health checks const response = await this.client.get('/health', { timeout: 5000 }); this.metrics.lastHealthCheck = new Date(); if (response.data && response.data.status === 'healthy') { if (!this.isHealthy) { logger.info('Health check succeeded - mcp_tryaii is now reachable', { baseURL: this.config.baseURL }); } this.emit('healthy'); return true; } else { logger.warn('Health check returned unexpected response', { data: response.data, baseURL: this.config.baseURL }); this.emit('unhealthy'); return false; } } catch (error) { const errorMessage = error.code === 'ECONNREFUSED' ? 'Connection refused - is mcp_tryaii running on port 4000?' : error.message; logger.error('Health check failed', { error: errorMessage, baseURL: this.config.baseURL, code: error.code }); this.emit('unhealthy'); return false; } } getMetrics() { return { ...this.metrics }; } getStatus() { return { healthy: this.isHealthy, baseURL: this.config.baseURL, semaphore: this.semaphore, maxConcurrentRequests: this.config.maxConcurrentRequests }; } isClientHealthy() { return this.isHealthy; } } // Create singleton instance export const httpClient = new HttpClient({ baseURL: config.http?.baseURL || 'http://localhost:4000', timeout: config.http?.timeout || 30000, maxRetries: config.http?.maxRetries || 3, retryDelay: config.http?.retryDelay || 1000, maxConcurrentRequests: config.http?.maxConcurrentRequests || 10 }); //# sourceMappingURL=httpClient.js.map