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
|
// Parent heartbeat: self-exit when parent dies (ppid=1 on Unix means orphaned)
|
||||||
async function cleanup() {
|
// 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');
|
logger.info('SYSTEM', 'MCP server shutting down');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -324,6 +350,9 @@ async function main() {
|
|||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
logger.info('SYSTEM', 'Claude-mem search server started');
|
logger.info('SYSTEM', 'Claude-mem search server started');
|
||||||
|
|
||||||
|
// Start parent heartbeat to detect orphaned MCP servers
|
||||||
|
startParentHeartbeat();
|
||||||
|
|
||||||
// Check Worker availability in background
|
// Check Worker availability in background
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const workerAvailable = await verifyWorkerConnection();
|
const workerAvailable = await verifyWorkerConnection();
|
||||||
|
|||||||
Reference in New Issue
Block a user