Skip to main content

Results

What a tool answers, and where a client should read it.

Until 2.1 a tool answered in prose. list_kernels returned a tab-separated table, and an agent wanting one field out of it split the text on tabs and hoped the columns had not moved. When they moved, nothing failed — the agent read a different column and carried on. Prose is a fine thing to show a model and a poor thing to make it parse.

Every result now carries both.

Both halves, always

A result has content — the text, and any images — and structuredContent beside it. Neither replaces the other:

  • Text is never dropped. A result with structure and empty content is invisible to a client that has not adopted structuredContent, which in practice means most of them.
  • Structure is never a rendering of the text. It is built from what the tool actually returned, in one place, before anything is turned into a string.

What is in structuredContent

Two keys, in the ordinary case:

{
"kind": "cell.delete",
"result": "Cell 3 deleted."
}

kind says what this result is, so a client can tell one answer from another without matching prose. It is always present. result holds the answer — a message, the rows of a listing, or the outputs of an execution, in order.

There is one exception worth knowing before you write a client. A tool whose answer is already an object carries that object's keys at the top level, and has no result:

{
"kind": "sandbox.launch",
"sandbox": { "id": "s-1", "url": "..." }
}

Putting such an answer under result as a string would hand you JSON to parse where you previously had an object — the exact failure this exists to prevent. So read result when it is there, and treat its absence as "this tool answers with fields of its own", which the tool's output schema names.

A tool answering with a list of records keeps result, and what is under it is the list, not a rendering of it:

{
"kind": "sandboxes.list",
"result": [{ "id": "s-1" }, { "id": "s-2" }]
}

The text half still shows the JSON, because text is what most clients show the model. Only structuredContent is guaranteed to hold data rather than a picture of it.

Kinds

ToolkindShape
clear_cell_outputcell.clear_outputmessage
connect_to_jupyterjupyter.connectmessage
delete_cellcell.deletemessage
edit_cell_sourcecell.editmessage
execute_cellcell.executeoutputs
execute_codecode.executeoutputs
insert_cellcell.insertmessage
insert_execute_code_cellcell.insert_executeoutputs
launch_sandboxsandbox.launchobject
list_filesfiles.listrows
list_kernelskernels.listrows
list_notebooksnotebooks.listrows
list_sandboxessandboxes.listlist
move_cellcell.movemessage
overwrite_cell_sourcecell.overwritemessage
read_cellcell.readoutputs
read_notebooknotebook.readmessage
restart_notebooknotebook.restartmessage
terminate_sandboxsandbox.terminatemessage
unuse_notebooknotebook.unusemessage
use_notebooknotebook.usemessage
use_sandboxsandbox.usemessage

Listings come back as rows

A tab-separated table is parsed once, here, rather than by every client:

{
"kind": "kernels.list",
"result": "ID\tName\nk-1\tPython 3",
"columns": ["ID", "Name"],
"items": [{ "ID": "k-1", "Name": "Python 3" }],
"count": 1
}

result keeps the text so nothing that read it before breaks. Something that is not a table — "No notebooks are open." — stays a message, with no columns or items.

Executions come back as outputs

{
"kind": "cell.execute",
"result": ["before", { "type": "image", "mimeType": "image/png", "data": "..." }],
"outputs": ["...same list..."],
"count": 2,
"images": 1
}

Order is the cell's order. Text outputs stay strings and images stay objects — rendering a text output as an object breaks a caller reading cell sources, and dropping an image breaks one reading a plot. An image is dropped silently, because the text beside it still arrives, so images is there to be counted against what you expected.

Every tool advertises its schema

All 22 tools declare an output schema, so a client can learn the shape without calling the tool, and the generated API reference prints it on each tool's page. Three schemas cover every tool — the message shape above, the rows shape and the outputs shape — and each allows extra keys, which is what lets an object-answering tool carry its own.

The schema is not documentation written beside the code: the server validates what it built against it on every call, so the two cannot drift apart quietly.

_meta

Two things arrive in _meta, both namespaced because _meta is shared with the protocol and with every extension.

Cache hints, under io.modelcontextprotocol/cache (SEP-2549):

{ "io.modelcontextprotocol/cache": { "ttlMs": 30000, "cacheScope": "private" } }

Only the three listings carry one — list_files and list_notebooks at 30s, list_kernels at 10s. Nothing that changes carries one: a cell read or an edit answers something that has just moved, and a stale hint is worse than no hint. cacheScope is private, meaning one caller's answer, never to be shared by a proxy.

The cell this acted on, under io.jupyter-mcp/cell_id — see Cell ids.