@toolprint/hypertool-mcp
Version:
HyperTool MCP proxy server for routing requests between clients and multiple underlying MCP servers
250 lines • 8.14 kB
JavaScript
/**
* 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