UNPKG

@sethdouglasford/claude-flow

Version:

Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology

406 lines 14.1 kB
/** * MCP Server Lifecycle Manager * Handles server lifecycle operations including start, stop, restart, and health checks */ import { EventEmitter } from "node:events"; import { MCPError } from "../utils/errors.js"; export var LifecycleState; (function (LifecycleState) { LifecycleState["STOPPED"] = "stopped"; LifecycleState["STARTING"] = "starting"; LifecycleState["RUNNING"] = "running"; LifecycleState["STOPPING"] = "stopping"; LifecycleState["RESTARTING"] = "restarting"; LifecycleState["ERROR"] = "error"; })(LifecycleState || (LifecycleState = {})); /** * MCP Server Lifecycle Manager * Manages the complete lifecycle of MCP servers with robust error handling */ export class MCPLifecycleManager extends EventEmitter { mcpConfig; logger; serverFactory; state = LifecycleState.STOPPED; server; healthCheckTimer; startTime; lastRestart; restartAttempts = 0; shutdownPromise; history = []; processListeners = new Map(); config = { healthCheckInterval: 30000, // 30 seconds gracefulShutdownTimeout: 10000, // 10 seconds maxRestartAttempts: 3, restartDelay: 5000, // 5 seconds enableAutoRestart: true, enableHealthChecks: true, }; constructor(mcpConfig, logger, serverFactory, config) { super(); this.mcpConfig = mcpConfig; this.logger = logger; this.serverFactory = serverFactory; if (config) { Object.assign(this.config, config); } this.setupEventHandlers(); } /** * Start the MCP server */ async start() { if (this.state !== LifecycleState.STOPPED) { throw new MCPError(`Cannot start server in state: ${this.state}`); } this.setState(LifecycleState.STARTING); this.logger.info("Starting MCP server lifecycle manager"); try { // Create server instance this.server = this.serverFactory(); // Start the server await this.server.start(); // Record start time this.startTime = new Date(); this.restartAttempts = 0; // Start health checks if (this.config.enableHealthChecks) { this.startHealthChecks(); } this.setState(LifecycleState.RUNNING); this.logger.info("MCP server started successfully"); } catch (error) { this.setState(LifecycleState.ERROR, error); this.logger.error("Failed to start MCP server", error); throw error; } } /** * Stop the MCP server gracefully */ async stop() { if (this.state === LifecycleState.STOPPED) { return; } if (this.shutdownPromise) { return this.shutdownPromise; } this.setState(LifecycleState.STOPPING); this.logger.info("Stopping MCP server"); this.shutdownPromise = this.performShutdown(); await this.shutdownPromise; this.shutdownPromise = undefined; } /** * Restart the MCP server */ async restart() { if (this.state === LifecycleState.STOPPED) { return this.start(); } this.setState(LifecycleState.RESTARTING); this.logger.info("Restarting MCP server"); try { await this.stop(); // Add restart delay if (this.config.restartDelay > 0) { await new Promise(resolve => setTimeout(resolve, this.config.restartDelay)); } await this.start(); this.lastRestart = new Date(); this.restartAttempts++; this.logger.info("MCP server restarted successfully"); } catch (error) { this.setState(LifecycleState.ERROR, error); this.logger.error("Failed to restart MCP server", error); throw error; } } /** * Perform comprehensive health check */ async healthCheck() { const startTime = Date.now(); const result = { healthy: false, state: this.state, uptime: this.getUptime(), lastRestart: this.lastRestart, components: { server: false, transport: false, sessions: false, tools: false, auth: false, loadBalancer: false, }, }; try { if (!this.server || this.state !== LifecycleState.RUNNING) { result.error = "Server not running"; return result; } // Check server health const serverHealth = await this.server.getHealthStatus(); result.components.server = serverHealth.healthy; result.metrics = serverHealth.metrics; if (serverHealth.error) { result.error = serverHealth.error; } // Check individual components (more lenient approach - missing metrics don't mean unhealthy) result.components.transport = serverHealth.healthy; result.components.sessions = true; // Sessions are managed internally result.components.tools = (serverHealth.metrics?.registeredTools || 0) > 0; result.components.auth = true; // Auth is optional result.components.loadBalancer = true; // Load balancer is optional // Overall health assessment - primarily based on server health // Tools are nice to have but not required for basic health result.healthy = result.components.server; const checkDuration = Date.now() - startTime; if (result.metrics) { result.metrics.healthCheckDuration = checkDuration; } this.logger.debug("Health check completed", { healthy: result.healthy, duration: checkDuration, components: result.components, }); return result; } catch (error) { result.error = error instanceof Error ? error.message : "Unknown error"; this.logger.error("Health check failed", error); return result; } } /** * Get current server state */ getState() { return this.state; } /** * Get server metrics */ getMetrics() { return this.server?.getMetrics(); } /** * Get active sessions */ getSessions() { return this.server?.getSessions() || []; } /** * Get server uptime in milliseconds */ getUptime() { return this.startTime ? Date.now() - this.startTime.getTime() : 0; } /** * Get lifecycle event history */ getHistory() { return [...this.history]; } /** * Force terminate server (emergency stop) */ async forceStop() { this.logger.warn("Force stopping MCP server"); // Stop health checks this.stopHealthChecks(); // Force close server if (this.server) { try { await this.server.stop(); } catch (error) { this.logger.error("Error during force stop", error); } this.server = undefined; } this.setState(LifecycleState.STOPPED); this.startTime = undefined; } /** * Clean up all resources */ destroy() { // Remove all process event listeners for (const [event, handler] of this.processListeners) { process.removeListener(event, handler); } this.processListeners.clear(); // Restore max listeners process.setMaxListeners(Math.max(process.getMaxListeners() - 4, 10)); // Stop health checks this.stopHealthChecks(); // Force stop server if still running if (this.server) { this.server.stop().catch(error => { this.logger.error("Error force stopping server in destroy:", error); }); this.server = undefined; } // Remove all event listeners this.removeAllListeners(); } /** * Enable or disable auto-restart */ setAutoRestart(enabled) { this.config.enableAutoRestart = enabled; this.logger.info("Auto-restart", { enabled }); } /** * Enable or disable health checks */ setHealthChecks(enabled) { this.config.enableHealthChecks = enabled; if (enabled && this.state === LifecycleState.RUNNING) { this.startHealthChecks(); } else { this.stopHealthChecks(); } this.logger.info("Health checks", { enabled }); } setState(newState, error) { const previousState = this.state; this.state = newState; const event = { timestamp: new Date(), state: newState, previousState, error, }; this.history.push(event); // Keep only last 100 events if (this.history.length > 100) { this.history.shift(); } this.emit("stateChange", event); this.logger.info("State change", { from: previousState, to: newState, error: error?.message, }); } setupEventHandlers() { // Increase max listeners to prevent warnings process.setMaxListeners(Math.max(process.getMaxListeners() + 4, 20)); // Handle uncaught errors const uncaughtHandler = (error) => { this.logger.error("Uncaught exception", error); this.handleServerError(error); }; this.processListeners.set("uncaughtException", uncaughtHandler); process.on("uncaughtException", uncaughtHandler); const unhandledHandler = (reason) => { this.logger.error("Unhandled rejection", reason); this.handleServerError(reason instanceof Error ? reason : new Error(String(reason))); }; this.processListeners.set("unhandledRejection", unhandledHandler); process.on("unhandledRejection", unhandledHandler); // Handle process signals const sigintHandler = () => { this.logger.info("Received SIGINT, shutting down gracefully"); this.stop().catch(error => { this.logger.error("Error during graceful shutdown", error); process.exit(1); }); }; this.processListeners.set("SIGINT", sigintHandler); process.on("SIGINT", sigintHandler); const sigtermHandler = () => { this.logger.info("Received SIGTERM, shutting down gracefully"); this.stop().catch(error => { this.logger.error("Error during graceful shutdown", error); process.exit(1); }); }; this.processListeners.set("SIGTERM", sigtermHandler); process.on("SIGTERM", sigtermHandler); } async handleServerError(error) { this.logger.error("Server error detected", error); this.setState(LifecycleState.ERROR, error); if (this.config.enableAutoRestart && this.restartAttempts < this.config.maxRestartAttempts) { this.logger.info("Attempting auto-restart", { attempt: this.restartAttempts + 1, maxAttempts: this.config.maxRestartAttempts, }); try { await this.restart(); } catch (restartError) { this.logger.error("Auto-restart failed", restartError); } } else { this.logger.error("Max restart attempts reached or auto-restart disabled"); await this.forceStop(); } } startHealthChecks() { if (this.healthCheckTimer) { return; } this.healthCheckTimer = setInterval(async () => { try { const health = await this.healthCheck(); if (!health.healthy && this.state === LifecycleState.RUNNING) { this.logger.warn("Health check failed", health); this.handleServerError(new Error(health.error || "Health check failed")); } } catch (error) { this.logger.error("Health check error", error); } }, this.config.healthCheckInterval); this.logger.debug("Health checks started", { interval: this.config.healthCheckInterval }); } stopHealthChecks() { if (this.healthCheckTimer) { clearInterval(this.healthCheckTimer); this.healthCheckTimer = undefined; this.logger.debug("Health checks stopped"); } } async performShutdown() { try { // Stop health checks this.stopHealthChecks(); // Graceful shutdown with timeout const shutdownPromise = this.server?.stop() || Promise.resolve(); let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error("Shutdown timeout")), this.config.gracefulShutdownTimeout); }); try { await Promise.race([shutdownPromise, timeoutPromise]); } finally { // Always clear the timeout to prevent Jest open handles if (timeoutId) { clearTimeout(timeoutId); } } this.server = undefined; this.setState(LifecycleState.STOPPED); this.startTime = undefined; this.logger.info("MCP server stopped successfully"); } catch (error) { this.logger.error("Error during shutdown", error); await this.forceStop(); throw error; } } } //# sourceMappingURL=lifecycle-manager.js.map