feat: parent heartbeat for MCP server orphan prevention (#992)
* feat: add parent heartbeat to MCP server to prevent orphaned processes MCP server now monitors its parent process every 30s. When the parent dies (ppid changes to 1 on Unix), the server self-exits to prevent orphaned node processes that accumulate over time. - Checks ppid every 30s after server start - Compares against initial ppid (handles reparenting) - Timer uses unref() to not keep process alive artificially - Unix-only (ppid=1 detection doesn't apply on Windows) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> * fix: make cleanup() synchronous for consistent shutdown behavior cleanup() only does synchronous work (clearInterval + process.exit), so remove async to avoid inconsistent behavior when called from setInterval callback vs signal handler vs awaited context. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -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<typeof setInterval> | 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();
|
||||
|
||||
Reference in New Issue
Block a user