Skip to content

fix(cli): os serve with the stdio MCP transport and no config argument answers an initialize written at spawn — its config positional no longer reads stdin - #24212

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24191-serve-config-ignore-stdin
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-24191-serve-config-ignore-stdin

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24191

Clause-②: no

os serve with the stdio MCP transport enabled and no config argument now answers the initialize an MCP client writes when it spawns the server, and it boots the configured app.

What was wrong (re-measured on origin/main f963d8e2b, built CLI through bin/run.js)

  • H1 holds. serve's config positional (the Args.string at about serve.ts:1400) had no ignoreStdin. In @oclif/core 5.1.2 (lib/parser/parse.js), _args() calls tryStdin for every positional that has no token, unless the arg sets ignoreStdin. A line already waiting in the pipe becomes the value.
  • The fixture is the shape the card names: a host config (defineStack plus a plugin that writes a marker from start()), a compiled dist/objectstack.json beside it, a key minted through serve --dev and POST /keys, and os serve with OS_MCP_STDIO_ENABLED=true, that key and NODE_ENV unset (production). The initialize is written at spawn in both rows.
shape before (f963d8e2b) after
os serve -p PORT --no-ui (no config argument) no answer within 60 s; No objectstack.config.ts found — booting from artifact (default host)... beside the config; host plugin marker absent answered (id 24191); host plugin marker present
control: os serve objectstack.config.ts -p PORT --no-ui answered, config loaded (1 passed) unchanged

The fix (packages/cli/src/commands/serve.ts)

ignoreStdin: true on serve's config positional. Stdin belongs to the stdio transport; the config path comes from argv or the default. The fix sits at the producer, the parser declaration that let a protocol message become a config path. Nothing downstream changes, and no consumer gains a fallback.

