Skip to content

feat(client): generic Dynamic View Content fallback renderers (bundled A2UI default) - #209

Draft
liady wants to merge 10 commits into
mainfrom
feat/a2ui-wrapper
Draft

feat(client): generic Dynamic View Content fallback renderers (bundled A2UI default)#209
liady wants to merge 10 commits into
mainfrom
feat/a2ui-wrapper

Conversation

@liady

@liady liady commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

A2UI is a generative-UI format: servers return a declarative, typed UI document (application/a2ui+json) in tool results, and a generic renderer displays it. This PR adds generic Dynamic View Content support to <AppRenderer /> — per the ext-apps spec proposal modelcontextprotocol/ext-apps#699 — with a bundled generic A2UI renderer as the default fallback. mcp-ui hosts can render A2UI-returning MCP servers even when the server ships no renderer resource at all, including servers not built with mcp-ui.

The content↔renderer matching is MIME-generic; only three things stay a2ui-specific: the bundled renderer artifact, its default registry entry, and the legacy allowance for unmarked a2ui-MIME blocks (existing A2UI servers don't set the _meta.ui.content marker yet).

What's included

  • fallbackContentRenderers prop — a MIME-keyed registry of fallback renderers, each entry either the renderer's self-contained HTML string (the UMD/no-dynamic-import escape hatch) or an async loader resolving to it:
    • undefined (default, opt-out): DEFAULT_FALLBACK_CONTENT_RENDERERS — the bundled generic A2UI renderer, lazily loaded, for application/a2ui+json + legacy application/json+a2ui. A2UI results "just work" with zero wiring.
    • {}: disable all fallbacks. A custom registry replaces the default; spread DEFAULT_FALLBACK_CONTENT_RENDERERS to extend.
    • Registry keys also gate the beyond-spec leniency: unmarked embedded resources count as view content only when their MIME is a registry key.
  • Renderer resolution orderhtml prop → toolResourceUri prop → tool's declared _meta.ui.resourceUriNEW, spec-native: first marked block's _meta.ui.content.rendererUri (read via client or onReadResource) → fallback registry → existing errors (late-arriving results still supersede the initial error).
  • Bundled generic A2UI renderer — a private, never-published a2ui-renderer workspace package (Lit-based via @a2ui/lit + @a2ui/web_core) built into a self-contained single-file HTML artifact, checked into git (linguist-generated) and re-emitted at build time as the lazy @mcp-ui/client/a2ui-renderer subpath export. Zero new runtime dependencies; main bundle unchanged (~117 KB).
  • Detection helpers — generic: isViewContentBlock, getViewContentBlocks (marked blocks + opt-in unmarkedMimeTypes), getViewContentRendererUri; a2ui wrappers: hasA2uiContent, getA2uiContentBlocks, isA2uiContentBlock + MIME constants. The generic module is the single swap point for the official ext-apps helpers once feat: Dynamic View Content SDK support (types, helpers, example) modelcontextprotocol/ext-apps#700 ships.
  • Host capabilityUI_EXTENSION_CONFIG advertises contentMimeTypes: ['application/a2ui+json', 'application/json+a2ui'], test-locked to Object.keys(DEFAULT_FALLBACK_CONTENT_RENDERERS) so the two can't drift.
  • Docs — client guide page reworked to lead with the generic Dynamic View Content model (docs/src/guide/client/a2ui.md).
  • Bumped client @modelcontextprotocol/ext-apps to ^1.7.4 (same protocol 2026-01-26; separate commit, droppable).

How the renderer works

The bundled renderer is a standard MCP App (ext-apps App class) with zero server-specific logic:

  1. A2UI payloads are extracted from tool-result content blocks by MIME type (text or base64 blob; single message or array); each ui/notifications/tool-result is a full re-render.
  2. A2UI user actions map back to server tools: action nametools/call, resolved context → arguments; responses are applied incrementally.

Verification

  • 123 unit tests pass (generic view-content semantics, a2ui wrapper regressions, the full AppRenderer fallback matrix incl. rendererUri paths and registry replacement, artifact invariants, capabilities drift-lock).
  • E2E: the actual built artifact was loaded in a sandboxed iframe against an ext-apps JSON-RPC host harness — the a2ui sample counter payload rendered, and clicking the button round-tripped tools/call increase_counter_v0_9 with incremental data-model updates (counter 0→1→2).

Possible next steps

  • Native direct rendering of application/a2ui+json by the host (rendering A2UI surfaces as host-DOM components, without the iframe HTML renderer).
  • Swap detection to the official ext-apps helpers when PR #700 is released, and swap the bundled artifact for an official a2ui-published renderer resource if one ships.

Notes

🤖 Generated with Claude Code

liady and others added 8 commits July 8, 2026 06:06
Adds a private a2ui-renderer workspace package (Lit-based, vanilla TS
entry) that builds a self-contained single-file HTML MCP App rendering
application/a2ui+json tool-result payloads via @a2ui/lit + @a2ui/web_core.
The built artifact (631 KB raw / 175 KB gz) is checked in as
linguist-generated and re-emitted at build time as the lazy
@mcp-ui/client/a2ui-renderer subpath export, so the published client
gains no runtime dependencies and the main bundle size is unchanged.
Regenerate with pnpm --filter @mcp-ui/client run regen:a2ui-renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
isViewContentBlock (spec ext-apps#699 _meta.ui.content marker),
isA2uiContentBlock / getA2uiContentBlocks / hasA2uiContent accept both
marked and unmarked embedded resources with the application/a2ui+json
(or legacy application/json+a2ui) MIME type. Single swap point for the
official ext-apps helpers once ext-apps#700 ships. Also adds an
invariant test for the checked-in single-file renderer artifact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New a2uiRenderer prop (undefined=auto-detect, false=off, true=always
when no declared renderer, {html}=custom HTML). In auto mode the
bundled generic renderer is injected only when the tool declares no
ui/resourceUri AND the toolResult carries an application/a2ui+json
embedded resource; tools with a declared renderer are untouched. The
renderer HTML lazy-loads via the external @mcp-ui/client/a2ui-renderer
subpath, with an actionable error for bundlers that cannot resolve it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds contentMimeTypes (Dynamic View Content host capability, ext-apps
PR #699) to UI_EXTENSION_CONFIG, exports the A2UI detection helpers and
MIME constants from @mcp-ui/client, and documents the A2UI auto-inject
flow in a new client guide page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aligns the host with the bundled A2UI renderer's ext-apps version; both
1.2.0 and 1.7.4 speak protocol 2026-01-26 so no behavioral change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the MIME-agnostic Dynamic View Content helpers (spec PR #699) into
src/view-content.ts: isViewContentBlock, getViewContentRendererUri, and a
getViewContentBlocks that accepts marked blocks of any MIME type plus an
explicit unmarkedMimeTypes legacy allowance. The a2ui detection module
becomes a thin configuration layer over them (public semantics unchanged)
and gains DEFAULT_FALLBACK_CONTENT_RENDERERS, mapping both a2ui MIME types
to a lazy loader of the bundled renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace AppRenderer's a2uiRenderer prop with fallbackContentRenderers, a
MIME-keyed registry of renderer HTML strings or async loaders. Omitting it
defaults to DEFAULT_FALLBACK_CONTENT_RENDERERS (the lazily loaded bundled
A2UI renderer), {} disables all fallbacks, and a provided registry replaces
the default. Renderer resolution gains the spec-native step: a marked
block's _meta.ui.content.rendererUri is read as the renderer (via client or
onReadResource) before consulting the registry. Registry keys also gate the
beyond-spec leniency for unmarked legacy blocks. The old true force mode is
dropped; the html prop covers full override.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lock contentMimeTypes to Object.keys(DEFAULT_FALLBACK_CONTENT_RENDERERS)
with a test so the advertisement and the default registry can't drift.
Export the generic view-content helpers, registry types, and the default
registry from the package root. Rework the a2ui guide to lead with the
generic Dynamic View Content model (resolution order, registry semantics,
rendererUri step, legacy unmarked-block allowance) before the a2ui
defaults, and rename the sidebar entry accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@liady liady changed the title feat(client): auto-inject bundled generic A2UI renderer feat(client): generic Dynamic View Content fallback renderers (bundled A2UI default) Jul 8, 2026
liady and others added 2 commits July 8, 2026 07:09
The '@mcp-ui/client/a2ui-renderer' subpath resolves to files emitted into
the client's dist at build time, so vite's import analysis failed on fresh
checkouts — CI runs tests before building (js_build_and_test was already
red on the previous branch tip). A vitest globalSetup now runs the
standalone emit script (it only wraps the checked-in HTML artifact) before
test files are transformed, covering the root run, package-local runs,
watch/coverage, and IDE runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves the exports conflict in the client package.json: keep main's CJS
fix (require -> dist/index.cjs, PR #211) and apply the same extension fix
to the a2ui-renderer subpath — the emit script now writes
a2ui-renderer.cjs, since "type": "module" makes a CJS-content .js file
unloadable by Node require().

Co-Authored-By: Claude Fable 5 <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.

1 participant