UNPKG

@toolprint/hypertool-mcp

Version:

HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers

250 lines • 8.14 kB
/** * Connection pool implementation for managing multiple MCP server connections */ import { EventEmitter } from "events"; import { createChildLogger } from "../utils/logging.js"; const logger = createChildLogger({ module: "connection/pool" }); import { DEFAULT_POOL_CONFIG, } from "./types.js"; import { ConnectionFactory } from "./factory.js"; /** * Connection pool manages a collection of connections to MCP servers */ export class ConnectionPool extends EventEmitter { connections = new Map(); config; factory; healthCheckTimer; isStarted = false; constructor(config = {}, factory) { super(); this.config = { ...DEFAULT_POOL_CONFIG, ...config }; this.factory = factory || new ConnectionFactory(); this.setupEventHandlers(); } /** * Get the total number of connections in the pool */ get size() { return this.connections.size; } /** * Get the number of active connections */ get activeConnections() { return Array.from(this.connections.values()).filter((conn) => conn.isConnected()).length; } /** * Get the status of all connections */ get status() { const status = {}; for (const [name, connection] of this.connections) { status[name] = connection.status; } return status; } /** * Add a new connection to the pool */ async addConnection(serverName, config) { if (this.connections.has(serverName)) { const existingServers = Array.from(this.connections.keys()); throw new Error(`❌ Server name conflict: Connection for server "${serverName}" already exists.\n` + `💡 Each server must have a unique name.\n` + `📋 Active servers: [${existingServers.join(", ")}]\n` + `🚫 Cannot create duplicate connections for the same server name.`); } if (this.size >= this.config.maxConcurrentConnections) { throw new Error(`Maximum number of connections (${this.config.maxConcurrentConnections}) reached`); } const connection = this.factory.createConnection(serverName, config, this.config.connectionOptions); // Setup connection event forwarding this.setupConnectionEventForwarding(connection); this.connections.set(serverName, connection); // Auto-connect if pool is started if (this.isStarted) { try { await connection.connect(); } catch (error) { this.connections.delete(serverName); throw error; } } this.emit("connectionAdded", { serverName, connection }); return connection; } /** * Remove a connection from the pool */ async removeConnection(serverName) { const connection = this.connections.get(serverName); if (!connection) { return; } try { await connection.disconnect(); } catch (error) { logger.warn(`Error disconnecting from server "${serverName}":`, error); } this.connections.delete(serverName); this.emit("connectionRemoved", { serverName }); } /** * Get a specific connection by server name */ getConnection(serverName) { return this.connections.get(serverName); } /** * Get all connections */ getConnections() { return Array.from(this.connections.values()); } /** * Get names of all connected servers */ getConnectedServers() { return Array.from(this.connections.entries()) .filter(([, connection]) => connection.isConnected()) .map(([name]) => name); } /** * Start the connection pool */ async start() { if (this.isStarted) { return; } this.isStarted = true; // Connect all existing connections const connectionPromises = Array.from(this.connections.entries()).map(async ([serverName, connection]) => { try { await connection.connect(); } catch (error) { logger.error(`Failed to connect to server "${serverName}":`, error); this.emit("error", { type: "failed", serverId: connection.id, serverName, timestamp: new Date(), error: error, }); } }); await Promise.allSettled(connectionPromises); // Start health check timer this.startHealthCheck(); this.emit("poolStarted"); } /** * Stop the connection pool */ async stop() { if (!this.isStarted) { return; } this.isStarted = false; // Stop health check timer this.stopHealthCheck(); // Disconnect all connections const disconnectionPromises = Array.from(this.connections.entries()).map(async ([serverName, connection]) => { try { await connection.disconnect(); } catch (error) { logger.warn(`Error disconnecting from server "${serverName}":`, error); } }); await Promise.allSettled(disconnectionPromises); this.emit("poolStopped"); } /** * Setup connection event forwarding to pool events */ setupConnectionEventForwarding(connection) { const eventTypes = [ "connecting", "connected", "disconnected", "reconnecting", "failed", "error", ]; eventTypes.forEach((eventType) => { connection.on(eventType, (event) => { this.emit(eventType, event); }); }); } /** * Setup general event handlers */ setupEventHandlers() { // Handle connection failures this.on("failed", (event) => { logger.error(`Connection to server "${event.serverName}" failed:`, event.error); }); // Handle connection errors this.on("error", (event) => { logger.error(`Connection error for server "${event.serverName}":`, event.error); }); } /** * Start periodic health checks */ startHealthCheck() { if (this.healthCheckTimer) { return; } this.healthCheckTimer = setInterval(async () => { await this.performHealthCheck(); }, this.config.healthCheckInterval); } /** * Stop periodic health checks */ stopHealthCheck() { if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = undefined; } } /** * Perform health check on all connections */ async performHealthCheck() { const healthCheckPromises = Array.from(this.connections.entries()).map(async ([serverName, connection]) => { try { const isHealthy = await connection.ping(); if (!isHealthy && connection.isConnected()) { logger.warn(`Health check failed for server "${serverName}"`); this.emit("error", { type: "error", serverId: connection.id, serverName, timestamp: new Date(), error: new Error("Health check failed"), }); } } catch (error) { logger.warn(`Health check error for server "${serverName}":`, error); } }); await Promise.allSettled(healthCheckPromises); } on(event, listener) { return super.on(event, listener); } off(event, listener) { return super.off(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } } //# sourceMappingURL=pool.js.map