fix(client): drain piped stderr so unread pipe cannot deadlock - #2788
Draft
tiagovilasboas wants to merge 4 commits into
Draft
fix(client): drain piped stderr so unread pipe cannot deadlock#2788tiagovilasboas wants to merge 4 commits into
tiagovilasboas wants to merge 4 commits into
Conversation
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 detectedLatest commit: 910cb45 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
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.
9 tasks
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
StdioClientTransportwithstderr: 'pipe'(or'overlapped') pipes child stderr into aPassThroughthat stays paused until a reader attaches. A server that logs a normal amount to stderr fills that buffer (16 KiBhighWaterMark, then the OS pipe), blocks onwrite(2), stops reading stdin, and the session hangs — noonerror, no rejection.await client.listTools()never returns.Solution
Issue option (1): after
child.stderr.pipe(passThrough), callpassThrough.resume()so the stream is in flowing mode. Unread chunks are drained; listeners attached beforestart()/connect()still receive every chunk.Late listeners (after
connect()) see only post-attach data — lost log lines beat a hung session. Documented onStdioServerParameters.stderrand thestderrgetter, including that paused-mode.read()is not supported afterstart().Test plan
stderr: 'pipe', no reader →listTools()completes (timeout = fail)start()still receives startup + post-request chunkspnpm --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