Summary
MCP App widgets do not render in Claude Cowork Desktop when deploymentMode: "3p" (third-party inference: gateway / Bedrock / Vertex / Foundry), despite identical MCP protocol negotiation between 1P and 3P modes. The same MCP server, the same Cowork Desktop binary, the same conversation flow renders MCP App iframes in 1P Chat and 1P Cowork but produces plain-text-only output in 3P Cowork. No public documentation lists MCP Apps as unsupported in 3P, and no configuration flag re-enables it.
Evidence
1. Identical MCP protocol negotiation across all three modes
A server-side diagnostic tool reading _client_params from the FastMCP session reports byte-for-byte identical handshakes in 1P Chat, 1P Cowork, and 3P Cowork:
{
"clientInfo": {
"name": "local-agent-mode-<server>",
"version": "1.0.0"
},
"protocolVersion": "2025-11-25",
"uiExtensionSupported": true,
"capabilities": {
"experimental": null,
"sampling": false,
"elicitation": false,
"roots": true,
"tasks": false,
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
The Cowork tabs (1P and 3P) use the local-agent-mode-{server} clientInfo identity (Cowork VM runtime path). The 1P Chat tab uses claude-ai clientInfo with empty capabilities — and still renders iframes correctly, despite not advertising the UI extension. So extension advertising is not the differentiator.
2. Tool definitions identical
Both nested _meta.ui.resourceUri and the deprecated flat _meta["ui/resourceUri"] keys are emitted on every UI-bearing tool (the latter as a workaround for #71). Identical across all three modes via FastMCP's standard registration.
3. Tool calls succeed; only the iframe is missing
In 3P Cowork:
tools/call mcp__<server>__<tool> returns successfully with content (text) and structuredContent (JSON).
- The conversation transcript shows the text result.
- The iframe is never mounted.
- No error surfaces in the UI.
In 1P Cowork (same Cowork Desktop binary, same MCP server, only deploymentMode differs):
- Same tool call succeeds.
- The iframe is mounted correctly and renders the widget.
4. tools/call and resources/read are not visible in MCP server logs in either Cowork mode
Cowork's VM runtime routes MCP traffic through LocalAgentModeSessions IPC (mcpCallTool, mcpReadResource, mcpListResources), bypassing the stdio transport. This is normal architecture and applies equally to 1P Cowork and 3P Cowork — but it means we can't observe whether 3P even attempts resources/read ui://.... The fact that the iframe never mounts is observable directly in the UI; whether the host called resources/read first is opaque from the server side.
The 1P Chat path (claude-ai stdio client) is the only mode where tools/call → resources/read ui://... → iframe mount is visible in MCP logs, and it works.
Reproduce
- Stand up an MCP server using
@modelcontextprotocol/ext-apps/server (TypeScript) or FastMCP (Python). Register a tool with _meta.ui.resourceUri pointing to a text/html;profile=mcp-app resource. Use stdio transport.
- Configure Cowork Desktop with first-party Anthropic inference. Quit and relaunch. Trigger the tool. Confirm the iframe renders.
- Reconfigure Cowork Desktop with
inferenceProvider: "gateway" (or bedrock / vertex / foundry) and the matching credentials. Quit and relaunch. Trigger the same tool. The iframe does not render; only text appears.
The MCP server, the Cowork Desktop binary, and the user-level prompt are unchanged between steps 2 and 3 — the only variable is deploymentMode.
Already excluded as the cause
Likely root cause (best guess; please correct)
The MCP Apps iframe-mount logic in the Cowork Desktop renderer is gated on something in the 1P deployment that 3P lacks. Two non-exclusive candidates:
- The Chat-tab renderer code path (which
claude-ai clientInfo uses) and the 1P Cowork-tab code path both include the blob:-URL iframe injection logic. The 3P Cowork-tab path does not, despite using the same local-agent-mode-{server} clientInfo.
- The artifact-preview iframe subsystem (documented dependencies on
api.anthropic.com / www.claudeusercontent.com, classified as a "non-essential service") provides infrastructure that MCP Apps rendering depends on, and that infrastructure is not fully wired in the 3P Cowork code path.
Either way: from the MCP server's perspective the surface is correct (capability negotiated, tool returns structured result, _meta.ui.resourceUri advertised in both formats). The gap is in the host renderer, conditioned on deploymentMode.
Documentation gap (also worth fixing)
The official Cowork 3P feature matrix lists "Local MCP" and "Remote MCP" as supported in 3P but does not mention MCP Apps (the io.modelcontextprotocol/ui extension) as a separately tracked feature. Either:
- 3P should support MCP Apps (and this issue is a bug to fix), or
- 3P intentionally does not support MCP Apps (and the feature matrix should explicitly list it as unsupported, similar to how "Anthropic 1P Connectors" is listed as
— *).
Environment
Cross-references
Suggested resolution paths
- Preferred: extend the Cowork Desktop renderer's iframe-mount logic to apply uniformly across
deploymentMode: "1p" and "3p" for the Cowork tab (so MCP Apps work parity with 1P).
- Alternative: if 3P intentionally does not support MCP Apps for architectural reasons (e.g. dependence on Anthropic-session state), document this in the Cowork 3P feature matrix explicitly, so MCP server developers know not to expect rendering and can degrade gracefully.
Either resolution closes the silent-failure mode for developers building MCP Apps against the SEP-1865 spec who deploy through 3P.
Summary
MCP App widgets do not render in Claude Cowork Desktop when
deploymentMode: "3p"(third-party inference: gateway / Bedrock / Vertex / Foundry), despite identical MCP protocol negotiation between 1P and 3P modes. The same MCP server, the same Cowork Desktop binary, the same conversation flow renders MCP App iframes in 1P Chat and 1P Cowork but produces plain-text-only output in 3P Cowork. No public documentation lists MCP Apps as unsupported in 3P, and no configuration flag re-enables it.Evidence
1. Identical MCP protocol negotiation across all three modes
A server-side diagnostic tool reading
_client_paramsfrom the FastMCP session reports byte-for-byte identical handshakes in 1P Chat, 1P Cowork, and 3P Cowork:{ "clientInfo": { "name": "local-agent-mode-<server>", "version": "1.0.0" }, "protocolVersion": "2025-11-25", "uiExtensionSupported": true, "capabilities": { "experimental": null, "sampling": false, "elicitation": false, "roots": true, "tasks": false, "extensions": { "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } } } }The Cowork tabs (1P and 3P) use the
local-agent-mode-{server}clientInfo identity (Cowork VM runtime path). The 1P Chat tab usesclaude-aiclientInfo with empty capabilities — and still renders iframes correctly, despite not advertising the UI extension. So extension advertising is not the differentiator.2. Tool definitions identical
Both nested
_meta.ui.resourceUriand the deprecated flat_meta["ui/resourceUri"]keys are emitted on every UI-bearing tool (the latter as a workaround for #71). Identical across all three modes via FastMCP's standard registration.3. Tool calls succeed; only the iframe is missing
In 3P Cowork:
tools/call mcp__<server>__<tool>returns successfully withcontent(text) andstructuredContent(JSON).In 1P Cowork (same Cowork Desktop binary, same MCP server, only
deploymentModediffers):4.
tools/callandresources/readare not visible in MCP server logs in either Cowork modeCowork's VM runtime routes MCP traffic through
LocalAgentModeSessionsIPC (mcpCallTool,mcpReadResource,mcpListResources), bypassing the stdio transport. This is normal architecture and applies equally to 1P Cowork and 3P Cowork — but it means we can't observe whether 3P even attemptsresources/read ui://.... The fact that the iframe never mounts is observable directly in the UI; whether the host calledresources/readfirst is opaque from the server side.The 1P Chat path (
claude-aistdio client) is the only mode wheretools/call→resources/read ui://...→ iframe mount is visible in MCP logs, and it works.Reproduce
@modelcontextprotocol/ext-apps/server(TypeScript) or FastMCP (Python). Register a tool with_meta.ui.resourceUripointing to atext/html;profile=mcp-appresource. Use stdio transport.inferenceProvider: "gateway"(orbedrock/vertex/foundry) and the matching credentials. Quit and relaunch. Trigger the same tool. The iframe does not render; only text appears.The MCP server, the Cowork Desktop binary, and the user-level prompt are unchanged between steps 2 and 3 — the only variable is
deploymentMode.Already excluded as the cause
_meta["ui/resourceUri"]key. Both formats are emitted; behavior in 3P unchanged.visibility: hiddenwhen server uses HTTP transport (works with native stdio) #149 — HTTP-transport iframe-mount bug. We use stdio.www.claudeusercontent.com) is reachable from the 3P machine; 1P Cowork on the same machine renders iframes successfully.app.connect()hangs. The View never gets mounted in 3P, soapp.connect()is not even reached._metakeys are present (Evidence §2).Likely root cause (best guess; please correct)
The MCP Apps iframe-mount logic in the Cowork Desktop renderer is gated on something in the 1P deployment that 3P lacks. Two non-exclusive candidates:
claude-aiclientInfo uses) and the 1P Cowork-tab code path both include the blob:-URL iframe injection logic. The 3P Cowork-tab path does not, despite using the samelocal-agent-mode-{server}clientInfo.api.anthropic.com/www.claudeusercontent.com, classified as a "non-essential service") provides infrastructure that MCP Apps rendering depends on, and that infrastructure is not fully wired in the 3P Cowork code path.Either way: from the MCP server's perspective the surface is correct (capability negotiated, tool returns structured result,
_meta.ui.resourceUriadvertised in both formats). The gap is in the host renderer, conditioned ondeploymentMode.Documentation gap (also worth fixing)
The official Cowork 3P feature matrix lists "Local MCP" and "Remote MCP" as supported in 3P but does not mention MCP Apps (the
io.modelcontextprotocol/uiextension) as a separately tracked feature. Either:— *).Environment
gateway(other providers untested but likely affected — same renderer path)AppConfig(resource_uri=..., visibility=...)standard registration; emits both nested_meta.ui.resourceUriand deprecated flat_meta["ui/resourceUri"](workaround for Claude.ai only recognizes MCP App tools via deprecated flat key _meta["ui/resourceUri"], ignores nested _meta.ui.resourceUri #71)text/html;profile=mcp-appCross-references
_metakey (mitigated, but irrelevant to this 3P issue)visibility: hiddenwhen server uses HTTP transport (works with native stdio) #149 — HTTP-transport iframe bug (different transport)app.connect()hangs (1P; different failure mode)tool-inputheld until tool completion deadlocks deferred tool results (ext-apps#390) #170 — tool-input held until tool completion (architecture relevant; explains why the text result completes normally when no iframe is mounted)resources/readmodelcontextprotocol/ext-apps#482 — host runtime not starting afterresources/read(similar symptom on 1P)Suggested resolution paths
deploymentMode: "1p"and"3p"for the Cowork tab (so MCP Apps work parity with 1P).Either resolution closes the silent-failure mode for developers building MCP Apps against the SEP-1865 spec who deploy through 3P.