Fix CallToolResult handling across all SDKs#1049
Conversation
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>
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>
There was a problem hiding this comment.
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>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1049
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1049
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>
Cross-SDK Consistency Review ✅This PR consistently implements
The .NET SDK additionally handles No cross-SDK consistency issues found.
|
Problem
When a tool handler returns an MCP
CallToolResultobject ({ 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
CallToolResultshape before the JSON-serialize fallback and converts it to the SDK's nativeToolResultObjectformat:textResultForLlm(multiple blocks joined with\n)binaryResultsForLlmwithtype: "image"textResultForLlm, blob goes tobinaryResultsForLlmisError: true→resultType: "failure"The .NET SDK additionally handles Microsoft.Extensions.AI content types (
TextContent,DataContent, and unknownAIContentsubtypes viaAIJsonUtilitiesserialization), since the MCP C# SDK'sMcpClientToolreturns these types fromInvokeCoreAsync.Changes by SDK
Node.js (
nodejs/src/types.ts,session.ts,client.ts)isCallToolResult()type guard +convertCallToolResult()converter_executeToolAndRespond(session API) andnormalizeToolResultV2(client API)Python (
python/copilot/tools.py)_is_call_tool_result()+_convert_call_tool_result()(private helpers)_normalize_resultGo (
go/definetool.go)convertCallToolResult()with JSON marshal/unmarshal fallback for typed Go structsnormalizeResult.NET (
dotnet/src/Types.cs,Session.cs,Client.cs)ToolResultObject.TryConvertFromCallToolResult(object?)— detectsJsonElementwith CallToolResult shapeToolResultObject.TryConvertFromAIContent(object?)— handlesTextContent,DataContent,IEnumerable<AIContent>, with fallback serialization for unknown subtypesExecuteToolAndRespondAsync(session) and client v2 tool response pathTests
nodejs/test/call-tool-result.test.tsTestConvertCallToolResultTestCallToolResultclass