mcp-response-types-lab

mcp-response-types-lab

Enables testing how different MCP clients handle tool results containing both content and structuredContent, including outputSchema enforcement and mismatch scenarios.

Category
访问服务器

README

mcp-response-types-lab

A local MCP server (protocol 2025-06-18) purpose-built to answer one question: when a tool result carries both content and structuredContent, which one do different MCP clients actually use — and does that change if the tool declares an outputSchema?

Background (2025-06-18 spec)

  • CallToolResult.content — an array of content blocks (text/image/audio/ resource). Required, but MAY be empty ([]).
  • CallToolResult.structuredContent — optional arbitrary JSON object.
  • Tool.outputSchema — optional JSON Schema. If a tool declares one, the spec says the server MUST return structuredContent conforming to it.
  • For backwards compatibility, the spec recommends servers that return structuredContent also return a serialized-JSON text block in content, so pre-2025-06-18 clients still get something.
  • The spec says clients SHOULD validate structuredContent against outputSchema when present.

None of that says what a client does with the model-facing context when both are present, what a user-facing UI renders, or what happens on a schema violation — that's what this lab is for.

The tool matrix

Every tool takes no arguments. Names encode the two variables under test: no_schema__* (tool has no outputSchema) vs with_schema__* (it does), followed by what content/structuredContent combination is returned.

Tool outputSchema content structuredContent
no_schema__text_only no text —
no_schema__structured_only no [] present
no_schema__text_and_structured no text present, matches text
with_schema__text_and_structured_matching yes text present, matches text and schema
with_schema__structured_only_no_text yes [] present, matches schema
with_schema__text_and_structured_mismatched yes text says price $25, in stock structuredContent says price $9.99, out of stock
with_schema__structured_violates_schema yes text present but violates schema (missing required field, wrong types)
with_schema__structured_missing yes text absent, despite schema being declared

Source: src/server.ts. Add more variants there if you find a client-specific edge case worth isolating — keep the naming convention so results stay diffable.

Baseline: raw server responses (via mcp-inspector CLI)

Before testing any chat client, every tool was called directly against the server over stdio using @modelcontextprotocol/inspector --cli — this uses the official TS SDK's Client class with no vendor UI logic on top, so it's the ground truth for what actually goes over the wire. Command used for each:

npx @modelcontextprotocol/inspector --cli node dist/server.js \
  --method tools/call --tool-name <tool_name>

no_schema__text_only

{
  "content": [
    { "type": "text", "text": "Widget costs $9.99 and is currently out of stock." }
  ]
}

no_schema__structured_only

{
  "content": [],
  "structuredContent": { "name": "Widget", "price": 9.99, "in_stock": false }
}

no_schema__text_and_structured

{
  "content": [
    { "type": "text", "text": "Widget costs $9.99 and is currently out of stock." }
  ],
  "structuredContent": { "name": "Widget", "price": 9.99, "in_stock": false }
}

with_schema__text_and_structured_matching

{
  "content": [
    { "type": "text", "text": "Widget costs $9.99 and is currently out of stock." }
  ],
  "structuredContent": { "name": "Widget", "price": 9.99, "in_stock": false }
}

with_schema__structured_only_no_text

{
  "content": [],
  "structuredContent": { "name": "Widget", "price": 9.99, "in_stock": false }
}

with_schema__text_and_structured_mismatched — text and structuredContent deliberately disagree:

{
  "content": [
    { "type": "text", "text": "Widget costs $25.00 and is in stock." }
  ],
  "structuredContent": { "name": "Widget", "price": 9.99, "in_stock": false }
}

with_schema__structured_violates_schema — server tried to return {"price":"twenty-five dollars","in_stock":"yes"} (missing required name, wrong types), but the call never succeeds. mcp-inspector rejects it client-side before returning anything to the caller:

Failed to call tool with_schema__structured_violates_schema: MCP error -32602:
Structured content does not match the tool's output schema: data must have
required property 'name', data/price must be number, data/in_stock must be boolean

with_schema__structured_missing — server declared outputSchema but returned only text, no structuredContent. Also rejected client-side:

