Skip to content

Commit 2180999

Browse files
fix(stdio): exit cleanly when fatal logging hits a dead pipe (#1276)
## Summary Fixes #1274. When the MCP client dies and closes stderr, the old fatal handlers could throw while logging, get re-invoked forever, and leave orphaned processes spinning at 100%+ CPU. ### Key Changes - One shared exit path for uncaught exceptions, unhandled rejections, and stdio startup failures - Catch logging/reporting failures so a dead stderr cannot throw inside the handler - Always `process.exit(1)`, including a timeout if Sentry flush hangs ### Breaking Changes - None <!-- junior-request-attribution:start --> Requested by **David Cramer**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:slack%3AC08J1NSPU6S%3A1787941530.610869 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3AC08J1NSPU6S%3A1787941530.610869) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC08J1NSPU6S%3A1787941530.610869/?project=4510944073809921) <!-- junior-session-footer:end --> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: David Cramer <david@sentry.io>
1 parent d024020 commit 2180999

1 file changed

Lines changed: 35 additions & 16 deletions

File tree

‎packages/mcp-server/src/index.ts‎

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -324,19 +324,41 @@ async function main() {
324324
process.on("SIGTERM", () => shutdown("SIGTERM"));
325325
process.on("SIGINT", () => shutdown("SIGINT"));
326326

327-
// Uncaught error handlers
328-
process.on("uncaughtException", async (error) => {
329-
console.error("Uncaught exception:", error);
330-
Sentry.captureException(error);
331-
await Sentry.flush(SENTRY_TIMEOUT);
332-
process.exit(1);
327+
// If the client dies and closes all pipes, console.error can throw EPIPE
328+
// inside these handlers. Without a guard, that recurses forever and burns CPU.
329+
// See getsentry/sentry-mcp#1274.
330+
let exiting = false;
331+
function exitAfterError(error: unknown, label: string) {
332+
if (exiting) {
333+
process.exit(1);
334+
return;
335+
}
336+
exiting = true;
337+
338+
try {
339+
console.error(label, error);
340+
} catch {
341+
// stderr may already be closed
342+
}
343+
344+
try {
345+
Sentry.captureException(error);
346+
} catch {
347+
// reporting must not block exit
348+
}
349+
350+
// Don't depend on flush finishing — exit even if it hangs or rejects.
351+
const timer = setTimeout(() => process.exit(1), SENTRY_TIMEOUT);
352+
timer.unref();
353+
void Sentry.flush(SENTRY_TIMEOUT).finally(() => process.exit(1));
354+
}
355+
356+
process.on("uncaughtException", (error) => {
357+
exitAfterError(error, "Uncaught exception:");
333358
});
334359

335-
process.on("unhandledRejection", async (reason) => {
336-
console.error("Unhandled rejection:", reason);
337-
Sentry.captureException(reason);
338-
await Sentry.flush(SENTRY_TIMEOUT);
339-
process.exit(1);
360+
process.on("unhandledRejection", (reason) => {
361+
exitAfterError(reason, "Unhandled rejection:");
340362
});
341363

342364
const context = {
@@ -362,11 +384,8 @@ async function main() {
362384
experimentalMode: cli.experimental,
363385
});
364386

365-
startStdio(server, context).catch(async (err) => {
366-
console.error("Server error:", err);
367-
Sentry.captureException(err);
368-
await Sentry.flush(SENTRY_TIMEOUT);
369-
process.exit(1);
387+
startStdio(server, context).catch((err) => {
388+
exitAfterError(err, "Server error:");
370389
});
371390
}
372391

0 commit comments

Comments
 (0)