Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#2,776 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@tiagovilasboas がすでに取り組んでいます。

2026年9月11日 から。

  • #2788 @tiagovilasboas による — マージされずにクローズ

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
72/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
node.js, typescript
領域
api, backend, testing

調査の方向性

StdioClientTransport の stderr 処理から始め、どの consumer も transport.stderr を読み取らない状態で stderr: 'pipe' にするとハングすることを再現します。pipe と default または inherit の動作を比較し、その後、reader が接続されていない chatty server を対象とする回帰テストを追加します。完了条件は、リクエストがブロックせずに完了し、consumer が引き続き stderr を読み取れることです。

索引モデルが issue の本文から書いたものです。

説明

v1 v2

StdioClientTransport accepts stderr: 'pipe' and exposes the stream as transport.stderr, which is the documented way to read a server's diagnostics. If nobody attaches a reader, a server that logs a normal amount to stderr blocks on write, stops reading stdin, and the session stops — silently. No error, no rejection, no transport-level timeout. await client.listTools() simply never returns.

Reproduction

// s.mjs — a server that logs to stderr, which is what stderr is for
import { createInterface } from 'node:readline';
const send = o => process.stdout.write(JSON.stringify(o) + '\n');
createInterface({ input: process.stdin }).on('line', line => {
  const m = JSON.parse(line);
  if (m.method === 'initialize') return send({ jsonrpc:'2.0', id:m.id, result:{
    protocolVersion:'2025-06-18', capabilities:{tools:{}}, serverInfo:{name:'chatty',version:'1.0.0'} }});
  if (m.method === 'notifications/initialized') return;
  for (let i = 0; i < 200; i++) process.stderr.write('log line '.repeat(5000) + '\n');
  send({ jsonrpc:'2.0', id:m.id, result:{ tools:[{name:'x',description:'d',inputSchema:{type:'object'}}] }});
});
// c.mjs
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const t = new StdioClientTransport({ command:'node', args:['s.mjs'], stderr:'pipe' });
const c = new Client({ name:'demo', version:'1.0.0' });
await c.connect(t);

if (process.argv[2] === 'drain') t.stderr.resume();   // the only difference

console.log('listTools()...');
const started = Date.now();
const r = await c.listTools();
console.log(`returned: ${r.tools.length} tools, ${Date.now()-started}ms`);
$ node c.mjs
listTools()...
            ← never returns

$ node c.mjs drain
listTools()...
returned: 1 tools, 19ms

@modelcontextprotocol/[email protected], Node 22.22.1. Same hang on stderr: 'overlapped'; inherit and the default are fine, since the OS drains them.

Why this bites in practice

The pipe fills, the child blocks in write(2), and a blocked child is not reading stdin either — so the request it was answering never gets answered. Standard pipe behaviour, but three things make it a bad failure here:

  • The option is offered by the SDK and there is no warning attached to it. stderr: 'pipe' reads as "let me see the server's logs", not "you are now responsible for draining a pipe or the session dies".
  • The failure is silent and looks like something else. No onerror, no rejection. It presents as a slow or unresponsive server, so the natural first move is to blame the server or raise the request timeout — neither of which helps.
  • Nothing unusual has to happen. The server in the repro is writing to stderr, which is what stderr is for. Anything with verbose logging enabled crosses the threshold on a single response.

A consumer who attaches a reader late — after connect() resolves but after a first request has already gone out — hits it intermittently, which is worse than hitting it every time.

Suggestion

Any of these would close it; the first seems most in keeping with the rest of the transport:

  1. When stderr: 'pipe' is requested and nothing is attached by the time the transport starts, drain it internally (.resume()) so the child never blocks. A consumer that attaches a listener still gets the data; one that does not gets a working session instead of a hang.
  2. Alternatively, document the requirement on StdioServerParameters.stderr and in the transport.stderr getter, in the form "you must consume this stream".
  3. Or surface it: if the stream is unread and buffered beyond some size, emit through onerror rather than hanging.

Happy to open a PR for (1) with a regression test if that direction is right.


Related, though a different mechanism: #2678 fixes protocol errors being dropped when no onerror is set, and #2775 covers the real error reaching onerror while the awaiting caller gets Connection closed. This one is a third shape of the same underlying experience — the SDK is in a position to say what went wrong and the caller ends up with nothing.

主要言語
TypeScript
スター
13.5k
フォーク
2.3k
平均マージ
1日 22時間
マージ済み PR(30日)
52

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

modelcontextprotocol/typescript-sdk のほかの issue

modelcontextprotocol/typescript-sdk の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。