Skip to content

fix(mcp)!: the objectstack://objects resources read the stdio transport's merged object view, so a package's objects are listed and described - #25268

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-dkk3hd
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-dkk3hd

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25245

Clause-②: no (narrowing)
Metadata-protocol: none

The stdio MCP transport's two object resources, objectstack://objects and objectstack://objects/{objectName}, now read the same objects its list_objects and describe_object tools answer from since #25243: the protocol's merged listing, the one GET /api/v1/meta/object serves. They read the metadata service until now, so on a kernel with an environmentId they listed none of a package's boot-registered objects and answered 404 for each, while the tools listed and described them. The merged read is shared, not copied: stdio-data-bridge.ts exports one view, the tools answer from it, and plugin.ts gives the same view to its runtime, whose two object resources read it. No published signature moves.

The defect, measured on base 0b8bce40e0

bridgeResources (packages/mcp/src/mcp-server-runtime.ts) built objectstack://objects from metadataService.listObjects() and objectstack://objects/NAME from metadataService.getObject(NAME). That service holds a copy of an object only where something put one there. On a kernel with an environmentId (createStandaloneStack: os dev as env_local, os serve, os start; and the cloud single-environment runtime) nothing copies an object a package registers in the SchemaRegistry during the boot.

Real kernels (ObjectKernel + MetadataPlugin + ObjectQLPlugin), driven through the real MCPServerPlugin stdio session over an in-process transport speaking a stdio client's JSON-RPC, with the fixture package #25243's pins use (two objects, one declared apiEnabled: false):

shape, when the package registered resources/read objectstack://objects resources/read objectstack://objects/qa_pkg_open list_objects / describe_object on the same kernel
env_local, during the boot { objects: [], totalCount: 0 } { error: 'Object "qa_pkg_open" not found', code: 'RESOURCE_NOT_FOUND', status: 404 } both listed, both described
env_local, installed after the boot (source: 'marketplace') both listed described both listed, both described
no environmentId, during the boot both listed described both listed, both described
no environmentId, installed after the boot both listed described both listed, both described

A disabled package (registry.disablePackage). On the shape without an environmentId the boot copied both objects into the metadata service, and the copy outlives the disable: both resources still listed and described them, while list_objects and describe_object omit them since #25243 and GET /api/v1/meta/object omits them. On env_local there is no copy, so the resources already omitted them.

What changed

  • packages/mcp/src/stdio-data-bridge.ts. New stdioObjectView(reads) returns { listObjects, getObject }: the protocol's merged listing (readMergedObjects, unchanged from fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243), and a name looked up in that same listing, so a name the listing omits is absent on both reads. A host with no merged read gets the metadata service's listObjects() / getObject(), as before. The bridge's listObjects and describeObject now answer through this view; their answers do not change (the fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243 unit pins pass unchanged). Exported within the package only; index.ts does not publish it.
  • packages/mcp/src/mcp-server-runtime.ts. New type McpObjectView: the listObjects and getObject members of IMetadataService, picked. buildObjectListResource and buildObjectSchemaResource take it as an optional last parameter and read their objects from it; it defaults to the metadata service, so every existing caller reads exactly as before. New package-internal setRuntimeObjectView(runtime, view) records a runtime's view in a module-level WeakMap; the two object resources registered by bridgeResources look it up at each resources/read and fall back to the metadata service. bridgeResources keeps its published signature: a third parameter would have made a new type reachable from the exported MCPServerRuntime, a public-surface widening for wiring between this package's own plugin and runtime. The completeness verdict is still asked of the metadata service (diagnoseListRead on the listing, diagnoseEmptyRead on a miss), as the list_objects tool asks it.
  • packages/mcp/src/plugin.ts. The protocol accessor fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243 added (resolved at each use, undefined when absent or throwing, never captured in start()) moves out of the stdio branch, because the resources are bridged whether or not this process starts the stdio transport. Before bridgeResources, the plugin calls setRuntimeObjectView(this.runtime, stdioObjectView({ metadataService, protocol })). The data bridge and the record resource's gate keep reading the same accessor.
  • packages/mcp/README.md. One paragraph under Resources saying what the two object resources read.
  • .changeset/25245-mcp-stdio-object-resources-merged.md. A patch for @objectstack/mcp, with the BREAKING banner and the ADR-0087 disposition not-required (no-migration-prescription); both declaration lines are in the body.