The one input-surface change: an omitted config argument is no longer read from stdin. H2 holds on this tree:

  • a config piped into serve, start or compile (a pipe into os, objectstack or bin/run(-dev).js followed by one of those commands): 0 hits;
  • a stdin redirect into those commands: 0 real hits (the 6 regex hits are prose placeholders such as a bracketed "config dir" after os serve);
  • stdin.write( / stdin.end( under packages/cli: 5 hits, 3 login prompts and 2 JSON-RPC initialize writes, none of them a config path.

The changeset states the change and its remedy: pass the path as the argument.

Re-grounding serve-mcp-stdio-answers.e2e.test.ts (#7645), not dropping it

H3 holds, measured. That file spawns serve with no positional because oclif's tryStdin used to pause stdin, and a paused stdin is the premise of the resume() in MCPServerRuntime.start(). After this fix nothing in the spawned CLI pauses stdin, so the premise was gone:

e2e file resume() removed (local ablation of packages/mcp, rebuilt, marker in 2 built files) resume() restored
as on f963d8e2b passed: vacuous, it no longer guards resume() passed
re-grounded (this PR) RED: the stdio transport started but never answered initialize, after its premise assertion passed passed

The re-grounded stdio boot runs with a --import preload, a data: URL so the whole host stays on the page. The preload is the "host that touched stdin before start()" that the resume() comment names. It pauses process.stdin before the CLI loads, which is what readline's Interface.close() did under oclif. It also prints readableFlowing each time a data listener attaches. The test asserts every reported state is false, so the premise is measured on every run. A later change that resumes stdin earlier cannot make the file vacuous again without reddening it. The file still passes no positional, the form users type, and still asserts a parsed answer.

The packages/mcp ablation was local only. It was restored through ablation-replace.mjs (blob 3ef6ce641 equals HEAD), the rebuilt dist/ no longer carries the marker (6 files), and the tree is clean. The file is nightly-tier by name, so this PR's CI does not run it. The readings above are local.

Pin

packages/cli/test/serve-mcp-stdio-early-initialize.integration.test.ts is in the integration tier, because it spawns bin/run.js. It runs one minting serve --dev boot, one os compile for the artifact, and then the boot under test with no config argument and the initialize written at spawn. It asserts:

  1. A parseable JSON-RPC frame with the request's own id (24191), no error, a protocolVersion and a serverInfo.name.
  2. The host config's plugin marker on stderr: the config was composed, not the artifact beside it.

Before the boot it asserts the positive control: dist/objectstack.json exists. The artifact is what makes the defect silent. Without one, a swallowed config path is a loud "Nothing to serve" exit.

The budget line is 15 s: 11.6 s, the slowest of four local runs on a shared container, x 1.25. There is no CI reading yet. The pin spawns bin/run.js, so scripts/check-cli-test-child-env.mjs's pinned built-entrypoint population gains it, going from eleven to twelve files, with the Joined the population deliberately note every member carries.

Reverse verification. The fix was committed first (49bd321ea). Both legs went through scripts/ablation-replace.mjs (wrapped restore) and scripts/ablation-dist-preflight.mjs, with the CLI rebuilt for each leg.

  • Mutate (, ignoreStdin: true deleted): the marker is absent from all 652 built files, and the pin is RED: initialize was never answered / no answer within 60000 ms, with No objectstack.config.ts found — booting from artifact (default host)... in the transcript.
  • Restore: blob cd1aa3d29 equals HEAD and git diff HEAD is empty. The rebuilt dist/ carries the marker again, the tree is clean, and the pin then passed three times.

Predicted direction: red. Observed: red.

Local verification (head 749f1a281, after merging origin/main e46ce7b6c)

  • A full turbo run build (76/76 tasks) ran first, because the merge moved packages/spec.
  • pnpm --filter @objectstack/cli typecheck: exit 0. tsc -p tsconfig.test.json --noEmit --listFiles lists both changed test files.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 310 files, 4599 tests passed.
  • vitest run --project integration on the new pin and start-mcp-stdio-early-initialize.integration.test.ts: 2 passed. The os start pin was run because its serve child is the command changed here. The rest of the integration tier is declared to CI.
  • OS_TEST_TIERS=nightly vitest run --project integration test/serve-mcp-stdio-answers.e2e.test.ts: 1 passed.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 87 commands, and all 87 exited 0 (each exit captured before any pipe). --ran reconciled: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN. This includes check:cli-test-child-env (15 built-CLI spawns, all out of development/test), check:pm-dispatch-gates (1884 cases), check:nul-bytes, check:dual-build-cjs-loads and check:i18n-coverage.
  • Edited gate script: node scripts/check-cli-test-child-env.mjs --self-test passed 152 cases. No test suite exercises the script; its only other mention is prose in serve-node-env-production-default.e2e.test.ts.
  • pnpm exec eslint --no-inline-config over the four changed source files: exit 0, no output. The repo-wide pnpm lint is left to CI.

Acceptance notes

  • The comment beside the resume() in packages/mcp/src/mcp-server-runtime.ts still says that os serve --dev with no path leaves stdin paused under oclif. After this PR that is history, not the current mechanism. The comment's general reason still holds: any host that touched stdin before start(). packages/mcp is outside this card's surface, so it is noted here and not edited.
  • os compile's optional config positional (no default, no ignoreStdin) still reads a pending stdin line. compile is not an MCP door, and os start's compile spawn already gives it no stdin. Noted only.
  • os dev is unchanged and is not a stdio door (see PR fix(cli): os start answers an MCP initialize written at spawn — its compile and serve children no longer read stdin as the config path #24184's notes).
  • The e2e's preload stands in for a host that pauses stdin; it is not a production caller. After this PR no production door pauses stdin before the transport attaches. Measured: the e2e as it stood on f963d8e2b passes with resume() removed.

Generated by Claude Code

… path

`serve`'s optional `config` positional now sets `ignoreStdin`. oclif
otherwise fills an omitted positional from a line already waiting on
stdin, and with the stdio MCP transport that line is the client's
`initialize`, written at spawn: it became the config path, the boot fell
back to the artifact without the config, and the request was never
answered.

The new integration pin spawns the built CLI with no positional, writes
`initialize` at spawn beside a compiled artifact, and asserts the same-id
answer and the config's own plugin starting.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
With `serve` no longer reading stdin for its `config` positional, nothing
in the spawned CLI paused stdin before the transport attached, so the
pin stayed green with the `resume()` in `MCPServerRuntime.start()`
removed. The stdio boot now runs with a `--import` preload that pauses
`process.stdin` before the CLI loads and reports `readableFlowing` at
each `data`-listener attach; the pin asserts that report reads `false`,
so the premise is measured on every run.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
…he serve early-initialize pin

The pin's budget line carries its measurement (11.6 s, slowest of four
local runs, x 1.25). It spawns `bin/run.js`, so it joins
check:cli-test-child-env's pinned built-entrypoint population, now
twelve files.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 2 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 36 pages), os serve (command, 34 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 8259ea03e2959129c688639f69d0782c3a820f89 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · PR #24212 at 749f1a281 (#24191)

domain:cli seat 2 · marchtian · session session_01QyoWKK9RwUNxBxmevy9Neb · 2026-10-17T19:25Z · holder of claim 6238317708. The dev's report is comment 6239487509 on #24191, and the seat reviewed it against this PR, not the report's own account.

  1. Shape. Draft → main. The first line is Fixes #24191, the only closing keyword. Clause-②: no is line-initial, and the assignee is marchtian. 5 files, inside the claimed surface:

    check-governed-merges.mjs --pr 24212: NOT governed, 458 changed lines.

  2. Premise, measured. H1 was re-measured on the built CLI at f963d8e2b: no answer in 60 s, "booting from artifact", while the named-positional control passed. The fix is one declaration at the producer: ignoreStdin: true on serve's config positional. Ablating it reddens the pin, and the restore is proven by blob. H2: 0 stdin-fed config callers measured. H3 resolved as triage required (6238094446: ⛔ not dropped). The mcp-stdio-fail-closed c3: the stdio MCP transport starts but is permanently deaf (oclif arg parser pauses process.stdin) #7645 e2e is re-grounded: a preload pauses process.stdin, the test asserts every data-listener attach reads readableFlowing === false, and with resume() ablated it is RED. The old form stayed green under that ablation (vacuous), so the re-grounding gives it back a reason to hold.

  3. Clause-②: no, read. The only input path removed is stdin as a source for serve's config path. That is CLI parsing behaviour, undocumented and with no measured feeders, and it is not a published schema, export or HTTP accept set. The changeset names it. No contract-review face is touched.

  4. Check-runs on head 749f1a281 at this act: 18 success, 6 skipped, 13 in_progress (the Lint lanes, Test Core 1–4, one Type Check lane). Landing waits on every check green. Local: 87 derived / 87 run (--ran exit 0); ESLint exit 0; the gate's --self-test passes 152 cases.

  5. Accepted deviations:

    • the local-only ablation of packages/mcp's resume() to measure H3, restored by blob and not in the diff;
    • two temporary measurement files, deleted and never committed;
    • one voided first red run (a fixture lacked OS_AUTH_SECRET), declared and re-run;
    • one merge of main.

    Changeset, read sentence by sentence: "What was wrong" matches H1. "os serve no longer reads its config path from stdin … to serve another config, pass its path as the argument" matches the diff. "Unchanged …" is true because the default and the named-argument path are untouched.

  6. Out of scope, both noted and not filed:

    • the stale resume() comment in packages/mcp (held off);
    • os compile's optional positional still reading stdin (not an MCP door, and os start gives it no stdin).
  7. Landing. Ordinary queue landing once every check on this head is green or a roster skip. Fixes #24191 closes the card, and the seat strips pm:dispatched in the same act.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 19:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 19:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit b7f29dd Oct 17, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24191-serve-config-ignore-stdin branch October 17, 2026 20:04
marchtian pushed a commit that referenced this pull request Oct 17, 2026
main brings #24212, which also joined check:cli-test-child-env's pinned
built-entrypoint population (serve-mcp-stdio-early-initialize). Both
branches bumped the self-test's count line from "eleven" to "twelve", so
git merged the two bumps as one identical change while the list grew to
thirteen entries. The count line now reads "thirteen"; both entries keep
their deliberate-join notes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants