structuredContent never reaches the model
Structured Content vs Content: What MCP clients actually do with tool results
The companion to the server survey. That one asks what servers put on the wire; this one asks what clients forward to the model. The MCP spec gives a tool result two places for data — unstructured content and a structuredContent object. For tools with an outputSchema, servers MUST populate structuredContent — and SHOULD also duplicate it into a content text block for backwards compatibility. What a client then forwards to the model is left open — hence this survey.
content text dropped when structured is present
The same tool result, handled four ways
Take one tool that returns a dataset. Because it declares an outputSchema, it must populate structuredContent — and it also serializes the rows into content, so the payload sits in both fields. Here is the one response — then what each variant forwards to the model.
One server response
a dataset-returning tool · ~250 rows × 9 fields
{
"content": [
{ "type": "text",
"text": "[{\"id\":\"row-0001\",\"name\":\"Acme Corp\",\"category\":\"Enterprise\",
\"revenue\":48200000,\"headcount\":1200,\"ageMonths\":31,\"score\":78,
\"openTickets\":4,\"nps\":62}, … 249 more ]" }
],
"structuredContent": { "rows": [ { … same ~250 × 9 … } ] } // identical payload
}
Model receives the content text — the full ~250-row string. structuredContent is never sent.
Model receives JSON.stringify(structuredContent). The content text is dropped.
Model receives both content and structuredContent — the dataset lands twice.
Both present → both forwarded; structuredContent substitutes only when content is empty. For this response, still a double dump.
Nineteen clients, four variants
Each row was verified by reading the client's tool-result handling at a pinned SHA — each client name links to its repo at that commit. Open-source clients only: the conversion is checked against source, so closed-source agents are out of scope.
Last updated · commit history
19 rows across 18 projects — agent-framework is counted twice (Python → Variant A, .NET → Variant C).
| Client | What reaches the model | Client Variant | Related links / PRs |
|---|---|---|---|
| strands | content only structuredContent kept as a separate field on the result for hooks/programmatic access; not forwarded to the model (providers serialize only content) | A | #528 |
| agent-framework Python | content only at top level structuredContent read only for embedded blocks; code mode reuses the same parser | A | #3313 · #4763 |
| cline | content text/images only empty → "(No response)"; no structuredContent read | A | — |
| crewAI + crewAI-tools | content[0].text only crewAI-tools delegates the same content-only path via mcpadapt | A | — |
| deepagents | content only structuredContent → LangChain artifact (tooling, not the model) | A | — |
| gemini-cli | content only reverse shim backfills content from structuredContent; never sends sC to the model | A | #27045 |
| goose | conversational: content only code-mode TS sandbox prefers structuredContent — two paths, one client | A B | — |
| LibreChat | content only empty → "(No response)"; PTC code paths decided in external @librechat/agents | A | #8447 |
| Roo-Code | content text/images only empty → "(No response)" | A | — |
| zed | content only structuredContent deserialized then discarded on the model path | A | — |
| codex | structuredContent as whole body if present, else content code-mode runtime/TS sandbox hands the whole result (both) — a second path | B | #10334 · #2594 |
| fast-agent | compact JSON of structuredContent replaces text, else content | B | #703 |
| mastra | structuredContent alone if present, else whole envelope execute_typescript code mode inherits the same wrapper | B | #10430 |
| VS Code + Copilot Chat | JSON.stringify(structuredContent), dropping content text; else content a separate agentHost/Claude path forwards both; copilot-chat inherits, no own conversion | B | #290063 |
| adk-python | entire CallToolResult dict (model_dump) — both fields generic dump, no field-specific logic | C | #3893 |
| agent-framework .NET | whole result serialized — both fields delegated to the .NET MCP SDK; same project as the Variant-A Python binding | C | — |
| kilocode | raw CallToolResult → AI SDK — both fields delegated | C | — |
| opencode | whole CallToolResult JSON.stringify'd — both fields incidental, no field-specific logic | C | #27263 · #28567 |
| hermes-agent | both when both present; structuredContent if content empty; else content closest to a clean fallback — yet still double-dumps this response | D | #7043 · #7118 |