The gap
A tool with an output schema sends its result twice: once as structured_content, and again as a serialized copy in a content text block. The spec marks that duplication as a SHOULD, not a MUST — it exists so clients that only read content still see the value — but for large payloads it is pure duplication on the wire, and there is no declarative way to turn it off for a tool. The size cost is real: #3717 documented ResponseLimitingMiddleware truncation breaking outputSchema tools, and the mirror is a 2× multiplier on exactly those responses.
Concrete use case
@mcp.tool
def list_accounts(segment: str) -> list[Account]:
return query_accounts(segment) # 10K tokens of structured data → sent twice
A server author whose clients consume structured_content (or who wants to keep responses under a size limit) currently has two workarounds, both with costs: set output_schema=None, which kills the schema and structured output entirely; or abandon the bare-return path and hand-build ToolResult(content=[], structured_content=...) for every call, which works but bypasses the framework's conversion and can't be seen in the tool's registration.
Expected vs. actual
- Actual: with an output schema present, the conversion always emits both fields; the only per-tool knobs are schema-destroying or per-call manual construction.
- Expected: a per-tool registration option to declare "send
structured_content only," with the default preserving today's dual-write.
Design proposal (brief)
A mirror_structured_content: bool = True parameter on tool registration, threaded exactly like the existing run_in_thread flag (ToolMeta → FunctionTool.from_function → the standalone tool decorator → provider decorators → FastMCP.tool), gating one line in the conversion's has-output-schema branch: content=content if self.mirror_structured_content else []. Schema-less tools are unaffected (their content is the only representation), and explicit ToolResult returns pass through untouched.
Default True means no behavior change; opting out is explicit, since the mirror is the compatibility path for content-only clients.
This mirrors what the official SDKs are adding for the same gap: python-sdk #3155 (same flag name, against python-sdk #1332) and rust-sdk #1046, so fastmcp tools can express the same intent as their SDK counterparts.
I have a working branch with tests and docs ready to PR if this is accepted: main...olaservo:fastmcp:structured-content-mirror-optout
Prepared with Claude Code assistance; I've reviewed it and the branch.
The gap
A tool with an output schema sends its result twice: once as
structured_content, and again as a serialized copy in acontenttext block. The spec marks that duplication as a SHOULD, not a MUST — it exists so clients that only readcontentstill see the value — but for large payloads it is pure duplication on the wire, and there is no declarative way to turn it off for a tool. The size cost is real: #3717 documentedResponseLimitingMiddlewaretruncation breaking outputSchema tools, and the mirror is a 2× multiplier on exactly those responses.Concrete use case
A server author whose clients consume
structured_content(or who wants to keep responses under a size limit) currently has two workarounds, both with costs: setoutput_schema=None, which kills the schema and structured output entirely; or abandon the bare-return path and hand-buildToolResult(content=[], structured_content=...)for every call, which works but bypasses the framework's conversion and can't be seen in the tool's registration.Expected vs. actual
structured_contentonly," with the default preserving today's dual-write.Design proposal (brief)
A
mirror_structured_content: bool = Trueparameter on tool registration, threaded exactly like the existingrun_in_threadflag (ToolMeta→FunctionTool.from_function→ the standalonetooldecorator → provider decorators →FastMCP.tool), gating one line in the conversion's has-output-schema branch:content=content if self.mirror_structured_content else []. Schema-less tools are unaffected (theircontentis the only representation), and explicitToolResultreturns pass through untouched.Default
Truemeans no behavior change; opting out is explicit, since the mirror is the compatibility path forcontent-only clients.This mirrors what the official SDKs are adding for the same gap: python-sdk #3155 (same flag name, against python-sdk #1332) and rust-sdk #1046, so fastmcp tools can express the same intent as their SDK counterparts.
I have a working branch with tests and docs ready to PR if this is accepted: main...olaservo:fastmcp:structured-content-mirror-optout
Prepared with Claude Code assistance; I've reviewed it and the branch.