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