Skip to content

fix(deps): require mcp>=1.19 for CallToolResult passthrough#855

Open
jujumilk3 wants to merge 1 commit intoanthropics:mainfrom
jujumilk3:fix/mcp-min-version-for-call-tool-result
Open

fix(deps): require mcp>=1.19 for CallToolResult passthrough#855
jujumilk3 wants to merge 1 commit intoanthropics:mainfrom
jujumilk3:fix/mcp-min-version-for-call-tool-result

Conversation

@jujumilk3
Copy link
Copy Markdown

Since #717, create_sdk_mcp_server() returns a CallToolResult from its @server.call_tool() handler. The mcp low-level server only has the isinstance(results, CallToolResult) passthrough branch starting at mcp==1.19.0. On mcp<=1.18.x, CallToolResult (a pydantic BaseModel) falls into the iterable branch, yielding (field, value) tuples that fail ContentBlock union validation with 20 errors (4 fields x 5 union members).

Bump the minimum mcp constraint so every install has the passthrough branch #717 relies on.

mcp<1.19.0 makes every SDK MCP tool call fail with:

pydantic_core._pydantic_core.ValidationError: 20 validation errors for CallToolResult
content.0.TextContent
  Input should be a valid dictionary or instance of TextContent
  [type=model_type, input_value=('meta', None), input_type=tuple]
content.1.TextContent
  ... input_value=('content', [TextContent(...)]) ...
content.2.TextContent
  ... input_value=('structuredContent', None) ...
content.3.TextContent
  ... input_value=('isError', False) ...

The four tuples are exactly the four fields of CallToolResult in declaration order (meta, content, structuredContent, isError) — the fingerprint of a pydantic BaseModel being iterated as a list.

Root cause

The mcp low-level server's result-normalization dispatch lives in mcp/server/lowlevel/server.py::call_tool. From mcp>=1.19.0 it starts with:

if isinstance(results, types.CallToolResult):
    return types.ServerResult(results)
elif isinstance(results, tuple) and len(results) == 2:
    ...
elif isinstance(results, dict):
    ...
elif hasattr(results, "__iter__"):
    unstructured_content = cast(UnstructuredContent, results)

In mcp<=1.18.x the first isinstance(results, CallToolResult) branch does not exist. A CallToolResult is a pydantic BaseModel, which:

  • fails isinstance(_, tuple)
  • fails isinstance(_, dict)
  • passes hasattr(_, "__iter__") ✓ (pydantic models yield (field, value) tuples)

So mcp<=1.18 falls into the iterable branch, does content=list(unstructured_content)[('meta', None), ('content', [TextContent(...)]), ('structuredContent', None), ('isError', False)], and re-wraps those tuples in CallToolResult(content=...), triggering 20 ContentBlock union errors (4 tuples × 5 union members).

#717's description said "The MCP framework already checks isinstance(results, CallToolResult) and passes it through as-is (confirmed in MCP server source)" — which is correct, but only from mcp==1.19.0 onward. The min-version bump was missed.

mcp versions checked

Downloaded each sdist from PyPI and grepped src/mcp/server/lowlevel/server.py for the isinstance(results, types.CallToolResult) branch:

version passthrough branch?
1.13.1
1.14.0
1.18.0
1.19.0 ✅ (first version with the branch)
1.20.0+

Since anthropics#717, create_sdk_mcp_server() returns a CallToolResult from its
@server.call_tool() handler. The mcp low-level server only has the
isinstance(results, CallToolResult) passthrough branch starting at
mcp==1.19.0. On mcp<=1.18.x, CallToolResult (a pydantic BaseModel) falls
into the iterable branch, yielding (field, value) tuples that fail
ContentBlock union validation with 20 errors (4 fields x 5 union members).

Bump the minimum mcp constraint so every install has the passthrough
branch anthropics#717 relies on.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant