fix: handle graceful shutdown in McpServer.run()#482
Conversation
Register SIGTERM and SIGINT handlers to close the HTTP server before process exit. This prevents EADDRINUSE errors when using nodemon or other process managers that restart the server. Fixes #481 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
||
| const shutdown = () => { | ||
| server.close(() => process.exit(0)); | ||
| setTimeout(() => process.exit(1), 3000); |
There was a problem hiding this comment.
Consider calling .unref() on the safety-net timer
The setTimeout timer will keep the Node.js event loop alive for 3 seconds even after server.close() completes (though process.exit(0) in the callback makes this moot in the happy path). Calling .unref() is a common best practice for safety-net timers so they don't prevent natural process exit in edge cases.
| setTimeout(() => process.exit(1), 3000); | |
| setTimeout(() => process.exit(1), 3000).unref(); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/server/server.ts
Line: 273:273
Comment:
**Consider calling `.unref()` on the safety-net timer**
The `setTimeout` timer will keep the Node.js event loop alive for 3 seconds even after `server.close()` completes (though `process.exit(0)` in the callback makes this moot in the happy path). Calling `.unref()` is a common best practice for safety-net timers so they don't prevent natural process exit in edge cases.
```suggestion
setTimeout(() => process.exit(1), 3000).unref();
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
I agree with greptile, and I'm not sure why you even need the setTimeout
There was a problem hiding this comment.
Can you use reject instead of process.exit(1) by the way ?
There was a problem hiding this comment.
Looking at the context, the setTimeout is a safety net for when server.close() hangs or takes too long. Using reject() here wouldn't work as expected because:
-
Wrong scope: The
rejectis already out of scope - it belongs to the Promise created at the start ofrun()that was resolved when the server started listening successfully (lines before this code) -
Already resolved: By the time we're in the shutdown handler, the Promise has already been resolved (server is running), so calling
rejectwould have no effect -
Process is exiting anyway: This is a signal handler for
SIGTERM/SIGINT- the process is meant to exit, not reject a promise
However, I do see an issue: the force-exit timer should probably use process.exit(0) instead of process.exit(1), since a timeout during graceful shutdown isn't necessarily an error state - it's just taking longer than expected. Or if you want to keep it as exit code 1 to signal the timeout issue, that's also reasonable.
The real question is whether you even need the timeout at all. For a simple HTTP server without persistent connections or complex cleanup, server.close() should complete quickly. You could simplify to:
| setTimeout(() => process.exit(1), 3000); | |
| const shutdown = () => { | |
| server.close(() => process.exit(0)); | |
| }; |
Unless you have long-lived connections or complex cleanup logic that might cause server.close() to hang, the timeout is probably unnecessary.
Summary
SIGTERMandSIGINTsignal handlers inMcpServer.run()to gracefully close the HTTP server before process exitEADDRINUSEerrors when usingnodemonor other process managers that restart the serverserver.close()hangsFixes #481
Test plan
nodemon, verify restarts no longer causeEADDRINUSESIGTERMto a running server, verify it exits cleanlySIGINT(Ctrl+C) to a running server, verify it exits cleanlypnpm --filter skybridge build)🤖 Generated with Claude Code
Greptile Summary
This PR adds graceful shutdown handling to
McpServer.run()by registeringSIGTERMandSIGINTsignal handlers that close the HTTP server before process exit. This preventsEADDRINUSEerrors when using process managers likenodemonthat restart the server. A 3-second forced-exit timeout acts as a safety net ifserver.close()hangs.SIGTERMandSIGINThandlers that callserver.close()followed byprocess.exit(0)setTimeoutfallback that callsprocess.exit(1)ifserver.close()doesn't completeConfidence Score: 4/5
run()method's lifecycle behavior. The logic is straightforward: close the server on signal, force-exit after 3 seconds if it hangs. One minor style suggestion (.unref()on the timer) but no functional issues.Last reviewed commit: 75daca4