Skip to content

Commit ff486ea

Browse files
czlonkowskiclaude
andauthored
fix: stdio exits on stdin close in containers + route bin through wrapper (v2.47.5) (czlonkowski#712)
* fix: stdio exits on stdin close in containers + route bin through wrapper (v2.47.5) Two layered root causes for Issue czlonkowski#711 (reported by @jbjardine), which surfaced as "Container stdio path ignores stdin close; default npx process exits only after SIGTERM". Layer 1 — src/mcp/index.ts container guard The stdio entrypoint guarded stdin end/close handler registration behind an isContainerEnvironment() check, so IS_DOCKER=true npx -y n8n-mcp </dev/null stayed alive until SIGTERM arrived. The guard was added to prevent premature shutdown when stdin closes in detached containers, but stdio MCP cannot operate without an open stdin, so the guard protected a non-functional scenario while breaking valid stateless stdio clients (mark3labs/mcp-go, MCPJungle). Removed the guard; stdin handlers now register unconditionally. isContainerEnvironment() and its fs.existsSync import were deleted. Layer 2 — .github/workflows/release.yml bin path Commit bc191b0 (v2.45.1) fixed Issue czlonkowski#693 (reported by @gjenkins20) by switching the bin from dist/mcp/index.js to dist/mcp/stdio-wrapper.js in package.json and both publish scripts — but missed release.yml:375 which hardcoded the old path. CI publishes via release.yml, so every release from v2.45.1 through v2.47.4 still shipped bin: dist/mcp/index.js. The fix never reached users. release.yml is now consistent with the other three sources, and tests/unit/bin-consistency.test.ts guards against the same drift recurring. Telemetry CLI preservation Once the published bin routes through stdio-wrapper.js, npx n8n-mcp telemetry enable|disable|status would stop working because the wrapper suppresses console and never inspects argv. Extracted the handler to src/telemetry/telemetry-cli.ts and called it from both index.ts and the wrapper (the wrapper lazy-requires it before MCP_MODE/console setup). Eliminates ~35 lines of near-identical duplication. Tests - tests/integration/mcp/stdio-shutdown.test.ts: 3 regression cases that spawn dist/mcp/index.js and assert exit-on-stdin-close (with and without IS_DOCKER=true) and exit-on-SIGTERM within a 500ms budget. - tests/unit/bin-consistency.test.ts: static drift guard asserting that package.json, both publish scripts, and release.yml agree on the stdio-wrapper bin path. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: apply simplify + review pass refinements - telemetry-cli: drop unreachable break statements after process.exit() (process.exit is typed 'never', noFallthroughCasesInSwitch stays green) - stdio-shutdown test: use describe.skipIf() instead of ternary - stdio-shutdown test: expectExitWithin early-return now also checks signalCode, closing a gap where a child already killed by signal (null exitCode, non-null signalCode) would fall through to the 'exit' event await and time out instead of returning immediately No behavioral change to production code beyond the telemetry-cli cleanup. Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore container guard in index.ts; test against stdio-wrapper.js CI caught that removing the container guard broke the Docker entrypoint tests (tests/integration/docker/docker-entrypoint.test.ts). Those tests run `docker run -d --user root` which hits the entrypoint's root-switch path at docker/docker-entrypoint.sh:122, which hardcodes `node /app/dist/mcp/index.js` — NOT the wrapper. Detached containers redirect stdin from /dev/null, so with the guard removed the MCP process exited immediately and subsequent `docker exec` commands failed with "container is not running". The container guard wasn't protecting a non-functional scenario — it was load-bearing for Docker's detached lifecycle, where the process receives SIGTERM from `docker stop` instead of reading stdin. Fix: - Restore isContainerEnvironment() helper and the guarded block in src/mcp/index.ts. Updated the surrounding comment to explain the split: stdio-wrapper.ts is the npx path and handles stdin close unconditionally (fixing czlonkowski#711); index.ts is the Docker path and keeps the guard. - Update tests/integration/mcp/stdio-shutdown.test.ts to spawn dist/mcp/stdio-wrapper.js instead of dist/mcp/index.js. The wrapper is the published bin entry (after the release.yml fix), so this matches the actual `npx n8n-mcp` path users hit. - Reframe the CHANGELOG to make clear the czlonkowski#711 fix is delivered via the release.yml bin path change (Layer 2), not via Layer 1 (index.ts). Layer 1 is intentionally left in place for Docker introspection. Verified locally: - IS_DOCKER=true node dist/mcp/stdio-wrapper.js </dev/null → exits - IS_DOCKER=true node dist/mcp/index.js </dev/null → stays alive (Docker introspection intact) - node dist/mcp/index.js </dev/null → exits (non-container guard removed) - npm test → 5313 pass, 74 skip, 0 fail Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d9d847f commit ff486ea

18 files changed

Lines changed: 344 additions & 86 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ jobs:
372372
import: './dist/index.js'
373373
}
374374
};
375-
pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' };
375+
pkg.bin = { 'n8n-mcp': './dist/mcp/stdio-wrapper.js' };
376376
pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' };
377377
pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation'];
378378
pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en';

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.47.5] - 2026-04-08
11+
12+
### Fixed
13+
14+
- **`npx n8n-mcp </dev/null` now exits promptly on stdin close (Issue #711, reported by @jbjardine).** The root cause was that the published bin entry was still `dist/mcp/index.js`, not `dist/mcp/stdio-wrapper.js`, so `IS_DOCKER=true npx -y n8n-mcp </dev/null` hit `index.js`'s container guard and stayed alive until SIGTERM arrived — breaking stateless stdio clients (e.g. `mark3labs/mcp-go`, MCPJungle) that close stdin to signal shutdown. The fix is to finally route the published bin through the wrapper (see below), which has always registered stdin handlers unconditionally. The container guard in `index.ts` is deliberately kept: Docker's detached-mode lifecycle (`docker run -d`) redirects stdin from `/dev/null` and relies on signals from `docker stop` for shutdown, not stdin close — the Docker entrypoint's root-switch path hardcodes `node /app/dist/mcp/index.js`, so the guard is load-bearing for every containerized deployment.
15+
- **Published bin entry finally routes through `stdio-wrapper.js` (Issue #693, reported by @gjenkins20).** Commit bc191b0 (v2.45.1) updated `package.json`, `scripts/publish-npm.sh`, and `scripts/publish-npm-quick.sh` to route the bin through the stdio wrapper, but missed `.github/workflows/release.yml:375` which hardcoded the old path. Every CI release from v2.45.1 through v2.47.4 therefore shipped `bin: dist/mcp/index.js` — the fix never reached users. `release.yml` is now consistent with the other three sources, and a static test in `tests/unit/bin-consistency.test.ts` guards against the same drift recurring.
16+
- **Telemetry CLI handler extracted to `src/telemetry/telemetry-cli.ts`** and called from both `src/mcp/index.ts` and `src/mcp/stdio-wrapper.ts`. This preserves `npx n8n-mcp telemetry enable|disable|status` (documented in `PRIVACY.md` and `README.md`) now that the published bin routes through the wrapper, and eliminates ~35 lines of duplication. The config manager is lazy-required so it stays off the stdio hot path when no CLI subcommand is present.
17+
18+
### Notes
19+
20+
- First-run telemetry banner is no longer printed on cold start via `npx n8n-mcp` because `stdio-wrapper.js` suppresses all `console.log` output before the server imports. This was already the behavior when users invoked the wrapper directly; it becomes user-visible now that the wrapper is the published bin. Run `npx n8n-mcp telemetry status` to see current telemetry state.
21+
- Added `tests/integration/mcp/stdio-shutdown.test.ts` with 3 regression cases that spawn `dist/mcp/stdio-wrapper.js` (the published bin entry, matching the `npx` path) and assert exit-on-stdin-close / exit-on-SIGTERM within a 500ms budget, covering the exact Issue #711 repro.
22+
23+
Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en
24+
1025
## [2.47.4] - 2026-04-08
1126

1227
### Security

dist/mcp/index.js

Lines changed: 2 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/mcp/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/mcp/stdio-wrapper.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)