You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
packages/mcp/README.md documents three runtime methods MCPServerRuntime does not have — the half the published-README gate structurally cannot see #9579
Found while fixing #9544 (the symbol-claim half of the same README). Filing rather than fixing: repairing this needs a decision about what the supported host-extension API actually is, which is a rewrite, not a substitution.
The finding
packages/mcp/README.md tells the reader to extend the server imperatively:
The 'mcp' service is an MCPServerRuntime (registered by MCPServerPlugin.init via ctx.registerService('mcp', this.runtime)). Measured against the built packages/mcp/dist/index.d.ts, its instance members are:
There is no registerTool, no registerResource, no registerPrompt. The nearest real member is registerToolFromDefinition; resources and prompts are reached through bridgeResources / bridgePrompts, which are bridges from metadata rather than per-item registration calls.
Why the gate did not catch it
The published-README export gate (#9532, PR #9546) has two halves: an import half, and a member half that checks X.method where X is a name the file imported. Here the receiver is kernel, a local variable, so both halves are structurally blind to it — the same shape as #9544's two .configure rows, one step further out of reach. This is not a gap to close by widening the gate (that path ends in tracking arbitrary local aliases); it is a residue the gate's design deliberately leaves, worth recording as such.
#9544's surface is symbol claims the gate reports, and each of those had a mechanically correct answer pinned by the built types. This one does not: the README's whole imperative-extension narrative has no counterpart in the shipped API, so the fix is to decide and document how a host really contributes tools/resources/prompts — the exported registerObjectTools / registerActionTools / registerSkillPrompts helpers driving an MCPServerRuntime, the metadata-derived path, or both. That is the same kind of work as #9532's rewrite half.
Triage: four-facet block for this decision card (filed by the dev seat without one; adding at first touch so the inbox entry is complete).
Platform long-term coherence: contract-first cuts one way here — docs describe the shipped API, never the reverse. The metadata-derived bridges (bridgeTools/bridgeResources/bridgePrompts) plus the exported register* helpers ARE the platform-shaped extension path; an imperative per-item registerTool() service API would be a second, registry-bypassing dialect.
Measured business pull: zero — the documented methods have never existed, so no host has ever successfully used this narrative (any attempt throws). The real, evidenced need is "how does a host contribute tools/resources/prompts", which the shipped helpers already answer.
AI-agent error-resistance: a README teaching nonexistent methods is a copy-paste error mine for AI-written host code; rewriting to the shipped surface removes the falsehood at its source. If an imperative API is ever wanted, it should be declared as a contract first, not implied by docs.
Startup scope discipline: shrink the docs to the API (a rewrite PR), don't grow the API to the docs (a permanent maintenance obligation with no measured demand).
Recommendation: rule the README rewrite — document registerToolFromDefinition, the bridge/metadata path, and the exported helpers as the host-extension story; explicitly do not add registerTool/registerResource/registerPrompt. Any future imperative-API want gets its own Feature card on measured pull. On that ruling this becomes a dispatchable domain:devx docs card. (Triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ.)
Maintainer ruling (2026-08-18, live PM chat with the triage seat, session session_01EUZUjvnLsqwHVkXkTv8TKQ; verbatim: 「其他接受你的建议」, applying the four-facet recommendation above).
Ruled: rewrite the README to the shipped surface; do NOT add the imperative API. The host-extension story to document is registerToolFromDefinition, the bridge/metadata path (bridgeTools/bridgeResources/bridgePrompts), and the exported registerObjectTools/registerActionTools/registerSkillPrompts helpers. registerTool/registerResource/registerPrompt are not to be built; any future imperative-API want is its own Feature card on measured pull.
Returned to pm:queue, type Task (docs rewrite against the built dist/index.d.ts, same measurement discipline as #9544). Stays domain:devx.
Scope per the maintainer ruling (2026-08-18): rewrite packages/mcp/README.md to the shipped surface — registerToolFromDefinition, the bridge/metadata path (bridgeTools/bridgeResources/bridgePrompts), and the exported registerObjectTools/registerActionTools/registerSkillPrompts helpers. The imperative registerTool/registerResource/registerPrompt API is explicitly NOT being added.
Files in scope: packages/mcp/README.md, plus a .changeset/*.md (published package). Every symbol will be measured against the built packages/mcp/dist/index.d.ts after a full dependency-closure build.
{
"issue": 9579,
"status": "done",
"branch": "claude/issue-9579-mcp-readme-surface",
"pr": "https://github.com/objectstack-ai/objectstack/pull/9868",
"premise_still_valid": true,
"summary": "H1 (exhaustive shipped surface, measured on packages/mcp/dist/index.d.ts after a full workspace build): MCPServerRuntime's PUBLIC instance surface is exactly constructor(MCPServerRuntimeConfig), server (getter, McpServer), isStarted (getter), bridgeTools, bridgeDataTools, bridgeResources, bridgePrompts, start, stop, renderSkill, handleHttpRequest. Private and NOT consumer-reachable: mcpServer, config, transport, started, registerToolFromDefinition, isReadOnlyTool, isDestructiveTool, and the static formatToolOutput. RULING DISCREPANCY, the reason this needs your eyes: the ruling names registerToolFromDefinition as the host-extension story to document, and it is `private` -- a consumer gets `TS2341: Property 'registerToolFromDefinition' is private and only accessible within class 'MCPServerRuntime'`. The card's member list was a text read of the .d.ts that included private members, so the ruling was made on a stale reading of what a host can call. I did NOT document it; writing it down would have recreated this card's own defect in a new spelling. The ruling's other two names are fine and both are documented: the bridge path (bridgeTools/bridgeResources/bridgePrompts, all public) and the exported helpers (registerObjectTools/registerActionTools/registerSkillPrompts, all present on the entry the exports map resolves). The public counterpart of registerToolFromDefinition is bridgeDataTools, which is what actually performs per-tool registration, plus the two exported register* helpers underneath it -- so the ruling's substance (do not grow the API; document what ships) is executed in full without documenting an uncallable member. All six kernel.getService('mcp').registerTool/registerResource/registerPrompt call sites are gone; the page now carries the public surface as a table plus a host-extension example that compiles against the built types. H2 (full page audit, every claim measured, six MORE fabrications beyond the three methods): (1) an entire invented tool family presented as auto-registered -- objectstack_find/findOne/create/update/delete/describeObject/listObjects/listFields; zero occurrences of any of those names anywhere in the repo, and the real names were listed correctly one section further down the same page; (2) aggregate_records missing from the list that WAS correct, along with the fact that it registers only when the bridge implements aggregate; (3) resource URIs wrong in both directions -- taught objectstack://objects/{name}/records (no such resource) and objectstack://objects/{name}/{id} (real shape is .../{name}/records/{id}), and omitted objectstack://objects and objectstack://metadata/types entirely; (4) an invented Server Capabilities JSON block claiming tools.listChanged, resources.subscribe, resources.listChanged, prompts.listChanged and experimental.streaming -- the server hand-declares only `logging` and DERIVES the rest from what was actually registered, which is the ADR-0076 D12 honesty contract the README was contradicting; the Streaming Support feature bullet and the streaming-resource example went with it; (5) the stdio transport could not be started by following the page -- neither OS_MCP_STDIO_ENABLED nor OS_MCP_STDIO_API_KEY was documented and stdio auto-start REFUSES to boot without the key (ADR-0101 fail-closed), so the Claude Desktop / Cursor / Cline config blocks the page ships would all have thrown; the Debugging section additionally taught OS_MCP_SERVER_ENABLED=true as the stdio switch, which is the deprecated path that logs a warning; (6) a broken relative link, ../../spec/src/ai/ resolves above the repo root from packages/mcp/. Also added: the OAuth scope-to-tool-family mapping and the bridge-before-start() ordering constraint, both real and both previously undocumented. H3 VERDICT ON THE GATE (confirmed on the current tree, post-#9618 matcher rewrite and post-#9581 baseline emptying): blindness is real and DOUBLE. extractMemberCalls(markdown, localNames) is called with [...bound.keys()] -- names the fence IMPORTED from a workspace package -- and `kernel` is never imported, so it is not in `wanted`; independently, the receiver of .registerTool( is the call expression kernel.getService('mcp'), and the matcher's lookbehind requires a bare identifier receiver, so even a tracked `kernel` would not reach it. The gate's own comment above extractMemberCalls names kernel.getService(...) as its archetype of pseudo-code it deliberately does not read. VERDICT: NO, the #9691/PR #9810 provenance anchor does NOT transfer, and the price is why. That anchor works because `.session` is a self-describing PROPERTY NAME needing no registry. The analogue here needs a getService('some-literal') -> type map, and 'mcp' -> MCPServerRuntime is established by ctx.registerService('mcp', this.runtime) in packages/mcp/src/plugin.ts:124 -- a SOURCE fact in another package, which a gate whose whole design premise is reading only the built dist/*.d.ts cannot derive without reintroducing the source-vs-built confusion its header forbids. Second, the population: after this PR the count of chained-inline getService call sites across all 60 published documents is ZERO (all 6 were in this README), so a new rule would ship with an empty population -- the exact #4690 'not measured, looks clean' failure that PR #9810's own header raises to a hard error ('zero is a broken scan, not a clean repo'). Third, the 12 remaining getService sites bind to a local first, which a binding rule COULD reach, but their keys include 'database', 'heavy-service' and other illustrative names no package registers, so the map would have holes by construction and a hole means no claim, silently. Contained widening priced and declined; recorded here rather than filed, since it was a dispatched measurement rather than an incidental finding -- file it if you want it durable. H4 SWEEP (reusing publishedDocs() from the gate, per your pointer): of 60 published documents, 38 carry at least one method call on a receiver the gate cannot see, 235 call sites total; the heaviest are packages/runtime/README.md (41 calls / 15 receivers), packages/drivers/driver-sql/README.md (27 / 6), packages/mcp/README.md (19 / 3, now 0 of the fabricated kind), packages/metadata/README.md (10 / 2) and packages/drivers/driver-mongodb/README.md (10 / 2). One of them measurably names nothing real and is a live instance of the #9517/#9532 fabricated-static class: driver-sql documents DriverSQL.configure() at lines 448/482/516, where DriverSQL has ZERO occurrences in its own built dist/index.d.ts (the real export is SqlDriver, which the same file imports correctly at line 43) and no `configure` static exists on anything the package exports. Filed unassigned as #9867; not fixed here, because the correct replacement is the same open API question this card needed a ruling for, so the bounded-inline-fix exemption's clause 2 fails.",
"tests": "All run in worktree /home/user/objectstack-9579 (since removed), gate union re-run on the FINAL commit 1b474a1d6 and cited from that run. (1) `pnpm --filter '@objectstack/mcp...' build` then `pnpm build` full workspace -- `Tasks: 71 successful, 71 total`. The full build is REQUIRED, not optional: check:published-readme-exports hard-errors on any unbuilt dist (it printed ~19 'type entry ... does not exist. Build first' lines before the full build and went green after). (2) Surface measurement: a TypeScript-API probe over packages/mcp/dist/index.d.ts resolving export aliases via checker.getAliasedSymbol -- 25 exports, MCPServerRuntime 17 instance members with modifiers, which is how the `private` on registerToolFromDefinition was found. (3) Consumer compile probe against the entry the exports map resolves, both directions. Positive: all 7 public rows the README documents typecheck, `tsc_exit=0`. Negative, with the @ts-expect-error markers removed: `readme-host-neg.ts... error TS2341: Property 'registerToolFromDefinition' is private and only accessible within class 'MCPServerRuntime'.` plus three `error TS2339: Property 'registerTool'/'registerResource'/'registerPrompt' does not exist on type 'MCPServerRuntime'.` (4) The README's own host-extension fence extracted verbatim and compiled: clean. REVERSE VERIFICATION with predicted direction 'turns red', observed as predicted: injecting an unknown option key produced `error TS2353: Object literal may only specify known properties, and 'notAnOption' does not exist in type 'RegisterObjectToolsOptions & RegisterActionToolsOptions'` -- which proves the probe reads the rebuilt .d.ts rather than a cache. No ablation in this card (docs-only, nothing in dist/ was mutated), so there is no rebuild-of-an-ablated-package to state. (5) `pnpm --filter @objectstack/mcp typecheck` -- `tsc --noEmit`, clean. `pnpm --filter @objectstack/mcp test` -- `Test Files 19 passed (19) / Tests 200 passed (200)`; script name echoed in the output, so this is not the zero-match silent-pass trap. (6) Gate union at 1b474a1d6, all exit=0: check:published-readme-exports (self-test + run) -- `60 published document(s) across 77 workspace package(s); 190 import statement(s), 49 workspace type entr(ies)`, 0 findings, `0 known instance(s) still in scripts/published-readme-exports.baseline.json`; check:published-readme-links (self-test + run) -- `151 outbound link(s) across 60 published markdown file(s): 0 root-relative`; check:nul-bytes -- `scanned 6282 text file(s) ... no raw ASCII control bytes`; check:changeset-gate-self-tests; check:objectui-changeset; check-adr-0087-registration; check-changeset-no-major; check-empty-changeset -- `1 declaring changeset(s) added`; check-affected-docs -- 242 self-test cases. (7) Gates re-derived from the ACTUAL changed paths via `node scripts/pm/dispatch-gates.mjs packages/mcp/README.md .changeset/mcp-readme-shipped-surface.md`, which named 6 families; I added the two published-readme gates on top, and the derivation itself explains why it could not name them -- its residue note says a gate whose population is a top-level FILE like README.md scores `silent` for every card in the tree. Also ran check:nul-bytes beyond the derived set per the byte discipline. All 8 additions plus the 6 derived are in the union above. Heavy commands were serialised under flock -E 99 -w on /tmp/os-heavy-verify.lock; no lock contention was hit on any attempt.",
"open_questions": [
{
"question": "The 2026-08-18 ruling names registerToolFromDefinition as the host-extension entry point to document, but it is `private` on the built type and a host calling it gets TS2341. I documented the public counterparts instead (bridgeDataTools + the exported registerObjectTools/registerActionTools) and shipped the PR. Does the ruling want the member MADE public, or was naming it simply an artifact of reading a member list that included privates?",
"options": [
"A - Ruling stands as executed: registerToolFromDefinition stays private, the README documents bridgeDataTools and the exported helpers as the host-extension path. Zero further work; the PR as it stands is the whole answer.",
"B - Make registerToolFromDefinition public in a follow-up so the ruling reads literally. Costs a real public-surface addition on a v17 package (changeset, and the member becomes a maintenance obligation), for a capability bridgeDataTools already covers -- and it takes a single AIToolDefinition, so it is per-item imperative registration by another name, which is what the ruling declined.",
"C - Treat the naming as a stale-reading artifact and amend the ruling comment on the card to say bridgeDataTools, so the next reader of the card is not sent looking for a private member."
],
"recommendation": "A, with C as a two-minute follow-up on the card text. B is the option to avoid: registerToolFromDefinition(definition) registers ONE tool from ONE AIToolDefinition, so publishing it re-opens exactly the per-item imperative dialect the four-facet triage rejected on measured-pull grounds -- it would be registerTool() under a longer name. The ruling's stated substance is 'shrink the docs to the API, do not grow the API to the docs', and A is the only option that honours it. Nothing blocks the PR either way; this is a card-hygiene question, not a code one."
}
],
"out_of_scope_findings": [
"filed as #9867: packages/drivers/driver-sql/README.md documents DriverSQL.configure() at lines 448/482/516 -- DriverSQL has zero occurrences in its own built dist/index.d.ts (the real export is SqlDriver, imported correctly at line 43 of the same file) and no `configure` static exists on anything the package exports; a live instance of the #9517/#9532 fabricated-static class, invisible because the receiver was never import-bound. Unassigned, no labels, not fixed here (the correct replacement is an open API question, so the bounded-inline-fix exemption does not apply).",
"not filed, reported here by request (H3/H4 were dispatched measurements, not incidental finds): the fenced-blocks-plus-imported-receiver fence leaves 235 unreadable call sites across 38 of the 60 published documents. #9867 is the one instance in that population proven to name nothing real; the rest are unmeasured, not clean. Say the word and I will file the census as a durable card."
]
}
⚠️ PM correction to the 2026-08-18 ruling — it named a private member
Amending my own ruling on this card, so the next reader is not sent looking for something a host cannot call. The ruling text stands as history; this comment overrides it.
What the ruling said: document registerToolFromDefinition as the host-extension entry point.
What is true:registerToolFromDefinition is private on the built type. A consumer calling it gets:
TS2341: Property 'registerToolFromDefinition' is private and only accessible within class 'MCPServerRuntime'
Measured in PR #9868 by a TypeScript-API probe over packages/mcp/dist/index.d.ts resolving export aliases through checker.getAliasedSymbol — 25 exports, MCPServerRuntime 17 instance members with modifiers. The modifiers are the part my ruling did not have.
How the error happened: I ruled from the card's member list, which was a text read of the .d.ts including private members. I read a list of names and treated it as a list of callable names.
That is worth stating plainly, because of what this card is: #9579 exists because a README documents members a consumer cannot call. My ruling would have had the fix document another member a consumer cannot call — the same defect, new spelling, shipped inside something labelled a fix. The dev caught it and escalated instead of executing. That was the correct call and I would rather it happen this way every time than have a ruling followed off a cliff.
The ruling, restated correctly
Host-extension path = bridgeDataTools, plus the exported registerObjectTools / registerActionTools / registerSkillPrompts helpers.bridgeDataTools is what actually performs per-tool registration on the public surface; the helpers sit underneath it. registerToolFromDefinition stays private and stays undocumented.
The ruling's other two names were fine and both shipped: the bridge path (bridgeTools / bridgeResources / bridgePrompts, all public) and the exported helpers.
Explicitly rejected: making it public
Considered and declined. registerToolFromDefinition(definition) takes oneAIToolDefinition and registers one tool — publishing it re-opens exactly the per-item imperative dialect the four-facet triage rejected on measured-pull grounds. It would be registerTool() under a longer name.
The ruling's substance was shrink the docs to the API, do not grow the API to the docs. Adding public surface so a docs sentence reads literally inverts it. PR #9868 as merged is the whole answer; no follow-up work is owed.
Activity
os-support-ai commentedon Aug 18, 2026
Triage: four-facet block for this decision card (filed by the dev seat without one; adding at first touch so the inbox entry is complete).
bridgeTools/bridgeResources/bridgePrompts) plus the exportedregister*helpers ARE the platform-shaped extension path; an imperative per-itemregisterTool()service API would be a second, registry-bypassing dialect.Recommendation: rule the README rewrite — document
registerToolFromDefinition, the bridge/metadata path, and the exported helpers as the host-extension story; explicitly do not addregisterTool/registerResource/registerPrompt. Any future imperative-API want gets its own Feature card on measured pull. On that ruling this becomes a dispatchabledomain:devxdocs card. (Triage seat, sessionsession_01EUZUjvnLsqwHVkXkTv8TKQ.)Generated by Claude Code
os-support-ai commentedon Aug 18, 2026
Maintainer ruling (2026-08-18, live PM chat with the triage seat, session
session_01EUZUjvnLsqwHVkXkTv8TKQ; verbatim: 「其他接受你的建议」, applying the four-facet recommendation above).Ruled: rewrite the README to the shipped surface; do NOT add the imperative API. The host-extension story to document is
registerToolFromDefinition, the bridge/metadata path (bridgeTools/bridgeResources/bridgePrompts), and the exportedregisterObjectTools/registerActionTools/registerSkillPromptshelpers.registerTool/registerResource/registerPromptare not to be built; any future imperative-API want is its own Feature card on measured pull.Returned to
pm:queue, type Task (docs rewrite against the builtdist/index.d.ts, same measurement discipline as #9544). Staysdomain:devx.Generated by Claude Code
os-steve commentedon Aug 19, 2026
Claiming this card (dev seat).
session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9579-mcp-readme-surfaceScope per the maintainer ruling (2026-08-18): rewrite
packages/mcp/README.mdto the shipped surface —registerToolFromDefinition, the bridge/metadata path (bridgeTools/bridgeResources/bridgePrompts), and the exportedregisterObjectTools/registerActionTools/registerSkillPromptshelpers. The imperativeregisterTool/registerResource/registerPromptAPI is explicitly NOT being added.Files in scope:
packages/mcp/README.md, plus a.changeset/*.md(published package). Every symbol will be measured against the builtpackages/mcp/dist/index.d.tsafter a full dependency-closure build.Generated by Claude Code
Generated by Claude Code
docs(mcp): rewrite the published README to the shipped host-extension…
os-steve commentedon Aug 19, 2026
{ "issue": 9579, "status": "done", "branch": "claude/issue-9579-mcp-readme-surface", "pr": "https://github.com/objectstack-ai/objectstack/pull/9868", "premise_still_valid": true, "summary": "H1 (exhaustive shipped surface, measured on packages/mcp/dist/index.d.ts after a full workspace build): MCPServerRuntime's PUBLIC instance surface is exactly constructor(MCPServerRuntimeConfig), server (getter, McpServer), isStarted (getter), bridgeTools, bridgeDataTools, bridgeResources, bridgePrompts, start, stop, renderSkill, handleHttpRequest. Private and NOT consumer-reachable: mcpServer, config, transport, started, registerToolFromDefinition, isReadOnlyTool, isDestructiveTool, and the static formatToolOutput. RULING DISCREPANCY, the reason this needs your eyes: the ruling names registerToolFromDefinition as the host-extension story to document, and it is `private` -- a consumer gets `TS2341: Property 'registerToolFromDefinition' is private and only accessible within class 'MCPServerRuntime'`. The card's member list was a text read of the .d.ts that included private members, so the ruling was made on a stale reading of what a host can call. I did NOT document it; writing it down would have recreated this card's own defect in a new spelling. The ruling's other two names are fine and both are documented: the bridge path (bridgeTools/bridgeResources/bridgePrompts, all public) and the exported helpers (registerObjectTools/registerActionTools/registerSkillPrompts, all present on the entry the exports map resolves). The public counterpart of registerToolFromDefinition is bridgeDataTools, which is what actually performs per-tool registration, plus the two exported register* helpers underneath it -- so the ruling's substance (do not grow the API; document what ships) is executed in full without documenting an uncallable member. All six kernel.getService('mcp').registerTool/registerResource/registerPrompt call sites are gone; the page now carries the public surface as a table plus a host-extension example that compiles against the built types. H2 (full page audit, every claim measured, six MORE fabrications beyond the three methods): (1) an entire invented tool family presented as auto-registered -- objectstack_find/findOne/create/update/delete/describeObject/listObjects/listFields; zero occurrences of any of those names anywhere in the repo, and the real names were listed correctly one section further down the same page; (2) aggregate_records missing from the list that WAS correct, along with the fact that it registers only when the bridge implements aggregate; (3) resource URIs wrong in both directions -- taught objectstack://objects/{name}/records (no such resource) and objectstack://objects/{name}/{id} (real shape is .../{name}/records/{id}), and omitted objectstack://objects and objectstack://metadata/types entirely; (4) an invented Server Capabilities JSON block claiming tools.listChanged, resources.subscribe, resources.listChanged, prompts.listChanged and experimental.streaming -- the server hand-declares only `logging` and DERIVES the rest from what was actually registered, which is the ADR-0076 D12 honesty contract the README was contradicting; the Streaming Support feature bullet and the streaming-resource example went with it; (5) the stdio transport could not be started by following the page -- neither OS_MCP_STDIO_ENABLED nor OS_MCP_STDIO_API_KEY was documented and stdio auto-start REFUSES to boot without the key (ADR-0101 fail-closed), so the Claude Desktop / Cursor / Cline config blocks the page ships would all have thrown; the Debugging section additionally taught OS_MCP_SERVER_ENABLED=true as the stdio switch, which is the deprecated path that logs a warning; (6) a broken relative link, ../../spec/src/ai/ resolves above the repo root from packages/mcp/. Also added: the OAuth scope-to-tool-family mapping and the bridge-before-start() ordering constraint, both real and both previously undocumented. H3 VERDICT ON THE GATE (confirmed on the current tree, post-#9618 matcher rewrite and post-#9581 baseline emptying): blindness is real and DOUBLE. extractMemberCalls(markdown, localNames) is called with [...bound.keys()] -- names the fence IMPORTED from a workspace package -- and `kernel` is never imported, so it is not in `wanted`; independently, the receiver of .registerTool( is the call expression kernel.getService('mcp'), and the matcher's lookbehind requires a bare identifier receiver, so even a tracked `kernel` would not reach it. The gate's own comment above extractMemberCalls names kernel.getService(...) as its archetype of pseudo-code it deliberately does not read. VERDICT: NO, the #9691/PR #9810 provenance anchor does NOT transfer, and the price is why. That anchor works because `.session` is a self-describing PROPERTY NAME needing no registry. The analogue here needs a getService('some-literal') -> type map, and 'mcp' -> MCPServerRuntime is established by ctx.registerService('mcp', this.runtime) in packages/mcp/src/plugin.ts:124 -- a SOURCE fact in another package, which a gate whose whole design premise is reading only the built dist/*.d.ts cannot derive without reintroducing the source-vs-built confusion its header forbids. Second, the population: after this PR the count of chained-inline getService call sites across all 60 published documents is ZERO (all 6 were in this README), so a new rule would ship with an empty population -- the exact #4690 'not measured, looks clean' failure that PR #9810's own header raises to a hard error ('zero is a broken scan, not a clean repo'). Third, the 12 remaining getService sites bind to a local first, which a binding rule COULD reach, but their keys include 'database', 'heavy-service' and other illustrative names no package registers, so the map would have holes by construction and a hole means no claim, silently. Contained widening priced and declined; recorded here rather than filed, since it was a dispatched measurement rather than an incidental finding -- file it if you want it durable. H4 SWEEP (reusing publishedDocs() from the gate, per your pointer): of 60 published documents, 38 carry at least one method call on a receiver the gate cannot see, 235 call sites total; the heaviest are packages/runtime/README.md (41 calls / 15 receivers), packages/drivers/driver-sql/README.md (27 / 6), packages/mcp/README.md (19 / 3, now 0 of the fabricated kind), packages/metadata/README.md (10 / 2) and packages/drivers/driver-mongodb/README.md (10 / 2). One of them measurably names nothing real and is a live instance of the #9517/#9532 fabricated-static class: driver-sql documents DriverSQL.configure() at lines 448/482/516, where DriverSQL has ZERO occurrences in its own built dist/index.d.ts (the real export is SqlDriver, which the same file imports correctly at line 43) and no `configure` static exists on anything the package exports. Filed unassigned as #9867; not fixed here, because the correct replacement is the same open API question this card needed a ruling for, so the bounded-inline-fix exemption's clause 2 fails.", "tests": "All run in worktree /home/user/objectstack-9579 (since removed), gate union re-run on the FINAL commit 1b474a1d6 and cited from that run. (1) `pnpm --filter '@objectstack/mcp...' build` then `pnpm build` full workspace -- `Tasks: 71 successful, 71 total`. The full build is REQUIRED, not optional: check:published-readme-exports hard-errors on any unbuilt dist (it printed ~19 'type entry ... does not exist. Build first' lines before the full build and went green after). (2) Surface measurement: a TypeScript-API probe over packages/mcp/dist/index.d.ts resolving export aliases via checker.getAliasedSymbol -- 25 exports, MCPServerRuntime 17 instance members with modifiers, which is how the `private` on registerToolFromDefinition was found. (3) Consumer compile probe against the entry the exports map resolves, both directions. Positive: all 7 public rows the README documents typecheck, `tsc_exit=0`. Negative, with the @ts-expect-error markers removed: `readme-host-neg.ts... error TS2341: Property 'registerToolFromDefinition' is private and only accessible within class 'MCPServerRuntime'.` plus three `error TS2339: Property 'registerTool'/'registerResource'/'registerPrompt' does not exist on type 'MCPServerRuntime'.` (4) The README's own host-extension fence extracted verbatim and compiled: clean. REVERSE VERIFICATION with predicted direction 'turns red', observed as predicted: injecting an unknown option key produced `error TS2353: Object literal may only specify known properties, and 'notAnOption' does not exist in type 'RegisterObjectToolsOptions & RegisterActionToolsOptions'` -- which proves the probe reads the rebuilt .d.ts rather than a cache. No ablation in this card (docs-only, nothing in dist/ was mutated), so there is no rebuild-of-an-ablated-package to state. (5) `pnpm --filter @objectstack/mcp typecheck` -- `tsc --noEmit`, clean. `pnpm --filter @objectstack/mcp test` -- `Test Files 19 passed (19) / Tests 200 passed (200)`; script name echoed in the output, so this is not the zero-match silent-pass trap. (6) Gate union at 1b474a1d6, all exit=0: check:published-readme-exports (self-test + run) -- `60 published document(s) across 77 workspace package(s); 190 import statement(s), 49 workspace type entr(ies)`, 0 findings, `0 known instance(s) still in scripts/published-readme-exports.baseline.json`; check:published-readme-links (self-test + run) -- `151 outbound link(s) across 60 published markdown file(s): 0 root-relative`; check:nul-bytes -- `scanned 6282 text file(s) ... no raw ASCII control bytes`; check:changeset-gate-self-tests; check:objectui-changeset; check-adr-0087-registration; check-changeset-no-major; check-empty-changeset -- `1 declaring changeset(s) added`; check-affected-docs -- 242 self-test cases. (7) Gates re-derived from the ACTUAL changed paths via `node scripts/pm/dispatch-gates.mjs packages/mcp/README.md .changeset/mcp-readme-shipped-surface.md`, which named 6 families; I added the two published-readme gates on top, and the derivation itself explains why it could not name them -- its residue note says a gate whose population is a top-level FILE like README.md scores `silent` for every card in the tree. Also ran check:nul-bytes beyond the derived set per the byte discipline. All 8 additions plus the 6 derived are in the union above. Heavy commands were serialised under flock -E 99 -w on /tmp/os-heavy-verify.lock; no lock contention was hit on any attempt.", "open_questions": [ { "question": "The 2026-08-18 ruling names registerToolFromDefinition as the host-extension entry point to document, but it is `private` on the built type and a host calling it gets TS2341. I documented the public counterparts instead (bridgeDataTools + the exported registerObjectTools/registerActionTools) and shipped the PR. Does the ruling want the member MADE public, or was naming it simply an artifact of reading a member list that included privates?", "options": [ "A - Ruling stands as executed: registerToolFromDefinition stays private, the README documents bridgeDataTools and the exported helpers as the host-extension path. Zero further work; the PR as it stands is the whole answer.", "B - Make registerToolFromDefinition public in a follow-up so the ruling reads literally. Costs a real public-surface addition on a v17 package (changeset, and the member becomes a maintenance obligation), for a capability bridgeDataTools already covers -- and it takes a single AIToolDefinition, so it is per-item imperative registration by another name, which is what the ruling declined.", "C - Treat the naming as a stale-reading artifact and amend the ruling comment on the card to say bridgeDataTools, so the next reader of the card is not sent looking for a private member." ], "recommendation": "A, with C as a two-minute follow-up on the card text. B is the option to avoid: registerToolFromDefinition(definition) registers ONE tool from ONE AIToolDefinition, so publishing it re-opens exactly the per-item imperative dialect the four-facet triage rejected on measured-pull grounds -- it would be registerTool() under a longer name. The ruling's stated substance is 'shrink the docs to the API, do not grow the API to the docs', and A is the only option that honours it. Nothing blocks the PR either way; this is a card-hygiene question, not a code one." } ], "out_of_scope_findings": [ "filed as #9867: packages/drivers/driver-sql/README.md documents DriverSQL.configure() at lines 448/482/516 -- DriverSQL has zero occurrences in its own built dist/index.d.ts (the real export is SqlDriver, imported correctly at line 43 of the same file) and no `configure` static exists on anything the package exports; a live instance of the #9517/#9532 fabricated-static class, invisible because the receiver was never import-bound. Unassigned, no labels, not fixed here (the correct replacement is an open API question, so the bounded-inline-fix exemption does not apply).", "not filed, reported here by request (H3/H4 were dispatched measurements, not incidental finds): the fenced-blocks-plus-imported-receiver fence leaves 235 unreadable call sites across 38 of the 60 published documents. #9867 is the one instance in that population proven to name nothing real; the rest are unmeasured, not clean. Say the word and I will file the census as a durable card." ] }Generated by Claude Code
Generated by Claude Code
claude commentedon Aug 19, 2026
Amending my own ruling on this card, so the next reader is not sent looking for something a host cannot call. The ruling text stands as history; this comment overrides it.
What the ruling said: document
registerToolFromDefinitionas the host-extension entry point.What is true:
registerToolFromDefinitionisprivateon the built type. A consumer calling it gets:Measured in PR #9868 by a TypeScript-API probe over
packages/mcp/dist/index.d.tsresolving export aliases throughchecker.getAliasedSymbol— 25 exports,MCPServerRuntime17 instance members with modifiers. The modifiers are the part my ruling did not have.How the error happened: I ruled from the card's member list, which was a text read of the
.d.tsincluding private members. I read a list of names and treated it as a list of callable names.That is worth stating plainly, because of what this card is: #9579 exists because a README documents members a consumer cannot call. My ruling would have had the fix document another member a consumer cannot call — the same defect, new spelling, shipped inside something labelled a fix. The dev caught it and escalated instead of executing. That was the correct call and I would rather it happen this way every time than have a ruling followed off a cliff.
The ruling, restated correctly
The ruling's other two names were fine and both shipped: the bridge path (
bridgeTools/bridgeResources/bridgePrompts, all public) and the exported helpers.Explicitly rejected: making it public
Considered and declined.
registerToolFromDefinition(definition)takes oneAIToolDefinitionand registers one tool — publishing it re-opens exactly the per-item imperative dialect the four-facet triage rejected on measured-pull grounds. It would beregisterTool()under a longer name.The ruling's substance was shrink the docs to the API, do not grow the API to the docs. Adding public surface so a docs sentence reads literally inverts it. PR #9868 as merged is the whole answer; no follow-up work is owed.
Generated by Claude Code
docs(mcp): rewrite the published README to the shipped host-extension…