feat(samples): add generic A2UI MCP App renderer in React#1953
Conversation
|
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. |
There was a problem hiding this comment.
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.
27e1cfd to
bf99c4d
Compare
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.
1a51417 to
eb027de
Compare
Important
Stacked on #1949 — this branch includes that PR's commits and must land after it. The new work is the three commits from
bf99c4deonward (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-mcpappssample: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-appsSDK (Appclass, including automaticsize-changediframe resizing), rendering is@a2ui/react(v0.9 basic catalog) driven by a@a2ui/web_coreMessageProcessor, and all content arrives through tool results. Any A2UI-speaking MCP server can serve the builtreact.htmlas its ownui://resource if it follows two conventions:EmbeddedResourcecontent blocks with mimeTypeapplication/a2ui+json.namematches an app-visible tool name (_meta.ui.visibilityincludes"app"), and the action's resolvedcontextbecomes the toolarguments; the response payload is applied incrementally to the live surfaces. (The mapping is overridable: theGenericA2uiAppcomponent takes an optionalactionToToolNameprop 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 (
GenericA2uiAppinsrc/generic-a2ui-app.tsx, withmain.tsxonly mounting it), so it can be reused either as the prebuiltreact.htmlor as a component.Changes
server/apps/react/— the renderer:GenericA2uiAppcomponent +extract-a2ui-messages.ts(unit-tested with vitest), built with Vite +vite-plugin-singlefileinto a self-containedpublic/react.html. Noinline.jspost-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/appresource,get_react_appentry tool (declares the template via_meta.ui.resourceUri) andincrease_counter_v0_9app 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 existingmcp/a2ui-in-mcpapps/server/apps/*workspace glob; added tobuild:weband the lockfile.server/apps/README.mdandserver/apps/react/README.mdcover the React build and the component's props.Noteworthy implementation detail
vite.config.tssetsresolve.dedupefor@a2ui/web_core/@preact/signals-core(and friends). With duplicated copies innode_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
yarn test, vitest, 8 cases: single/array payloads, legacy mime type, non-object JSON, invalid JSON, non-resource blocks).ui/initializehandshake via the ext-apps SDK; the entry tool's embedded v0.9 payload renders the counter card; clicking "Increase counter" relaystools/call increase_counter_v0_9through the host and the returnedupdateDataModelpatches the counter in place (no surface rebuild).tsc --noEmitclean;yarn build:allemits a single-filereact.htmlwith no external references.Depends on #1949 for the
_meta.uitemplate declaration, the standardtools/callrelay, and the host fix for JSON-RPC id-0 responses (the ext-apps SDK numbers requests from 0).🤖 Generated with Claude Code