[BUG] MCP App widgets lose the tool result's _meta when a Code-tab conversation is reloaded

Status Open
Reported on v2.1.286
Maintainer reply None cached
Activity 1 comment · opened Oct 5, 2026

In the Claude desktop app's Code tab, an MCP App widget receives its tool result's _meta only while the result is live. After the conversation is loaded from its transcript (relaunching the app or reopening the session), every widget gets { content, isError } without _meta. That happens even though the transcript saved the full _meta. In the same session, the newest widget renders and every earlier one fails.

Use case

Our MCP server has a preview tool linked to an MCP App widget. The tool renders a page of a user's document as a PNG, and the widget shows it with a couple of action buttons. Each widget has to keep showing the image from its own tool call, not the document's latest state, so the image travels with the result.

We send the image in the result's _meta, as a base64 data URI, and keep content to one short sentence for the model. Putting the image in content as an image block would make it survive a reload. But the model would then receive every preview image, flooding the context with image tokens it has no use for. _meta is the part of a result that hosts keep from the model, so it is the right place for widget-only data. Because _meta is dropped on reload, every replayed widget has nothing to render and falls back to an error state.

Environment

  • Claude desktop app 2.19675.0 (Windows), Code tab
  • Claude Code 2.1.286 (transcript entrypoint: "claude-desktop")
  • Remote MCP server (claude.ai connector) using MCP Apps; protocol 2026-07-28

Steps to reproduce

  1. Connect an MCP server with a tool linked to an MCP App (_meta.ui.resourceUri). The tool returns its widget data in the result's _meta, for example { content: [{ type: "text", text: "The preview is shown." }], _meta: { "com.example/preview": { image: "data:image/png;base64,…" } } }.
  2. In a Code-tab session, call the tool. The widget renders, and its ui/notifications/tool-result carries _meta.
  3. Call it a second time, then relaunch the app (or reopen the session).
  4. Neither widget's ui/notifications/tool-result carries _meta now. Before the relaunch, the earlier widget fails too, while the newest one renders.

Expected: a widget loaded from the transcript gets the same _meta it got live.

What the transcript stores

Each tool result line in the session JSONL keeps the _meta, but under mcpMeta. toolUseResult is stored as a plain content array:

{
  "toolUseResult": [{ "type": "text", "text": "The preview is shown." }],
  "mcpMeta": { "_meta": { "com.example/preview": { "image": "data:image/png;base64,…" } } }
}

Where it is dropped (current Code-tab bundle)

  1. v1/cef1e6037-CWGzET4T.js builds the widget's tool result. It takes _meta only from an object-shaped toolUseResult:

``js
function A(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:void 0}
function ke(e){let t=A(e.toolUseResult), … if(r)return{content:r.map(_e),isError:e.isError,
...t?.structuredContent!=null&&{structuredContent:t.structuredContent},
...t?._meta!=null&&{_meta:t._meta}}}
``

A replayed toolUseResult is an array, so A() returns undefined. Both _meta and structuredContent are left out.

  1. v1/c06cf64bb-XzOmj9Qz.js replays transcript lines. It reads mcpMeta (D=s.mcpMeta) and passes it to Ns(…), but Ns attaches it only to error results:

``js
e.isError&&o.push(e) … return void 0!==s&&1===a&&1===o.length&&(o[0].resultMcpMeta=s), …
``

For a successful result, the saved _meta is never attached, and nothing in ke() reads resultMcpMeta.

Suggested fix

When replaying, attach mcpMeta._meta to successful tool results too. Have ke() fall back to it (for example t?._meta ?? e.resultMcpMeta?._meta), and restore structuredContent the same way.

Related reports

  • modelcontextprotocol/ext-apps#696 (open): Claude Desktop delivers ui/notifications/tool-result with only content and isError, and structuredContent is missing. That matches what ke() produces from an array-shaped toolUseResult, so it may be the same root cause. The workaround suggested there is to call the tool again from the widget. It doesn't work for us: the widget would then show the document's current state instead of the result it was created with.
  • #78387 (closed): the same notification, there with image blocks arriving in the Messages-API shape. The current bundle converts those back to the MCP shape before passing them to the widget (r.map(_e) in ke()), so content survives a replay while _meta and structuredContent don't.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