Skip to content

MCP-Apps UI Widget Host Runtime Not Starting After Successful resources/read #482

Open

Description

@chris-page-gov

Bug Report: MCP-Apps UI Widget Host Runtime Not Starting After Successful resources/read

Component: Claude client (claude.ai / Claude in Chrome extension)
Severity: Functional gap — MCP-Apps interactive widgets are non-functional
Date: 2026-02-14
Reporter: Chris Page (DSIT/Warwickshire County Council) — mcp-geo MCP server developer


Summary

Claude's client successfully completes MCP-Apps resource discovery and fetch (tools/call → resources/read of ui:// URIs), but never mounts the returned HTML widget in an iframe/webview or starts the postMessage bridge. Interactive MCP-Apps widgets are therefore completely non-functional despite the protocol's discovery and fetch phases working correctly.

This is a runtime interoperability bug, not a feature request. The client already participates in the MCP-Apps flow up to resource fetch — it simply does not complete the last mile.


Environment

  • MCP Server: mcp-geo (custom server providing UK geographic data via OS/ONS APIs)
  • Transport: STDIO adapter
  • Client fingerprint in trace: clientInfo.name=claude-ai, clientInfo.version=0.1.0
  • Client surfaces tested by operator: claude.ai web interface, Claude in Chrome extension
  • Widgets affected: All os_apps_render_* tools (boundary explorer, geography selector, statistics dashboard, route planner, feature inspector)

