Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

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

Aperta
#2,776 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@tiagovilasboas ci sta già lavorando.

Dal 11/9/2026.

  • #2788 di @tiagovilasboas — chiusa senza merge

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
72/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
node.js, typescript
Ambito
api, backend, testing

Direzione di ricerca

Inizia dalla gestione di stderr di StdioClientTransport e riproduci il blocco con stderr: 'pipe' quando nessun consumer legge transport.stderr. Confronta il comportamento di pipe con quello di default o inherit, quindi aggiungi un test di regressione che copra un server loquace senza un reader collegato; il lavoro è completato quando la richiesta termina senza bloccarsi e un consumer può ancora leggere stderr.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.

Lingua principale
TypeScript
Stelle
13.6k
Fork
2.3k
Merge medio
1g 15h
PR unite (30g)
51

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di modelcontextprotocol/typescript-sdk

Tutte le issue di modelcontextprotocol/typescript-sdk

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.