structuredContent
What MCP servers actually put on the wire
The companion to the client survey. That one asked what clients forward to the model; this one asks what servers actually emit. 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 mirror it into a content text block for backwards compatibility. We read 22 popular servers' tool-result code at a pinned SHA to see what they put on the wire.
structuredContent + a content mirror reach the client
outputSchema authored, then stripped on the wire
The same data, four wire shapes
Take one tool that returns a dataset — ~250 rows × 9 fields. Here is the same payload as the four variants put it on the wire.
The rows are JSON.stringify'd into one text block. A client can reparse it, but there is no schema and no structuredContent.
{ "content": [ { "type": "text", "text": "[{\"id\":\"row-0001\", … 249 more}]" } ] }
The rows are flattened to prose/markdown/YAML. The structure is gone — only a rendering survives.
{ "content": [ { "type": "text", "text": "Acme Corp — Enterprise — $48.2M — 1200 staff\n… 249 more" } ] }
structuredContent carries the typed rows; content carries a mirror. Schema-aware clients get data; legacy clients still get text.
{
"content": [
{ "type": "text", "text": "```json\n{\"rows\":[ … ]}\n```" }, // mirror (Apify fences it)
{ "type": "text", "text": "250 accounts; top by revenue: Acme Corp" } // narrative
],
"structuredContent": { "rows": [ { … same ~250 × 9 … } ] }
}
The tool authors an outputSchema, but the wrapper strips it from tools/list and ships content-only JSON — the type-safety work never reaches the wire.
// outputSchema declared in code · absent from tools/list · no structuredContent on the wire
{ "content": [ { "type": "text", "text": "{\"rows\":[ … ]}" } ] }
Variant C, done leanly: keep content a concise, model-useful summary and put the bulk payload in structuredContent (large or binary blobs behind a resource). That satisfies the outputSchema contract, still hands content-only clients something readable, and avoids shipping the full dataset twice. Apify's full duplication does dump it twice; Desktop Commander's summary-in-content + data-in-structuredContent split is the better template.
Twenty-two servers, four variants
Each row was verified by reading the server's tool-result construction at a pinned SHA — each server name links to its repo at that commit. The set is a representative slice of the June-2026 popularity leaderboard plus three widely-installed servers (context7, browser-use, tavily) — each row chosen to show a distinct way of handling the two fields. Open-source servers only.
Last updated · commit history
| Server | What's on the wire | Server Variant | SDK / framework |
|---|---|---|---|
| github-mcp-server | content only, JSON.stringify'd via MarshalledTextResult no tool declares OutputSchema; csv_output.go explicitly nils StructuredContent |
A | official go-sdk v1.6.1 |
| notion-mcp-server | content only, JSON.stringify(response.data) OpenAPI returnSchema parsed but never surfaced as outputSchema |
A | TS-SDK 1.25 |
| blender-mcp | content only, json.dumps(result, indent=2) in text every tool typed -> str, so FastMCP derives no schema |
A | FastMCP |
| browser-use-mcp-server | content only, json.dumps inside a legacy list[TextContent] pre-2025-06-18 lowlevel @server.call_tool shape |
A | lowlevel mcp.server |
| agent-toolkit Stripe | content only, upstream JSON as text 4-arg this.tool() has no outputSchema slot; proxy drops the upstream schema; throws on error | A | TS-SDK 1.17 |
| firecrawl-mcp-server | content only, asText = JSON.stringify(data, null, 2) the firecrawl-fastmcp fork gives authors no structuredContent channel at all |
A | firecrawl-fastmcp |
| mcp-server-cloudflare | content only, JSON.stringify(...) via a shared accountTool helper ~12 monorepo apps share one content-only helper |
A | TS-SDK (custom wrapper) |
| unity-mcp | content only, a dict auto-stringified to JSON handlers return dict; FastMCP wraps it as text |
A | FastMCP |
| Figma-Context-MCP | content only, YAML (default outputFormat: yaml) content treated as a model-facing text channel, not serialized data |
B | TS-SDK 1.29 |
| tavily-mcp | content only, Title:/URL:/Content: prose the API returns rich JSON (scores, raw_content) but formatResults flattens it before the wire |
B | raw Server |
| context7 | content only, rendered docs (markdown/prose) appropriate here: the text is the product | B | TS-SDK registerTool |
| serena | content only, mostly -> str text tools registered manually so FastMCP derives no schema; some diagnostics JSON-stringified into text |
B | FastMCP (manual registration) |
| mcp-server-chart | content only, a chart URL in text routes the chart spec through _meta.spec instead of structuredContent (a misuse) | B | raw Server |
| magic-mcp | content only, markdown (UI component code) BaseTool.execute's return type is content-only by construction | B | TS-SDK registerTool |
| spec-workflow-mcp | content only, a Toon-encoded struct as text toMCPResponse serializes the ToolResponse object into one text block | B | TS-SDK setRequestHandler |
| playwright-mcp | content only, markdown accessibility snapshots thin shim; the tool code now lives in playwright-core upstream | B | custom defineTool |
| sentry-mcp | content only, markdown narrative the test client has a dead hasStructuredContent check the server never satisfies | B | TS-SDK defineTool |
| exa-mcp-server | split — web_search_exa → prose; web_search_advanced_exa → JSON.stringify; both attach a content-block _meta.searchTime two conventions in one repo |
B A | TS-SDK 1.12 (5-arg) |
| actors-mcp-server Apify | structuredContent + content content[0] a fenced ```json mirror, content[1] a narrative; outputSchema on ~every tool — the full-duplication gold standard | C | TS-SDK 1.29 (raw Server) |
| DesktopCommanderMCP | structuredContent + content content kept a text summary "for broad host compatibility"; structuredContent carries file/preview data for a UI widget — leaner than full duplication | C | TS-SDK setRequestHandler |
| XcodeBuildMCP | structuredContent + content a render session emits a typed StructuredOutputEnvelope (schema + version) alongside text/image content | C | TS-SDK registerTool |
| supabase-mcp | content only, JSON.stringify(result) every DB tool declares a Zod outputSchema (required by the wrapper's types), but @supabase/mcp-utils drops it from tools/list and never emits structuredContent; the README tells clients to "fall back to parsing JSON from content" |
D | custom @supabase/mcp-utils |
_meta-as-payload misuse — antvis/mcp-server-chart routes the chart spec through_meta.spec; exa attaches_meta.searchTimeto a content block. Per spec,_metais for protocol metadata, not tool payload.- Wrappers decide the wire shape — the firecrawl-fastmcp fork and
@supabase/mcp-utilsboth prevent or dropstructuredContentregardless of whether the underlying SDK supports it. - SDKs disagree on the mirror — the Go SDK writes a byte-identical mirror when
Contentis nil; Python pretty-prints (indent=2), so its text mirror is not byte-identical tostructuredContent; the TypeScript SDK does not auto-mirror at all — it only validates thatstructuredContentis present when anoutputSchemawas declared. - The three dual-writers do it for rendering — Apify fences JSON for the model; Desktop Commander and XcodeBuild emit
structuredContentto drive UI/preview widgets — none adopted it purely to satisfy SEP-2200.
The finding
The popular-server ecosystem is overwhelmingly content-only: 18 of 22 surveyed servers never put structuredContent on the wire. Only three dual-write — and a common claim that Apify is the lone dual-writer turns out to be wrong: Desktop Commander and XcodeBuild do too. But all three do it to feed UI / preview rendering, not to satisfy SEP-2200 — which only reinforces that structuredContent today is pulled by UI use-cases, not by general structured-output conformance. Supabase is the cautionary case: it authors a Zod outputSchema on every database tool, then a custom wrapper drops both the schema and the structured field on the way out.
The bottleneck is adoption and wrapper ergonomics, not SDK capability — TypeScript, Python and Go SDKs have all supported structuredContent since the 2025-06-18 revision. And this is the server-side mirror of the client survey: because most clients forward only content to the model, servers rationally pour everything into content. The two surveys describe the same degenerate equilibrium from opposite ends.