fix: exit on stdin EOF / SIGTERM so the stdio server isn't orphaned#41
Merged
bvisible merged 1 commit intoJun 9, 2026
Merged
Conversation
A stdio MCP server is torn down by its host closing stdin (and/or SIGTERM/ SIGHUP), not by SIGINT — which only arrives on an interactive Ctrl-C. Handling SIGINT alone meant the process was never signalled on normal teardown: it was reparented to init and leaked one node process (~83MB, plus active keepalive timers) per session. Over a day of normal use this accumulated into dozens of orphaned processes consuming gigabytes of RAM. Fix: - Extract shutdown into an idempotent shutdown() and trigger it on SIGTERM, SIGHUP, and stdin 'end'/'close' (the actual MCP teardown signals), in addition to the existing SIGINT. - unref() the keepalive and periodic-cleanup intervals so a timer alone can never keep the event loop alive after the transport has closed. - Flush stdout best-effort before exiting, with a short unref'd timer that forces exit so teardown can never hang if the host has stopped reading.
bvisible
added a commit
that referenced
this pull request
Jun 9, 2026
…40) - Add tests/test-lifecycle.js: black-box teardown tests asserting the server exits cleanly on stdin EOF, SIGTERM, SIGINT and overlapping signals (regression guard for the orphan-process bug). - Extend test-server-config-manager.js: laziness (no reload without change), reload-failure keeps last valid config + recovery, deleted-file robustness. - Wire test:lifecycle into the npm test chain.
bvisible
added a commit
that referenced
this pull request
Jun 9, 2026
- feat: live SSH config hot reload (#40, @EnjoySR) - fix: exit on stdin EOF / SIGTERM so the stdio server isn't orphaned (#41, @LegendaryGatz) - test: new lifecycle suite + extended hot-reload edge cases
Owner
|
Merged and shipped in v3.6.0 🎉 Thanks @LegendaryGatz — excellent diagnosis and a clean, idempotent fix. I reproduced the before/after on my side:
I also added a black-box regression test ( Your follow-up note on the module-level intervals in |
bvisible
added a commit
that referenced
this pull request
Jun 9, 2026
…he event loop Follow-up to #41. tunnel-manager.js (monitorTunnels, 30s) and session-manager.js (session cleanup, 5min) registered module-level setInterval timers that were never unref'd, so importing either module kept the Node event loop alive on its own. The forced-exit in #41 already made these harmless for the orphan bug, but unref'ing them restores proper teardown hygiene: process lifetime tracks the stdio transport, not a background timer. Verified: before, importing either module never exits; after, both exit naturally.
bvisible
added a commit
that referenced
this pull request
Jun 9, 2026
- fix: unref module-level monitor/cleanup intervals (follow-up to #41) - docs: remove MseeP.ai security badge from README footer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Running as a stdio MCP server,
mcp-ssh-manageris never terminated when the host ends a session: it's reparented to init and leaks. Each orphan holds ~83 MB plus live keepalive timers. They accumulate one-per-session (I found 28 ≈ 2.3 GB after ~1.5 days of normal use).Root cause
The only shutdown handler was
process.on('SIGINT'). A stdio MCP host tears the server down by closing the child's stdin (EOF) and/or sending SIGTERM — not SIGINT (interactive Ctrl-C only). So normal teardown signalled nothing. The keepalive and 10-min cleanupsetIntervals are also ref'd, keeping the event loop alive regardless.Fix
shutdown(reason)and register it onSIGTERM,SIGHUP, and stdin'end'/'close', in addition to the existingSIGINT..unref()the per-connection keepalive interval and the periodic cleanup interval, so process lifetime tracks the transport, not a timer.unref'd timer that forces exit — teardown can never hang if the host has stopped reading.ssh.dispose()in try/catch so one bad connection can't block the rest.Testing
node --check src/index.jspasses.code=0) 11 ms after stdin close, logging🔌 Closing SSH connections (stdin ended)....Out of scope (follow-up)
tunnel-manager.js(setInterval(monitorTunnels, …)) andsession-manager.jshave module-level intervals that are also neverunref'd. The forced exit here makes them harmless for this bug, but they're worth the same treatment in a follow-up.Developed with Claude Code, then reviewed and verified end-to-end on my side: I reproduced the leak, confirmed the root cause in the source, and ran the before/after exit test above. Happy to iterate on anything.