[BUG] MCP App widgets lose the tool result's _meta when a Code-tab conversation is reloaded
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
- 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,…" } } }. - In a Code-tab session, call the tool. The widget renders, and its
ui/notifications/tool-resultcarries_meta. - Call it a second time, then relaunch the app (or reopen the session).
- Neither widget's
ui/notifications/tool-resultcarries_metanow. 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)
v1/cef1e6037-CWGzET4T.jsbuilds the widget's tool result. It takes_metaonly from an object-shapedtoolUseResult:
``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.
v1/c06cf64bb-XzOmj9Qz.jsreplays transcript lines. It readsmcpMeta(D=s.mcpMeta) and passes it toNs(…), butNsattaches 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-resultwith onlycontentandisError, andstructuredContentis missing. That matches whatke()produces from an array-shapedtoolUseResult, 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)inke()), socontentsurvives a replay while_metaandstructuredContentdon't.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