Skip to content

feat: add experimental MCP Apps support for rich iframe UIs#15926

Open
tristan-stahnke-GPS wants to merge 13 commits intoanomalyco:devfrom
tristan-stahnke-GPS:feat/mcp-apps
Open

feat: add experimental MCP Apps support for rich iframe UIs#15926
tristan-stahnke-GPS wants to merge 13 commits intoanomalyco:devfrom
tristan-stahnke-GPS:feat/mcp-apps

Conversation

@tristan-stahnke-GPS
Copy link

Add MCP Apps protocol integration behind OPENCODE_EXPERIMENTAL_MCP_APPS flag, allowing MCP servers to render interactive UIs in sandboxed iframes.

  • Feature flag, AppMeta schema, tool metadata registry, and resource cache
  • Visibility filtering (_meta.ui.visibility: ["app"]) hides tools from LLM
  • structuredContent + resourceUri + server threaded into tool metadata
  • 3 experimental API routes (apps list, resource fetch, tool-call proxy)
  • McpAppTool renderer with AppBridge handshake and iframe auto-sizing
  • Configurable maxHeight (default 640px) via _meta.ui.maxHeight
  • FetchAppResourceFn wired through DataProvider
  • SDK regenerated
  • 15 tests across 3 test files
  • Demo fixture (server.py, app.html, README.md)

Issue for this PR

Closes #10884

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

This adds support for the MCP Apps protocol behind the OPENCODE_EXPERIMENTAL_MCP_APPS flag. MCP servers can now declare tools with _meta.ui.resourceUri pointing to a ui:// resource, and opencode will render the tool result inside a sandboxed iframe with a full AppBridge handshake.

How it works end-to-end:

  1. When MCP tools are listed, any tool with _meta.ui.resourceUri gets registered in a toolMetaRegistry. Tools with visibility: ["app"] are filtered out of the LLM's tool list (they're only callable by the iframe itself).
  2. When a tool result comes back, structuredContent, resourceUri, server, and maxHeight are threaded into the tool part metadata so the frontend can pick them up.
  3. On the frontend, any tool part with a resourceUri in its metadata renders as a McpAppTool instead of the generic tool renderer.
  4. McpAppTool fetches the HTML resource via a new experimental API route, renders it in a sandboxed <iframe srcdoc>, and establishes an AppBridge connection over PostMessageTransport.
  5. Once the bridge handshake completes (oninitialized), the host sends toolInput and toolResult (with structuredContent) to the iframe, which renders the data.
  6. The iframe auto-sizes to its content height via onsizechange, capped at a configurable maxHeight (default 640px, overridable per-tool via _meta.ui.maxHeight).

Key implementation details worth calling out:

  • The AppBridge and PostMessageTransport are statically imported (not dynamic import()) to avoid a race condition where the iframe sends ui/initialize before the host bridge is listening.
  • structuredContent from SolidJS reactive state is deep-cloned via JSON.parse(JSON.stringify()) before passing to bridge.sendToolResult(), because SolidJS Proxy objects fail postMessage's structured clone algorithm (DataCloneError).
  • The iframe height is managed via a SolidJS createSignal rather than direct DOM manipulation, so re-renders don't clobber the height set by onsizechange.
  • A wrapper <div> with overflow: hidden constrains the layout box so the parent container doesn't grow to the iframe's scroll height.

New files:

  • packages/opencode/src/flag/flag.tsOPENCODE_EXPERIMENTAL_MCP_APPS flag (respects OPENCODE_EXPERIMENTAL too)
  • packages/opencode/src/server/routes/experimental.ts — 3 routes: list app tools, fetch HTML resource, proxy tool calls
  • packages/ui/src/components/message-part.tsxMcpAppTool renderer (~100 lines)
  • packages/ui/src/context/data.tsxFetchAppResourceFn type + prop
  • Demo fixture in packages/opencode/test/fixture/mcp-app-demo/ (Python MCP server + dashboard HTML app)

How did you verify your code works?

  • 15 tests across 3 test files covering: extractAppMeta, visibility filtering, toolMeta registry, apps() list, appResource fetch/cache, structuredContent passthrough, route handlers, and maxHeight extraction
  • Manual end-to-end testing with the demo fixture server registered in local opencode config — verified the full flow from tool call through bridge handshake to rendered dashboard with live data in the iframe
  • Verified no DataCloneError in browser console after the structured clone fix
  • Typecheck passes across all packages (bun turbo typecheck)

Screenshots / recordings

Demo dashboard rendering inside opencode after a demo_dashboard tool call:

The iframe shows metric cards (Requests, Latency, Error Rate, Uptime), a throughput chart, traffic-by-region bars, and action buttons — all rendered from structuredContent delivered via the MCP Apps bridge protocol.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Add MCP Apps protocol integration behind OPENCODE_EXPERIMENTAL_MCP_APPS
flag, allowing MCP servers to render interactive UIs in sandboxed iframes.

- Feature flag, AppMeta schema, tool metadata registry, and resource cache
- Visibility filtering (_meta.ui.visibility: ["app"]) hides tools from LLM
- structuredContent + resourceUri + server threaded into tool metadata
- 3 experimental API routes (apps list, resource fetch, tool-call proxy)
- McpAppTool renderer with AppBridge handshake and iframe auto-sizing
- Configurable maxHeight (default 640px) via _meta.ui.maxHeight
- FetchAppResourceFn wired through DataProvider
- SDK regenerated
- 15 tests across 3 test files
- Demo fixture (server.py, app.html, README.md)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Add Support for MCP Apps in the desktop app

1 participant