Skip to content

Latest commit

 

History

History
137 lines (91 loc) · 15.5 KB

react-consumers.md

File metadata and controls

137 lines (91 loc) · 15.5 KB

Installing the React renderer with MCP

React and its MCP stack are a supported application path for both custom A2UI catalogs and FaithfulCard applications. The stack owns MCP connection ordering and the React view lifecycle; application code supplies its data interpretation, actions and presentation.

One stack, plus React

An application installs one stack package, @saga-a2ui/react-mcp-stack, plus its framework peers (react and react-dom, singletons at the version SAGA supports), and imports from the stack only:

import {startA2uiView, defineSurface, z, ActionButton, createReactActions} from '@saga-a2ui/react-mcp-stack';
import {FaithfulCardApp, startFaithfulCardApp} from '@saga-a2ui/react-mcp-stack/faithful-card';

The main entry is for an application's own catalog. Use its exported z for catalog schemas; an application that also defines MCP tools declares Zod 4 separately for those tool schemas. An application that installs the stack writes it against the stack's exports alone (Catalog, CommonSchemas, createComponentImplementation, ReactCatalogComponent, defineSurface, z, the action names and the failures it catches), without a direct @a2ui/* dependency, a separate Zod dependency for the catalog, or another SAGA package; the /faithful-card entry starts a card application from one validated definition. A server composes the same application from the stack's /content-policy entry (createContentPolicy, embedContentPolicy) and its Node-only /server entry (createServerBudgets, registerPublicOperation, registerProtectedOperation, operationViewFromHtml, viewUriForHtml, attachRequestLifetime), so one install serves both sides without a browser bundle ever importing a server module. The definition's one result reader receives the whole delivered result (error flag, content blocks and structured content) and supplies its own rejection wording; readingProfile tells its hostContext callback whether the host is read on a device, and the application maps that onto its own tokens (details). The stack connects the React renderer to MCP Apps delivery, so the application writes no A2UI message, imports no bridge and orders no handshake: it states its layout, its catalog, what a result means and what ending the view takes with it. @saga-a2ui/core, @saga-a2ui/components, @saga-a2ui/mcp, @saga-a2ui/react and the MCP Apps bridge are regular dependencies of the stack, so one install suffices under a strict (pnpm) layout.

Two footprints follow, and they are different things. An own-catalog application's installed tree includes @saga-a2ui/components and markdown-it as declared dependencies of the stack; that is the correct install result. Its emitted browser bundle and runtime import graph exclude them, because the main entry imports nothing from components. The stack's exports.test.ts bundles each entry's module graph from workspace source to show this, and scripts/pack1.test.ts builds the own-catalog and card pages from the packed copies; npm run test:pnpm-react and npm run candidate:verify check the install, not the bundle.

The stack belongs to the public release set. See public installation for its exact-version recipe and registry availability, and release preparation for exact-artifact checks. Both React and Lit implement the typed-surface and FaithfulCard application contracts. Their framework APIs and teardown mechanics differ. The host compatibility guide records the observed flows and remaining host limits; the generated React starter covers one server (react-mcp on Hono), and an application on another server or an existing React application integrates the stack directly, which is a working application path.

Add a message to the conversation

A view can reach the conversation around it. The actions callback of startA2uiView receives host.conversation beside host.server, and appendMessage adds a message to the conversation as the person, which gives the assistant its turn:

actions: (host) => {
  const play = Action.server<ActionButtonActivation>('play-square', host.server)
    .run(({params, call}) => call('play_move', params))
    .onSuccess(({result}) => {
      const game = board.present(result);
      // Start the append; the hook has a five-second budget and the host may wait for the person to confirm.
      void host.conversation
        .appendMessage(`Your move in game ${game.gameId}. Read it with get_game, then play O.`, {
          context: {text: 'The accepted game.', data: {game}, bestEffort: true},
        })
        .then((result) => {
          if (!board.isShowing(game)) return; // a newer game or move is on screen
          if (result.outcome !== 'appended') board.say('Tell the assistant it is their move.');
        });
    })
    .build();
  return createReactActions({attach: (view) => view.watchById('square', ActionButton, (handle) => handle.bindAction('activate', play))});
},

appendMessage(text, {role?, context?}) resolves, and never rejects, with an AppendResult:

Result Meaning
{outcome: 'appended'} The host accepted the message. This is a prompt delivered, not the assistant having acted.
{outcome: 'appended', context} As above, but a best-effort context was skipped; context says why ('denied', 'unavailable' or 'failed').
{outcome, stage: 'context'} The context could not be delivered and was not best effort, so the message was not sent.
{outcome, stage: 'message'} The message itself was refused ('denied'), not offered by the host or the view has ended ('unavailable'), or could not complete ('failed').

context is information for the assistant's next turn that the conversation does not show: text becomes a content block and data becomes structuredContent of a ui/update-model-context request, which the stack sends before the ui/message request, never in parallel, and only for a host that advertises the matching capability (updateModelContext, message). Context blocks the message by default; set bestEffort: true only when the assistant has another way to get the information, such as a tool the message names. Nothing is retried: a request that fails or times out may still have been carried out, and a repeated message would ask the assistant twice. When the view ends, an outstanding or later call resolves unavailable and sends nothing further.

The application owns everything about the conversation: whether to append, what the text says, what context goes with it, and what the person sees when nothing was appended (compare the application's own identity for the state on screen first, because the append settles after the action's current() has gone false). The server stays authoritative; an appended message authorizes nothing. The Lit stack offers the same host.conversation. Whether Claude, ChatGPT or Alexa+ show the message, or how their assistants respond, is not established by the local checks; see the host compatibility guide.

A server can also declare an operation with no view. Omit view from the declaration given to registerPublicOperation or registerProtectedOperation (both from the stack's /server entry); the operation keeps its input and output contracts, annotations, budgets, cancellation and safe failures, and carries no UI association: no view resource and no _meta.ui on the tool definition or on any result.

For a local Node server, the optional local MCP listener handles HTTP transport and request lifetimes while your app supplies its operations.

React in Ansari Faithful Answers

Ansari Faithful Answers uses the React MCP stack for its answer-card application. The public destination integration PR records a candidate using @saga-a2ui/react-mcp-stack@0.7.6-rc.1 and React/React DOM 19.3.1, with clean public-package installation, application checks and browser validation. Its view source uses the stack’s FaithfulCard application entry. This is a concrete application consumer alongside the SDK examples; it does not depend on a generated React starter.

A prior React integration was also exercised in ChatGPT web, Claude web and the Alexa+ development simulator; the recorded SDK integration identifies that earlier candidate. Those observations are separate from the current destination PR, which remains a candidate, and from its version-specific installation and browser checks. The host compatibility guide separately identifies demonstrated assistant-host flows and their limits. Neither adoption by Ansari nor a local browser pass establishes every host or all renderer features.

Follow a complete React application

The notes React view is a complete stack composition. It uses the notes layout and React catalog, with the same controller and backend as the Lit application:

  1. The page supplies a #root container and calls startNotesReactView(). That function creates the application controller and starts the stack with the notes layout and catalog.
  2. Its actions callback receives the host transport. The controller builds the Archive and Mark read definitions, and createReactActions binds them to each rendered row. Archive reads the row's current note ID as its subject, so a reused row cannot receive an old note's completion message.
  3. onToolResult gives the delivered result to the controller. The controller validates the application data and updates the typed surface; rejected input is handled by the application instead of being cast into the view.
  4. When the view ends, the stack retires the surface, calls onTeardown to retire the application controller and its resources, and unmounts the React root. The action provider disposes its bindings during unmount; application cleanup must not depend on those bindings already being disposed inside onTeardown.

Follow the action guide for the complete server authorization and outcome trace. An expired session can stop Archive before its protected request; cancellation after sending that request leaves its effect uncertain. The application presents that uncertainty and does not retry the write automatically.

This is an existing application example. To start a new React application on Hono, run saga init --a2ui-stack react-mcp --server hono as Getting started describes; the starter holds the same public status, protected account and local Clear action as the Lit starters. Use public installation for the stack and exact peers.

The React integration surface

Use SAGA's React A2UI entry point for surfaces and catalogs when you compose below the stack:

import {Catalog, Surface, openSurface, type BoundSurface, type ReactCatalogComponent} from '@saga-a2ui/react/a2ui';
import {faithfulCardEntry} from '@saga-a2ui/react/faithful-card';
import {defineSurface} from '@saga-a2ui/core/protocols/a2ui/v0_9';

The entry point forwards the supported upstream A2UI v0.9 APIs; it does not implement another renderer or parser. It also exports SurfaceModel, CommonSchemas, createComponentImplementation, ReactComponentImplementation and NodeViewProps for custom catalogs.

Open a typed surface

For a lower-level integration without MCP delivery, define the surface once with defineSurface, open it with your catalog and mount it. The following is the lifecycle outline: layout, catalog and data are your application's validated definition, component catalog and next value. The complete notes composition above uses startA2uiView to own this lifecycle for MCP delivery.

const bound = openSurface(layout, catalog); // throws, with the session's message, if the layout is not accepted
<Surface of={bound.handle} />
bound.update(data); // {ok: true} | {ok: false, failure: {kind, message}}
bound.dispose();    // idempotent

openSurface owns the message session, the decoder and the order of the opening messages, so application code imports none of them. update is typed from the layout, validates against its schema and applies the data in place: the mounted controls keep their DOM nodes, focus, local state and action bindings. A rejected update returns the failure and changes nothing. After dispose, update fails and <Surface> renders nothing. Open the surface once per surface lifetime, outside render (for example in state or a store), and dispose it where you created it: <Surface> never disposes the handle, so Strict Mode's replay of effects cannot end it.

A2uiSurface and SurfaceModel stay exported for custom use over a session of your own.

Let SAGA own its A2UI installation

Applications using this integration should not add direct dependencies on @a2ui/react or @a2ui/web_core, or import their catalog/surface APIs directly. Migrate those imports to @saga-a2ui/react/a2ui and remove the direct dependencies. Keep React and React DOM as the application's declared peers at the versions supported by SAGA.

SAGA React owns A2UI 0.13.0 and explicitly depends on Zod 3.25.76. MCP Apps and MCP tool schemas use Zod 4. Both versions are intentionally installed. A direct application-level A2UI React dependency can receive the application's Zod 4 peer under pnpm, making its catalog types incompatible with A2UI's Zod 3 types. A re-export alone does not solve this: the explicit Zod 3 dependency and removal of the application's direct A2UI dependencies are both necessary.

For custom A2UI schemas use z from @saga-a2ui/core/protocols/a2ui/zod3. For MCP tool schemas use the application's Zod 4. Do not pass schemas between these interfaces, force a single Zod version globally, cast away the mismatch, or suppress it with skipLibCheck.

An application that independently uses A2UI outside this SAGA integration owns that additional dependency combination and must qualify it separately; it is not covered by SAGA's installed-consumer tests.

Pin the compatible upstream Markdown package

A2UI 0.13.0 allows @a2ui/markdown-it ^0.2.1. Version 0.2.2 requires @a2ui/web_core ^0.14.0; this is a different upstream combination. Pin 0.2.1 at the consumer workspace root, where package-manager overrides take effect.

For pnpm, merge this into the destination root pnpm-workspace.yaml (preserve its existing workspace globs and other overrides):

overrides:
  '@a2ui/markdown-it': '0.2.1'

For npm, use root package.json:

{"overrides": {"@a2ui/markdown-it": "0.2.1"}}

Then regenerate and commit the destination's root lockfile. An override in a dependency's manifest is not inherited by consumers; a direct dependency pin in SAGA also does not constrain every transitive resolution. This is a required consumer setting until the supported upstream combination changes.

Run a fresh pnpm install --strict-peer-dependencies and the application typecheck. The SDK's npm run test:pnpm-react performs that check using packed SDK packages, a real MCP/A2UI consumer and no source aliases or direct A2UI dependencies. Public SDK release availability is a separate prerequisite; this test does not publish packages.