Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Abierto
#2,776 3 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@tiagovilasboas ya está trabajando en esto.

Desde el 11/9/2026.

  • #2788 de @tiagovilasboas — cerrado sin fusionar

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
72/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Activo
Stack tecnológico
node.js, typescript
Área
api, backend, testing

Línea de trabajo

Empieza por el manejo de stderr de StdioClientTransport y reproduce el bloqueo con stderr: 'pipe' cuando ningún consumidor lee transport.stderr. Compara el comportamiento de pipe con el de default o inherit y añade después una prueba de regresión que cubra un servidor que genera mucha salida sin un lector conectado; se considera terminado cuando la solicitud se completa sin bloquearse y un consumidor aún puede leer stderr.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
TypeScript
Estrellas
13.5k
Forks
2.3k
Merge medio
1 d 22 h
PR fusionados (30 d)
52

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de modelcontextprotocol/typescript-sdk

Todos los issues de modelcontextprotocol/typescript-sdk

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.