Skip to content

fix(client): drain piped stderr so unread pipe cannot deadlock - #2788

Draft
tiagovilasboas wants to merge 4 commits into
modelcontextprotocol:mainfrom
tiagovilasboas:fix/stdio-stderr-pipe-deadlock-2776
Draft

fix(client): drain piped stderr so unread pipe cannot deadlock#2788
tiagovilasboas wants to merge 4 commits into
modelcontextprotocol:mainfrom
tiagovilasboas:fix/stdio-stderr-pipe-deadlock-2776

Conversation

@tiagovilasboas

@tiagovilasboas tiagovilasboas commented Sep 11, 2026

Copy link
Copy Markdown

Problem

StdioClientTransport with stderr: 'pipe' (or 'overlapped') pipes child stderr into a PassThrough that stays paused until a reader attaches. A server that logs a normal amount to stderr fills that buffer (16 KiB highWaterMark, then the OS pipe), blocks on write(2), stops reading stdin, and the session hangs — no onerror, no rejection. await client.listTools() never returns.

Solution

Issue option (1): after child.stderr.pipe(passThrough), call passThrough.resume() so the stream is in flowing mode. Unread chunks are drained; listeners attached before start() / connect() still receive every chunk.

Late listeners (after connect()) see only post-attach data — lost log lines beat a hung session. Documented on StdioServerParameters.stderr and the stderr getter, including that paused-mode .read() is not supported after start().

Test plan

  • Chatty-stderr child, stderr: 'pipe', no reader → listTools() completes (timeout = fail)
  • Listener attached before start() still receives startup + post-request chunks
  • Late listener: session still completes; only post-attach chunks are received
  • pnpm --filter @modelcontextprotocol/client exec vitest run test/client/stdio.test.ts — 9/9 passed locally (1.21s)

Follow-up

v1.x backport (src/client/stdio.ts) is out of scope for this PR.

Fixes #2776

Put the stderr PassThrough in flowing mode after piping so a chatty
child cannot fill the unread pipe and hang the session. Listeners
attached before start() still receive chunks.

Fixes modelcontextprotocol#2776

Co-authored-by: Tiago Vilas Boas <tcarvalhovb@gmail.com>
Assert listTools() completes when stderr is piped with no reader, that
a pre-start listener still receives chunks, and that a late listener
does not deadlock and sees only post-attach output.

Co-authored-by: Tiago Vilas Boas <tcarvalhovb@gmail.com>
Co-authored-by: Tiago Vilas Boas <tcarvalhovb@gmail.com>
@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 910cb45

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/core-internal Patch

Not sure what this means? Click here to learn what changesets are.

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

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

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2788

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2788

@modelcontextprotocol/server

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

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2788

@modelcontextprotocol/express

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

@modelcontextprotocol/fastify

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

@modelcontextprotocol/hono

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

@modelcontextprotocol/node

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

commit: 910cb45

Avoid racing stdout completion against flowing-mode stderr delivery,
and keep flood size large enough to fill a paused pipe without dumping
megabytes into assertion output.

Co-authored-by: Tiago Vilas Boas <tcarvalhovb@gmail.com>
BerkantACUN pushed a commit to BerkantACUN/typescript-sdk that referenced this pull request Sep 11, 2026
v1.x backport of modelcontextprotocol#2788.

`StdioClientTransport` with `stderr: 'pipe'` (or `'overlapped'`) pipes
child stderr into a PassThrough that stays paused until a reader
attaches. A server that logs a normal amount fills that buffer and the
OS pipe, blocks on write(2), stops reading stdin, and the session hangs
with no error, no rejection and no timeout. After piping, the
PassThrough is put in flowing mode so unread chunks drain; listeners
attached before start() still receive every chunk, a late listener sees
only post-attach data.

Fixes modelcontextprotocol#2776 on v1.x.
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.

stderr: 'pipe' deadlocks the session if the consumer never reads transport.stderr

1 participant