UNPKG

@toolprint/hypertool-mcp

Version:

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

213 lines • 6.09 kB
/** * Base connection implementation with common functionality */ import { EventEmitter } from "events"; import { v4 as uuidv4 } from "uuid"; import { createChildLogger } from "../../utils/logging.js"; const logger = createChildLogger({ module: "clients/base" }); import { ConnectionState, DEFAULT_CONNECTION_OPTIONS, } from "../types.js"; /** * Base connection implementation providing common functionality */ export class BaseConnection extends EventEmitter { id; serverName; config; options; connectionStatus; retryTimer; pingTimer; constructor(serverName, config, options = {}) { super(); this.id = uuidv4(); this.serverName = serverName; this.config = config; this.options = { ...DEFAULT_CONNECTION_OPTIONS, ...options }; this.connectionStatus = { state: ConnectionState.DISCONNECTED, serverId: this.id, serverName, retryCount: 0, transport: config.type, }; } /** * Get the current connection status */ get status() { return { ...this.connectionStatus }; } /** * Get the underlying client instance */ get client() { if (!this._client) { throw new Error(`Client not initialized for server "${this.serverName}"`); } return this._client; } /** * Check if the connection is active */ isConnected() { return this.connectionStatus.state === ConnectionState.CONNECTED; } /** * Connect to the MCP server */ async connect() { if (this.isConnected()) { return; } this.updateStatus(ConnectionState.CONNECTING); this.emit("connecting", this.createEvent("connecting")); try { await this.doConnect(); this.updateStatus(ConnectionState.CONNECTED, { connectedAt: new Date(), retryCount: 0, lastError: undefined, }); this.emit("connected", this.createEvent("connected")); this.startPing(); } catch (error) { this.updateStatus(ConnectionState.FAILED, { lastError: error.message, }); this.emit("failed", this.createEvent("failed", { error: error })); if (this.connectionStatus.retryCount < this.options.maxRetries) { await this.scheduleReconnect(); } throw error; } } /** * Disconnect from the MCP server */ async disconnect() { if (this.connectionStatus.state === ConnectionState.DISCONNECTED) { return; } this.stopPing(); this.clearRetryTimer(); try { await this.doDisconnect(); } catch (error) { logger.warn(`Error during disconnect for server "${this.serverName}":`, error); } this.updateStatus(ConnectionState.DISCONNECTED); this.emit("disconnected", this.createEvent("disconnected")); } /** * Ping the server to check if it's still responsive */ async ping() { if (!this.isConnected()) { return false; } try { const result = await this.doPing(); this.updateStatus(this.connectionStatus.state, { lastPing: new Date(), }); return result; } catch (error) { this.handlePingError(error); return false; } } /** * Update connection status */ updateStatus(state, updates = {}) { this.connectionStatus = { ...this.connectionStatus, ...updates, state, }; } /** * Create a connection event */ createEvent(type, overrides = {}) { return { type, serverId: this.id, serverName: this.serverName, timestamp: new Date(), ...overrides, }; } /** * Schedule a reconnection attempt */ async scheduleReconnect() { this.clearRetryTimer(); this.connectionStatus.retryCount++; this.updateStatus(ConnectionState.RECONNECTING); this.emit("reconnecting", this.createEvent("reconnecting")); const delay = Math.min(this.options.retryDelay * Math.pow(this.options.backoffMultiplier, this.connectionStatus.retryCount - 1), this.options.maxRetryDelay); this.retryTimer = setTimeout(async () => { try { await this.connect(); } catch { // Error handling is done in connect() method } }, delay); } /** * Start periodic ping checks */ startPing() { if (this.options.pingInterval <= 0) { return; } this.pingTimer = setInterval(async () => { await this.ping(); }, this.options.pingInterval); } /** * Stop periodic ping checks */ stopPing() { if (this.pingTimer) { clearInterval(this.pingTimer); this.pingTimer = undefined; } } /** * Clear retry timer */ clearRetryTimer() { if (this.retryTimer) { clearTimeout(this.retryTimer); this.retryTimer = undefined; } } /** * Handle ping errors */ handlePingError(error) { this.emit("error", this.createEvent("error", { error })); // If ping fails, consider the connection as failed and try to reconnect if (this.isConnected() && this.connectionStatus.retryCount < this.options.maxRetries) { this.scheduleReconnect(); } } 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=base.js.map