Skip to content

Python: serialize Anthropic MCP tool result content on replay - #9972

Open
Kip (uragoodman) wants to merge 5 commits into
microsoft:mainfrom
uragoodman:fix/anthropic-mcp-tool-result-replay
Open

Kip (uragoodman) wants to merge 5 commits into
microsoft:mainfrom
uragoodman:fix/anthropic-mcp-tool-result-replay

Conversation

@uragoodman

Copy link
Copy Markdown

Motivation & Context

Anthropic's provider-hosted MCP connector returns mcp_tool_result blocks whose content is parsed into list[Content]. On the next turn, _prepare_message_for_anthropic copied content.output directly into the request body, so the Anthropic SDK failed with:

TypeError: Object of type Content is not JSON serializable

The first request succeeds; every subsequent turn that replays the MCP result fails before the request is sent.

This change normalizes MCP tool result output into the shapes Anthropic accepts for mcp_tool_result.content: a string or a list of text blocks.

Description & Review Guide

  • What are the major changes?
    • Added _prepare_mcp_tool_result_content to convert None, strings, top-level mappings, and Sequence[Content] | Sequence[Mapping] into Anthropic-compatible content.
    • mcp_server_tool_result now uses this helper instead of emitting content.output unchanged.
    • Text Content items become {"type": "text", "text": ...}; non-text items are skipped with a debug log because the Anthropic mcp_tool_result schema only accepts text.
    • Top-level text mappings become a one-item text-block list; other mappings are JSON-encoded.
  • What is the impact of these changes?
    • Multi-turn conversations using Anthropic's hosted MCP connector now replay their tool results instead of raising TypeError.
    • Local MCP tools and normal function tools are unchanged; they use function_result.
    • Existing string and None outputs keep their previous wire shape.
  • What do you want reviewers to focus on?
    • The accepted input shapes and the decision to skip non-text items, since the Anthropic schema cannot represent image/file blocks in mcp_tool_result.
    • The top-level Mapping handling and whether JSON-encoding non-text mappings is the right fallback.
    • Test coverage for parsed list content, top-level mappings, and persisted-history replay.

The diff also contains one ruff format adjustment to an existing over-wrapped call in the same file; it is required by the repository formatter.

Testing

  • pytest packages/anthropic/tests -m "not integration" -q — 259 passed
  • ruff format --check — pass
  • ruff check --no-fix — pass
  • pyright --pythonpath .venv/Scripts/python.exe packages/anthropic/agent_framework_anthropic — 0 errors
  • mypy packages/anthropic/tests/test_anthropic_client.py — no issues

Full workspace build and the full unit-test suite were not run locally; CI will run them.

Related Issue

Fixes #9970

The issue is filed; opening this focused fix for maintainer review. No ready-for-implementation label was present when this PR was opened.

AI Assistance

  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: implementation, tests, and PR description were drafted with an AI coding assistant; I reproduced and verified the failure and the fix locally.

Contribution Checklist

  • The changed package imports cleanly and the focused Anthropic test suite passes locally.
  • New regression tests were added and pass.
  • Ruff format, Ruff lint, Pyright source check, and Mypy test check pass locally.
  • Full workspace build and full unit-test suite were not run locally; CI will run them.
  • This is not a breaking change.
  • The PR links to issue Python: [Bug]: Anthropic MCP tool result replay sends Content objects instead of text blocks #9970; no ready-for-implementation label was present when opened, and the PR is submitted for early direction feedback.

Render parsed Anthropic MCP results as JSON-serializable text blocks instead of Content objects, and cover list, top-level mapping, and persisted-history replay with regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Nov 2, 2026
"type": "mcp_tool_result",
"tool_use_id": content.call_id,
"content": content.output if content.output is not None else "",
"content": self._prepare_mcp_tool_result_content(content.output),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kip (@uragoodman) This replay path also needs to preserve the provider’s error status. The parser does not retain is_error in provider metadata, and this block never restores it, so a failed hosted MCP call is replayed as a successful-looking result; persistence also removes the only remaining copy in raw_representation. Please persist and restore is_error, with streaming and non-streaming round-trip coverage for error results. Also, please give _prepare_mcp_tool_result_content’s output parameter a concrete union matching the supported shapes instead of Any; this helper defines a replay wire contract and should be type-checked.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed both points.

  • is_error is now persisted in the Anthropic provider metadata on parse and
    restored when replaying mcp_server_tool_result. False is preserved, a
    missing field is omitted, and the value survives Message.to_dict() /
    from_dict().
  • _prepare_mcp_tool_result_content now takes a concrete McpToolResultOutput
    union (str | Mapping | Sequence[Content | Mapping] | None) instead of Any.
    The generic str(output) / default=str fallback has been removed;
    unsupported shapes now raise ContentError, so the runtime contract matches
    the annotation.
  • Note: the generic fallback being removed was introduced by this PR's initial
    revision, not by existing main code, so this does not change pre-existing
    behavior.

Verification on the revised commit (4d3ad2fc1):

  • Focused MCP tests: 21 passed.
  • Full Anthropic suite: 269 passed, 10 deselected.
  • Ruff format and Ruff check on the Anthropic package: pass.
  • mypy, zuban, pyrefly, ty, and Pyright source/tests: pass locally.

Pushed as 4d3ad2fc1. The main workflows are currently waiting for maintainer
approval (action_required); CLA and API compatibility have passed.

Kip (uragoodman) and others added 2 commits November 3, 2026 19:44
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Persist the provider's is_error flag in Anthropic metadata so MCP tool
results replay with the same error status after serialization. Tighten
the replay output contract to a concrete union and reject unsupported
shapes instead of stringifying them.

Add streaming, non-streaming, persistence, and unsupported-output
regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
github-app-auth — 4d3ad2fc Deployed Nov 3, 2026 by uragoodman via add_label #26960
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Anthropic MCP tool result replay sends Content objects instead of text blocks

3 participants