Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1612,9 +1612,57 @@ async function main() {
}
}

// Parent death guard — catches SIGKILL, crashes, terminal close on ALL platforms.
// process.kill(pid, 0) throws ESRCH when the process no longer exists.
const parentPid = process.ppid;
if (parentPid > 1) {
const parentGuard = setInterval(() => {
try {
process.kill(parentPid, 0);
} catch (err: unknown) {
// ESRCH = process gone → exit. EPERM = process alive, different UID → ignore.
if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
process.exit(0);
}
}
}, 5_000);
Comment on lines +1619 to +1628
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Bare catch swallows EPERM, falsely kills server

process.kill(pid, 0) throws two distinct errors:

  • ESRCH — process does not exist → correct to exit
  • EPERM — process exists but we lack permission to signal it → incorrect to exit

The bare catch block catches both, meaning if the parent process is still alive but running in a slightly different permission context (e.g., after a setuid call, or on certain Linux configurations where a parent can have a different UID), the server will exit spuriously every 5 seconds.

The fix is to only exit on ESRCH:

Suggested change
const parentGuard = setInterval(() => {
try {
process.kill(parentPid, 0);
} catch {
process.exit(0);
}
}, 5_000);
const parentGuard = setInterval(() => {
try {
process.kill(parentPid, 0);
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
process.exit(0);
}
}
}, 5_000);

parentGuard.unref();
}

const transport = new StdioServerTransport();
await server.connect(transport);

// Register cleanup before any handler that calls process.exit(), so the
// exit listener is always in place when stdin/onclose/signals fire.
const stopAllWatchers = () => {
for (const project of getAllProjects()) {
project.stopWatcher?.();
}
};

process.once('exit', stopAllWatchers);
process.once('SIGINT', () => {
stopAllWatchers();
process.exit(0);
});
process.once('SIGTERM', () => {
stopAllWatchers();
process.exit(0);
});
process.once('SIGHUP', () => {
stopAllWatchers();
process.exit(0);
});

// Detect stdin pipe closure — the primary signal that the MCP client is gone.
// StdioServerTransport only listens for 'data'/'error', never 'end'.
process.stdin.on('end', () => process.exit(0));
process.stdin.on('close', () => process.exit(0));

// Handle graceful MCP protocol-level disconnect.
// Fires after SDK internal cleanup when transport.close() is called.
server.onclose = () => process.exit(0);
Comment on lines +1657 to +1664
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New exit paths bypass watcher cleanup window

The stdin and server.onclose handlers call process.exit(0) directly, relying on process.once('exit', stopAllWatchers) (registered at line 1668) to clean up watchers. However, stopAllWatchers is a const that is not yet declared at this point in main().

If either handler fires after initProject() starts a watcher (around line 1648) but before process.once('exit', stopAllWatchers) is registered (line 1668), the exit listener won't be in place and watchers will be abandoned rather than stopped gracefully. The window is small but real — initProject() is awaited, so the event loop can yield during that call.

One safe pattern is to hoist the stopAllWatchers registration to right after server.connect(transport), before these handlers are set, so the exit listener is always in place when the handlers could possibly fire.


if (process.env.CODEBASE_CONTEXT_DEBUG) console.error('[DEBUG] Server ready');

await refreshKnownRootsFromClient();
Expand All @@ -1634,23 +1682,6 @@ async function main() {
/* best-effort */
}
});

// Cleanup all watchers on exit
const stopAllWatchers = () => {
for (const project of getAllProjects()) {
project.stopWatcher?.();
}
};

process.once('exit', stopAllWatchers);
process.once('SIGINT', () => {
stopAllWatchers();
process.exit(0);
});
process.once('SIGTERM', () => {
stopAllWatchers();
process.exit(0);
});
}

// Export server components for programmatic use
Expand Down
Loading