Skip to content

mcp-stdio-fail-closed c3: the stdio MCP transport starts but is permanently deaf (oclif arg parser pauses process.stdin) #7645

Description

@huangyiirene

The fail-closed contract this item exists to pin (clauses 0–2, 7) PASSES and is correct — please do not "fix" it: stdio enabled with no key throws at plugin start and the kernel rolls back with the port closed; an unknown/revoked key draws the same refusal with no anonymous-but-serving fallback; a real member key binds the principal to that member (not admin); and stdio off boots cleanly with an explanatory line while HTTP is still served. The defect below is only that the started transport never answers.

Symptom

With the stdio transport successfully started ([MCP] Server started (transport: stdio) logged), the transport never answers a single JSON-RPC request.

  • Observed:initialize, tools/list, resources/list, resources/read all time out with zero bytes on stdout, across 4 separate runs. Malformed input draws no error either. Clause 3 fails; clauses 4–6 are blocked as a consequence.
  • Expected: a started, principal-bound stdio transport answers initialize (and the rest) over the child's stdin/stdout.

An instrumented probe 40 s after boot reports listeners(data)=1 readableFlowing=false isPaused=true bytesRead=0 — the data listener is attached, but process.stdin is paused.

Root cause

oclif's argument parser opens a readline Interface over process.stdin and closes it on its abort signal; Interface.close() calls stdin.pause(). Captured stack:

at Interface.pause (node:internal/readline/interface:539)
at Interface.close (node:internal/readline/interface:525)
at signal.addEventListener.once (@oclif/core/lib/parser/parse.js:64)

So the deafness is in the host process, above the plugin. Two controls isolate it there: (a) a bare @modelcontextprotocol/sdkMcpServer + StdioServerTransport answers the identical bytes, and (b) ObjectStack's ownMCPServerRuntime({transport:'stdio'}) answers too — both in a plain node process. MCPServerRuntime.start() (packages/mcp/src/mcp-server-runtime.ts:807-811) simply constructs new StdioServerTransport() and connect()s it; nothing resumes process.stdin after oclif's parser has paused it. On origin/main the connect region carries no stdin.resume() mitigation.

Consequence: every objectstack serve/dev stdio transport is started-but-deaf — OS_MCP_STDIO_ENABLED=true advertises and logs a principal-bound stdio server that no MCP client can ever talk to. The 17/17 unit pins are green because the gap is above the plugin.

Reproduction

  1. Spawn node packages/cli/bin/run.js serve -p <port> --dev with stdio:['pipe','pipe','pipe'], OS_MCP_STDIO_ENABLED=true, and a valid osk_ key.
  2. Wait for [MCP] Server started (transport: stdio).
  3. Write a JSON-RPC initialize line to the child's stdin.

Expected an initialize result. Actual nothing, ever (zero bytes on stdout).

Source

Extracted from the QA run #7627 (framework 92f26f7, console 6314e87f).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions