Skip to content

fix: prevent unhandled rejections on close and detect stdin EOF#1853

Open
EtanHey wants to merge 2 commits intomodelcontextprotocol:mainfrom
EtanHey:fix/connection-closed-unhandled-rejection
Open

fix: prevent unhandled rejections on close and detect stdin EOF#1853
EtanHey wants to merge 2 commits intomodelcontextprotocol:mainfrom
EtanHey:fix/connection-closed-unhandled-rejection

Conversation

@EtanHey
Copy link
Copy Markdown

@EtanHey EtanHey commented Apr 5, 2026

Summary

Fixes two related bugs that together cause the "MCP error -32000: Connection closed" crash (#1049):

  • Defer pending response handler rejections in Protocol._onclose() — When the transport closes with in-flight requests, _onclose() rejects all pending promises. Previously this happened synchronously, which could trigger unhandled promise rejections if callers hadn't attached .catch() handlers yet (especially on Node.js 24's strict unhandled-rejection behavior). Now rejections are deferred to the next microtask via Promise.resolve().then(), giving callers time to attach handlers.

  • Detect stdin EOF in StdioServerTransport — The server transport only listened for data and error events on stdin, but never end. When the client process exits, stdin emits end (EOF), but the server had no listener for it — leaving the server running indefinitely with pending requests that would never resolve. Now StdioServerTransport listens for the end event and triggers a clean close().

What

File Change
packages/core/src/shared/protocol.ts Defer handler(error) calls in _onclose() to next microtask; wrap in try/catch routing to onerror
packages/server/src/server/stdio.ts Add _onend handler for stdin end event; register/unregister in start()/close()

Why

When a stdio-based MCP server's client process exits unexpectedly:

  1. The server has no way to detect the disconnection (no stdin EOF listener)
  2. If the server does eventually close, all pending request promises are rejected synchronously
  3. If any of those promises don't have .catch() attached yet, Node.js triggers unhandledRejection
  4. On Node.js 24 (default --unhandled-rejections=throw), this kills the entire process

This crashes BrainLayer, VoiceLayer, and other MCP servers in production environments daily.

Test Plan

  • New test: should reject pending requests with ConnectionClosed when transport closes
  • New test: should not cause unhandled promise rejections when transport closes with pending requests
  • New test: should close transport when stdin emits end (EOF)
  • New test: should not fire onclose twice when stdin EOF followed by explicit close()
  • New test: should process remaining messages before closing on stdin EOF
  • All existing tests pass (491 core + 58 server + 350 client + integration)

Related

🤖 Generated with Claude Code

Two fixes for the "Connection closed" crash (MCP error -32000):

1. Protocol._onclose() now defers pending response handler rejections
   to the next microtask via Promise.resolve().then(), giving callers
   time to attach .catch() handlers. This prevents Node.js from
   triggering unhandled promise rejections when the transport closes
   unexpectedly while requests are in-flight.

2. StdioServerTransport now listens for the stdin 'end' event (EOF)
   and triggers a clean close(). Previously, when the client process
   exited, the server had no way to detect the disconnection, leaving
   it running indefinitely with pending requests that would never
   resolve.

Together these fixes address the crash reported in modelcontextprotocol#1049 where
stdio-based MCP servers would die with "MCP error -32000: Connection
closed" unhandled rejections when the client process exits.

Also addresses the root cause described in modelcontextprotocol#392 (promise/async
handling causes unhandled rejections).

Fixes modelcontextprotocol#1049
Refs modelcontextprotocol#392
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 5, 2026

⚠️ No Changeset found

Latest commit: 6bd5c9d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 5, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1853

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1853

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1853

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1853

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1853

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1853

commit: 6bd5c9d

The handler variable using PromiseRejectionEvent was dead code — the
test already uses the Node.js process.on('unhandledRejection') handler.
Removing it fixes the TS2304 type error in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@EtanHey EtanHey requested a review from a team as a code owner April 5, 2026 16:32
@EtanHey
Copy link
Copy Markdown
Author

EtanHey commented Apr 5, 2026

Note: I noticed PR #1814 also touches stdin EOF detection and Protocol._onclose(). This PR takes a complementary approach — deferring handler rejections via Promise.resolve().then() and adding a simpler stdin 'end' listener. Happy to rebase onto #1814 if maintainers prefer to land that first, or we can coordinate to avoid duplicate behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MCP-SDK] stdio client crashes with "MCP error -32000: Connection closed" when spawned server exits unexpectedly

1 participant