The api-canvas skill requires that a server emitting a canvas_id also register dataframe_query / dataframe_describe. It says nothing about the emitting response naming them. A server can satisfy the checklist in full and still hand the agent an opaque 10-character token with no reachable next step.
That is the gap this proposes closing, in the skill that teaches the pattern.
Why registration alone is not enough
spillover() auto-names the staged table spilled_<8-hex>. An agent holding a canvas_id therefore needs dataframe_describe before it can write a single valid FROM clause — and nothing in the current guidance puts describe in front of query, or in the response at all.
The failure is quiet. An agent that gets truncated: true plus a token, with no pointer, either reports the truncation and stops, or re-calls the producing tool with a narrower filter — re-fetching rows already staged one query away. Neither path errors, so nothing surfaces it.
There is a second, sharper reason describe is load-bearing rather than polish: staged column names routinely differ from the inline preview's field names, because the rows registered to the canvas are a projection built for SQL (snake_case, flattened, joined arrays) while the preview reflects the output schema. SQL inferred from the preview references columns that do not exist.
What the skill currently teaches
The "minimum viable spillover server" example in skills/api-canvas/SKILL.md sets a reasonable canvas_id output .describe(), then returns from the spill path with no enrichment call. Copying it faithfully produces exactly the gap above — the pointer exists in tools/list prose, never in the payload.
The surface distinction is the part worth making explicit, because it is easy to get wrong while believing it is handled: a pointer written into a tool's format() reaches content[] only. A client forwarding structuredContent sees the token and nothing else. ctx.enrich.notice(...) reaches both surfaces in one call. The skill documents both mechanisms but never states which one a canvas pointer belongs in.
Proposal
Amend skills/api-canvas/SKILL.md:
-
Add a checklist line under the existing registration item:
- [ ] Every response that returns a `canvas_id` names the dataframe tools in the
response payload — `<prefix>_dataframe_describe` first (table and column
names), then `<prefix>_dataframe_query`. Schema `.describe()` prose is not
sufficient; use `ctx.enrich.notice(...)` so the pointer reaches
`structuredContent` and `content[]` alike.
-
Extend the spillover example's spill branch so the canonical snippet models it:
if (result.spilled) {
ctx.enrich.notice(
`Staged ${result.handle.rowCount} rows to table "${result.handle.tableName}" — ` +
`use <prefix>_dataframe_describe to inspect columns, then ` +
`<prefix>_dataframe_query to analyze the full set with SQL.`,
);
}
-
Note the last-wins constraint on notice where the example introduces it. A handler that already calls ctx.enrich.notice(...) on another branch (a degraded-fetch warning, an unparseable-label warning) will silently drop that warning if the spill path issues a second call — the two must be composed into one string.
Scope
skills/api-canvas/SKILL.md: checklist item, spillover example, and the notice last-wins caveat.
- The
Tools row of the simple-shape defaults table, which currently frames dataframe_describe as "required alongside" — it should also say the emitting response names it.
Out of scope
- Any runtime change to
spillover(), CanvasInstance, or the canvas providers. A framework-emitted notice would have to guess the consumer's tool prefix, and prefixes are server-owned — this belongs in guidance, not in the primitive.
- The
lint:mcp definition linter. Detecting "the response names the right tools" needs cross-referencing a handler's enrichment strings against the server's registered tool names; worth considering separately if the guidance alone does not hold.
The
api-canvasskill requires that a server emitting acanvas_idalso registerdataframe_query/dataframe_describe. It says nothing about the emitting response naming them. A server can satisfy the checklist in full and still hand the agent an opaque 10-character token with no reachable next step.That is the gap this proposes closing, in the skill that teaches the pattern.
Why registration alone is not enough
spillover()auto-names the staged tablespilled_<8-hex>. An agent holding acanvas_idtherefore needsdataframe_describebefore it can write a single validFROMclause — and nothing in the current guidance putsdescribein front ofquery, or in the response at all.The failure is quiet. An agent that gets
truncated: trueplus a token, with no pointer, either reports the truncation and stops, or re-calls the producing tool with a narrower filter — re-fetching rows already staged one query away. Neither path errors, so nothing surfaces it.There is a second, sharper reason
describeis load-bearing rather than polish: staged column names routinely differ from the inline preview's field names, because the rows registered to the canvas are a projection built for SQL (snake_case, flattened, joined arrays) while the preview reflects the output schema. SQL inferred from the preview references columns that do not exist.What the skill currently teaches
The "minimum viable spillover server" example in
skills/api-canvas/SKILL.mdsets a reasonablecanvas_idoutput.describe(), then returns from the spill path with no enrichment call. Copying it faithfully produces exactly the gap above — the pointer exists intools/listprose, never in the payload.The surface distinction is the part worth making explicit, because it is easy to get wrong while believing it is handled: a pointer written into a tool's
format()reachescontent[]only. A client forwardingstructuredContentsees the token and nothing else.ctx.enrich.notice(...)reaches both surfaces in one call. The skill documents both mechanisms but never states which one a canvas pointer belongs in.Proposal
Amend
skills/api-canvas/SKILL.md:Add a checklist line under the existing registration item:
Extend the spillover example's spill branch so the canonical snippet models it:
Note the last-wins constraint on
noticewhere the example introduces it. A handler that already callsctx.enrich.notice(...)on another branch (a degraded-fetch warning, an unparseable-label warning) will silently drop that warning if the spill path issues a second call — the two must be composed into one string.Scope
skills/api-canvas/SKILL.md: checklist item, spillover example, and thenoticelast-wins caveat.Toolsrow of the simple-shape defaults table, which currently framesdataframe_describeas "required alongside" — it should also say the emitting response names it.Out of scope
spillover(),CanvasInstance, or the canvas providers. A framework-emitted notice would have to guess the consumer's tool prefix, and prefixes are server-owned — this belongs in guidance, not in the primitive.lint:mcpdefinition linter. Detecting "the response names the right tools" needs cross-referencing a handler's enrichment strings against the server's registered tool names; worth considering separately if the guidance alone does not hold.