Skip to content

Per-tool opt-out for serializing structured output into a duplicate content text block #4637

Description

@olaservo

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 (ToolMetaFunctionTool.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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementImprovement to existing functionality. For issues and smaller PR improvements.proposalA proposal for a feature or enhancement either requiring or seeking comments on its design.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions