Skip to content

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

@chris-page-gov

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/callresources/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/callos_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)

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