Repository navigation
fix(mcp): keep every content block in MCP tool results - #1960
Conversation
executeToolCall only read result.content[0]. Text in later blocks was dropped, and a leading image block reached the model as base64 JSON. Join text blocks with newlines. Summarise image, audio and binary resource blocks by type and decoded size. Keep embedded text resources as text and other unknown blocks as JSON. Closes Nano-Collective#1932 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nc-review: needs work — 1 blocking, 2 nits@Mahdi-Hasan — there is a blocking item below. The change replaces MCPClient.executeToolCall's
🔴 blocking · PR #1948 ( The author is explicit about this in the PR description ("Note: #1948 also targets this issue. I claimed #1932 on the issue and addyCooks asked me to pick it up"), which suggests the duplication is already known and sanctioned — so the resolution is likely a maintainer closing #1948 rather than a change to this diff. Please confirm that before merge so the queue does not end up with two competing implementations of the same formatter. ⚪ nit · An embedded resource carrying text is rendered as the bare text, dropping its if (typeof resource.text === 'string') {
return resource.text;
}When a tool returns two embedded resources from different files, the two texts are concatenated with no marker, so the model cannot tell which file each came from. Prefixing with the URI (the blob branch already emits it) — e.g. ⚪ nit ·
const padding = data.endsWith('==') ? 2 : data.endsWith('=') ? 1 : 0;
return Math.max(0, Math.floor((data.length * 3) / 4) - padding);The MCP spec requires raw base64, so this is right for conforming servers, but servers that send a data URL ( 🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with |
Prefix an embedded text resource with its URI, so texts from different resources stay apart. Strip a data URL prefix and whitespace before measuring base64, so the placeholder size is right for servers that send them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed the nc-review nits in 29546c8:
The duplicate flag (#1948) is for a maintainer to decide. I claimed #1932 on the issue, as addyCooks asked. |
addyCooks
left a comment
There was a problem hiding this comment.
Thanks @Mahdi-Hasan, the block formatting does what #1932 asks, and mcp-client.spec.ts passes on this branch (91 tests). The branch now conflicts with main in source/mcp/mcp-client.ts, because #1926 (the isError flag) and #1651 changed the same function. Could you merge main and:
- keep your
formatToolResultContent(result.content), and return it through #1926'soutput(...)wrapper so a failed call still returns{llmContent, isError: true} - update the test "a non-text first part keeps its JSON text and is still flagged": with your change its image block now becomes
[image/png, 1 bytes]instead of JSON, which is the intended behaviour, so the expectation needs updating
Happy to re-review once it's updated.
Resolve the conflict in MCPClient.executeToolCall with Nano-Collective#1926: keep formatToolResultContent(result.content) and return it through the output() wrapper, so a failed call still returns {llmContent, isError}. Update the isError spec: a non-text block is now summarised as "[image/png, 1 bytes]" instead of JSON, and a failed call with several blocks keeps all of them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@addyCooks Thanks! Merged main in addd206:
I also removed the "Generated with Claude Code" line from this description, as on #1961. |
addyCooks
left a comment
There was a problem hiding this comment.
Thanks @Mahdi-Hasan, that covers it. I checked this merged with current main: the type check and Biome pass, the MCP, tool-manager and tool-catalog specs pass (207), and the new tests fail without the change. A failed call with several blocks still returns {llmContent, isError: true} through #1926's output(...).
Approving 🎉
Description
MCPClient.executeToolCallonly readresult.content[0]. Text in later blocks was dropped, and a leading image block reached the model as raw base64 JSON. Fixes #1932.Every block is now formatted, in order, and joined with newlines:
textimage,audio[image/png, 3 bytes](MIME type and decoded size, no base64; adata:prefix and line breaks are ignored when measuring)resourcewithtext[resource <uri>]then the textresourcewithblob[resource file:///b.bin, application/zip, 2 bytes]resource_link[resource link: file:///c.md]Tool executed successfully (no output).blockCount.output(...)wrapper, so a failed call still returns{llmContent, isError: true}.Type of Change
Changeset
pnpm changeset) describing this change for the changelogTesting
Automated Tests
New features include passing tests in
.spec.ts/tsxfilesAll existing tests pass (
pnpm test:allcompletes successfully)Tests cover both success and error scenarios
6 new tests in
source/mcp/mcp-client.spec.ts: multiple text blocks, image and audio with later text, text and blob resources plus a resource link, data URL and wrapped base64 sizes, unknown blocks, empty content.The text-block and image tests fail on
mainwith the exact output from the issue.mcp-client.spec.ts: 91 pass, 8 skipped (live tests). Types, biome and knip pass.Developed on Windows, where
pnpm test:avahas unrelated failures onmain(POSIX paths). CI on Linux should settle it.Manual Testing
Checklist