[BUG] [regression] MCP tool results invisible in 2.1.88 — outputSchema safeParse guard returns null on schema mismatch
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In Claude Code 2.1.88, all MCP tool results are invisible — the tool executes, data returns to the model context, but nothing renders in the terminal. The progress indicator briefly appears then the output area is blank. This affects ALL MCP servers (tested with two independent servers: fsuite and ShieldCortex memory).
Root Cause (verified via binary analysis)
2.1.88 added output schema validation in the tool result display component. The guard at two offsets (113884757 and 221458957 in the binary) reads:
let P = f.outputSchema?.safeParse(H.toolUseResult);
if (P && !P.success) return null;
let J = P?.data ?? H.toolUseResult;
The built-in MCP client tool declares its output schema as E.string() (Zod string validator), but MCP tool results are objects with content arrays — not plain strings. So safeParse always fails, and the component always returns null (renders nothing).
In 2.1.87, the same safeParse existed but was used passively:
// 2.1.87 — graceful fallback on parse failure
T = w.outputSchema?.safeParse(W);
k = T?.success ? T.data : void 0;
// rendering continues regardless
In 2.1.88 — hard bail:
// 2.1.88 — kills the render
P = f.outputSchema?.safeParse(H.toolUseResult);
if (P && !P.success) return null; // <-- NOTHING RENDERS
Steps to Reproduce
- Install Claude Code 2.1.88
- Configure any MCP server (stdio or SSE)
- Call any MCP tool
- Observe: progress indicator appears briefly, then output area is blank
- The model receives the result (it can reason about it), but the human sees nothing
Verification
- 2.1.87 binary: MCP tool output renders correctly (confirmed by swapping binary)
- 2.1.88 with patch: Changing
if(P&&!P.success)return nulltoif(P&&!P.success)P=null;(graceful fallback matching 2.1.87 behavior) restores all MCP tool output - Affects ALL MCP servers: Tested with two independent MCP servers — both invisible on 2.1.88, both visible on 2.1.87
What Should Happen?
When outputSchema.safeParse fails on a tool result, the renderer should fall back to rendering the raw result (2.1.87 behavior), not return null. The fix is one character change:
- if(P&&!P.success)return null;let J=P?.data??H.toolUseResult
+ if(P&&!P.success)P=null; let J=P?.data??H.toolUseResult
This preserves validation when the schema matches and falls through to raw rendering when it doesn't — exactly how 2.1.87 behaved.
Investigation Path (for reproducibility)
The root cause was found using fprobe (binary pattern scanner):
# Find the guard in the binary
fprobe scan ~/.local/share/claude/versions/2.1.88 \
--pattern 'outputSchema?.safeParse' --context 300
# Compare with 2.1.87
fprobe scan ~/.local/share/claude/versions/2.1.87 \
--pattern 'outputSchema?.safeParse' --context 300
The 2.1.88 changelog entry "Fixed StructuredOutput schema cache bug causing ~50% failure rate in workflows with multiple schemas" is likely related — the fix appears to have introduced this hard guard.
Error Messages/Logs
No error messages are shown to the user. MCP tool results silently disappear. The model still receives the data (it can reference tool output in its responses), but the terminal displays nothing.
Claude Model
Claude Opus 4.6
Is this a regression?
Yes, this worked in 2.1.87
Last Working Version
2.1.87
Claude Code Version
2.1.88
Platform
Anthropic API (Max plan)
Operating System
Ubuntu/Debian Linux (6.12.74+deb13+1-amd64)
Terminal/Shell
Bash
Additional Information
Binary patch workaround (apply to both JS bundle copies at offsets 113884757 and 221458957):
target = b"if(P&&!P.success)return null;let J=P?.data??H.toolUseResult"
fix = b"if(P&&!P.success)P=null; let J=P?.data??H.toolUseResult"
This was discovered and verified across 8+ restart cycles with controlled binary swaps between 2.1.87 and 2.1.88.
20 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Patch concept confirmed working. My claude (macOS, native install) had a different minified JS, so what ultimately worked was:
and then since macOS needed the codesign:
Not a duplicate. #41270 is a symptom report with no root cause analysis. #41271 is completely unrelated (slash commands). #25081 is about tools being dropped entirely, not about rendering.
This issue identifies the exact root cause: the
outputSchemasafeParse guard added in 2.1.88 (if(!P.success) return null) silently kills rendering for any MCP tool whose output doesn't match a declared schema. Includes the binary patch and independent confirmation from @trevyn. Please keep open.@trevyn Nice, glad the patch concept translates. Found the root cause via binary analysis using
fprobefrom fsuite. Same guard, just different minified variable names per platform. The codesign step on macOS is a good callout for others hitting this.Bug still present in 2.1.89 fwiw
Still present in 2.1.89: confirmed, same root cause, same binary fix works
We hit this on 2.1.88, binary-patched it, then got auto-updated to 2.1.89 and it came right back. The bug is byte-identical across both versions.
Root cause: The
safeParseguard on MCP tool output validates against a string schema, but MCP tools return objects. The validation always fails, and the guard returnsnullthus killing all MCP tool output rendering.How to apply (same-length byte replacement, safe for the binary):
The pattern has been identical across 2.1.87, 2.1.88, and 2.1.89. Each auto-update requires re-patching. Would be great to get an official fix as the
safeParseguard needs to either skip validation for MCP tool results or handle the schema mismatch gracefully instead of returning null.My Claude says your binary analysis is outstanding — tracing the exact regression from graceful fallback to hard guard, identifying the one-character fix, and verifying across 8+ controlled binary swaps between versions. This is the kind of root cause analysis that turns a "something broke" report into an immediately actionable fix for the team.
Still present in 2.1.94. Had Claude Code patch its own binary — same one-character fix works:
The irony of Claude fixing itself is not lost on me.
Confirmed locally that the same patch method still applies cleanly to
2.1.97from aWSL2terminal on Windows.My environment:
2.1.97Debian GNU/Linux 12 (bookworm)underWSL2on Windows6.6.87.2-microsoft-standard-WSL2 x86_64zshI tested the active binary at
~/.local/share/claude/versions/2.1.97.Before patch:
0d43fcd11d29206563eeef3a1f787f0615c21cd703cc91f3a180915fd5797ef6if(P&&!P.success)return null;let J=P?.data??H.toolUseResultfound 2 matches at offsets:
115427538(0x6e148d2)225188778(0xd6c1baa)Patch used:
Patch results:
2.1.97binary first2 replacements2 replacements3e133cfaa535313414c0f851f9746368ece3b90140c54d53b90dd250f4eee85d0 matches2 matches2.1.97 (Claude Code)via--versionSo on the current
2.1.97setup I tested, the regression pattern is still present and the same one-character fallback patch still lands cleanly. This was from aWSL2terminal on Windows rather than a native Linux install.One precision note: this verification pass was binary-level confirmation plus executable sanity, not a fresh interactive TUI repro after patching. But the guard is still there unchanged in
2.1.97, and the patch continues to apply exactly as expected.Follow-up to my earlier
2.1.97comment: this is still present in2.1.101on the sameWSL2setup, and the same fallback patch still works after adjusting for the new minified variable names.My environment:
2.1.101Debian GNU/Linux 12 (bookworm)underWSL2on Windows6.6.87.2-microsoft-standard-WSL2 x86_64zshI tested the active binary at
~/.local/share/claude/versions/2.1.101.Before patch:
d064b4f28cf0950f1f9c355b471fdefaae6c00cda1a8ea895c7518330cee0cd82.1.97literal no longer matched, but the same hard-bail render guard is still present with renamed minified vars:let J=A.outputSchema?.safeParse(H.toolUseResult);if(J&&!J.success)return null;let X=J?.data??H.toolUseResultfound 2 matches at offsets:
115599170226962058Patch used:
Exact
fprobeworkflow:Patch results:
fprobedry run reported2 replacements2 replacementsfprobecreated a backup at~/.local/share/claude/versions/2.1.101.bak1e01c7a91c711546318246690ab852f2fc86a2cc96bd2851da2b8b934d6fd3df0 matches2 matches2.1.101 (Claude Code)via--versionSo on the current
2.1.101Linux binary I tested, the regression is still there. The exact byte pattern from2.1.97changed because the minified variable names changed, but the behavior is the same: the renderer still bails out onoutputSchema.safeParse(...)failure instead of falling back to the raw MCP result.One precision note: like my
2.1.97check, this was binary-level confirmation plus executable sanity, not a fresh interactive TUI repro after patching. But the same render-killing guard is still present in2.1.101, and the fallback patch still lands cleanly at both occurrences.I checked the active Linux binary at
~/.local/share/claude/versions/2.1.104, and the same render-killingoutputSchema.safeParse(...)guard is still present in this version.My local verification:
2.1.104~/.local/share/claude/versions/2.1.104Before patch:
f5fe84d4b8a5a322b83a8ae63ac117adb143d2a9a0bfd73a201a5201d64238692.1.101workaround still matched in2.1.104let J=A.outputSchema?.safeParse(H.toolUseResult);if(J&&!J.success)return null;let X=J?.data??H.toolUseResultfound 2 matches at:
0x6e3e9e0(115599840)0xd8766a8(226977448)Patch used:
Patch results:
fprobedry run reported2 replacements2 replacementsfprobecreated a backup at~/.local/share/claude/versions/2.1.104.bak4bd53fb87500e7c5f6cd8dcf1beb6b930cda19787fbf96a8af582779b773c0fb0 matches2 matches2.1.104 (Claude Code)via--versionSo at least on the Linux
2.1.104binary I checked, this regression is still present and the same fallback patch still lands cleanly at both duplicated bundle locations.Does this mean that MCP tool results can be plain strings and not the structured result? Or is there a specific structure that would pass? I only use my own custom mcp servers so I would be happy to alter the output to make this work if I knew what the expectations were.
@cseickel my agent and I dug into this further, and the short answer is:
content: [{ type: "text", text: "..." }].structuredContentis optional in MCP.outputSchema, the spec says servers MUST provide structured results conforming to it, and clients SHOULD validate them.TextContentblock for backwards compatibility.Relevant spec refs:
So from a spec perspective, a server returning only:
is valid. It does not need
structuredContentjust to be a legal MCP tool result.I also ran a controlled 4-way repro on Claude Code
2.1.107with four MCP tools that differed only in whether they returned:content[].textonlycontent[].textonlycontent[].text+structuredContentcontent[].text+structuredContentObserved matrix:
plain_text_only-> blank bodyansi_text_only-> blank bodyplain_text_plus_structured-> body rendersansi_text_plus_structured-> body rendersSo on the current
2.1.107TUI:structuredContentcontent[].textresponses still reach the model, but are not shown in the user-facing bodyThat means:
structuredContentdoes make the result visible.contentis still valid and should not requirestructuredContentjust to be a valid MCP result.So if your goal is "make it show up today", adding
structuredContentis the most reliable workaround right now.If your goal is "make Claude Code behave correctly", then this looks like a second client-side rendering regression, separate from the earlier
outputSchema.safeParse(...)null-bail bug higher up in the thread.One nuance: if you want the TUI to show the human-facing text exactly (for example ANSI-colored output), adding
structuredContentis only a visibility workaround, not a perfect fix... on2.1.107it looks like Claude Code prefers rendering the structured JSON body oncestructuredContentis present.Follow-up to the original
outputSchema.safeParse(...)null-bail bug higher up in this thread.After patching
2.1.107with the same one-character fallback used for the earlier versions, MCP calls no longer disappear completely. But there still appears to be a second rendering regression in the TUI.I ran a controlled 4-way repro with a minimal stdio MCP server where the payload text stayed the same and only two variables changed:
content[].textonly vscontent[].text+structuredContentNone of the four test tools declared an
outputSchema, so the originalsafeParseguard cannot explain the blank rendering in this repro.Observed matrix on Claude Code
2.1.107:| |
content[].textonly |content[].text+structuredContent||---|---|---|
| plain text | blank body | body renders |
| ANSI text | blank body | body renders |
So on
2.1.107:structuredContentcontent[].textresponses still reach the model, but are not shown in the user-facing bodyRelevant MCP spec refs:
Per the MCP schema,
contentis required andstructuredContentis optional onCallToolResult. So a tool returning only:is still a valid MCP tool result.
That makes this look like a second client-side rendering regression, separate from the earlier schema-validation null-bail bug: valid
content[].text-only results are staying blank unlessstructuredContentis also present.Practical impact:
structuredContentis a workable visibility workaround on current builds2.1.107seems to prefer rendering the structured JSON body oncestructuredContentexistsIf helpful, I can post the minimal repro server I used, but the core result is the matrix above:
structuredContentpresence is the visibility gate, not ANSI.Small clarification on the repro: when ANSI-bearing output was visible, it still rendered plain/uncolored in the TUI rather than preserving ANSI color styling. So this still does not restore colored MCP output.
Ah interesting. I do love my colored output tho. Q: Have you seen any useful features since 2.1.88? I've just been running with the patched version of that, and having the actual source for reference is also very convenient.
In 2.1.119, we patched the structuredOutput path to allow ANSI color output to render:
@sosukesuzuki Thank you! Could you provide more details about the flavor of the fix and what version it will be available in?
I don't think it was actually completed, I think it was closed along with other duplicates of #45839, which is still open.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.