Expected Behaviour

  1. tools/call → os_apps_render_* → server returns ui://mcp-geo/{widget} in _meta.ui.resourceUri (and optionally resource_link content) ✅
  2. Client calls resources/read on the ui:// URI → server returns text/html;profile=mcp-app content ✅
  3. Client mounts the returned HTML in an iframe/webview ❌
  4. Client starts the postMessage bridge:
    • Widget sends ui/initialize with appCapabilities
    • Client relays tools/call JSON-RPC requests from the widget to the MCP server
    • Client forwards ui/notifications/* events
    • Widget becomes interactive ❌

Actual Behaviour

Steps 1 and 2 succeed. After resources/read completes, nothing further happens:

  • The HTML content is never rendered to the user
  • The ui:// URI string is passed back to the LLM as opaque text
  • No widget-runtime activity reaches the server
  • os_apps_log_event is never called
  • The widget's ui/initialize handshake never occurs

Trace Evidence

From claude-trace.jsonl, four independent sessions show the same pattern:
both tools/call os_apps_render_* and resources/read ui://... occur, then no
widget-runtime activity follows. (Ordering between the two calls can vary.)

Session window (Unix) Event sequence Result
1771085229.* tools/call 1771085229.544111 + resources/read 1771085229.659928 URI + HTML returned
1771081739.* resources/read 1771081739.394575 + tools/call 1771081739.837377 URI + HTML returned
1771075574.* tools/call 1771075574.657804 + resources/read 1771075574.771148 URI + HTML returned
1771068355.* tools/call 1771068355.396543 + resources/read 1771068355.423197 URI + HTML returned
After each pair (no ui/* or widget bridge activity) session goes silent for MCP-Apps runtime

The gap is consistent: resources/read succeeds → no iframe mount → no ui/initialize → no tools/call relay → no os_apps_log_event.

Concrete trace excerpt (same session):

  • 1771085229.544111 client->server tools/call os_apps_render_boundary_explorer
  • 1771085229.548673 server->client tool result includes:
    • content[1].type = "resource_link"
    • content[1].uri = "ui://mcp-geo/boundary-explorer"
    • content[1].mimeType = "text/html;profile=mcp-app"
  • 1771085229.659928 client->server resources/read ui://mcp-geo/boundary-explorer
  • 1771085229.66x server->client resources/read result includes:
    • contents[0].mimeType = "text/html;profile=mcp-app"
    • contents[0].text contains full HTML document (<!DOCTYPE html>...)
  • No subsequent client->server calls to os_apps_log_event or any ui/* methods.

Server-Side Architecture (Confirmed Working)

The mcp-geo server correctly implements the MCP-Apps protocol:

  • Resource publishing: resources/list + resources/read via resource_catalog.py — serves HTML content with text/html;profile=mcp-app media type
  • Tool metadata: os_apps_render_* tools return ui:// URIs in _meta.ui.resourceUri and optional resource_link content via os_apps.py
  • STDIO adapter: Defaults Claude app calls to contentMode=resource_link via stdio_adapter.py
  • Widget protocol: All widgets implement the MCP-Apps postMessage bridge contract:
    • window.parent.postMessage({jsonrpc: "2.0", ...}) for RPC
    • ui/initialize handshake on load
    • tools/call requests for data fetching
    • ui/notifications/size-changed for responsive layout
    • ResizeObserver-based height reporting

Verification: The os_apps_render_ui_probe tool with contentMode=embedded successfully returns the full widget HTML, confirming the server serves valid content. A standalone reference host (Svelte + Playwright) in the dev repo successfully completes the full widget lifecycle including the postMessage bridge.


Diagnosis

The break point is between resources/read (step 2) and iframe mount (step 3). The client:

  1. Does recognise ui:// URIs as resources requiring resources/read — this is implemented
  2. Does successfully fetch the HTML content from the server
  3. Does not mount the returned HTML in an iframe/webview
  4. Does not wire up the postMessage bridge to relay JSON-RPC between the iframe and the MCP server

The profile=mcp-app media type hint should signal that this resource requires
interactive mounting rather than text-only consumption.

Important transport detail:

  • This reproduction is over STDIO JSON-RPC, so there is no HTTP Content-Type
    header at this step.
  • The media-type signal is present in MCP payload fields:
    • tool result resource_link.mimeType
    • resources/read result contents[*].mimeType

Impact

All MCP-Apps interactive widgets are non-functional in Claude's client surfaces. This affects any MCP server that implements the MCP-Apps UI protocol for interactive data exploration, including:

  • Geographic boundary selection and exploration
  • Statistical data dashboards with interactive filtering
  • Route planning with map visualisation
  • Any widget requiring bidirectional communication between UI and MCP server tools

Suggested Fix

The Claude client needs to:

  1. Detect text/html;profile=mcp-app content from resources/read responses (or detect _meta.ui.resourceUri in tool call results)
  2. Mount the HTML in a sandboxed iframe within the chat interface
  3. Implement the postMessage bridge:
    • Listen for window.postMessage events from the iframe
    • Relay tools/call requests to the MCP server via the existing transport
    • Forward responses back to the iframe via postMessage
    • Handle ui/notifications/* events (e.g., size-changed for responsive layout)
  4. Complete the handshake: Respond to the widget's ui/initialize call to signal host readiness

Optional diagnostics that would accelerate triage:

  • Client-side log line when a ui:// resource is resolved but iframe mount is skipped.
  • Client-side log line when iframe mounts but bridge registration fails.
  • Surface-level indication in chat when MCP-Apps runtime is unavailable for a specific message.

This is architecturally similar to how first-party tools like places_map_display_v0 render interactive map widgets — the difference is that MCP-Apps widgets are served by third-party MCP servers rather than built into the client.


Workarounds (Current)

  • Manual data retrieval: Claude can call the underlying MCP tools directly (e.g., admin_lookup_containing_areas, os_features_query) and present results as text/tables, bypassing the interactive widget
  • Static HTML export: Widget HTML can be saved as a file for the user to open in a browser, though without the postMessage bridge it shows "Awaiting host"
  • Local reference host: Developers can use the standalone dev/test host shell for full widget lifecycle testing

References

  • MCP-Apps protocol specification: postMessage-based JSON-RPC bridge between iframe widgets and MCP servers
  • mcp-geo server: UK geographic data MCP server implementing OS NGD + ONS statistical APIs with interactive UI widgets
  • Trace file: claude-trace.jsonl (available on request)

Activity

  1. chris-page-gov commented on Feb 14, 2026

    @chris-page-gov
    Author

    Repro Steps (Server-Agnostic)

    1. Connect any MCP server via STDIO that publishes at least one ui:// resource in resources/list and returns text/html;profile=mcp-app content from resources/read.
    2. Call a tool whose result includes a resource_link with a ui:// URI in _meta.ui.resourceUri.
    3. Observe in the server-side trace that resources/read fires automatically for the ui:// URI — the client does resolve the resource.
    4. Observe that no iframe mount or postMessage bridge activity follows: no ui/initialize, no tools/call relay from the widget, no ui/notifications/*.
    5. The widget HTML is passed to the LLM as text content rather than rendered to the user as an interactive element.

    Minimum server fixture: Any MCP server that returns a static HTML page with a window.parent.postMessage({jsonrpc:"2.0", method:"ui/initialize", ...}) call on load, served via resources/read with mimeType: "text/html;profile=mcp-app", is sufficient to reproduce. The mcp-geo server is not required.

  2. flekschas commented on Feb 16, 2026

    @flekschas

    I run into the same issue. What's strange is that it worked once and ever since failed. However, in my case, the MCP app is mounted when I navigate to another chat and come back! Unclear if it's related but I see this error in both the claude.ai and unknown-window logs:

    [error] TypeError: Cannot read properties of undefined (reading 'map')
    [error] Uncaught TypeError: Cannot read properties of undefined (reading 'map')
    
  3. added
    upstream-hostHost implementation gap (Claude.ai/Desktop/iOS/VS Code), not SDK/spec
    on Apr 21, 2026
  4. ochafik commented on Apr 21, 2026

    @ochafik
    Contributor

    Thank you for the very thorough trace, and apologies for the slow response.

    Your report is from Feb 14 and the claude.ai host has had a number of updates since — could you confirm whether this still reproduces today? If the iframe still never mounts (no ui/initialize reaching your server after resources/read), that's a host-side gap and the right place to track it is https://github.com/anthropics/claude-ai-mcp/issues — anthropics/claude-ai-mcp#61 and anthropics/claude-ai-mcp#165 look like the same symptom, so adding your trace there would be more useful than a new issue. Happy to keep this open here as a cross-reference in the meantime.

    Your server's text/html;profile=mcp-app mimeType matches the spec, so I don't see anything to change on your side.

    @flekschas the works-once / mounts-on-nav-back behavior with the undefined.map TypeError in the claude.ai console points at a host JS error during mount — also worth retesting, and if it still happens it'd be a good addition to anthropics/claude-ai-mcp#61 with the console stack trace.

  5. koshak01 commented on May 18, 2026

    @koshak01

    Confirming this issue with a different transport and server stack — same
    behaviour from the Claude client side.

    Our setup (different from OP):

    Confirming this issue with a different transport and server stack — same
    behaviour from the Claude client side.

    Our setup (different from OP):

    • MCP server: Rust, custom implementation (xxxxxxxxx)
    • Transport: Streamable HTTP (not STDIO) — Remote MCP with OAuth 2.0 + DCR
    • Client: Claude desktop 1.7196.3 (macOS), also tested on Claude.ai web

    Same outcome as OP:

    • tools/list returns tool with _meta.ui.resourceUri = "ui://synapse/hello/<seed>"
    • Client calls resources/list → resource registered ✅
    • Client calls resources/read ui://synapse/hello/<seed> → server returns
      contents[0] with mimeType: "text/html;profile=mcp-app" and full HTML ✅
    • In the model's context Claude inserts a system marker
      [This tool call rendered an interactive widget in the chat. The user can already see the result — do not repeat it in text or with another visualization tool.] — so the backend does recognise the widget
    • In the user's chat UI nothing is rendered. Only the text-fallback from
      content[0].text shows. No iframe, no widget, no postMessage bridge.

    This rules out the STDIO-specific theory — same break point between
    resources/read (succeeds) and iframe mount (never happens) holds for
    Streamable HTTP / Remote MCP as well.

    Server-side verified spec-compliant via 3rd-party host:
    We tested the same server in MCPJam Inspector
    (connected the same Remote MCP endpoint, completed OAuth) — the widget
    renders correctly, the postMessage bridge initialises, ui/initialize
    handshake completes, bidirectional tools/call from the iframe back to
    the server works end-to-end (button click → callback tool invoked →
    button label updated in-iframe). This confirms the server implementation
    is correct and the break is specifically in Claude's client surfaces.

    Two hours of debugging on our side trying every spec variation
    (embedded type: "resource" in content[], predeclared via _meta,
    both, neither) before finding this issue. A short note in Anthropic's
    MCP-Apps docs that "UI rendering is currently only available for…"
    [marketplace partners / TBD] would save other implementers the same
    journey.

    Happy to share full Streamable HTTP request/response traces if useful.

    Image
  6. jcollas commented on Jul 17, 2026

    @jcollas

    Seeing what looks like the same underlying bug, with a pattern that might help narrow it down: it's not tied to any specific downstream server, but specifically to whether a tool is MCP-Apps-linked at all.

    Setup: self-hosted MCP aggregator (mcphub, proxying multiple downstream MCP servers to Claude via a single endpoint) fronting several independent MCP servers, three of which implement MCP Apps (_meta.ui.resourceUri, text/html;profile=mcp-app) with three completely independent server implementations (one official Anthropic-pattern server, two custom-built), plus several plain tool-only servers with no widget/UI component at all.

    Pattern observed over one extended session:

    • Plain, non-app-linked tool calls (no _meta.ui) — consistently reliable, every time, across the whole session.
    • MCP-Apps-linked tool calls — intermittently fail to mount the widget. resources/read for the ui:// resource succeeds server-side (confirmed via direct testing against the downstream servers, bypassing the client entirely), but the client never renders it — instead showing the raw tool result (text/JSON) as if no _meta.ui linkage existed at all. This happened across all three independent app-linked servers over the course of the session, not consistently on any one of them — ruling out a bug in any single server's implementation.
    • Sometimes accompanied by a client-side popup reading "client server capabilities not available," and separately, the whole MCP connection occasionally reports "Unable to reach apps MCP" even though the aggregator and all downstream servers were confirmed healthy and responsive (checked directly, no errors, no restarts, sub-50ms response times) at the time.
    • The failure appears correlated with session/widget age or idle time rather than being reproducible on a fixed trigger — a widget that rendered successfully earlier in a session, or in another chat, can be followed by a brand-new chat window immediately failing to render a widget for the same or a different app-linked tool.
    • We tested and ruled out several environment-side hypotheses that turned out not to matter: disabling one of the three app-linked servers entirely (removing it from routing, connection, and the client's tools/list/resources/list) had no effect on whether the other two rendered correctly. A clean restart of the aggregator process also had no effect. This points away from any per-server or per-process state and back toward something in the client's own MCP-Apps handshake/iframe-mounting logic, consistent with what's described in this issue and in Bug Report: MCP Apps UI Resource Not Rendering in Claude Desktop / claude.ai #671.

    Happy to provide more detail (redacted logs, timing data) if useful — this reproduced multiple times across a few hours of otherwise-normal usage, not as a rare one-off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingupstream-hostHost implementation gap (Claude.ai/Desktop/iOS/VS Code), not SDK/spec

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions