Repository navigation
fix(mcp): report an MCP isError result as a failed tool call - #1926
Conversation
executeToolCall read the text out of a tool result but never looked at
isError, so a server that answered with isError: true reached
processToolUse as an ordinary string. ACP clients showed the call as
completed and nanocoder run --json filed the text under result.
The handler now returns the existing {llmContent, isError} output when the
server flags a failure, which processToolUse already carries onto the
ToolResult. The text the model receives is unchanged. A clean result is
still a plain string.
nc-review: comments — 1 nit@addyCooks — a few things worth a look, none blocking. The change makes MCPClient.callTool return the existing StructuredToolOutput shape ({llmContent, isError}) when a server answers with isError: true, so processToolUse propagates the failure flag to the ACP status, the --plain --json telemetry and the interactive display, while a clean result stays a plain string. I traced the flag through the registry handler, processToolUse, the ACP conversation, the plain runtime and the subagent path; all of them already read isError, and the two new tests would fail against the pre-change code, so the fix is correct and reasonably scoped. One edge case is worth a look: a failed MCP call with empty content still tells the model the tool "executed successfully". ⚪ nit · The failure flag travels to the client, but never to the model — the model only ever sees return 'Tool executed successfully (no output)';so the model is told the call succeeded while the editor now shows it as failed. The new spec even pins this text ( 🔴 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 |
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>
Description
An MCP server reports a failed call with
isError: trueand the reason incontent.MCPClientturned the content into text but never read the flag, so the call looked like a success downstream. ACP clients (the VS Code extension, the desktop app, editors such as Zed) showed it ascompleted, andnanocoder run --jsonfiled the text undertoolCalls[].resultinstead of.error. The model itself always received the text.executeToolCallnow returns the existing{llmContent, isError}output when the server setsisError: true, whichprocessToolUsealready carries ontoToolResult.isError(the same mechanismexecute_bashuses for a non-zero exit). The text the model receives is unchanged, and a clean result is still a plain string. ACP, the plain runtime and the interactive loop already readisError, so none of them needed a change.Checked with a stdio MCP server (
echoand an always-failingboom) behind a scripted OpenAI-compatible stub:boomwascompletedbefore and isfailednow;echostayscompleted.nanocoder --plain --json run:boommoved fromresulttoerror."boom failure").Type of Change
Changeset
pnpm changeset) describing this change for the changelogTesting
Automated Tests
.spec.ts/tsxfilespnpm test:allcompletes successfully)Manual Testing
Checklist