What problem does this solve?
apps/mcp's generated tool catalog expresses object-only params — comment.target, edit.target, delete_block_range start/end — as $refs into #/$defs/… (often wrapped in nested oneOf unions), but emits no $defs definitions block and no inline type:"object" on the param. Verified against packages/sdk/tools/catalog.json @ current main:
#/$defs/… references: 94
"$defs": definition containers: 0
So every $ref is dangling (e.g. #/$defs/BlockNodeAddress, #/$defs/SelectionTarget, #/$defs/TextAddress are referenced but never defined), and the param schema carries no resolvable type.
For an MCP client that normalizes/strips unknown or unresolvable JSON-Schema keywords before serializing tool-call arguments — Claude Code does exactly this — a param with no top-level type (object/array) is treated as untyped and JSON-stringified. The object value {kind:"text", blockId, range} reaches the server as the string "{\"kind\":\"text\",…}". Server-side validateCommentTarget → isRecord(target) returns false for a string, so every documented target shape fails identically with:
target must be a TextAddress, TextTarget, SelectionTarget, or tracked-change target
How it was hit / proof it's transport, not shape
- Three structurally-distinct valid targets —
{kind:"text",blockId,range} (TextAddress), the segments form (TextTarget), {kind:"selection",start,end} (SelectionTarget) — all rejected with the identical error. A genuine shape error would distinguish them.
superdoc_edit with a valid object target ({kind:"block",nodeType,nodeId}) fails the same way — same type-less-param stringify.
- Counter-proof — params with a top-level type transport intact:
search.select (type:"object") works; mutations.steps (type:"array") preserves its nested object selectors; ref works because it's already a string. Only object-only params declared via $ref/oneOf-without-type break.
- The validator source accepts the documented shapes — the server is fine; the break is schema-emission / transport.
Root cause (generator)
The catalog is // Auto-generated from packages/sdk/tools/catalog.json by packages/sdk/codegen/src/generate-intent-tools.mjs. The contract→JSON-Schema lowering keeps $ref/oneOf references to named Address/Target/Locator/Range schemas but never emits the corresponding $defs block, and adds no inline type:"object" fallback. The result is a schema that is correct-by-reference but unusable by any client that can't resolve the (absent) definitions.
Proposed solution (durable fix, generator-side)
Either of these makes the object-ness self-describing without relying on $defs resolution:
- Emit the
$defs block — serialize the referenced contract schemas (BlockNodeAddress, TextAddress, TextTarget, SelectionTarget, DeletableBlockNodeAddress, DeleteBehavior, …) into a $defs map on each tool's inputSchema (or once at catalog root) so the 94 dangling refs resolve. Robust clients then see the object type through the ref.
- Inline
type:"object" on object-only params (simpler, lossy but sufficient) — when a param is an object-only $ref/all-object oneOf union, stamp type:"object" (optionally inline the resolved properties) so even clients that strip $ref/oneOf keep the type. Same idea for the array params already carrying type:"array".
(1) is the fuller fix; (2) is a one-line lowering tweak that unblocks the stringify regression immediately. They compose.
Current workaround (client/fork-side, not a durable fix)
I shimmed this in a fork's apps/mcp/src/tools/intent.ts (isObjectOnlySchema() recurses oneOf/anyOf/allOf and resolves $refs heuristically by the /(Address|Target|Locator|Range)$/ naming convention, emitting z.looseObject({}) → type:"object"). It restores end-to-end comment/edit-by-object-target on Claude Code (live-verified: comment create + list round-trip writes to word/comments.xml). But it's a downstream band-aid keyed on a naming convention — the catalog should emit resolvable/typed schemas so no client needs the heuristic.
Additional context
- Affects any MCP client that doesn't carry a
$ref resolver and strips unrecognized keywords; clients that pass schemas through verbatim are unaffected, which is why this hid for a while.
- The 10-tool catalog's object-only params are the blast radius; string/enum/number params and
type:"array" params are unaffected.
Environment: apps/mcp run from source via bun run --conditions source; catalog inspected at packages/sdk/tools/catalog.json @ current main.
What problem does this solve?
apps/mcp's generated tool catalog expresses object-only params —comment.target,edit.target,delete_block_rangestart/end— as$refs into#/$defs/…(often wrapped in nestedoneOfunions), but emits no$defsdefinitions block and no inlinetype:"object"on the param. Verified againstpackages/sdk/tools/catalog.json@ currentmain:#/$defs/…references: 94"$defs":definition containers: 0So every
$refis dangling (e.g.#/$defs/BlockNodeAddress,#/$defs/SelectionTarget,#/$defs/TextAddressare referenced but never defined), and the param schema carries no resolvable type.For an MCP client that normalizes/strips unknown or unresolvable JSON-Schema keywords before serializing tool-call arguments — Claude Code does exactly this — a param with no top-level
type(object/array) is treated as untyped and JSON-stringified. The object value{kind:"text", blockId, range}reaches the server as the string"{\"kind\":\"text\",…}". Server-sidevalidateCommentTarget→isRecord(target)returnsfalsefor a string, so every documented target shape fails identically with:How it was hit / proof it's transport, not shape
{kind:"text",blockId,range}(TextAddress), thesegmentsform (TextTarget),{kind:"selection",start,end}(SelectionTarget) — all rejected with the identical error. A genuine shape error would distinguish them.superdoc_editwith a valid objecttarget({kind:"block",nodeType,nodeId}) fails the same way — same type-less-param stringify.search.select(type:"object") works;mutations.steps(type:"array") preserves its nested object selectors;refworks because it's already a string. Only object-only params declared via$ref/oneOf-without-typebreak.Root cause (generator)
The catalog is
// Auto-generated from packages/sdk/tools/catalog.jsonbypackages/sdk/codegen/src/generate-intent-tools.mjs. The contract→JSON-Schema lowering keeps$ref/oneOfreferences to named Address/Target/Locator/Range schemas but never emits the corresponding$defsblock, and adds no inlinetype:"object"fallback. The result is a schema that is correct-by-reference but unusable by any client that can't resolve the (absent) definitions.Proposed solution (durable fix, generator-side)
Either of these makes the object-ness self-describing without relying on
$defsresolution:$defsblock — serialize the referenced contract schemas (BlockNodeAddress,TextAddress,TextTarget,SelectionTarget,DeletableBlockNodeAddress,DeleteBehavior, …) into a$defsmap on each tool'sinputSchema(or once at catalog root) so the 94 dangling refs resolve. Robust clients then see the object type through the ref.type:"object"on object-only params (simpler, lossy but sufficient) — when a param is an object-only$ref/all-objectoneOfunion, stamptype:"object"(optionally inline the resolvedproperties) so even clients that strip$ref/oneOfkeep the type. Same idea for the array params already carryingtype:"array".(1) is the fuller fix; (2) is a one-line lowering tweak that unblocks the stringify regression immediately. They compose.
Current workaround (client/fork-side, not a durable fix)
I shimmed this in a fork's
apps/mcp/src/tools/intent.ts(isObjectOnlySchema()recursesoneOf/anyOf/allOfand resolves$refs heuristically by the/(Address|Target|Locator|Range)$/naming convention, emittingz.looseObject({})→type:"object"). It restores end-to-end comment/edit-by-object-target on Claude Code (live-verified:comment create+listround-trip writes toword/comments.xml). But it's a downstream band-aid keyed on a naming convention — the catalog should emit resolvable/typed schemas so no client needs the heuristic.Additional context
$refresolver and strips unrecognized keywords; clients that pass schemas through verbatim are unaffected, which is why this hid for a while.type:"array"params are unaffected.Environment:
apps/mcprun from source viabun run --conditions source; catalog inspected atpackages/sdk/tools/catalog.json@ currentmain.