Conversation
Call-side display templates (tool-level via namespaced _meta, branch-scoped inside oneOf/anyOf subschemas) and result-side rendered display text on content-block _meta. Includes normative rendering semantics, security implications (call-side omission indicator with const-discriminator carve-out), and a reference implementation: https://github.com/maxiboch/soundboard
…ated open questions
The draft argued from first principles that field descriptions are the wrong
channel for human-facing display. That argument holds, but it was unsupported:
the only evidence cited was OpenAI's Apps SDK `_meta` keys, which is one
vendor's convention.
Surveying three open-source agentic CLI clients by reading their rendering code
turned up the same shape in all of them, and it is a stronger argument than the
abstract one:
- goose dispatches on tool NAME to hand-written renderers for shell, write,
edit, execute_code and delegate; everything else falls through to a generic
key-and-value dump.
- gemini-cli defines the mechanism outright. `ToolInvocation.getDescription()`
returns a markdown string describing what the tool will do, with an optional
`getDisplayTitle()` for the UI, and built-ins override both. MCP-discovered
tools reach neither: `DiscoveredMCPToolInvocation.getDescription()` returns
`safeJsonStringify(this.params)`.
- Codex renders `server.tool(args)` as compact JSON, except for Node REPL
servers, where it reads a `title` out of the arguments and shows that
instead, capped at 80 graphemes.
Three incompatible in-house mechanisms, and in each the bespoke path is
reachable only by first-party tools. Codex's carve-out is the closest prior art
and is this proposal in miniature: a display hint, supplied by the caller,
honoured by the client, available to exactly one blessed server.
It also answers the field-descriptions objection more directly than the
Rationale did. None of these clients asks a model to narrate its tool calls;
they render deterministically, client-side, off data they already hold. The
question is not whether this rendering should happen, since it already does. It
is whether the server that owns the encoding gets any say in it.
Claude Code shows the same behaviour but is deliberately left out of the spec
text: it is closed source, so the claim could not be tied to a citation.
The previous commit surveyed three clients as prose. Two more open-source
harnesses were readable and had been skipped, so the evidence was thinner than
it needed to be. The survey is now a table of six, which is also easier to check
than a paragraph:
- opencode maps tool names to `run` functions in `TOOL_RULES`; unmapped tools
hit `fallbackInline()` and render `⚙ <name> <JSON.stringify(input)>`.
- cline switches on tool name in `formatApprovalParams()` across nine cases and
defaults to `JSON.stringify(rawInput, null, 2)` truncated to 200 characters,
with no case for `use_mcp_tool`.
Claude Code is included as a row but marked in the prose as read off observed
output, since it is closed source and the claim cannot be tied to a citation the
way the other five can.
opencode joins Codex as prior art that stops just short: its fallback prefers a
`state.title` over the JSON dump whenever one is present, and nothing in MCP
lets a server put one there. Two clients have now built partial versions of this
mechanism without a protocol path to it.
Adds the decision record CONTRIBUTING asks for, because this changes what the
proposal argues. Not that clients ought to render tool calls legibly, which
invites a debate about whether it is worth doing, but that they already do,
privately and incompatibly, and MCP servers are the only participants excluded.
The survey also raised an open question that review had not: gemini-cli needed
two strings where this draft defines one, a description of the call and a
shorter title for the UI, and Codex caps its title at 80 graphemes. Logged in
docs/open-questions.md and mirrored into the draft.
google-gemini/gemini-cli#23018 closed on 2026-09-15: raw JSON was leaking into a JetBrains approval dialog, so users could not read what they were approving. The fix (#23179) replaced safeJsonStringify with three new functions, and the shape of them is this proposal in miniature. formatToolDescription renders toolName(key: value, …). formatToolDisplayTitle returns a bare command parameter unwrapped, the comment says, so IDE clients can syntax-highlight it. extractToolExplanation peels conversational text out into separate ACP content blocks. That is a dual-audience split: one string for the human reading the approval, another for the explanation. Built client-side, by hand, because nothing carries it over the wire. And it works by guessing. CONVERSATIONAL_PARAM_KEYS is a hardcoded set -- description, explanation, reason, thought -- deciding which of a servers arguments are prose, and a hardcoded check for a parameter literally named command decides which one is executable. A server whose field is called note, or cmd, or rationale gets the wrong answer and has no way to say so. The client is inferring semantics from spelling. This strengthens the survey rather than dating it. The third_party_parity cell stays unsupported: a server still cannot influence its own rendering. gemini-cli just picked a better default than raw JSON, and had to invent a private convention to do it. The table citation is corrected too -- it pointed at safeJsonStringify(this.params), which no longer exists on main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposes a new experimental extension per CONTRIBUTING's "Proposing a
new extension": display strings for tool calls & results, solving the
dual-audience problem (compact encodings for the model, one legible
line for the human) raised in modelcontextprotocol#930 and discussed
in the #tool-annotations-ig thread "proposal for call-side string
templates".
What's in the draft:
templateunder_meta["io.modelcontextprotocol/display-templates"]on
Tool, with branch-scoped templates insideoneOf/anyOfsubschemas for one-tool-many-actions shapes
texton content-block_meta(no templating;the server knows the outcome)
...formissing/non-scalar, raw fallback, first-matching-branch
reachable, unreferenced args SHOULD be visibly counted (const
discriminators exempt), destructive confirmations SHOULD show raw
Reference implementation (runs on stock Python SDK 2.x):
https://github.com/maxiboch/soundboard
Prior discussion: #tool-annotations-ig, with guidance from
@SamMorrowDrums to target this repo as an Extensions Track proposal.
Seeking a sponsor. Open questions are listed in the draft (branch-key
spelling per SEP-2356, result-level vs per-block text, inner key
naming).