Skip to content

fix(mcp): stdio bridge throws the shared RECORD_NOT_FOUND envelope - #8507

Draft
os-zhuang wants to merge 3 commits into
mainfrom
claude/issue-8422-stdio-shared-not-found-envelope
Draft

fix(mcp): stdio bridge throws the shared RECORD_NOT_FOUND envelope#8507
os-zhuang wants to merge 3 commits into
mainfrom
claude/issue-8422-stdio-shared-not-found-envelope

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8422

What changed

packages/mcp/src/stdio-data-bridge.ts minted its own local recordNotFound(object, id) — a bare Error with neither code nor status — thrown from the update() and remove() by-id write seams on a missing id. The HTTP bridge's callData path throws the repo's ONE not-found envelope, recordNotFoundError (code: 'RECORD_NOT_FOUND', status: 404, packages/core/src/utils/record-not-found.ts, #4435/#5138/#7867), for the identical miss. Same operation, two transports, two envelopes.

The local mint is deleted; both seams now throw recordNotFoundError(object, id).

Line numbers were stale (#8439 / #8266 touched this file since the card was filed) — located both call sites by symbol (recordNotFound(object, id) calls inside update()/remove()), not by the card's cited :337/:347.

Import path: import { recordNotFoundError } from '@objectstack/core', not @objectstack/metadata-protocol's re-export. @objectstack/mcp's package.json already declares a direct @objectstack/core dependency, and packages/mcp/src/plugin.ts already imports named exports from it — @objectstack/core is also the lower of the two packages that carry the factory (packages/objectql/src/engine.ts's own #7867 comment explains why: @objectstack/objectql cannot import @objectstack/metadata-protocol at all, ADR-0076 D2). No new dependency needed either way.

Preserved, not touched: the local factory's comment explaining why it throws rather than returns (registerObjectTools turns a throw into a tool error) — that reasoning is correct and now sits at the update() call site (the first of the two seams), with remove() pointing back at it. Only which error object is thrown changed.

Adjacent deliverable — folded in, not filed as a follow-up

scripts/check-engine-double-contract.mjs's consumer-seam invariant (#8194) reported the two stdio seams as refusal: 'local' rather than reddening, because the gate's own header names this as a deliberate, temporary state pending this exact card, with a noted SHARED_ONLY-shaped one-line tightening. Since the seam list was already both-directions complete and the tightening really is mechanical, I folded it into this PR rather than filing a follow-up:

  • File surface: scripts/check-engine-double-contract.mjs — the seam filter changed from seams.filter((x) => !x.refusal) (only "no refusal at all" failed) to seams.filter((x) => x.refusal !== 'shared') (a local mint now fails too), plus an updated header comment (## WHICH not-found envelope (#8194, tightened to SHARED_ONLY by #8422)) and an adjusted REFUSES message that distinguishes "refuses through a locally minted error" from "does not refuse anywhere before it".
  • Self-test (--self-test) still green — none of its synthetic fixtures assert the pre-tightening filter behavior, only the refusalclassification (shared/local/null), which is unchanged.
  • Real-tree run: all four consumer seams now report [shared] (was 2 [shared] + 2 [local]); gate is green.

Reverse verification

Prediction (recorded before running): reverting only stdio-data-bridge.ts to its pre-fix state, while keeping the tightened gate and the new test file, goes red in both places — the new unit tests (asserting code/status) fail, and check-engine-double-contract.mjs reddens because the two stdio seams revert to refusal: 'local', which the tightened SHARED_ONLY filter now rejects. Ordinary direction (more red), no inversion expected.

Measured, via git checkout HEAD~1 -- packages/mcp/src/stdio-data-bridge.ts (fix already committed, so this is a real restore point) against the tightened gate + new tests, then restored via git checkout claude/issue-8422-stdio-shared-not-found-envelope -- packages/mcp/src/stdio-data-bridge.ts:

  • Tests: 3/3 new tests failed — expected undefined to be 'RECORD_NOT_FOUND'.
  • Gate: 2 REFUSES errors — stdio-data-bridge.ts:351 and :361, both "refuses through a locally minted error rather than the shared envelope".

Matches the prediction. Restored cleanly (git diff HEAD -- packages/mcp/src/stdio-data-bridge.ts empty); both re-verified green afterward.

Tests

  • New: packages/mcp/src/stdio-data-bridge.not-found.test.ts — covers both by-id write seams (update() and remove()) against a missing id, asserting err.code === 'RECORD_NOT_FOUND' and err.status === 404 specifically (not just that something threw), plus a same-shape-on-both-seams check.
  • pnpm --filter @objectstack/mcp test → 16 test files, 173 tests passed.
  • pnpm --filter @objectstack/mcp typecheck → clean.
  • pnpm --filter @objectstack/mcp build → clean (also built the dependency closure first: pnpm --filter '@objectstack/mcp^...' build).
  • node scripts/check-engine-double-contract.mjs --self-test && node scripts/check-engine-double-contract.mjs → self-test OK, real run OK (197 pinned, 133 DEBT, 2 exempt; all 4 consumer seams [shared]).
  • node scripts/check-error-code-casing.mjs → OK.
  • node scripts/check-nul-bytes.mjs → OK.
  • node scripts/check-cross-package-test-inputs.mjs → OK (new test file's package-scoped read is already covered).
  • pnpm run check:query-options-erasure → OK (test surface unaffected — the new file has no query-options sites).
  • pnpm run check:type-check-coverage → OK, ledger unchanged (13 packages / 436 frozen errors in DEBT, unchanged; new test file typechecks cleanly, no debt raised).
  • pnpm exec eslint on the three changed files → clean.
  • node scripts/pm/dispatch-gates.mjs <changed paths> → surfaced check:cross-package-test-inputs, check:engine-double-contract (both already run above) and, as convention-triggered by adding a test file, check:query-options-erasure + check:type-check-coverage (both run above, both green — no addition beyond the prompt's named families was otherwise needed).

Changeset

.changeset/mcp-stdio-record-not-found-envelope.md@objectstack/mcp, patch (behavior fix, no exported symbol or authorable metadata moves).

Not in scope

Every other divergence between the two MCP transports noted in the file's own docblock (callData's protocol-service preference, ingress readonly strip, existence probes, expand/select) — deliberate, filed, and untouched here. Also not addressed: #8328 (mcp-server-runtime.ts, needs-user-decision) — out of this card's file surface.

Refs #8194, #8083, #8266, #7867, #5138, #4435.


Generated by Claude Code

The stdio MCP bridge's update()/remove() by-id write seams minted their own
bare Error on a missing id. The HTTP bridge's callData path already throws
recordNotFoundError (code RECORD_NOT_FOUND, status 404) for the identical
miss, so the two transports answered the same operation with two different
envelopes.
Also tightens check-engine-double-contract.mjs's consumer-seam invariant
from "refuses at all" to SHARED_ONLY, now that all four seams reach the
shared envelope.
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 7:40pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/mcp.

11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx(via @objectstack/mcp)
  • content/docs/ai/agents.mdx(via @objectstack/mcp)
  • content/docs/ai/connect-mcp.mdx(via @objectstack/mcp)
  • content/docs/ai/index.mdx(via @objectstack/mcp)
  • content/docs/ai/natural-language-queries.mdx(via @objectstack/mcp)
  • content/docs/api/index.mdx(via @objectstack/mcp)
  • content/docs/deployment/environment-variables.mdx(via @objectstack/mcp)
  • content/docs/permissions/authorization.mdx(via @objectstack/mcp)
  • content/docs/permissions/system-context.mdx(via packages/mcp)
  • content/docs/plugins/packages.mdx(via @objectstack/mcp)
  • content/docs/protocol/knowledge.mdx(via @objectstack/mcp)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/mcp)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 13, 2026
The third test's `.catch((e) => e)` idiom inferred the settled value as
`unknown` (TResult from an `any`-typed catch parameter widens to `unknown`
here), which is a second, unrelated way to fail tsc from the same file that
already had this idiom's cousin (`await res.json()`) in the package's
53-error TEST_DEBT baseline. Replaced it with the same explicit-cast helper
the other two tests already used, extracted once as `catchError`.
pnpm check:type-check-debt (the real ratchet command, not
check:type-check-coverage) on a full built closure: OK, 33 ledger entries
re-measured, none above their recorded number — @objectstack/mcp back to
its recorded 53.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P7vaLs7bhBPi9m3JyzkhDj
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

CI follow-up on the two red checks flagged.

TypeScript Type Check (check:type-check-debt) — was real, now fixed. My new stdio-data-bridge.not-found.test.ts added exactly 5 raw tsc errors to @objectstack/mcp's TEST_DEBT baseline (recorded 53, measured 58). All 5 were TS2571 ("object is of type 'unknown'") on updateErr/removeErr in the third test's .catch((e) => e) idiom — TypeScript infers the settled TResult from an any-typed catch parameter as unknown here, a second, unrelated instance of the same category of idiom already sitting in this package's 53-error baseline (await res.json()). Fixed at the source: replaced the untyped .catch((e) => e) with the same explicit-cast pattern the other two tests already used (.then(() => null, (e: unknown) => e) cast to Error & { code?: string; status?: number }), extracted once as a shared catchError helper. No ledger entry touched.

Verified with the actual command, not the one I ran the first time: pnpm check:type-check-debt (check-type-check-coverage.mjs --self-test && --re-measure) on a full pnpm build closure → OK — 33 ledger entr(ies) re-measured ... none above its recorded number, @objectstack/mcp back to exactly 53. Re-ran pnpm --filter @objectstack/mcp test (173/173) and check-engine-double-contract.mjs (self-test + real run, all 4 seams [shared]) — both still green.

ESLint — confirmed a flake, not mine. Polled the new SHA's check-runs after push: ESLint completed success at 19:45Z (TypeScript Type Check followed at 19:48Z), no check-regen-pending.mjs git-plumbing error this time. One occurrence, as predicted — leaving check-regen-pending.mjs untouched.

Pushed as f051980d0. Full check-run sweep on that SHA: every completed job is success (ESLint, TypeScript Type Check, Test Core ×3 + aggregate, Build Core, Check Changeset, ADR maintainer approval, Temporal Conformance, Dogfood Verify CLI, Dogfood Regression Gate 1/3 + 3/3, the two duplicate/scope guard checks, etc.); Dogfood Regression Gate (2/3) was still in_progress at last check, unrelated to either flagged job. Still draft, as instructed.


Generated by Claude Code


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude