Build1 distinct publisher3 min readUpdated
story-cli's MCP server answered nothing in a real repo while its protocol tests passed. The fault was process.exit in the CLI entry point, and no unit test owns a process.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A test that imports a handler and calls it never learns whether the handler was reachable. The story-cli suite covered JSON-RPC parsing and serialization, tool registration, and every tool handler, and all of it passed [2]; the protocol module was written as pure functions and described as fully tested [14]. The defect sat in the CLI entry file, above everything the suite could see: `await run(process.argv)`, then `process.exit(exitCode)` [5]. Because `startMcpServer()` registers stdin listeners and returns instead of blocking, `run()` resolved with 0 and the exit fired before readline read a byte [6].
The detail that should bother anyone who owns a CI config: under `spawnSync`, the process reported exit code 0 with nothing on stdout and nothing on stderr [4]. So an integration test that spawns the built binary and asserts a clean exit passes as well. The only check that separates a server from a process that agreed to start and then quit is one that writes a request in and requires bytes back. Absent that, the author debugged by hand and spent the first stretch suspecting his own shell, trying printf, a file redirect, and a sleep to hold stdin open, all of which returned the same nothing [3].
The other two iron rules in the writeup have the same shape. stdout is the protocol channel, so any non-JSON output pollutes the message stream and the client can parse no response at all [7]. That makes a stray debug print in code shared with the ordinary CLI harmless in one caller and fatal in the other, which is precisely the sort of asymmetry a unit test on a pure function cannot express. And `close` on stdin means the input stream ended, not that in-flight callbacks finished [8], so a handler can be correct in isolation and still have its last reply truncated in production.
The shipped fix is where the story gets honest. It guards the exit by comparing `process.argv[2]` against the strings `mcp-server` and `mcp` [9], while the article's own first rule says to extract a long-running abstraction such as `isLongRunning` rather than enumerate specific commands, and names `--watch` modes as long-running too [10]. The author flags that the guard's limitation surfaced the same day, under the heading Bug #1.5 [11]. A literal argv position check breaks the moment a global flag precedes the subcommand.
One more number. The piece asks how there could be a bug when 404 tests were green, having already reported 401 of 404 passing [1], which leaves three that were not [13]. The count quoted in the suite's defence is not the count the suite reported, and that gap is the same class of error as the bug: a property assumed at the boundary, never asserted there.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
Piping a tools/list JSON-RPC line into `node bin/index.ts mcp-server` against a real story repository produced empty output; the author also tried printf, redirecting from a file, and holding stdin open with a trailing sleep, with the same result.
When the request was sent through Node.js spawnSync, the process exit code was 0 and both stdout and stderr were empty.
At the time of the failure, story-cli had 404 automated tests, with 401 passing.
tests/mcp.test.ts covered JSON-RPC protocol parsing, serialization, tool registration, and every tool handler, and all of those tests passed.
The CLI entry file bin/index.ts read: `const exitCode = await run(process.argv)` followed by `process.exit(exitCode)`.
Running `story mcp-server` invoked run(), which called runMcpServer() and then startMcpServer(); because startMcpServer registers stdin listeners and returns rather than blocking, run() returned 0 immediately, process.exit(0) executed, and the JSON-RPC request in stdin was never read by readline.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand, reproducible mechanism from a single self-reported source
The causal chain is unusually well specified for a single-source story: the entry-point code is quoted, the failing invocations are listed verbatim, the spawnSync symptom (exit 0, empty streams) is reported, and the fix is shown. That evidence is internally coherent and mechanistically plausible. It is capped by having no independent reproduction, no linked commit or CI artifact, and by an internal arithmetic inconsistency between '404 tests green' and 401 of 404 passing.
No adoption data supplied
The only usage signal is the maintainer's own disclosure that story-cli exposes 6 MCP tools and treats the server as a P0 roadmap item. There are no downloads, installs, dependent projects, client integrations, or third-party deployments in the supplied source, so adoption cannot be scored without inventing facts.
Mildly overstated framing over a sound technical core
The technical substance is modest and well grounded — a real bug, a shown mechanism, a shown fix — so the gap is small. It is positive rather than zero because the framing of '404 tests green' overstates a suite in which three tests were failing, the '3 hidden traps' packaging generalises one project's experience into universal rules, and the article itself concedes that the fix presented as resolved regressed the same day on `build --watch`.
Maintainer writing about their own project on a self-publishing platform
The author is the maintainer of story-cli and the post doubles as a project introduction, describing the CLI's feature set and its P0 strategic roadmap on a developer self-publishing platform. That creates a promotional incentive alongside the technical account. It is mid-range rather than high because the post's core content is a candid disclosure of the author's own bugs, including an admission that the first fix was wrong, which cuts against pure marketing.
Plausible and specific, but single-source and unverified
Confidence is limited by structure rather than content: one publisher, one author, one project, no independent reproduction, and no adoption measurement. The detailed code and command-level evidence supports the narrow claim that this bug existed and was fixed in story-cli; the broader claim that these are three general traps of stdio MCP servers rests on the author's judgement alone.
build
Rate limit your MCP servers, because a retrying agent turns one error into a billing incident1 distinct publisher
product
A 2x LLM bill is not a bug report: token spend is an observability problem1 distinct publisher
build
The Slack CLI that skips admin approval keeps live tokens in a file your agent can read1 distinct publisher
build
Thirty minutes a day, and none of it from letting the agent write Swift1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 23, 2026