Skip to content

Fix CallToolResult handling across all SDKs#1049

Open
stephentoub wants to merge 6 commits intomainfrom
stephentoub/fix-call-tool-result-handling
Open

Fix CallToolResult handling across all SDKs#1049
stephentoub wants to merge 6 commits intomainfrom
stephentoub/fix-call-tool-result-handling

Conversation

@stephentoub
Copy link
Copy Markdown
Collaborator

Problem

When a tool handler returns an MCP CallToolResult object ({ content: [{type, text/data/resource}], isError?: boolean }), all four SDKs JSON-serialize it and send the raw JSON string over RPC. The LLM then sees '{"content":[{"type":"text","text":"actual result"}]}' instead of the actual tool output.

Fixes #937

Approach

Each SDK's tool result normalization path now detects the CallToolResult shape before the JSON-serialize fallback and converts it to the SDK's native ToolResultObject format:

  • Text content blocks → textResultForLlm (multiple blocks joined with \n)
  • Image content blocks → binaryResultsForLlm with type: "image"
  • Resource content blocks → text goes to textResultForLlm, blob goes to binaryResultsForLlm
  • isError: trueresultType: "failure"

The .NET SDK additionally handles Microsoft.Extensions.AI content types (TextContent, DataContent, and unknown AIContent subtypes via AIJsonUtilities serialization), since the MCP C# SDK's McpClientTool returns these types from InvokeCoreAsync.

Changes by SDK

Node.js (nodejs/src/types.ts, session.ts, client.ts)

  • isCallToolResult() type guard + convertCallToolResult() converter
  • Integrated into both _executeToolAndRespond (session API) and normalizeToolResultV2 (client API)
  • Defensive guards for malformed input (optional chaining on resource, typeof check on text)

Python (python/copilot/tools.py)

  • _is_call_tool_result() + _convert_call_tool_result() (private helpers)
  • Integrated into _normalize_result

Go (go/definetool.go)

  • convertCallToolResult() with JSON marshal/unmarshal fallback for typed Go structs
  • Integrated into normalizeResult

.NET (dotnet/src/Types.cs, Session.cs, Client.cs)

  • ToolResultObject.TryConvertFromCallToolResult(object?) — detects JsonElement with CallToolResult shape
  • ToolResultObject.TryConvertFromAIContent(object?) — handles TextContent, DataContent, IEnumerable<AIContent>, with fallback serialization for unknown subtypes
  • Integrated into both ExecuteToolAndRespondAsync (session) and client v2 tool response path

Tests

  • Node.js: 21 unit tests in nodejs/test/call-tool-result.test.ts
  • Go: 9 subtests in TestConvertCallToolResult
  • Python: 13 tests in TestCallToolResult class
  • Coverage includes: text-only, multiple text, isError mapping, image content, resource text/blob, mixed content, empty content, malformed input rejection, and edge cases

When a tool handler returns an MCP CallToolResult object
({ content: [...], isError?: bool }), all four SDKs were
JSON-serializing it instead of converting it to ToolResultObject.
This caused the LLM to see raw JSON instead of actual tool output.

Add detection and conversion of CallToolResult in Node.js, Python,
Go, and .NET. The .NET SDK additionally handles Microsoft.Extensions.AI
content types (TextContent, DataContent, and unknown subtypes via
AIJsonUtilities serialization).

Fixes #937

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@stephentoub stephentoub requested a review from a team as a code owner April 9, 2026 00:51
Copilot AI review requested due to automatic review settings April 9, 2026 00:51
stephentoub and others added 2 commits April 8, 2026 20:54
Run prettier on Node.js files, ruff format on Python files,
and remove unused ToolResultObject import from test file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes tool result normalization so MCP CallToolResult objects are converted into each SDK’s native ToolResultObject shape (instead of being JSON-stringified and shown to the LLM as raw JSON).

Changes:

  • Add CallToolResult detection + conversion logic in Node, Python, Go, and .NET tool-result normalization paths.
  • Integrate conversion into both “session” and “client v2” execution paths (where applicable).
  • Add/extend unit tests for CallToolResult scenarios (Node/Python/Go).
Show a summary per file
File Description
python/e2e/test_tools_unit.py Adds Python unit tests covering CallToolResult normalization behavior.
python/copilot/tools.py Implements CallToolResult shape detection and conversion in Python normalization.
nodejs/test/call-tool-result.test.ts Adds Node unit tests for CallToolResult type guard + conversion.
nodejs/src/types.ts Introduces CallToolResult type, isCallToolResult, and convertCallToolResult.
nodejs/src/session.ts Converts CallToolResult in the session tool execution path before JSON stringify fallback.
nodejs/src/index.ts Exposes CallToolResult helpers/types in the public entrypoint exports.
nodejs/src/client.ts Converts CallToolResult in the client v2 tool result normalization path.
go/definetool.go Adds CallToolResult conversion in Go normalization logic.
go/definetool_test.go Adds Go tests for CallToolResult conversion behavior.
dotnet/src/Types.cs Adds conversion helpers for CallToolResult (JsonElement) and AIContent tool results.
dotnet/src/Session.cs Integrates new conversion helpers into session tool execution.
dotnet/src/Client.cs Integrates new conversion helpers into client v2 tool-call handling.

Copilot's findings

  • Files reviewed: 12/12 changed files
  • Comments generated: 7

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1049

@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1049

Sort imports in copilot.tools import block to satisfy ruff I001 rule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Generated by SDK Consistency Review Agent for issue #1049

Copy link
Copy Markdown
Collaborator Author

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Fixed the inconsistency.

These are internal implementation details used by session.ts and client.ts.
Go and Python already keep them private (lowercase/underscore-prefixed).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

Cross-SDK Consistency Review ✅

This PR consistently implements CallToolResult handling across all four SDKs. The feature is well-aligned:

Behavior Node.js Python Go .NET
Text blocks → textResultForLlm
Multiple text blocks joined with \n
Image blocks → binaryResultsForLlm
Resource text → textResultForLlm
Resource blob → binaryResultsForLlm
isError: trueresultType: "failure"
Empty binary list → omit/null field
Malformed input guards
Integrated into both session & client paths

The .NET SDK additionally handles Microsoft.Extensions.AI content types (TextContent, DataContent, IEnumerable<AIContent>) via TryConvertFromAIContent. This is intentionally .NET-specific because the MCP C# SDK's McpClientTool returns these types from InvokeCoreAsync — there is no equivalent abstraction in the other language ecosystems.

No cross-SDK consistency issues found.

Generated by SDK Consistency Review Agent for issue #1049 ·

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.

SDK does not accept CallToolResult type of MCP/SDK type for Tool Response

2 participants