UNPKG

@toolprint/hypertool-mcp

Version:

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

329 lines • 10.6 kB
/** * Connection manager for handling multiple MCP server connections */ import { EventEmitter } from "events"; import { ConnectionPool } from "./pool.js"; import { HealthMonitor } from "./healthMonitor.js"; import { createChildLogger } from "../utils/logging.js"; import { RecoveryCoordinator } from "../errors/recovery.js"; /** * Connection manager orchestrates connections to multiple MCP servers */ export class ConnectionManager extends EventEmitter { _pool; _healthMonitor; _logger; _recoveryCoordinator; servers = {}; isInitialized = false; isStarted = false; constructor(poolConfig = {}, connectionFactory) { super(); this._pool = new ConnectionPool(poolConfig, connectionFactory); this._healthMonitor = new HealthMonitor(); this._logger = createChildLogger({ module: "ConnectionManager" }); this._recoveryCoordinator = new RecoveryCoordinator(); this.setupPoolEventForwarding(); } /** * Get the connection pool */ get pool() { return this._pool; } /** * Get the status of all connections */ get status() { return this._pool.status; } /** * Initialize the connection manager with server configurations */ async initialize(servers) { if (this.isInitialized) { throw new Error("Connection manager is already initialized"); } // Deduplicate server names (case-insensitive, trimmed) before adding to pool const dedupedServers = {}; const seen = new Set(); for (const [serverName, config] of Object.entries(servers)) { const normalized = serverName.trim().toLowerCase(); if (seen.has(normalized)) { this._logger.error(`Duplicate server name detected during initialization: "${serverName}"`, { serverName, }); continue; } seen.add(normalized); dedupedServers[serverName] = config; } this.servers = dedupedServers; // Validate all server configurations this.validateServerConfigurations(); // Add all servers to the pool concurrently const serverEntries = Object.entries(this.servers); const results = await Promise.allSettled(serverEntries.map(([serverName, config]) => this._pool.addConnection(serverName, config))); // Log failures but continue initialization results.forEach((result, index) => { const [serverName] = serverEntries[index]; if (result.status === "rejected") { this._logger.error(`⚠️ Failed to initialize server "${serverName}": ${result.reason.message}`); // Remove failed server from registry delete this.servers[serverName]; } }); this.isInitialized = true; this.emit("initialized", { serverCount: Object.keys(this.servers).length, servers: Object.keys(this.servers), }); } /** * Connect to a specific server */ async connect(serverName) { this.ensureInitialized(); const connection = this._pool.getConnection(serverName); if (!connection) { throw new Error(`Server "${serverName}" not found in pool`); } if (connection.isConnected()) { return; } try { await connection.connect(); } catch (error) { this._logger.error(`Failed to connect to server "${serverName}"`, { error, }); throw error; } } /** * Disconnect from a specific server */ async disconnect(serverName) { this.ensureInitialized(); const connection = this._pool.getConnection(serverName); if (!connection) { return; } try { await connection.disconnect(); } catch (error) { this._logger.error(`Failed to disconnect from server "${serverName}"`, { error, }); throw error; } } /** * Reconnect to a specific server */ async reconnect(serverName) { this.ensureInitialized(); await this.disconnect(serverName); await this.connect(serverName); } /** * Get a specific connection */ getConnection(serverName) { return this._pool.getConnection(serverName); } /** * Get names of all connected servers */ getConnectedServers() { return this._pool.getConnectedServers(); } /** * Check if a specific server is connected */ isServerConnected(serverName) { const connection = this._pool.getConnection(serverName); return connection?.isConnected() ?? false; } /** * Start the connection manager */ async start() { this.ensureInitialized(); if (this.isStarted) { return; } try { await this._pool.start(); this.isStarted = true; this.emit("started", { serverCount: Object.keys(this.servers).length, connectedServers: this.getConnectedServers(), }); } catch (error) { this._logger.error("Failed to start connection manager", { error }); throw error; } } /** * Stop the connection manager */ async stop() { if (!this.isStarted) { return; } try { await this._pool.stop(); this.isStarted = false; this.emit("stopped", { serverCount: Object.keys(this.servers).length, }); } catch (error) { this._logger.error("Failed to stop connection manager", { error }); throw error; } } /** * Add a new server configuration */ async addServer(serverName, config) { this.ensureInitialized(); if (this.servers[serverName]) { const error = new Error(`❌ Server name conflict detected: "${serverName}" already exists.\n` + `💡 Resolution: Use a unique server name or remove the existing server first.\n` + `📋 Existing servers: ${Object.keys(this.servers).join(", ")}`); this._logger.error("Server name conflict detected", { serverName, existingServers: Object.keys(this.servers), }); throw error; } this.servers[serverName] = config; await this._pool.addConnection(serverName, config); // Add to health monitor const connection = this._pool.getConnection(serverName); if (connection) { this._healthMonitor.addConnection(connection); } // Auto-connect if manager is started if (this.isStarted) { try { await this.connect(serverName); } catch (error) { this._logger.warn(`Failed to auto-connect to new server "${serverName}"`, { error }); } } this.emit("serverAdded", { serverName, config }); } /** * Remove a server configuration */ async removeServer(serverName) { this.ensureInitialized(); if (!this.servers[serverName]) { return; } await this._pool.removeConnection(serverName); delete this.servers[serverName]; this.emit("serverRemoved", { serverName }); } /** * Get server configuration */ getServerConfig(serverName) { return this.servers[serverName]; } /** * Get all server names */ getServerNames() { return Object.keys(this.servers); } /** * Get connection statistics */ getStats() { const serverNames = this.getServerNames(); const connectedServers = this.getConnectedServers(); return { totalServers: serverNames.length, connectedServers: connectedServers.length, disconnectedServers: serverNames.length - connectedServers.length, connectionRate: serverNames.length > 0 ? connectedServers.length / serverNames.length : 0, activeConnections: this._pool.activeConnections, poolSize: this._pool.size, }; } /** * Validate server configurations */ validateServerConfigurations() { for (const [serverName, config] of Object.entries(this.servers)) { if (!config || typeof config !== "object") { throw new Error(`Invalid configuration for server "${serverName}"`); } if (!config.type || (config.type !== "stdio" && config.type !== "http" && config.type !== "sse")) { throw new Error(`Invalid transport type for server "${serverName}": ${config.type}`); } } } /** * Setup event forwarding from the pool */ setupPoolEventForwarding() { const eventTypes = [ "connecting", "connected", "disconnected", "reconnecting", "failed", "error", ]; eventTypes.forEach((eventType) => { this._pool.on(eventType, (event) => { this.emit(eventType, event); }); }); // Forward pool-specific events this._pool.on("poolStarted", () => { this.emit("poolStarted"); }); this._pool.on("poolStopped", () => { this.emit("poolStopped"); }); this._pool.on("connectionAdded", (data) => { this.emit("connectionAdded", data); }); this._pool.on("connectionRemoved", (data) => { this.emit("connectionRemoved", data); }); } /** * Ensure the manager is initialized */ ensureInitialized() { if (!this.isInitialized) { throw new Error("Connection manager not initialized. Call initialize() first."); } } 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=manager.js.map