[Cowork UI] "Unhandled case: [object Object]" banner — MCP tool-result envelope + SDK stream-end-with-error renderer paths

Resolved 💬 3 comments Opened May 14, 2026 by hnatloz Closed Jun 15, 2026

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 an
error 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

  1. 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}))]
``

  1. Call the tool in a way that triggers the exception branch.
  2. The Cowork UI displays a red banner:

Unhandled case: [object Object]

  1. 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:

  1. Pretty-print the unknown object (JSON.stringify(value, null, 2)), or
  2. 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 was
eb87764f-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)

  1. The Claude API responds slowly (30+ second first-byte / idle periods).
  2. An SDK-level error happens (in occurrence 2 it was a

Write tool validation error: File has been modified since read).

  1. The SDK aborts the current stream and emits the webview event

sdk_stream_ended_no_result { had_error: true, ... }.

  1. The Cowork renderer receives an Error object (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 error cases
  • 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 processes
sdk_stream_ended_no_result { had_error: true, ... } should also:

  1. Inspect eventData.error (or whatever it carries) and surface

error.message instead of relying on implicit toString().

  1. 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 /start slash command (multiple matching skills) and

using fully-qualified names like /productivity:start reduces ambiguity
during the slow-startup window.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