Skip to main content
Glama
shutdown.ts7.56 kB
/** * Central shutdown coordinator for graceful termination * Handles LSP shutdown sequence and child process management * * Provides automatic cleanup on SIGINT/SIGTERM signals without exposing * shutdown functionality as an MCP tool to users. */ import { once } from 'node:events'; import { ChildProcessWithoutNullStreams } from 'child_process'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { LspClient } from '../types.js'; import logger from '../utils/logger.js'; const DEFAULT_TIMEOUT_MS = 5_000; export interface ShutdownOptions { timeoutMs?: number; } /** * Sets up graceful shutdown handlers for SIGINT and SIGTERM, plus LSP crash detection * Returns a disposer function for cleanup (useful in tests) */ export function setupShutdown( server: McpServer, lspClient: LspClient, lspProcess: ChildProcessWithoutNullStreams, options: ShutdownOptions = {} ): () => void { const { timeoutMs = DEFAULT_TIMEOUT_MS } = options; let shuttingDown = false; const createSignalHandler = (signal: string) => async (): Promise<void> => { if (shuttingDown) { logger.debug(`Ignoring duplicate ${signal} signal during shutdown`); return; } logger.info(`Received ${signal} signal, initiating graceful shutdown`, { signal, lspProcessPid: lspProcess.pid, lspProcessKilled: lspProcess.killed, lspClientInitialized: lspClient.isInitialized, timeoutMs, }); shuttingDown = true; try { // Set up exit listener BEFORE sending signals to avoid race condition const exitPromise = once(lspProcess, 'exit'); // 1. Send LSP shutdown sequence with timeout // These requests might hang if the LSP connection is broken or the process exits quickly if (lspClient.isInitialized) { logger.debug('Sending LSP shutdown request'); const shutdownTimeout = new Promise<void>((resolve) => { setTimeout(() => { logger.debug('Shutdown request timed out (connection may be closed)'); resolve(); }, 500); // 500ms timeout for shutdown request }); const shutdownRequest = lspClient.connection .sendRequest('shutdown') .then(() => { logger.debug('Shutdown request acknowledged by LSP'); }) .catch((error) => { logger.debug('Shutdown request error (expected if connection closed)', { error: error instanceof Error ? error.message : String(error), }); }); await Promise.race([shutdownRequest, shutdownTimeout]); logger.debug('Sending LSP exit notification'); // Exit notification is fire-and-forget, don't wait for it // Wrap in try/catch since connection might already be closed try { void lspClient.connection.sendNotification('exit', null); } catch { // Connection already closed, that's fine logger.debug('Exit notification failed (connection already closed)'); } // 2. Send SIGTERM to LSP process if (!lspProcess.killed) { logger.debug( `Sending SIGTERM to LSP process (PID: ${lspProcess.pid})` ); lspProcess.kill('SIGTERM'); } } else { logger.debug( 'LSP client not initialized, skipping LSP shutdown sequence' ); // Still try to kill the process if it exists if (!lspProcess.killed) { logger.debug( `Sending SIGTERM to uninitialized LSP process (PID: ${lspProcess.pid})` ); lspProcess.kill('SIGTERM'); } } // 3. Race: natural process exit vs. timeout → SIGKILL const timer = setTimeout(() => { if (!lspProcess.killed) { logger.warn( `LSP process did not exit within ${timeoutMs}ms, sending SIGKILL`, { pid: lspProcess.pid, } ); lspProcess.kill('SIGKILL'); } }, timeoutMs); // Wait for LSP process to exit naturally (using the promise we set up earlier) logger.debug('Waiting for LSP process to exit'); await exitPromise; clearTimeout(timer); logger.info('LSP process exited successfully'); // 4. MCP server shutdown with timeout // The StdioServerTransport might hang waiting for stdin to close, // so we race against a timeout to ensure we exit logger.debug('Closing MCP server'); const serverClosePromise = server.close().catch((error) => { logger.debug('MCP server close error', { error: error instanceof Error ? error.message : String(error), }); }); const timeoutPromise = new Promise<void>((resolve) => { setTimeout(() => { logger.debug('MCP server close timed out, forcing exit'); resolve(); }, 1000); }); await Promise.race([serverClosePromise, timeoutPromise]); logger.debug('MCP server close completed or timed out'); logger.info('Shutdown complete, exiting...'); } catch (error) { logger.error('Error during graceful shutdown', { signal, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, lspProcessPid: lspProcess.pid, lspProcessKilled: lspProcess.killed, }); // Ensure process exits even if shutdown fails if (!lspProcess.killed) { logger.warn('Force killing LSP process due to shutdown error'); lspProcess.kill('SIGKILL'); } // Use non-zero exit code for failed shutdown logger.info('Exiting with error code 1'); process.exit(1); } // Successful shutdown - exit immediately logger.info('Exiting with code 0'); process.exit(0); }; // Handle LSP process crash - if LSP dies unexpectedly, shut down cleanly const lspCrashHandler = (code: number | null, signal: string | null) => { if (!shuttingDown && code !== null && code !== 0) { logger.error('LSP process crashed, initiating emergency shutdown', { exitCode: code, signal, pid: lspProcess.pid, }); // LSP crashed with non-zero exit code, initiate clean shutdown void createSignalHandler('LSP_CRASH')(); // Fire-and-forget shutdown sequence } else if (!shuttingDown && signal !== null) { logger.warn('LSP process terminated by signal during normal operation', { signal, pid: lspProcess.pid, }); void createSignalHandler('LSP_SIGNAL')(); // Fire-and-forget shutdown sequence } }; // Create signal handlers with proper logging const sigintHandler = () => void createSignalHandler('SIGINT')(); const sigtermHandler = () => void createSignalHandler('SIGTERM')(); // Register signal handlers logger.debug('Registering shutdown signal handlers', { signals: ['SIGINT', 'SIGTERM'], lspProcessPid: lspProcess.pid, }); process.on('SIGINT', sigintHandler); process.on('SIGTERM', sigtermHandler); // Register LSP crash detection lspProcess.once('exit', lspCrashHandler); logger.debug('Shutdown handlers configured successfully', { timeoutMs, lspProcessPid: lspProcess.pid, }); // Return disposer for cleanup return () => { logger.debug('Removing shutdown signal handlers'); process.off('SIGINT', sigintHandler); process.off('SIGTERM', sigtermHandler); lspProcess.off('exit', lspCrashHandler); }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/p1va/symbols-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server