Failed to call tool with_schema__structured_missing: MCP error -32600:
Tool with_schema__structured_missing has an output schema but did not return
structured content

So for any client built on the official SDK's Client class, outputSchema isn't just documentation — the client actively enforces it and refuses to hand a malformed or incomplete result to the caller at all. Whether Claude Code / Desktop / claude.ai run that same validation path, or roll their own handling, is what the client comparison below checks.

Setup

npm install
npm run build

Node version note: this machine's default node (Homebrew 18.11.0) is currently broken — dyld: Library not loaded: libicui18n.71.dylib — because a later brew upgrade moved icu4c out from under it. It has nothing to do with this project, but it will silently break any tool here that shells out to node. A working Node 20 was installed alongside it (brew install node@20, keg-only, does not touch the default node symlink). All commands below use it explicitly:

export PATH="/opt/homebrew/opt/node@20/bin:$PATH"
export NODE_EXTRA_CA_CERTS=/etc/ssl/cert.pem   # see note below

Cert note: Node 18.11's bundled CA store can't validate npm's current TLS chain (UNABLE_TO_GET_ISSUER_CERT_LOCALLY) even though curl works fine using the system store. Pointing NODE_EXTRA_CA_CERTS at the system bundle (/etc/ssl/cert.pem) fixes npm install. Only needed for install / running the inspector via npx; the built server itself makes no network calls.

Testing each client

1. mcp-inspector (reference client / GUI)

export PATH="/opt/homebrew/opt/node@20/bin:$PATH"
npx @modelcontextprotocol/inspector node dist/server.js

Opens a local web UI. Call each tool, note what the "Structured Content" and "Content" panes show, and whether schema-violating calls are blocked in the UI (they are, per the CLI results above) or just visually flagged.

CLI form (no UI, scriptable), one tool at a time:

npx @modelcontextprotocol/inspector --cli node dist/server.js \
  --method tools/call --tool-name <tool_name>

2. Claude Code (this CLI)

Already registered at project scope — see .mcp.json. From an interactive claude session in this repo:

/mcp

to confirm response-types-lab is connected, then ask Claude to call each of the 8 tools by name and report back verbatim what it received in content vs structuredContent, and whether any call errored. Because I (this background session) can't hot-reload newly-registered MCP servers into my own running process, this step needs a fresh interactive session — I can't self-report Claude Code's live behavior from here.

3. Claude Desktop

Installed on this Mac but not something I can drive (no GUI access from a background job). Add to Desktop's config — ~/Library/Application Support/Claude/claude_desktop_config.json — merging in:

{
  "mcpServers": {
    "response-types-lab": {
      "command": "/opt/homebrew/opt/node@20/bin/node",
      "args": ["/Users/chiragchadha/Code/mcp-response-types-lab/dist/server.js"]
    }
  }
}

Fully restart Claude Desktop (quit, not just close the window), then in a chat ask it to call each tool and report what it saw. Also worth eyeballing the UI directly — does it render a distinct "structured data" view, a raw JSON block, or nothing when content is empty?

4. claude.ai / Claude Mobile

Both only connect to remote MCP servers (HTTP transport with OAuth), not local stdio processes. The server already supports Streamable HTTP:

export PATH="/opt/homebrew/opt/node@20/bin:$PATH"
MCP_TRANSPORT=http npm start   # listens on http://localhost:3939/mcp

To reach it from claude.ai or mobile you'd need to expose that port publicly (e.g. ngrok http 3939 or cloudflared tunnel). I haven't done this — it means opening this machine's server to the internet, even if briefly, so it's your call whether/how to do it. Say the word and I'll wire up a tunnel and walk through adding it as a custom connector in claude.ai's settings; otherwise this is a manual step for you.

Results (captured 2026-08-11)

Tested against the deployed MCP_TRANSPORT=http server via a Cloudflare quick tunnel, added as a custom connector on claude.ai (name response-types-lab). Claude Desktop's and Claude Code's results below are both reached through that same connector — Desktop natively, Claude Code via the claude.ai account-connector bridge (shows up prefixed claude.ai response-types-lab in its tool-call log) — so this is one server, three different client-side handling layers.