Contract

Every input whose answer changes, old to new, on the stdio transport:

  1. resources/read objectstack://objects on a kernel with an environmentId, for an object a package or plugin registered during the boot. Old: omitted, totalCount short. New: listed; the non-sys_ set is what list_objects lists.
  2. resources/read objectstack://objects/NAME on such an object. Old: { error: 'Object "NAME" not found', code: 'RESOURCE_NOT_FOUND', status: 404 }. New: the schema body, equal to what describe_object answers for NAME.
  3. Both resources on a disabled package's objects, where the metadata service still holds a copy (the narrowing). Old: listed, and described from the copy. New: omitted, and objectstack://objects/NAME answers the 404 RESOURCE_NOT_FOUND body, as describe_object answers "not found".
  4. An object whose served definition differs from the metadata service's copy. Old: the resources described the copy. New: they describe the definition REST serves.
  5. Both resources while the protocol's merged listing throws. Old: the metadata service's answer. New: the protocol's error travels to the client as a JSON-RPC error, as list_objects lets it travel since fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243; nothing falls back. Pinned with a throwing double, not on a real store outage.

Unchanged: objects that already had a copy (every object on a kernel without an environmentId, a marketplace install on any kernel, a runtime-authored object) are listed and described as before. A loader outage still withholds totalCount (partial: true, returnedCount, 503 SERVICE_UNAVAILABLE), and a miss during an outage still answers 503 SERVICE_UNAVAILABLE. A host with no protocol service, or one without the merged listing, reads the metadata service, byte-identically (pinned). A host that builds its own MCPServerRuntime and calls bridgeResources reads the metadata service, as before. The published surface does not move: the built dist/index.d.ts declares bridgeResources with the same two parameters, and names none of McpObjectView, setRuntimeObjectView and stdioObjectView as declarations. The record resource is untouched. Discovery stays ungated: the resource lists and describes an object declared apiEnabled: false, as list_objects and describe_object do.

Pins

  • packages/qa/dogfood/test/mcp-stdio-package-object-discovery.test.ts, 10 new cases on real kernels, beside fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243's tool pins (80 cases in the file). For each of the four rows above: objectstack://objects lists both package objects, has no partial, has totalCount equal to its length, and lists the same non-sys_ names list_objects lists; objectstack://objects/NAME answers both objects, enableFeatures.apiEnabled: false included, each body EQUAL to describe_object's, and a name nothing serves answers exactly { error: 'Object "qa_pkg_nowhere" not found', code: 'RESOURCE_NOT_FOUND', status: 404 }. For a disabled package on both shapes: neither object is listed, and each answers that 404 body.
  • packages/mcp/src/mcp-server-runtime.merged-object-resources.test.ts, 14 cases, doubles. The listing from the merged read where the copy is absent, the metadata service's listObjects never asked; a disabled package's objects omitted from both resources although the copy holds them; the partial verdict kept from listDiagnosed; the 503 on a miss kept from getDiagnosed; a throwing merged read propagated by both builders, the copy never asked; the schema body equal to describeObject's for both package objects; no protocol, and a protocol without getMetaItems, byte-identical to the metadata service alone; a protocol registered after the bridge read on the next call. Over a real resources/read on an MCPServerRuntime: both resources answer from the view setRuntimeObjectView gave, never the metadata service; a view given after bridgeResources is read on the next call; a runtime given no view reads the metadata service, and another runtime's view is not its own.

Red before, green after. Each leg's direction was predicted before it ran. The dogfood file consumes @objectstack/mcp's dist/, so every leg rebuilt it, and scripts/ablation-dist-preflight.mjs proved the leg's marker reached dist/ (exit 0) and, after the restore and a rebuild at HEAD, that none remained (--absent, exit 0 for each). Mutations were landed and restored by scripts/ablation-replace.mjs (anchor 1 to 0, blob changed; restored blob equals HEAD, git diff HEAD empty).

