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