# Structured Content vs Content: What MCP servers actually put on the wire

*MCP tool results · server-side `content` vs `structuredContent` survey*

The companion to the [client survey](client-survey.md). That one asked what *clients* forward to the model; this one asks what *servers* actually emit. The [MCP spec](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-result) gives a tool result two places for data — unstructured `content` and a `structuredContent` object. For tools with an [`outputSchema`](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#output-schema), 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.

Four variants for what a server emits:

- **A · content only, JSON text** — payload `JSON.stringify`'d into a text block; no `structuredContent`, no `outputSchema`
- **B · content only, rendered text** — `content` is prose / markdown / YAML; the structure is flattened away
- **C · dual-write** — `structuredContent` **and** a `content` mirror (the spec-recommended shape)
- **D · schema declared, dropped** — an `outputSchema` is authored in code but stripped before the wire

| Count | Variant | What it means |
|------:|------|---------------|
| **8** | content only, JSON text | structure survives only as reparseable text — no schema, no `structuredContent` |
| **10** | content only, rendered text | structure discarded into prose/markdown/YAML |
| **3** | dual-write | `structuredContent` + a `content` mirror reach the client |
| **1** | schema declared, dropped | `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.

**Server Variant A — content only, JSON text.** The rows are `JSON.stringify`'d into one text block. A client can reparse it, but there is no schema and no `structuredContent`.

```json
{ "content": [ { "type": "text", "text": "[{\"id\":\"row-0001\", … 249 more}]" } ] }
```

**Server Variant B — content only, rendered text.** The rows are flattened to prose/markdown/YAML. The structure is gone — only a rendering survives.

```json
{ "content": [ { "type": "text", "text": "Acme Corp — Enterprise — $48.2M — 1200 staff\n… 249 more" } ] }
```

**Server Variant C — dual-write.** `structuredContent` carries the typed rows; `content` carries a mirror. Schema-aware clients get data; legacy clients still get text.

```json
{
  "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 … } ] }
}
```

**Server Variant D — schema declared, dropped.** 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.

```json
// outputSchema declared in code · absent from tools/list · no structuredContent on the wire
{ "content": [ { "type": "text", "text": "{\"rows\":[ … ]}" } ] }
```

---

## 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 2026-06-04 · [commit history](https://github.com/olaservo/research-hub/commits/main)*

| Server | What's on the wire | Server Variant | SDK / framework |
|--------|--------------------|------|-----------------|
| [github-mcp-server](https://github.com/github/github-mcp-server/tree/7e79ae9) | 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](https://github.com/makenotion/notion-mcp-server/tree/e79f35f) | content only, `JSON.stringify(response.data)` — *OpenAPI `returnSchema` parsed but never surfaced as `outputSchema`* | A | TS-SDK 1.25 |
| [blender-mcp](https://github.com/ahujasid/blender-mcp/tree/7636d13) | content only, `json.dumps(result, indent=2)` in text — *every tool typed `-> str`, so FastMCP derives no schema* | A | FastMCP |
| [browser-use-mcp-server](https://github.com/co-browser/browser-use-mcp-server/tree/a9864a4) | content only, `json.dumps` inside a legacy `list[TextContent]` — *pre-2025-06-18 lowlevel `@server.call_tool` shape* | A | lowlevel `mcp.server` |
| [agent-toolkit](https://github.com/stripe/agent-toolkit/tree/38cc559) (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](https://github.com/firecrawl/firecrawl-mcp-server/tree/01e3b8a) | 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](https://github.com/cloudflare/mcp-server-cloudflare/tree/f625075) | content only, `JSON.stringify(...)` via a shared `accountTool` helper — *~12 monorepo apps share one content-only helper* | A | TS-SDK (custom wrapper) |
| [unity-mcp](https://github.com/CoplayDev/unity-mcp/tree/2d2bdc5) | content only, a `dict` auto-stringified to JSON — *handlers return `dict`; FastMCP wraps it as text* | A | FastMCP |
| [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP/tree/82a89ef) | content only, **YAML** (default `outputFormat: yaml`) — *`content` treated as a model-facing text channel, not serialized data* | B | TS-SDK 1.29 |
| [tavily-mcp](https://github.com/tavily-ai/tavily-mcp/tree/fc09f6e) | 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](https://github.com/upstash/context7/tree/eb57986) | content only, rendered docs (markdown/prose) — *appropriate here: the text **is** the product* | B | TS-SDK `registerTool` |
| [serena](https://github.com/oraios/serena/tree/8a54795) | 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](https://github.com/antvis/mcp-server-chart/tree/3317809) | content only, a chart **URL** in text — *routes the chart spec through `_meta.spec` instead of `structuredContent` (a misuse)* | B | raw `Server` |
| [magic-mcp](https://github.com/21st-dev/magic-mcp/tree/b07bc9d) | content only, markdown (UI component code) — *`BaseTool.execute`'s return type is content-only by construction* | B | TS-SDK `registerTool` |
| [spec-workflow-mcp](https://github.com/Pimzino/spec-workflow-mcp/tree/b63e6cd) | content only, a Toon-encoded struct as text — *`toMCPResponse` serializes the `ToolResponse` object into one text block* | B | TS-SDK `setRequestHandler` |
| [playwright-mcp](https://github.com/microsoft/playwright-mcp/tree/3201718) | content only, markdown accessibility snapshots — *thin shim; the tool code now lives in playwright-core upstream* | B | custom `defineTool` |
| [sentry-mcp](https://github.com/getsentry/sentry-mcp/tree/e16047f) | content only, markdown narrative — *the test client has a dead `hasStructuredContent` check the server never satisfies* | B | TS-SDK `defineTool` |
| [exa-mcp-server](https://github.com/exa-labs/exa-mcp-server/tree/ad888a1) | 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 `server.tool`) |
| [actors-mcp-server](https://github.com/apify/actors-mcp-server/tree/171d61a) (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](https://github.com/wonderwhy-er/DesktopCommanderMCP/tree/673111c) | **`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](https://github.com/getsentry/XcodeBuildMCP/tree/2266589) | **`structuredContent` + content** — *a render session emits a typed `StructuredOutputEnvelope` (schema + version) alongside text/image content* | C | TS-SDK `registerTool` |
| [supabase-mcp](https://github.com/supabase-community/supabase-mcp/tree/da182d6) | 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` |
