Skip to content

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

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

feat(samples): add generic A2UI MCP App renderer in React#1953
liady wants to merge 5 commits into
a2ui-project:mainfrom
liady:feat/generic-a2ui-react-renderer

Conversation

@liady

@liady liady commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Important

Stacked on #1949 — this branch includes that PR's commits and must land after it. The new work is the three commits from bf99c4de onward (feature, component refactor, review fixes); everything before is #1949. Once #1949 merges, this branch will be rebased and the PR diff will collapse to just the new work.

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 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.
  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.)

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.
  • client/ — third option in the app selector; entry-tool lookup becomes a map; null-guard on the relay target when delivering tool input/result. 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, 8 cases: single/array payloads, legacy mime type, non-object JSON, invalid JSON, non-resource blocks).
  • 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.

Depends on #1949 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
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