feat(client): generic Dynamic View Content fallback renderers (bundled A2UI default) - #209
Draft
liady wants to merge 10 commits into
Draft
feat(client): generic Dynamic View Content fallback renderers (bundled A2UI default)#209liady wants to merge 10 commits into
liady wants to merge 10 commits into
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.contentmarker yet).What's included
fallbackContentRenderersprop — 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, forapplication/a2ui+json+ legacyapplication/json+a2ui. A2UI results "just work" with zero wiring.{}: disable all fallbacks. A custom registry replaces the default; spreadDEFAULT_FALLBACK_CONTENT_RENDERERSto extend.htmlprop →toolResourceUriprop → tool's declared_meta.ui.resourceUri→ NEW, spec-native: first marked block's_meta.ui.content.rendererUri(read via client oronReadResource) → fallback registry → existing errors (late-arriving results still supersede the initial error).a2ui-rendererworkspace 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-renderersubpath export. Zero new runtime dependencies; main bundle unchanged (~117 KB).isViewContentBlock,getViewContentBlocks(marked blocks + opt-inunmarkedMimeTypes),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.UI_EXTENSION_CONFIGadvertisescontentMimeTypes: ['application/a2ui+json', 'application/json+a2ui'], test-locked toObject.keys(DEFAULT_FALLBACK_CONTENT_RENDERERS)so the two can't drift.docs/src/guide/client/a2ui.md).@modelcontextprotocol/ext-appsto^1.7.4(same protocol2026-01-26; separate commit, droppable).How the renderer works
The bundled renderer is a standard MCP App (ext-apps
Appclass) with zero server-specific logic:ui/notifications/tool-resultis a full re-render.name→tools/call, resolvedcontext→ arguments; responses are applied incrementally.Verification
rendererUripaths and registry replacement, artifact invariants, capabilities drift-lock).tools/call increase_counter_v0_9with incremental data-model updates (counter 0→1→2).Possible next steps
application/a2ui+jsonby the host (rendering A2UI surfaces as host-DOM components, without the iframe HTML renderer).Notes
dist/a2ui-renderer.cjs(not.js), matching therequire()extension fix from fix: emit CJS build so require('@mcp-ui/client') exposes exports #211 ("type": "module"makes CJS-content.jsfiles unloadable by Node).🤖 Generated with Claude Code