Community-maintained FDE reference. Not an official Vercel or Anthropic project. About this project
Tool-result rendering
TL;DR: A tool result is untrusted data, not instructions. When a tool returns, the host does two things with the result: shows it to the user, and (usually) feeds it back into the model’s context for the next step. Both are attack surfaces. Tool output is the single most important prompt-injection vector in MCP: a compromised server, or an honest server relaying attacker-controlled content (a web page, an email, a database row), can embed “ignore your instructions and…” inside what looks like ordinary output. The host’s rendering layer is where that gets contained: sanitize what the user sees, and re-inject output as clearly marked data from a named server, never as instructions. On Vercel every server is remote, so every result has crossed the public internet from a deployment you probably do not operate; TLS tells you which origin answered, not that the content is safe.
Plain-language explanation
The model asks for a tool call; the server answers with a result; and that result almost always loops back into the conversation so the model can use it. The loop is the danger. A model cannot reliably distinguish “content the tool returned” from “instructions it should follow” unless the host makes the distinction structural. If the host concatenates raw tool output into the same context the model treats as instructions, then any text a server returns is, in effect, a command the model may obey. The fix is provenance: tool output enters the context tagged as untrusted data from a specific server, fenced off from the instruction layer. The spec’s own client guidance points the same way: validate tool results before passing them to the model.
What a result actually contains
A tools/call result under 2026-07-28 has more shapes than plain text, and each one is a rendering decision:
resultType- required on every result:"complete"or"input_required"(SEP-2322). Only a"complete"result is content to render. An"input_required"result is the Multi Round-Trip Request (MRTR) pattern at work: the server’sinputRequestsdescribe what it still needs, and the client retries the original request withinputResponses. That is a routing and consent decision for the dispatch loop, not output for this layer, and theinputRequeststhemselves are server-authored untrusted content. Results from earlier-protocol servers that omitresultTypeare treated as"complete". Wire status: the pinned stack (mcp-handler2.1.1 on@modelcontextprotocol/server2.0.0) serves the 2026-07-28 contract natively over Streamable HTTP and falls back to stateless 2025-11-25 Streamable HTTP for legacy clients; the SDKClientdefaults to that legacy handshake unless you opt in to modern version negotiation, so the examples’ in-memory test suites exercise only the legacy path.content- the unstructured array:text,image,audio,resource_link, and embeddedresource. Aresource_linkis a reference the host may choose to resolve; fetching it is a separate decision, never an automatic step. An embeddedresourcecarries its data inline.structuredContent- structured JSON output, and since 2026-07-28 it may be any JSON value, not only an object (SEP-2106). If the tool declares anoutputSchema, the server MUST conform to it and the client SHOULD validate against it: a typed, safer path than parsing free text. Schemas may now use any JSON Schema 2020-12 keywords, with defined$refresolution requirements and resource bounds on composition keywords, and the default dialect when no$schemafield is present is still JSON Schema 2020-12, so validate with a full 2020-12-capable validator.isError- optional, defaults to false. An error result is still server-controlled content: render it as an error with its provenance visible, but do not treat its text as authoritative or let it steer control flow unexamined. Under 2026-07-28, input-validation failures also arrive this way, as Tool Execution Errors (isError: trueresult content) rather than JSON-RPC protocol errors, so the model can read the message and self-correct; an unknown tool name, by contrast, is a protocol error, not a result. From the rendering layer’s side that means an “invalid arguments” failure is ordinary result content: useful to the model, still untrusted to you.
The two destinations of a result
Diagram source (Mermaid)
flowchart TB
s["Tool result: resultType, content, structuredContent, isError"] --> rt{"resultType?"}
rt -- input_required --> loop["Back to the dispatch loop and consent gate, never rendered as output"]
rt -- complete --> host["Host rendering layer"]
host --> disp["Display to user: sanitized, provenance shown"]
host --> ctx["Back into model context: tagged untrusted output from server X"]
host -. never .-> instr["Instruction / system layer"]The dashed line is the boundary that must not be crossed: server output never flows into the instruction or system layer. It reaches the model only as labeled data, and it reaches the user only after sanitizing. The input_required branch exits before rendering at all: those results belong to the dispatch loop and the consent gate, and their inputRequests deserve the same suspicion as any other server-authored text.
Rendering safely to the user
- Escape before display. Strip or escape ASCII control characters, and neutralize UI-injection paths: raw HTML or markdown from a tool must not render as live markup, scripts, or auto-loading images in the host UI.
- Show provenance. The user should see which server produced the content (the namespaced origin), so “your bank says…” cannot be spoofed by an unrelated server in a composed surface.
- Don’t auto-act on references. A
resource_linkis a pointer. Resolving it, following a URL, or rendering a remote image is a network fetch the host decides on, not a default; if the host is itself a Vercel Function, it is also uncontrolled egress from your infrastructure. - Treat icon metadata as untrusted too. Servers can attach icons to tools, resources, and prompts. An icon makes a tool feel legitimate in the UI, which is exactly why it is a spoofing surface: a remote icon URL is an unconsented fetch (and a tracking beacon) like any other remote image, and an icon must never let a server impersonate the host’s own chrome or another server’s branding. Render icons next to the namespaced provenance, not in place of it; fetch and cache through the host rather than hot-linking; constrain size and type.
Re-injecting into model context
- Mark it as data. Wrap tool output in a clear, consistent envelope identifying it as untrusted output from a named server, structurally distinct from system and user turns.
- Prefer structured output. When a tool provides
structuredContentunder a declaredoutputSchema, validate it (JSON Schema 2020-12 by default) and pass typed fields rather than free text. There is far less room for an instruction to hide in a validated number than in a paragraph. Remember the value can now be any JSON shape the schema declares, so validate the shape you were promised rather than assuming an object. - Minimize at both ends. Output minimization is the server’s half: return only what the tool contract promises. The host defends in depth: even a minimal result is untrusted on arrival, because the host cannot verify what discipline a remote deployment actually applied.
Common pitfalls
- Concatenating tool output into the instruction or system context - the canonical injection path; output must enter as fenced, attributed data.
- Rendering an
input_requiredresult as output. ItsinputRequestsare a server’s request for another round trip, not content; displaying them as an answer, or worse feeding them to the model as instructions, hands the injection layer exactly the channel it wants and skips the consent gate the retry must pass. - Auto-fetching
resource_links or auto-rendering remote content - turns a reference into an unconsented network call or a UI-injection surface. - Rendering raw HTML or markdown from a tool as live markup - script and image-beacon injection into the host UI.
- Trusting
isErrortext. An error message is still server-supplied; surface it, don’t obey it. This includes input-validation failures, which arrive as Tool Execution Errors so the model can self-correct: handy for the model, still untrusted for the renderer. - Treating
structuredContentas validated without checkingoutputSchema- structure is only a safety gain if you actually run the validator. - Rendering an
isErrorresult as success - a failure that reads like a result lets a server smuggle content past every gate that only watches the happy path.
Example implementation
The server examples demonstrate the sending half of output trust, the discipline the host complements on arrival, and the host example shows the receiving half’s error handling:
examples/secure-tools-server(in the repository) - output minimization: its write tool returns only what the contract promises (a count, a status), never internal identifiers or tokens that a downstream injection could harvest.examples/db-adapter-server(in the repository) - per-row output sanitization at the adapter boundary: an internal-only column is dropped and control characters are escaped on every row before anything leaves the server, with tests asserting the redaction actually removes.examples/orchestrator-host(in the repository) - the host half in miniature: a server result withisError: trueis surfaced as a typed error in the host, never as a success the model can build on.
A full host-side rendering layer (provenance tagging plus UI sanitization plus structured-output validation) is not yet a runnable example in this repository; it is a natural future addition alongside the orchestrator host.
Related
- trust-boundaries - tool output as the primary prompt-injection vector, untrusted regardless of which upstream produced it.
- adapter - treating a backend’s responses as untrusted before they ever leave the server.
- Consent UX - the approval gate before a call and around every
input_requiredretry; this page is the discipline after acompleteresult returns. - capability primitives - the protocol definition of result content,
structuredContent, andoutputSchema. - Output trust - the checklist items this page expands.
Bibliography
- Model Context Protocol Specification, Tools (content types, structuredContent, outputSchema, isError, tool execution errors, icons, JSON Schema 2020-12 default), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol Specification, Multi Round-Trip Requests (
resultType,inputRequests), version 2026-07-28 - https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr - Model Context Protocol Documentation, Security Best Practices - https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices
- OWASP Top 10 for Large Language Model Applications (LLM01: Prompt Injection) - https://owasp.org/www-project-top-10-for-large-language-model-applications/