[Cowork UI] "Unhandled case: [object Object]" banner — MCP tool-result envelope + SDK stream-end-with-error renderer paths
Cowork UI bug report — Unhandled case: [object Object]
Summary
The Claude desktop application's Cowork mode renders an error banner with the
literal text Unhandled case: [object Object] when a tool returns a successful
MCP CallToolResult whose content is a JSON-serialized object containing anerror key (instead of a proper JSON-RPC error response).
The banner appears at the top of the chat with a red bar. Closing and
re-opening the session usually clears it.
Environment
- Claude desktop version: 1.7196.0.0 (x64)
- OS: Windows 11 (64-bit)
- Cowork mode (research preview)
- Date observed: 2026-05-14
Reproduction
- Configure a stdio MCP server whose tool handler catches all exceptions and,
on failure, returns a successful result containing JSON of the form:
``python``
return [types.TextContent(type='text',
text=json.dumps({'error': str(e), 'tool': name}))]
- Call the tool in a way that triggers the exception branch.
- The Cowork UI displays a red banner:
Unhandled case: [object Object]
- The actual error JSON is correctly delivered to the assistant turn — only
the UI banner is broken.
A minimal reproducer is the case where an MCP wrapper passes an unsubstituted"${BW_SESSION}" placeholder into the spawned server's environment, but any
"valid result, contains 'error' key" pattern triggers the same UI path.
Expected behavior
The UI should either:
- Render the error JSON as text (with formatting), or
- Recognize the
{ error: ... }shape and surface it like a JSON-RPC error
response (showing error.message).
Observed (incorrect) behavior
The UI's error renderer falls into a default switch branch and stringifies
the JS error object with the implicit toString(), producing the literal[object Object]. This makes the banner uninformative and the underlying
problem invisible without inspecting View output logs.
Workaround at the MCP-server side
In our plugin we replaced
except Exception as e:
return _ok({'error': str(e), 'tool': name})
with
except Exception:
logger.exception(f"Tool error: {name}")
raise # let the mcp framework convert to JSON-RPC error
Once tools raise instead of wrapping the error in a "successful" envelope,
the Cowork UI renders errors correctly (Error.message in the banner).
Suggested fix
In the Cowork client's tool-result renderer, the default branch of the
switch handling tool result content should either:
- Pretty-print the unknown object (
JSON.stringify(value, null, 2)), or - Detect the common
{ error: string, tool?: string }envelope shape and
render it as an error.
Either change avoids the [object Object] artifact without requiring any
server-side change.
Context / chat reference
This issue was reproducible reliably in our cegcsoport-mail MCP plugin
during a debugging session on 2026-05-14, before we patched the server to
re-raise exceptions. The chat session id where it occurred waseb87764f-d944-42bd-9b4a-ce0e79aa6fdc (recorded by the VSCode extension's
session log at 17:34 local time).
---
Update — Second code path (added 2026-05-14 18:41)
After patching all of our MCP plugins (so they raise instead of returning_ok({'error': ...}) envelopes) and adding a defensive_ensure_no_error_envelope decorator, the [object Object] banner
still appears, especially under parallel sessions. The actual trigger
is now demonstrably NOT an MCP tool-result envelope — it is a
stream-end-with-error event in the Claude Code SDK that the Cowork UI
renderer also fails to handle.
Observed log sequence (VS Code Claude extension,
cc_version=2.1.141.055, cc_entrypoint=claude-vscode)
Session id: 2c87ceb4-9eed-41af-a6ba-4d163bdb04a7, title /start.
Occurrence 1, 18:36:
18:36:38 /start indul (agent:builtin:Plan)
18:36:44 Streaming stall detected: 84.5s gap between events
18:36:45 bitwarden MCP server connected after 18.9s (slow!)
18:36:54 Streaming completed with 1 stall(s), total stall time: 84.5s
18:36:54 Second API request created (source=sdk, retry)
18:37:03 sdk_stream_ended_no_result {"had_error":true,
"subagent_count":0,
"message_count":146}
18:37:14 Stream re-starts
18:37:26 AskUserQuestion permission requested
[user sees `Unhandled case: [object Object]` banner]
Occurrence 2, 18:40 (same session, ~3 minutes later):
18:40:15 Stream idle 15s (0 bytes received)
18:40:26 Slow first byte: no stream chunk 30.0s after request sent (attempt 1)
18:40:27 Stream finally starts
18:40:42 Stream idle 15s (701 bytes)
18:40:45 Write tool validation error: "File has been modified since read,
either by the user or by a linter. Read it again before
attempting to write it."
18:40:50 Read tool dispatched (success)
18:40:57 Stream idle 30s
18:40:57 sdk_stream_ended_no_result {"had_error":true,
"subagent_count":0,
"message_count":155}
18:41:04 Streaming stall 36.6s gap
[user sees `Unhandled case: [object Object]` banner]
Pattern (both occurrences)
- The Claude API responds slowly (30+ second first-byte / idle periods).
- An SDK-level error happens (in occurrence 2 it was a
Write tool validation error: File has been modified since read).
- The SDK aborts the current stream and emits the webview event
sdk_stream_ended_no_result { had_error: true, ... }.
- The Cowork renderer receives an
Errorobject (or similar) it does not
handle and falls into the same default switch branch as before,
stringifying it as [object Object].
Why our MCP-server fixes do not solve this case
We verified by source audit that all of our MCP plugins are now clean
of _ok({'error': ...}) envelopes (cegcsoport-mail, cegcsoport-cpanel,pg-cegcsoport-ro, helpgoweb, plus _ensure_no_error_envelope
decorator on the two large servers). The trigger has moved upstream into
the SDK/UI layer.
Aggravating factor: parallel sessions
The bug fires noticeably more often with 3–4 parallel sessions running.
Likely contributors:
- shared file-system state → more
Write tool validation errorcases - competing MCP server startups → longer initial stalls
- multiple long-lived streams → more occasions for stream-end-with-error.
Updated suggested fix
In addition to the original suggestion (handle { error: string } in the
tool-result renderer), the renderer that processessdk_stream_ended_no_result { had_error: true, ... } should also:
- Inspect
eventData.error(or whatever it carries) and surface
error.message instead of relying on implicit toString().
- Provide a more informative banner — at minimum the
error.constructor.name and message, optionally the request id
(x-client-request-id) so users can correlate with their output logs.
A defensive JSON.stringify(value, getCircularReplacer(), 2) would also be
strictly better than the current [object Object] artifact, even if the
shape isn't recognised.
Workarounds that we found practical
- After the banner appears, closing and reopening the session usually
recovers it.
- Reducing the number of simultaneously running parallel sessions reduces
the frequency.
- Avoiding the bare
/startslash command (multiple matching skills) and
using fully-qualified names like /productivity:start reduces ambiguity
during the slow-startup window.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