mcp-response-types-lab
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-response-types-lab Run the with_schema__text_and_structured_mismatched tool"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 returnstructuredContentconforming to it.For backwards compatibility, the spec recommends servers that return
structuredContentalso return a serialized-JSON text block incontent, so pre-2025-06-18 clients still get something.The spec says clients SHOULD validate
structuredContentagainstoutputSchemawhen 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.
Related MCP server: mcp-server-framework-cost
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 | text | — |
| no |
| present |
| no | text | present, matches text |
| yes | text | present, matches text and schema |
| yes |
| present, matches schema |
| yes | text says price $25, in stock | structuredContent says price $9.99, out of stock |
| yes | text | present but violates schema (missing required field, wrong types) |
| 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 booleanwith_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 contentSo 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 buildNode 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 belowCert 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.jsOpens 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:
/mcpto 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/mcpTo 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
structuredContentobject (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 |
|
| text: | text: | text: |
|
| JSON: | JSON: | JSON: |
| both present in envelope | text: | text: | JSON: |
| both present in envelope | text: | text: | JSON: |
|
| JSON: | JSON: | JSON: |
| both present, unreconciled (text says $25/in stock, structuredContent says $9.99/out of stock) | text: | text: | JSON: |
| call rejected client-side, JSON-RPC | text: | text: | malformed JSON passed through: |
| call rejected client-side, JSON-RPC | text: | text: | text: |
Key findings
Text-vs-structured preference is inconsistent even within Anthropic's own surfaces. Claude Desktop and claude.ai's native chat both prefer
contenttext when both fields are present — they never showedstructuredContentunlesscontentwas empty. Claude Code, hitting the same connector, does the opposite: it prefersstructuredContentwhenever 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.outputSchemais not enforced by any of the three real clients. The deliberately-invalidstructuredContent(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 referenceClient()class (as used by mcp-inspector) validates client-side and refuses the call outright (-32602for a schema violation,-32600whenoutputSchemais declared butstructuredContentis missing).Practical takeaway: write
contenttext as a complete, standalone answer — don't assume a client will readstructuredContentinstead of or in addition to it. And don't rely onoutputSchemato catch a server bug before it reaches a client; validatestructuredContentyourself 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).
Available Tools
8 toolsno_schema__structured_onlyA
No outputSchema declared, but the result carries structuredContent and an EMPTY content array. Tests whether a client that never saw an outputSchema will still surface structuredContent, or shows nothing because content is empty.
Inputs: no named parameters.
no_schema__text_and_structuredA
No outputSchema declared, but the result carries BOTH a text content block and structuredContent (matching values). Tests whether structuredContent is used opportunistically even without a declared schema.
Inputs: no named parameters.
no_schema__text_onlyA
No outputSchema. Returns a plain text content block only. This is the pre-2025-06-18 baseline.
Inputs: no named parameters.
with_schema__structured_missingA
outputSchema declared, but the result omits structuredContent entirely (only text content is returned) — a non-compliant server. Tests client behavior when a promised schema is never fulfilled.
Inputs: no named parameters. Returns: name, price, in_stock
with_schema__structured_only_no_textA
outputSchema declared. Returns structuredContent with an EMPTY content array (no text fallback). Tests whether the client/model can operate on structuredContent alone.
Inputs: no named parameters. Returns: name, price, in_stock
with_schema__structured_violates_schemaA
outputSchema declared, but the returned structuredContent violates it (wrong types, missing required field). Tests whether the client validates structuredContent against outputSchema and how it fails.
Inputs: no named parameters. Returns: name, price, in_stock
with_schema__text_and_structured_matchingA
outputSchema declared. Returns matching text + structuredContent (the spec's recommended backward-compatible pattern).
Inputs: no named parameters. Returns: name, price, in_stock
with_schema__text_and_structured_mismatchedA
outputSchema declared. Text content and structuredContent DISAGREE on the actual values. Tests which one the client actually feeds to the model / shows the user.
Inputs: no named parameters. Returns: name, price, in_stock
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 8 tools
Each tool targets a distinct response-type scenario (schema presence × content composition), with descriptions that clearly differentiate matching, mismatched, missing, and violating structured content. No two tools are functionally interchangeable.
All tool names follow a uniform pattern: [with_schema|no_schema]__[content description]. The consistent use of double underscores and ordered descriptors makes the naming scheme predictable and scannable.
8 tools is well-scoped for a response-types lab, covering the combinatorial space of schema declarations and text/structured content variations without redundancy or bloat.
The tool set covers the major response-type permutations: no schema (text-only, structured-only, both), with schema (matching, mismatched, structured-only, schema violation, missing structured content). This is a thorough and coherent test surface for the lab's purpose.
Maintenance
Related MCP Connectors
One MCP endpoint for Claude, GPT & Gemini: 100+ tools + no-code connectors + agent workers.
Remote MCP for C2PA intake verifier MCP, structured receipts, audit logs, and reviewer-ready evidenc
Remote MCP for Gemini upgrade evals, prompt regressions, output diffs, and eval receipts.
Convert, lint and generate LLM tool schemas for OpenAI, Anthropic, Gemini and MCP
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables input/output policy validation via MCP, including scanning text for policy and safety issues, generating test suite outlines, and checking configuration health.33 npmISC
- AlicenseNot gradedqualityCmaintenanceEnables measuring how the same MCP tools implemented in FastMCP, the official MCP Python SDK, and fastapi-mcp differ in token overhead, schema fidelity, and call latency.MIT
- AlicenseNot gradedqualityCmaintenanceEnables external MCP clients to invoke governed tools with schema validation, budgets, policy checks, human approval for side-effecting actions, and prompt-injection quarantine.MIT
- AlicenseAqualityBmaintenanceEnables MCP clients to extract validated typed records from unstructured text using named schemas, with schema listing and extraction tools. It returns validated JSON or tool errors, optionally using an offline parser or an OpenAI-compatible model with retries on validation failures.2MIT