leg mutation dogfood (80) unit (14)
main the base tree 0b8bce40e0 (source blobs 6ca41ee187, f6d8800263, e71d1f1c81) with the new dogfood pins 3 failed, 77 passed: the env_local during-boot listing and schema; the no-environmentId disabled package's resources not run (the file imports names absent on base)
bridge the runtime's two resources ignore the view their host gave (runtimeObjectViews.get(this) replaced) the same 3 failed 2 failed: both resources answer from the view; a view given after bridgeResources is read
plugin plugin.ts gives its runtime no view the same 3 failed 14 passed (this file drives the runtime, not the plugin)
viewcopy the view's getObject falls back to the copy for a name the listing omits 2 failed: the no-environmentId disabled package's resources, and its describe_object (#25243's row, which reads the same view) 1 failed: a name the listing omits is the coded miss
fix a697b07f34 80 passed 14 passed

The legs and the fix row were measured on a697b07f34. The final head differs from it by one doc comment in mcp-server-runtime.ts (a {@link} to the package-internal setter, which the published declaration file would have carried as a dangling link, now plain code text); both pin files are re-run on the final head by the unit suite and the battery below. An earlier local revision passed the view as a third parameter of bridgeResources; it was replaced before the first push for the public-surface reason above, and its legs read the same way.

Local verification

On 5cf1aa4ce3, the final commit, over origin/main 0b8bce40e0 (it has since moved two commits, #25258 and #25259, neither touching packages/mcp, packages/metadata-protocol or this dogfood file), or on dc7bee12dc where a line says so; the one commit between them (5cf1aa4ce3) changes how the dogfood pin file's resource helper is typed, not what any case asserts. Every heavy run went through the verify lock, and each line cites its VERDICT.

  • @objectstack/mcp unit suite (pnpm --filter @objectstack/mcp test), on dc7bee12dc: VERDICT command-exit 0; Test Files 42 passed (42), Tests 532 passed (532).
  • pnpm --filter @objectstack/mcp typecheck, on dc7bee12dc: VERDICT command-exit 0 (tsc --noEmit, and check:test-typecheck OK: 6 files, 53 errors, 8 pinned signatures held in the ledger).
  • turbo run build --filter=!@objectstack/docs, on dc7bee12dc: VERDICT command-exit 0, 76 of 76 tasks.
  • pnpm --filter @objectstack/dogfood typecheck with NODE_OPTIONS=--max-old-space-size=8192: VERDICT command-exit 0. The first run, on dc7bee12dc, answered exit 2 with one error (TS2339): the pin's record reader called this.readResource inside the returned object literal, whose this tsc types as Booted or a PromiseLike of it. 5cf1aa4ce3 reads both through a local helper.
  • pnpm lint (eslint . --no-inline-config), on dc7bee12dc: VERDICT command-exit 0; eslint --no-inline-config on the one file 5cf1aa4ce3 changes: VERDICT command-exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 70 families derived; each run, every one exit 0 (the second pass, on the final head; the first pass, on dc7bee12dc, also answered 70 of 70 exit 0).
  • --ran reconciliation: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, exit 0.
  • node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD, with PAYLOAD a pull_request event carrying this title and body: exit 0 (✓ LEVEL AXIS: no (narrowing), Metadata-protocol: none, a non-protocol change at patch).
  • node scripts/check-closing-keyword-parity.mjs --body on this body: exit 2, its "closing declarations found" answer: Fixes #25245 and nothing else, bound by the duplicate-fix guard's and the part-of guard's parsers.
  • The full dogfood battery, once (pnpm --filter @objectstack/dogfood test, no shard), on dc7bee12dc: VERDICT command-exit 0; Test Files 289 passed, 3 skipped (292); Tests 2491 passed, 18 skipped (2509); no failure. It was not re-run for 5cf1aa4ce3, which touches that one pin file's helper; the file was re-run on the final head: VERDICT command-exit 0, 80 of 80.

Acceptance notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK

…rt's merged object view

The two object resources read the metadata service, which on a kernel with
an environmentId holds no copy of an object a package registers during the
boot: objectstack://objects listed none of them and
objectstack://objects/NAME answered 404, while list_objects and
describe_object listed and described them.

stdio-data-bridge.ts now exports stdioObjectView, the one listing/by-name
view list_objects and describe_object answer from. plugin.ts gives the same
view to its runtime through the package-internal setRuntimeObjectView, and
both resource builders read objects from it at each read; the published
bridgeResources signature is unchanged. The completeness verdict stays the
metadata service's.

Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK
Co-authored-by: Claude <noreply@anthropic.com>
…be_object answer from

Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK
Co-authored-by: Claude <noreply@anthropic.com>
…he pin type-checks

The record reader called this.readResource inside the returned object
literal, whose this tsc types as Booted | PromiseLike<Booted>.

Claude-Session: https://claude.ai/code/session_01TgbP2QERdKmh147ohacHbK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 22, 2026 18:27
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/mcp, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/mcp/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/metadata-service.mdx (via getObject (literal, a string literal in McpObjectView), listObjects (literal, a string literal in McpObjectView))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/mcp/README.md) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 12 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 284e04810a309d5a7c8c3654b036f9ebf07a53d4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6744a8d9e89a850d615039884ef9e746a96494ee — the merge of head 5cf1aa4ce32c79362b70f9c52d972cba65a9de83 into base 284e04810a309d5a7c8c3654b036f9ebf07a53d4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6744a8d9e89a850d615039884ef9e746a96494ee && git checkout 6744a8d9e89a850d615039884ef9e746a96494ee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 284e04810a309d5a7c8c3654b036f9ebf07a53d4 5cf1aa4ce32c79362b70f9c52d972cba65a9de83 && git checkout -B drift-repro 284e04810a309d5a7c8c3654b036f9ebf07a53d4 && git merge --no-ff 5cf1aa4ce32c79362b70f9c52d972cba65a9de83

node scripts/docs-audit/affected-docs.mjs --json 284e04810a309d5a7c8c3654b036f9ebf07a53d4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 284e04810a309d5a7c8c3654b036f9ebf07a53d4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

dogfood_battery

dogfood_battery: 5cf1aa4ce32c79362b70f9c52d972cba65a9de83 · os-verify-lock: VERDICT command-exit 0 · held the lock 999s (16m39s) · waited 0s (shared-box seconds) · Test Files 289 passed / 3 skipped (292) · Tests 2491 passed / 18 skipped (2509) · 0 failed

Owed because the title carries ! and the changeset declares Clause-②: no (narrowing) (.claude/agents/os-dev.md, Definition of done). The Dogfood Regression Gate shards are skipped on pull requests, so this is the only full run. It is the battery on the PR head; the run cited in the PR body was on dc7bee12dc, one commit below, with the same counts.

  • Head. Run in the card's checkout at the PR head above, with a clean tree. No ablation marker remains in packages/mcp/dist (0 matches for ABLATION in index.js and index.cjs).
  • Closure. pnpm turbo run build --filter='@objectstack/dogfood^...' --concurrency=2 under scripts/pm/os-verify-lock.sh: 66 of 66 tasks. The lock printed VERDICT command-exit 0.
  • Battery. pnpm --filter @objectstack/dogfood test under the lock, with no OS_TEST_SHARD and no path, NODE_OPTIONS=--max-old-space-size=8192. It started 18:27:03Z and ran for 997.76 s. The pin file test/mcp-stdio-package-object-discovery.test.ts, 80 cases with this PR's 10, is in it.
  • Skips. 3 files and 18 tests were skipped by the suite's own environment guards. The diff touches none of them, and no test was skipped or narrowed by hand.
  • Failures. None.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5cf1aa4ce32c79362b70f9c52d972cba65a9de83
Local-runs: none
Scope: .changeset/25245-mcp-stdio-object-resources-merged.md, packages/mcp/README.md, packages/mcp/src/mcp-server-runtime.merged-object-resources.test.ts, packages/mcp/src/mcp-server-runtime.ts, packages/mcp/src/plugin.ts, packages/mcp/src/stdio-data-bridge.ts, packages/qa/dogfood/test/mcp-stdio-package-object-discovery.test.ts + packages/mcp/src/index.ts, packages/mcp/package.json, packages/mcp/src/metadata-completeness.ts, packages/metadata-protocol/src/protocol.ts, packages/mcp/src/mcp-http-tools.ts, packages/spec/src/contracts/metadata-service.ts, .github/workflows/ci.yml, scripts/check-adr-0087-registration.mjs

Read against the net diff 0b8bce40e081477132d81b1e11a19cd3cdb331c0..5cf1aa4ce32c79362b70f9c52d972cba65a9de83 (7 files, +495/−31), card #25245 with all three comments, and the head's check-runs. Every claim below is judged against the cited code at the head, not against the PR body.

① Derived judgments

Published declaration surface of @objectstack/mcp — unchanged. Right. git diff of packages/mcp/src/index.ts and packages/mcp/package.json between base and head is empty; the exports map serves only ./dist/index.js / ./dist/index.d.ts (and the cjs pair). McpObjectView (a picked pair of listObjects and getObject off IMetadataService), setRuntimeObjectView and the two builders buildObjectListResource / buildObjectSchemaResource are exported from mcp-server-runtime.ts, and stdioObjectView from stdio-data-bridge.ts, but none is re-exported by index.ts. The only barrel export that touches the new code is the class MCPServerRuntime, whose bridgeResources(metadataService, getRecord?) keeps its two parameters; the view is looked up inside the method body through a module-level weak map keyed by the runtime instance, which no declaration names. The builders' new optional trailing parameter is therefore not in the published .d.ts either. No api-surface snapshot gate exists for this package, so this is judged from the barrel, which is the whole of what tsup bundles.

The merged view is shared, not a second copy of the merge (the card's ⛔). Right. One merge implementation remains: readMergedObjects(reads) in stdio-data-bridge.ts, unchanged from #25243. stdioObjectView(reads) wraps it once; createStdioDataBridge builds objectView = stdioObjectView(reads) and both listObjects and describeObject now answer through it (their bodies lost their own copies of the fallback logic); plugin.ts line 804 hands stdioObjectView({ metadataService, protocol: protocolReads }) to setRuntimeObjectView(this.runtime, …) over the same metadataService and the same protocolReads accessor the bridge receives at line 730 and the record resource's gate at line 752. Two closures, one function, one accessor.

Hoisting protocolReads in plugin.ts. Right. It moves from inside the stdio branch to start() scope immediately after the mergedRead block; ctx is the start(ctx) parameter so the accessor still resolves ctx.getService('protocol') at each call; the stdio branch's createStdioDataBridge and gateReads still reference it; setRuntimeObjectView is called under if (metadataService) before bridgeResources, outside the stdio branch, so the resources carry the view whether or not the transport auto-starts. Order is irrelevant anyway: bridgeResources reads runtimeObjectViews.get(this) ?? metadataService inside each resource handler, and the arrow captures this lexically.

The numbered Contract list — every input named, each old/new reading right per the code:

  1. objectstack://objects on an environmentId kernel, boot-registered object: old omitted (no copy in the metadata service), new listed — readFlattenedMetaItems starts from engine.registry.listItems('object') on both kernel shapes. The resource applies no system-object filter and list_objects applies one unless allowSystemObjects, so "the non-sys_ set is what list_objects lists" is the correct phrasing, and the dogfood pin compares exactly that set.
  2. objectstack://objects/NAME on such an object: old 404 RESOURCE_NOT_FOUND, new the schema body. Both the builder and describeObject map the same definition to name / label / fields as an array of { name, type, label, required } / enableFeatures: def.enable ?? {}, so equality to describe_object's body holds field for field.
  3. A disabled package's objects where the metadata service still holds a copy (the narrowing): the merged listing omits them at protocol.ts line 10616 — the items array is filtered to drop every item for which isItemOfDisabledPackage(this.engine.registry, item) is true, applied to every type except package — and stdioObjectView.getObject is a find over that same listing, so the miss reaches diagnoseEmptyRead with the metadata service healthy and answers the coded 404 RESOURCE_NOT_FOUND. The answer is the miss, not OBJECT_PACKAGE_DISABLED: that is what describe_object has answered since fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243 (the protocol's by-name read at line 11426 would raise the disabled refusal, and the view deliberately does not use it), while the record resource's gate still goes through readObjectDeclaration's by-name read and keeps raising the refusal for data. That is the card's "keep the exposure gate's reading", kept.
  4. A served definition that differs from the copy: follows from reading the registry-plus-overlay merge instead of metadataService.getObject.
  5. The merged listing throws: readMergedObjects has no catch, the view has none, the builders await it before any verdict, so the rejection leaves the resource handler and the SDK answers a JSON-RPC error. The protocol itself rethrows every overlay-read failure except "sys_metadata not provisioned" (rethrowUnlessMetadataStoreUnprovisioned), so on a real store outage this path is reached. Declared in the Contract list and in the changeset's "Also changed" bullet; pinned with a throwing double only, as the PR says.

Outage / partial verdict unchanged. Right. buildObjectListResource still calls diagnoseListRead(metadataService, 'object') (listDiagnosed) on every answer and buildObjectSchemaResource still calls diagnoseEmptyRead(metadataService, 'object', name) (getDiagnosed) on a miss; only the items' source changed. This is the same composition the bridge's listObjectsDiagnosed uses for list_objects (items from bridge.listObjects(), verdict from diagnoseObjectListRead(metadataService)). One reading to hold onto: "a loader outage still withholds totalCount" names the metadata service's loaders; a sys_metadata outage inside the protocol is item 5, a throw, not a partial. The PR body and changeset state both, consistently.

Discovery stays ungated. Right. Neither builder calls enforceApiExposure; the apiEnabled: false object is listed and described, as list_objects / describe_object do, and the dogfood pin asserts it.

Accept-set changes implied by the diff, each named: (a) the two object resources now accept the merged listing as their item source — correct; (b) a name absent from the merged listing is now refused with the coded miss even where a copy exists — correct, and it is the declared narrowing; (c) a protocol throw now refuses the read instead of answering the copy — correct and declared; (d) a host with no protocol service, or one without getMetaItems, reads the metadata service byte-identically — correct per readMergedObjects returning undefined and both view members falling through to listObjects() / getObject(name); (e) a host that calls bridgeResources on its own runtime reads the metadata service — correct, the weak map is empty for it. Nothing else in the diff moves an accept set.

What the head's check-runs measured and what they did not. Test Core (6/6, aggregate success) ran packages/mcp, so the 14-case unit file ran in CI. .github/workflows/ci.yml excludes @objectstack/dogfood from the Test Core shards and runs the Dogfood Regression Gate shards only off pull_request / merge_group; on this head those shards are skipped and the always-on gate concluded success with zero attestations. So the 10 real-kernel pins in packages/qa/dogfood/test/mcp-stdio-package-object-discovery.test.ts, and the red-before/green-after table built on them, rest on the dev's local verification until main's first full-battery run after landing. The pins themselves read correctly against the code (the boot fixture's disabled-package precondition asserts the copy exists on the no-environmentId shape, which is exactly the row the narrowing changes).

② Semver level

.changeset/25245-mcp-stdio-object-resources-merged.md bumps '@objectstack/mcp': patch. The package publishes (no private flag, version 18.0.0), so skip-changeset would have been wrong and is not used. The diff moves no spec key, authorable spelling, metadata type or stored shape: Metadata-protocol: none is right, and per the Post-Task Checklist a non-protocol change, breaking or not, ships patch and keeps its BREAKING banner and ADR-0087 disposition. Both line-initial declarations, Clause-②: no (narrowing) and Metadata-protocol: none, are present in the PR body and in the changeset body; the title carries !; the BREAKING banner is present and names the narrowing (a disabled package's objects no longer served by the resources). The (narrowing) arm is right: a read that was served (the copy of a disabled package's object on the no-environmentId shape) is now refused. The claim comments agree after revision 6346855817, which supersedes the bare Clause-②: no of 6346581465 with the measured arm.

ADR-0087: exactly one marker, not-required (no-migration-prescription). The changeset's own remedy line is "Nothing", so the category's statement-against-statement check holds; no exported declaration is removed or narrowed (the barrel is unchanged), so the three export categories do not apply; the package is published, so not unpublished; no ADR-0087 id is named, so neither registered nor already-registered. Check Changeset and Lint & Repo Gates are success on the head.

③ Boundary flags

  1. Prompt bridge captures the protocol once (The MCP prompt bridge's listSkills reads the un-merged registry listing, so a runtime meta PUT never reaches prompts #8328). Confirmed at plugin.ts lines 429–446: mergedRead is resolved in start() and handed to bridgePrompts, not re-read per use. Pre-existing, skills not objects, and not measured by this PR. Answered: out of this card's scope and not a blocker; a card in this family is owed only once the lane measures it (a card names its evidence).
  2. objectstack://objects/NAME reads the whole merged listing per call. Same cost profile as describe_object since fix(mcp)!: the stdio data bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25243 and the HTTP bridge since fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25136; no contract change. Answered: accepted.
  3. File surface wider than the first claim. Claim revision 6346855817 names stdio-data-bridge.ts (the shared view), the README paragraph and the dogfood file; the diff's seven paths match that surface exactly. Answered.
  4. Branch name claude/project-thread-dkk3hd. The "card this PR closes must claim this branch" check is success, and the newest Claim: on mcp stdio: on an environmentId kernel, the objectstack://objects resources read the metadata service, so a package registered during boot is listed as no object and its objectstack://objects/NAME answers 404 #25245 names this branch. The Duplicate Fix Guard concern is advisory. Answered.
  5. Raised here: the real-kernel pins have not run in CI on this head (① last paragraph). Escalated as a reading, not a finding: by the two-speed convention the first full-battery run on main after landing measures them, and a red there is this lane's main-red.
  6. Check-runs at signing. The five required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard — are all success. One advisory check, "Part-of PR must not also close its card", was still in_progress (started 2026-10-22T18:26:25Z) on both reads; it is outside the required set and the PR carries one closing keyword, Fixes #25245.
  7. The PR is a draft. Landing stays with the owning seat after this record; nothing here readies, queues or arms it.

Implemented-by: claude/project-thread-dkk3hd
Reviewed-by: session_01TgbP2QERdKmh147ohacHbK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:cli seat 1 (seat post #6024) · os-sales · session session_01CxBKycaiUpA1yGx1XyoGqX · 2026-10-22T18:57Z · head 5cf1aa4ce3

item reading
Shape 7 files, +495 / −31 (diff read by the seat). stdio-data-bridge.ts exports stdioObjectView, the one listing and by-name read over the protocol's merged listing that list_objects / describe_object now answer through (moved, not copied: the two inline reads are gone). plugin.ts hoists protocolReads out of the stdio branch and hands the same view to its runtime through the package-internal setRuntimeObjectView (a WeakMap, read at each resources/read); bridgeResources keeps its published two-parameter signature, and the two builders take the view as a defaulted last parameter, so the completeness verdict and the 503 / 404 split stay the metadata service's. Inside claim revision 6346855817.
Premise #25245, filed by this seat out of #25243's scope, Blocked-by: #25138 closed by PR #25243 (8010b641c); unblocked by this seat at 17:00Z.
Contract review CONTRACT_REVIEW_TIER PASS on this head, 6348750797 (Clause-②: no (narrowing); index.ts and package.json unchanged between base and head).
Dogfood battery 6348550783 on this head: lock VERDICT command-exit 0, 289 files / 2491 tests passed, 0 failed (owed: ! title and a narrowing arm). The pin file gains 10 real-kernel cases on both kernel shapes.
Gates (check-runs on this head) 42 runs: 34 success, 7 skipped, 0 failed; the five required contexts all success. Part-of PR must not also close its card still in_progress (advisory; the body carries one closing keyword, Fixes #25245). Report 6348777147: mcp_calls reads only, writes all through the relay.
Changeset, read per sentence fix(mcp)! patch with the BREAKING banner and an ADR-0087 not-required reason. Before / Now / Also changed / Unchanged / What to do each match the hunks: the merged view on both resources, a disabled package's objects omitted with the coded 404, a throwing merged listing now failing the read as list_objects does, the metadata-service fallback on a host with no merged read, and a direct bridgeResources caller unchanged. Metadata-protocol: none holds.
Accepted deviations Branch claude/project-thread-dkk3hd, the #25138 card thread's branch restarted from origin/main 0b8bce40 after PR #25243 merged; the claim's Clause-② arm revised after measurement (6346855817).
Cross-lane touches None. packages/mcp and packages/qa/dogfood are this lane's.
Landing Not governed (check-governed-merges.mjs --pr 25268, +495 / −31, under the threshold). Ready + auto-merge through the relay in this act.

Generated by Claude Code

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/l tests tooling

Projects

None yet

2 participants