MCP Fusion/Protocol and runtime/Wire format

Wire format

Ask AI about Vinkius

The exact bytes a Presenter sends: the ordered text blocks, ui_passthrough XML, domain rules, action suggestions, the _select guillotine, embeds and TOON.

A tool call with a Presenter does not return JSON.stringify. It returns an ordered sequence of text blocks, each with a stable shape the agent (and any client UI) can parse. This page is the specification of that format.

The block sequence

ResponseBuilder.build() emits up to six blocks, always in this order:

#BlockShape
1datathe masked, serialized payload as JSON text
2UI blocksone <ui_passthrough> per block
3embedsraw blocks from child Presenters
4directives<llm_directives> list
5rules<domain_rules> list
6suggestions<action_suggestions> list

A UI block looks like this on the wire:

xml
<ui_passthrough type="echarts" title="Revenue by month" width="full" priority="1">
  echarts-fenced-content: the chart config as formatted JSON
</ui_passthrough>

The type, title, width (full, half, third) and priority attributes tell a client renderer what the fenced content is. ui.table and ui.list are markdown; ui.json is fenced json; ui.codeBlock keeps its language tag. There is no native table block type: the fence is the contract, and fence() is the single place that changes if MCP standardizes UI blocks.

The rules and suggestions blocks are plain bullet lists:

xml
<domain_rules>
- amount_cents is in CENTS. Divide by 100.
</domain_rules>
<action_suggestions>
- billing.remind: Send a payment reminder
</action_suggestions>

structuredContent is a separate system

The MCP 2.0 structuredContent field is not produced by Presenters. To emit it use successStructured(data), which sends the JSON text block for MCP 1.0 clients plus the machine-readable structuredContent field, and declare the shape on the wire with .withOutputSchema(schema). Presenters own the human-and-LLM text; successStructured owns the parsed-object channel. Do not confuse the two.

_select: the field guillotine

.enableSelect() adds a _select array parameter to every action of the tool, its enum being the union of root schema keys across all actions. When the agent passes _select, the data block is filtered to those keys. Everything else (UI blocks, rules, suggestions) still runs on the full object, so a client rendering the response for a human sees the whole picture while the model context carries only what it asked for. This is the cheapest token win on wide models, and it is opt-in per builder.

embed: relational composition

.embed('lines', InvoiceLinesPresenter) reads data.lines through a child Presenter and merges the child's blocks and rules into the parent response. The parent owns one entity; a graph of entities is a tree of Presenters, each with its own schema, redaction and limits.

TOON: descriptions in half the tokens

.toonDescription() and toonSuccess(data) use the pipe-delimited TOON format for uniform collections: repeated keys collapse into one header row and the array becomes a table of values. For a list of ten thousand identically-shaped items the token count is roughly half. The format is generated from the action schema, so the agent sees action|desc|required as layer one and the data as layer two.

The testing side door

Every Presenter response carries a non-enumerable symbol with { data, systemRules, uiBlocks }. @mcpfusion/testing reads the structured view through it instead of parsing the XML back, which is how result.data, result.systemRules and result.uiBlocks in Testing are exact. JSON.stringify never sees the symbol.

Implicit wrapping rules

A handler's return value is interpreted with one rule: branded ToolResponse objects pass through untouched, everything else is wrapped as success(data). A null or undefined return becomes the text OK. This is why you never hand-build { content: [...] } shapes, and why adding a Presenter never requires touching the handler.

Next steps