diff --git a/src/servers/mcp-server.ts b/src/servers/mcp-server.ts index 0ffeec1a..624c148f 100644 --- a/src/servers/mcp-server.ts +++ b/src/servers/mcp-server.ts @@ -307,8 +307,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } }); -// Cleanup function -async function cleanup() { +// Parent heartbeat: self-exit when parent dies (ppid=1 on Unix means orphaned) +// Prevents orphaned MCP server processes when Claude Code exits unexpectedly +const HEARTBEAT_INTERVAL_MS = 30_000; +let heartbeatTimer: ReturnType | null = null; + +function startParentHeartbeat() { + // ppid-based orphan detection only works on Unix + if (process.platform === 'win32') return; + + const initialPpid = process.ppid; + heartbeatTimer = setInterval(() => { + if (process.ppid === 1 || process.ppid !== initialPpid) { + logger.info('SYSTEM', 'Parent process died, self-exiting to prevent orphan', { + initialPpid, + currentPpid: process.ppid + }); + cleanup(); + } + }, HEARTBEAT_INTERVAL_MS); + + // Don't let the heartbeat timer keep the process alive + if (heartbeatTimer.unref) heartbeatTimer.unref(); +} + +// Cleanup function — synchronous to ensure consistent behavior whether called +// from signal handlers, heartbeat interval, or awaited in async context +function cleanup() { + if (heartbeatTimer) clearInterval(heartbeatTimer); logger.info('SYSTEM', 'MCP server shutting down'); process.exit(0); } @@ -324,6 +350,9 @@ async function main() { await server.connect(transport); logger.info('SYSTEM', 'Claude-mem search server started'); + // Start parent heartbeat to detect orphaned MCP servers + startParentHeartbeat(); + // Check Worker availability in background setTimeout(async () => { const workerAvailable = await verifyWorkerConnection();