The two possible outcomes below, spelled out so the table cells are unambiguous:

  • text = the model was shown the human-readable sentence from content (e.g. "Widget costs $9.99 and is currently out of stock.")
  • JSON = the model was shown the raw structuredContent object (e.g. {"name":"Widget","price":9.99,"in_stock":false}), not a sentence

The mcp-inspector column shows the actual raw wire response instead (it's not a chat client, so there's no "which one wins" decision to observe — see the full payloads above).

Tool mcp-inspector (raw wire) Claude Desktop showed claude.ai showed Claude Code showed
no_schema__text_only content text only text: "Widget costs $9.99 and is currently out of stock." text: "Widget costs $9.99 and is currently out of stock." text: "Widget costs $9.99 and is currently out of stock."
no_schema__structured_only structuredContent only, content: [] JSON: {"name":"Widget","price":9.99,"in_stock":false} JSON: {"name":"Widget","price":9.99,"in_stock":false} JSON: {"name":"Widget","price":9.99,"in_stock":false}
no_schema__text_and_structured both present in envelope text: "Widget costs $9.99 and is currently out of stock." (JSON not shown) text: "Widget costs $9.99 and is currently out of stock." (JSON not shown) JSON: {"name":"Widget","price":9.99,"in_stock":false} (text not shown)
with_schema__text_and_structured_matching both present in envelope text: "Widget costs $9.99 and is currently out of stock." text: "Widget costs $9.99 and is currently out of stock." JSON: {"name":"Widget","price":9.99,"in_stock":false}
with_schema__structured_only_no_text structuredContent only, content: [] JSON: {"name":"Widget","price":9.99,"in_stock":false} JSON: {"name":"Widget","price":9.99,"in_stock":false} JSON: {"name":"Widget","price":9.99,"in_stock":false}
with_schema__text_and_structured_mismatched both present, unreconciled (text says $25/in stock, structuredContent says $9.99/out of stock) text: "Widget costs $25.00 and is in stock." text: "Widget costs $25.00 and is in stock." JSON: {"name":"Widget","price":9.99,"in_stock":false}
with_schema__structured_violates_schema call rejected client-side, JSON-RPC -32602 text: "Widget costs twenty-five dollars, in stock: yes" (server's bad text, but no error) text: "Widget costs twenty-five dollars, in stock: yes" (server's bad text, but no error) malformed JSON passed through: {"price":"twenty-five dollars","in_stock":"yes"} (no error)
with_schema__structured_missing call rejected client-side, JSON-RPC -32600 text: "Widget costs $9.99 and is currently out of stock." (no error) text: "Widget costs $9.99 and is currently out of stock." (no error) text: "Widget costs $9.99 and is currently out of stock." (no error)

Key findings

  1. Text-vs-structured preference is inconsistent even within Anthropic's own surfaces. Claude Desktop and claude.ai's native chat both prefer content text when both fields are present — they never showed structuredContent unless content was empty. Claude Code, hitting the same connector, does the opposite: it prefers structuredContent whenever present, even over conflicting text. On the mismatched-values tool the two groups showed genuinely different numbers to the model/user for an identical server response.
  2. outputSchema is not enforced by any of the three real clients. The deliberately-invalid structuredContent (string where a number was required, missing required field) passed through untouched on Desktop, claude.ai, and Claude Code — no validation error, no rejection. Only the bare reference Client() class (as used by mcp-inspector) validates client-side and refuses the call outright (-32602 for a schema violation, -32600 when outputSchema is declared but structuredContent is missing).
  3. Practical takeaway: write content text as a complete, standalone answer — don't assume a client will read structuredContent instead of or in addition to it. And don't rely on outputSchema to catch a server bug before it reaches a client; validate structuredContent yourself before returning it.

Not yet tested: Claude Mobile (should inherit claude.ai's account connectors — worth a quick spot check to confirm it matches claude.ai's native behavior rather than Claude Code's).

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选