Summary
When an MCP tool returns an array, the SDK emits the result twice — a serialized-JSON TextContent block (ToolReference::formatResult()) and structuredContent (ToolReference::extractStructuredContent(), wired in CallToolHandler). Different clients want opposite shapes, and there's no first-class way to vary the result per client.
The problem
- ChatGPT connectors: when a result carries both a JSON text block and
structuredContent, ChatGPT treats it as a large payload, truncates it ("Response output was truncated … to fit the tool response budget"), stores it as a resource, and chains internal "Read resource" paging that elides fields — so identifiers (ids, etc.) never reach the model. Returning only structuredContent (empty content) fixes it. (See OpenAI community report 1383071.)
- Claude Desktop / Claude Code / Cursor: read the text
content block; dropping it leaves the model with nothing.
So the correct content shape is genuinely client-dependent, but the framework gives no hook for it.
What we had to do (works, but boilerplate + a footgun)
For every tool: declare a ?RequestContext $context parameter, read $context->getSession()->get('client_info'), and hand-build a CallToolResult choosing content: [] + structuredContent for OpenAI clients vs. text + structuredContent for the rest.
Footgun encountered: our first attempt used a parameter named $_session. SchemaGenerator throws on reserved parameter names (_session/_request) during discovery, which aborts discovery and registers zero tools — while the DI container still compiles, so it looks healthy but the live server returns "no tools". RequestContext is the correct injection, but this isn't obvious from the docs.
Feature request
First-class support for client-conditional structured results, e.g. one or more of:
- Honor the negotiated client capabilities /
client_info so clients that consume structuredContent don't also receive the redundant JSON text dump (and vice-versa) — ideally automatic.
- A per-tool toggle (attribute/option, e.g.
structuredContentOnly) and/or a small documented helper to shape the result from the injected RequestContext.
- Docs for the
RequestContext injection pattern and the reserved _session/_request parameter names (so the silent "zero tools" discovery failure is avoidable).
Environment
symfony/mcp-bundle v0.10.0
mcp/sdk v0.6.0
- PHP 8.4.20
Summary
When an MCP tool returns an array, the SDK emits the result twice — a serialized-JSON
TextContentblock (ToolReference::formatResult()) andstructuredContent(ToolReference::extractStructuredContent(), wired inCallToolHandler). Different clients want opposite shapes, and there's no first-class way to vary the result per client.The problem
structuredContent, ChatGPT treats it as a large payload, truncates it ("Response output was truncated … to fit the tool response budget"), stores it as a resource, and chains internal "Read resource" paging that elides fields — so identifiers (ids, etc.) never reach the model. Returning onlystructuredContent(emptycontent) fixes it. (See OpenAI community report 1383071.)contentblock; dropping it leaves the model with nothing.So the correct
contentshape is genuinely client-dependent, but the framework gives no hook for it.What we had to do (works, but boilerplate + a footgun)
For every tool: declare a
?RequestContext $contextparameter, read$context->getSession()->get('client_info'), and hand-build aCallToolResultchoosingcontent: []+structuredContentfor OpenAI clients vs. text +structuredContentfor the rest.Footgun encountered: our first attempt used a parameter named
$_session.SchemaGeneratorthrows on reserved parameter names (_session/_request) during discovery, which aborts discovery and registers zero tools — while the DI container still compiles, so it looks healthy but the live server returns "no tools".RequestContextis the correct injection, but this isn't obvious from the docs.Feature request
First-class support for client-conditional structured results, e.g. one or more of:
client_infoso clients that consumestructuredContentdon't also receive the redundant JSON text dump (and vice-versa) — ideally automatic.structuredContentOnly) and/or a small documented helper to shape the result from the injectedRequestContext.RequestContextinjection pattern and the reserved_session/_requestparameter names (so the silent "zero tools" discovery failure is avoidable).Environment
symfony/mcp-bundlev0.10.0mcp/sdkv0.6.0