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
- 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. - Wait for
[MCP] Server started (transport: stdio). - 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).
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.initialize,tools/list,resources/list,resources/readall 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.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, butprocess.stdinis paused.Root cause
oclif's argument parser opens a
readlineInterface overprocess.stdinand closes it on its abort signal;Interface.close()callsstdin.pause(). Captured stack:So the deafness is in the host process, above the plugin. Two controls isolate it there: (a) a bare
@modelcontextprotocol/sdkMcpServer+StdioServerTransportanswers 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 constructsnew StdioServerTransport()andconnect()s it; nothing resumesprocess.stdinafter oclif's parser has paused it. Onorigin/mainthe connect region carries nostdin.resume()mitigation.Consequence: every
objectstack serve/devstdio transport is started-but-deaf —OS_MCP_STDIO_ENABLED=trueadvertises 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
node packages/cli/bin/run.js serve -p <port> --devwithstdio:['pipe','pipe','pipe'],OS_MCP_STDIO_ENABLED=true, and a validosk_key.[MCP] Server started (transport: stdio).initializeline to the child's stdin.Expected an
initializeresult. Actual nothing, ever (zero bytes on stdout).Source
Extracted from the QA run #7627 (framework 92f26f7, console 6314e87f).