When oecd_query_dataset spills to DataCanvas it returns canvas_id, table_name, and truncated: true in structuredContent, but no field in that payload names the tools that read the staged table. The only text that names one is the format() note, which lands in content[]. A client that forwards structuredContent therefore hands the model an opaque canvas handle and no route to the rows behind it — the likely outcomes are reporting the truncation and stopping, or re-running the query with a narrower key to refetch data already staged one SQL call away.
Related: #7, #11
Proposal
Put the pointer where the token is. Emit a ctx.enrich notice on the spill path naming both dataframe tools — oecd_dataframe_describe first, since column names are needed before valid SQL can be written — and name them in the table_name output description as well. ctx.enrich merges into structuredContent and appends a trailer to content[], so a single call covers both surfaces; format() covers only one.
Where the pointer is missing today
Line numbers are src/mcp-server/tools/definitions/query-dataset.tool.ts at 0.3.0.
| Layer |
Where |
Status |
| Runtime response |
spill return, lines 390–403 |
Missing — no ctx.enrich call on this path. The enrichment block (lines 172–187) declares only content_table_capped and content_table_rows, both set on the inline path (line 415), so nothing is emitted when a canvas handle is actually minted |
Output schema — canvas_id |
lines 149–156 |
Present — names oecd_dataframe_query and oecd_dataframe_describe |
Output schema — table_name |
lines 157–160 |
Missing — "Canvas table name holding the full result — present when canvas_id is set" names neither tool |
| Tool description |
line 85 |
Partial — "follow up with oecd_dataframe_query" omits oecd_dataframe_describe, so the describe-then-query order is not plannable from tools/list |
oecd_query_dataset is the only tool that can emit a canvas handle, and only on the spill branch — spillover() registers a table exclusively when the source exceeds the preview budget, so the inline branch stages nothing. oecd_search_datasets, oecd_get_dataset_info, oecd_get_dimension_values, and oecd_list_agencies never touch the canvas. oecd_dataframe_describe and oecd_dataframe_query are the destination, not a source.
Proposed behavior
// enrichment block — add alongside content_table_capped / content_table_rows
notice: z
.string()
.optional()
.describe(
'Present when the result was staged on DataCanvas — names the staged table and the tools that read it.',
),
// spill path, before the early return
ctx.enrich({
notice:
`Staged ${result.handle.rowCount} rows to table "${result.handle.tableName}" — use ` +
'oecd_dataframe_describe to inspect columns, then oecd_dataframe_query to analyze the ' +
'full set with SQL.',
});
// table_name output .describe()
'Canvas table name holding the full result — present when canvas_id is set. Use as the FROM ' +
'target in oecd_dataframe_query SQL; oecd_dataframe_describe lists its columns.',
The tool description gains oecd_dataframe_describe next to the existing oecd_dataframe_query mention, so the two-call order is visible before the tool is ever called.
notice is already the established enrichment field for this in the server — oecd_get_dimension_values declares it and calls ctx.enrich.notice(...) for its paging and no-codelist cases.
Scope
src/mcp-server/tools/definitions/query-dataset.tool.ts — enrichment block, spill return path, table_name output .describe(), tool description
Out of scope
- Spillover thresholds, the preview character budget, table naming, and TTLs
- The inline path, which stages nothing and already explains itself through
content_table_capped / content_table_rows
- Adding canvas staging to tools that don't have it
Alternatives considered
Leaving the pointer in format() alone. It reaches content[] only, and the guidance needs to travel with the token on the surface where the token is machine-readable. Widening the canvas_id description further was also considered and is not enough on its own — static schema prose is read when the model plans a call, not necessarily when it reads a result.
When
oecd_query_datasetspills to DataCanvas it returnscanvas_id,table_name, andtruncated: trueinstructuredContent, but no field in that payload names the tools that read the staged table. The only text that names one is theformat()note, which lands incontent[]. A client that forwardsstructuredContenttherefore hands the model an opaque canvas handle and no route to the rows behind it — the likely outcomes are reporting the truncation and stopping, or re-running the query with a narrower key to refetch data already staged one SQL call away.Related: #7, #11
Proposal
Put the pointer where the token is. Emit a
ctx.enrichnotice on the spill path naming both dataframe tools —oecd_dataframe_describefirst, since column names are needed before valid SQL can be written — and name them in thetable_nameoutput description as well.ctx.enrichmerges intostructuredContentand appends a trailer tocontent[], so a single call covers both surfaces;format()covers only one.Where the pointer is missing today
Line numbers are
src/mcp-server/tools/definitions/query-dataset.tool.tsat 0.3.0.ctx.enrichcall on this path. Theenrichmentblock (lines 172–187) declares onlycontent_table_cappedandcontent_table_rows, both set on the inline path (line 415), so nothing is emitted when a canvas handle is actually mintedcanvas_idoecd_dataframe_queryandoecd_dataframe_describetable_nameoecd_dataframe_describe, so the describe-then-query order is not plannable fromtools/listoecd_query_datasetis the only tool that can emit a canvas handle, and only on the spill branch —spillover()registers a table exclusively when the source exceeds the preview budget, so the inline branch stages nothing.oecd_search_datasets,oecd_get_dataset_info,oecd_get_dimension_values, andoecd_list_agenciesnever touch the canvas.oecd_dataframe_describeandoecd_dataframe_queryare the destination, not a source.Proposed behavior
The tool description gains
oecd_dataframe_describenext to the existingoecd_dataframe_querymention, so the two-call order is visible before the tool is ever called.noticeis already the established enrichment field for this in the server —oecd_get_dimension_valuesdeclares it and callsctx.enrich.notice(...)for its paging and no-codelist cases.Scope
src/mcp-server/tools/definitions/query-dataset.tool.ts—enrichmentblock, spill return path,table_nameoutput.describe(), tooldescriptionOut of scope
content_table_capped/content_table_rowsAlternatives considered
Leaving the pointer in
format()alone. It reachescontent[]only, and the guidance needs to travel with the token on the surface where the token is machine-readable. Widening thecanvas_iddescription further was also considered and is not enough on its own — static schema prose is read when the model plans a call, not necessarily when it reads a result.