/** * Process lifecycle wiring for the stdio MCP server. * * Extracted into its own side-effect-free module so the shutdown logic can be * unit-tested in isolation, without importing (and booting) the whole server. */ /** The bit of `process.stdout` shutdown needs: is anything still buffered? */ export interface WritableLike { writableLength?: number; write: (chunk: string, callback?: () => void) => unknown; } export interface ShutdownDeps { /** Runs the actual resource cleanup. Must be safe to call once. */ cleanup: () => Promise; /** Signal/exit target. Defaults to the real `process`; injectable for tests. */ proc?: NodeJS.EventEmitter & { exit: (code?: number) => void; }; /** stdin stream. Defaults to `process.stdin`; injectable for tests. */ stdin?: NodeJS.EventEmitter; /** JSON-RPC output stream. Defaults to `process.stdout`; injectable for tests. */ stdout?: WritableLike; /** Longest wait for stdout to drain before exiting anyway. */ flushTimeoutMs?: number; } /** * Wire every process-termination path to a single guarded shutdown so that * cleanup() + exit run EXACTLY ONCE. * * Why one guard for all paths: they race each other. A signal can arrive while * stdin is closing, and stdin itself can emit both 'end' and 'close' (plus * 'error') for a single disconnect. Without a shared guard, cleanup() runs * concurrently and process.exit() is called more than once. * * Why the stdin handlers (the core fix, PR #177): a stdio MCP server MUST exit * when its parent (the MCP client, e.g. Claude Code) dies. On Windows a killed * parent sends NO signal to the child, so SIGINT/SIGTERM never fire on * orphaning. The only reliable "parent is gone" signal is stdin closing/ending/ * erroring (the client's pipe write-end goes away). Without it the process * leaks forever: the prune timer is unref'd (does not pin the loop), but * StdioServerTransport's active stdin read handle keeps the process alive. */ export declare function installShutdownHandlers(deps: ShutdownDeps): void; //# sourceMappingURL=lifecycle.d.ts.map