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
- 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
…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>
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, each marked with_meta: {"ui": {"content": {}}}, and theui://resource serving the renderer declares_meta.ui.contentMimeTypes: ["application/a2ui+json"](onresources/listandresources/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).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.)The sample host negotiates dynamic content by advertising
contentMimeTypes: ["application/a2ui+json"](alongsidemimeTypes) in theio.modelcontextprotocol/uiextension capability atinitialize.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. All A2UI payload blocks are built by ana2ui_content_block()helper that sets the_meta.ui.contentmarker, and theui://resources declarecontentMimeTypesin bothresources/listandresources/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 theio.modelcontextprotocol/uiextension capability withmimeTypes+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 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, 16 cases: single/array payloads, legacy mime type, non-object JSON, invalid JSON, non-resource blocks, marked vs unmarked_meta.ui.contentselection,isViewContentBlock).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.Builds on #1949 (merged) 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