Skip to content

feat(samples): add generic A2UI MCP App renderer in React - #1953

Open
liady wants to merge 9 commits into
a2ui-project:mainfrom
liady:feat/generic-a2ui-react-renderer
Open

liady wants to merge 9 commits into
a2ui-project:mainfrom
liady:feat/generic-a2ui-react-renderer

Conversation

@liady

@liady liady commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds a third micro-app to the a2ui-in-mcpapps sample: server/apps/react/, a generic, server-agnostic A2UI renderer for MCP Apps, selectable in the sample host as "Generic React Renderer".

Unlike the Basic and Editor apps, it contains zero server-specific logic. It is pure chrome: the MCP Apps handshake comes from the official @modelcontextprotocol/ext-apps SDK (App class, including automatic size-changed iframe resizing), rendering is @a2ui/react (v0.9 basic catalog) driven by a @a2ui/web_core MessageProcessor, and all content arrives through tool results. Any A2UI-speaking MCP server can serve the built react.html as its own ui:// resource if it follows two conventions:

  1. A2UI payloads travel as marked embedded resources — tool results (including the entry tool's result, which produces the initial render) carry A2UI messages as EmbeddedResource content blocks with mimeType application/a2ui+json, each marked with _meta: {"ui": {"content": {}}}, and the ui:// resource serving the renderer declares _meta.ui.contentMimeTypes: ["application/a2ui+json"] (on resources/list and resources/read). This is the MCP Apps Dynamic View Content contract: the marker tells hosts to forward the block to the View unmodified and keep it out of model context; the declaration is the reviewable contract of what the View can parse; routing stays on the tool's _meta.ui.resourceUri. The renderer still accepts unmarked A2UI blocks as a legacy allowance for servers that predate the marker (extractA2uiMessages(content, {allowUnmarked: false}) turns it off).
  2. A2UI actions map to tools — each action name matches an app-visible tool name (_meta.ui.visibility includes "app"), and the action's resolved context becomes the tool arguments; the response payload is applied incrementally to the live surfaces. (The mapping is overridable: the GenericA2uiApp component takes an optional actionToToolName prop for servers whose action vocabulary differs from their tool names.)

The sample host negotiates dynamic content by advertising contentMimeTypes: ["application/a2ui+json"] (alongside mimeTypes) in the io.modelcontextprotocol/ui extension capability at initialize.

This is intended as the seed of an extractable/reusable artifact: the renderer is a standalone React component (GenericA2uiApp in src/generic-a2ui-app.tsx, with main.tsx only mounting it), so it can be reused either as the prebuilt react.html or as a component.

Changes

  • server/apps/react/ — the renderer: GenericA2uiApp component + extract-a2ui-messages.ts (unit-tested with vitest), built with Vite + vite-plugin-singlefile into a self-contained public/react.html. No inline.js post-processing needed (React has no forced chunking, unlike the Angular apps).
  • server/ — v0.9-vocabulary counter payload (simple_counter_a2ui_v0_9.json), ui://react/app resource, get_react_app entry tool (declares the template via _meta.ui.resourceUri) and increase_counter_v0_9 app tool. All A2UI payload blocks are built by an a2ui_content_block() helper that sets the _meta.ui.content marker, and the ui:// resources declare contentMimeTypes in both resources/list and resources/read.
  • client/ — third option in the app selector; entry-tool lookup becomes a map; null-guard on the relay target when delivering tool input/result; advertises the io.modelcontextprotocol/ui extension capability with mimeTypes + contentMimeTypes. Everything else (tool allowlist, resource URI) already derives from _meta.ui.
  • samples/community/package.json + yarn.lock — the new app is picked up by the existing mcp/a2ui-in-mcpapps/server/apps/* workspace glob; added to build:web and the lockfile.
  • READMEs — sample README documents the two conventions and build "Option C"; server/apps/README.md and server/apps/react/README.md cover the React build and the component's props.

Noteworthy implementation detail

vite.config.ts sets resolve.dedupe for @a2ui/web_core / @preact/signals-core (and friends). With duplicated copies in node_modules (hoisted vs nested), the bundle gets two signal-library instances and data-model updates stop propagating to rendered components — the UI renders once and silently never updates. Worth knowing for anyone who copies this app as a template.

Testing

  • Unit tests for the payload extraction (yarn test, vitest, 16 cases: single/array payloads, legacy mime type, non-object JSON, invalid JSON, non-resource blocks, marked vs unmarked _meta.ui.content selection, isViewContentBlock).
  • Verified end-to-end in the sample host (Angular client + Python SSE server): selecting "Generic React Renderer" completes the ui/initialize handshake via the ext-apps SDK; the entry tool's embedded v0.9 payload renders the counter card; clicking "Increase counter" relays tools/call increase_counter_v0_9 through the host and the returned updateDataModel patches the counter in place (no surface rebuild).
  • Regression: the Basic (v0.8) and Editor apps still load and work.
  • tsc --noEmit clean; yarn build:all emits a single-file react.html with no external references.

Builds on #1949 (merged) for the _meta.ui template declaration, the standard tools/call relay, and the host fix for JSON-RPC id-0 responses (the ext-apps SDK numbers requests from 0).

🤖 Generated with Claude Code

@google-cla

google-cla Bot commented Jul 8, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a generic, server-agnostic React A2UI renderer and updates the MCP Apps host and server to support it, aligning with the 2026-01-26 protocol version. The feedback highlights several important improvements: adding safety checks to prevent runtime crashes when messaging the iframe target, implementing a connection guard for the singleton MCP app to avoid duplicate connections, cleaning up event handlers on unmount to prevent memory leaks, and robustly validating parsed JSON payloads. Additionally, unit tests should be added for the new React renderer to comply with the repository's style guide.

Comment thread samples/community/mcp/a2ui-in-mcpapps/client/src/app/app.ts
Comment thread samples/community/mcp/a2ui-in-mcpapps/server/apps/react/src/main.tsx Outdated
Comment thread samples/community/mcp/a2ui-in-mcpapps/server/apps/react/src/main.tsx Outdated
Comment thread samples/community/mcp/a2ui-in-mcpapps/server/apps/react/src/main.tsx Outdated
Comment thread samples/community/mcp/a2ui-in-mcpapps/server/apps/react/src/main.tsx Outdated
Comment thread samples/community/mcp/a2ui-in-mcpapps/server/apps/react/package.json Outdated
@liady
liady force-pushed the feat/generic-a2ui-react-renderer branch from 27e1cfd to bf99c4d Compare July 8, 2026 01:50
@github-actions github-actions Bot added the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 9, 2026
@github-actions github-actions Bot removed the status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs label Jul 15, 2026
liady added 3 commits July 15, 2026 19:42
Adds server/apps/react to the a2ui-in-mcpapps sample: a server-agnostic
MCP Apps view built with the official @modelcontextprotocol/ext-apps SDK
and @a2ui/react (v0.9). It contains zero server-specific logic — any
A2UI-speaking MCP server can serve the built react.html as its ui://
resource if (1) tool results carry A2UI payloads as embedded resources
with mimeType application/a2ui+json, and (2) A2UI action names map to
app-visible tool names with the resolved context as arguments.

- Vite + vite-plugin-singlefile emits a self-contained public/react.html
  (no inline.js needed; React has no forced chunking). resolve.dedupe
  keeps a single instance of the A2UI signals stack — duplicated copies
  silently break data-model reactivity.
- Server: v0.9 counter payload, ui://react/app resource, get_react_app
  entry tool and increase_counter_v0_9 app tool.
- Host: third selectable app wired via the _meta.ui-driven entry-tool map.
Split the generic renderer out of main.tsx into generic-a2ui-app.tsx so
embedders can use the component directly; main.tsx now only mounts it.
The component accepts an optional actionToToolName map to route A2UI
action names to differently-named server tools (unmapped actions keep
the name-equals-tool convention).

Also addresses review feedback:
- guard connect() so it runs once per App instance even if the mount
  effect re-runs (e.g. StrictMode), and clear ontoolresult on unmount
- ignore non-object JSON payloads in extractA2uiMessages
- move extraction into extract-a2ui-messages.ts and cover it with
  vitest unit tests (test script now runs vitest)
The other message handlers already null-check the target window before
posting; do the same in the ui/notifications/initialized branch.
@sugoi-yuzuru
sugoi-yuzuru force-pushed the feat/generic-a2ui-react-renderer branch from 1a51417 to eb027de Compare July 15, 2026 20:00
sugoi-yuzuru and others added 4 commits July 15, 2026 20:35
- extract-a2ui-messages: drop null/scalar entries inside array payloads
  (MessageProcessor's 'in'-operator dispatch throws on non-objects) and
  match A2UI mime types case-insensitively, ignoring parameters
- GenericA2uiApp: validate the tool-result payload before wiping
  surfaces, and surface tool-call failures (rejections and isError
  results) in a visible error line instead of only console.error
- client: fix resource-content predicate so the mcp-app HTML block is
  preferred over arbitrary text blocks
- server: reflect the live COUNTER in get_react_app's initial payload so
  reloads don't render a stale 0
- declare prettier in the react app's devDependencies so the package
  works when extracted standalone
@liady
liady marked this pull request as ready for review July 20, 2026 22:18
@github-actions github-actions Bot added status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs and removed status: needs-triage auto-managed: https://github.com/a2ui-project/a2ui/blob/main/scripts/triage.mjs labels Aug 17, 2026
liady and others added 2 commits September 16, 2026 23:13
…eact-renderer

# Conflicts:
#	samples/community/yarn.lock
Follow the revised ext-apps spec proposal (modelcontextprotocol/ext-apps#699):

- server: mark every A2UI embedded resource with `_meta.ui.content` via a
  shared `a2ui_content_block()` helper, and declare
  `_meta.ui.contentMimeTypes: ["application/a2ui+json"]` on the ui://
  resources in both resources/list and resources/read
- client: advertise the `io.modelcontextprotocol/ui` extension capability
  with `mimeTypes` and a non-empty `contentMimeTypes`, negotiating dynamic
  content forwarding
- react renderer: expose `isViewContentBlock`, prefer marked blocks and keep
  unmarked A2UI blocks as an opt-out legacy allowance
  (`allowUnmarked: false`); add unit tests for marked vs unmarked selection
- docs: describe the marker and `contentMimeTypes` conventions in the sample
  and renderer READMEs

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants