What does an MCP tools/call return, and when is structuredContent required?
answer
- Two channels: readable and typed
- outputSchema is what makes it mandatory
- resource_link for payloads too big to inline
- Mirror the JSON into a text block
- Call results carry no ttlMs
basics
~20 sA tools/call result carries a content array of blocks — text, image, audio, resource_link or embedded resource — plus an optional isError flag. If the tool declares an outputSchema, the result must also carry structuredContent conforming to that schema.
solid answer
~40 s`tools/call` answers with a `CallToolResult`. Its `content` field is an array of content blocks meant to be read: `text`, `image`, `audio`, `resource_link` (a URI pointing at a resource the client can fetch) and embedded `resource` blocks. That array is what typically flows back into the model's context. Separately, a tool may declare an `outputSchema` in its `tools/list` entry; when it does, the result MUST include `structuredContent` — a JSON object validating against that schema — so that programmatic consumers get typed data instead of parsing prose. Servers commonly also mirror the same JSON into a text block for clients that only read `content`. In MCP revision 2026-07-28 the result additionally carries the required `resultType`, `"complete"` for an ordinary result. `tools/call` results are not cacheable — they carry no `ttlMs`/`cacheScope`, unlike `tools/list`.
code
json · 11 lines{
"jsonrpc": "2.0",
"id": 21,
"result": {
"resultType": "complete",
"content": [
{ "type": "text", "text": "{\"temperatureC\":14.5,\"conditions\":\"overcast\"}" }
],
"structuredContent": { "temperatureC": 14.5, "conditions": "overcast" }
}
}go deeper
Know that a tool result comes back as a content array of blocks such as text or image, and that isError marks a failed execution.
Explain that structuredContent is required exactly when the tool's tools/list entry declares an outputSchema, and that the same JSON is usually mirrored into a text block.
Show payload judgment: link large artefacts with resource_link, keep content lean because it lands in the context window, and never fake a schema-valid structured result on the error path.
Own output contracts across servers — which tools promise an outputSchema, how those schemas version, and the token-cost budget of tool output feeding back into every agent turn.
## The two channels of a tool result A `CallToolResult` has two payload channels that answer two different questions. **`content`** — an ordered array of content blocks, the *readable* channel. This is what the host feeds back into the conversation as the tool's output. Block types include: - `text` — plain text; by far the most common. - `image` and `audio` — base64 data plus a `mimeType`. - `resource_link` — a URI naming a resource the client can then fetch with `resources/read`, used when the payload is large or the client may not need it. - an embedded `resource` — the resource's contents inlined directly into the tool result. **`structuredContent`** — the *typed* channel: a single JSON object intended for programmatic consumption by the host, not primarily for reading. Plus the flag **`isError`**, which marks the result as a tool execution failure while remaining a successful JSON-RPC result. ## When structuredContent is required It is driven entirely by the tool definition. A `Tool` entry in `tools/list` may declare an `outputSchema` — a JSON Schema object, exactly parallel to `inputSchema`. If a tool declares one, its results MUST include `structuredContent` that validates against that schema, and clients SHOULD validate what they receive. If a tool declares no `outputSchema`, `structuredContent` is optional and most tools simply return content blocks. Declaring an `outputSchema` is a promise, so declare it only when the shape really is stable. The payoff is real: the host can bind fields to UI, downstream code can consume typed values without regex-scraping prose, and a mismatch is detectable rather than silently mis-parsed. For compatibility, a server returning `structuredContent` typically also serialises the same JSON into a `text` block in `content`, because a client that only knows how to read content blocks would otherwise see an empty tool output. That duplication is intentional and cheap. ## Choosing what to put where - Returning a paragraph of prose the model should reason about? `content` with a `text` block. - Returning rows of records the host will render as a table? `structuredContent` with an `outputSchema`, mirrored as text. - Returning a 40 MB CSV? A `resource_link` block, so the client decides whether to pull it. Inlining it burns context for everyone. - Returning a chart the user should see? An `image` block. The deciding question is who consumes it. Everything in `content` is heading for the model's context window, so size discipline there is real engineering, not tidiness. ## Errors and the structured channel On a failure the server sets `isError: true` and explains in `content`. It should not fabricate a `structuredContent` value that happens to satisfy `outputSchema`, because that makes a failure indistinguishable from a real result to any consumer that only validates the schema. ## Revision 2026-07-28 specifics Every result in this revision carries a required `resultType`. An ordinary tool result is `resultType: "complete"`; clients MUST treat its absence — which happens when talking to an older server — as `"complete"`. A tool that cannot finish without more input from the client instead returns the interim `"input_required"` result type, and a tool running under the `io.modelcontextprotocol/tasks` extension may return `"task"`. Both are separate mechanisms; the ordinary success path is `"complete"`. Note also what a tool result does **not** carry: `ttlMs` and `cacheScope`. Those belong to `CacheableResult`, which `ListToolsResult` extends but `CallToolResult` does not. Listing your tools is cacheable; invoking one is not — an invocation has side effects and a moment-in-time answer, and caching it would be wrong by default. ## A worked shape A weather tool with an `outputSchema` of `{ temperatureC: number, conditions: string }` returns `structuredContent: { "temperatureC": 14.5, "conditions": "overcast" }` and, alongside it, a text block reading `{"temperatureC":14.5,"conditions":"overcast"}` or a friendlier `14.5 °C, overcast`. The host binds the structured values to a widget; the model reads the text. ## What interviewers listen for The strong answer names both channels, ties `structuredContent` to the *tool's declared* `outputSchema` rather than to a client request, mentions `resource_link` as the escape hatch for large payloads, and knows that a tool result is not a cacheable result.
- Why do servers duplicate structuredContent into a text content block?Compatibility and reach. A client that only renders `content` — including older ones — would otherwise show an empty tool result, and the model itself reads content blocks, not the structured channel. Serialising the same JSON into a text block costs a few tokens and guarantees both consumers see the answer. Nothing forbids a friendlier prose rendering there instead of raw JSON.
- Should a tool return a resource_link or an embedded resource?Embed when the payload is small and the model almost certainly needs it — inlining saves a round trip. Link when it is large, optional, or better handled by the host: the client can decide whether to call `resources/read` at all, and the model's context is not spent on bytes nobody reads. Large inline blobs are the most common cause of blown context in tool-heavy sessions.
- Can a tools/call result be cached by the client the way tools/list can?No. `ListToolsResult` extends `CacheableResult` and therefore carries the required `ttlMs` and `cacheScope`; `CallToolResult` does not. An invocation may have side effects and reflects a moment in time, so MCP gives no protocol-level cache directive for it. Any memoisation is an application-level decision the server has not sanctioned.
saying these in an interview costs you the question
- Thinking the client asks for structuredContent per call
- Returning structuredContent that ignores the declared outputSchema
- Inlining megabytes of data instead of using resource_link
- Fabricating a schema-valid structured result on failure
- Assuming a tools/call result carries ttlMs and cacheScope