> ## Documentation Index
> Fetch the complete documentation index at: https://docs.saga-a2ui.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Installing the React renderer with MCP

> Install the React renderer with MCP, import from the stack and add the removable credit.

> Documents saga a2ui **0.11.0-rc.1** (prerelease). This is the SDK guide [`docs/react-consumers.md`](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/docs/react-consumers.md) at source revision [`fb4126c`](https://github.com/MachineWisdomAI/saga-a2ui/tree/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8). It is published as a snapshot: later source changes do not alter it. Licensed under [Apache-2.0](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/LICENSE).

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:

```ts theme={null}
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](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/docs/development-reference.md#reading-the-whole-result)). 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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/public-install.md) for its exact-version recipe and registry availability, and [release preparation](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/docs/release-packages.md) 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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/host-compatibility.md) 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:

```ts theme={null}
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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/host-compatibility.md).

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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/local-mcp-server.md) handles HTTP transport and request lifetimes while your app supplies its operations.

## Removable credit

An application with its own catalog can credit the framework without a FaithfulCard. `SagaAttribution` is a small line, "Built with saga a2ui", whose name asks the host to open `https://saga-ai.dev/`. Render it where you want it and pass the host's link seam from the actions factory:

```tsx theme={null}
import {SagaAttribution, startA2uiView, type HostLinks} from '@saga-a2ui/react-mcp-stack';
import '@saga-a2ui/react-mcp-stack/attribution.css';

let links: HostLinks | undefined;
startA2uiView({
  // ...
  actions: (host) => {
    links = host.links; // asks the host to open a link; ends with the view
    return undefined;
  },
});

// Wherever the application renders its footer:
<footer><SagaAttribution links={links} /></footer>
```

To remove the credit, delete the element; there is nothing to configure and no account or payment. Optional `prefix`, `name` and `href` (an absolute `https:` URL) change the wording and destination, and only your own code supplies them. Without `links`, or with a destination that is not acceptable, nothing is clickable and nothing navigates. A host that does not open the link leaves the credit in place with a short message and the destination as text. The component makes no network request of its own and carries no card, Markdown parser or components package. Style it with `.saga-attribution` or the `--saga-attribution-*` tokens documented at the top of the stylesheet. Removing it removes no licence notice or answer-source attribution; those are not part of it. The design is [own-catalog attribution](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/blueprints/features/own_catalog_attribution.blueprint.md).

## React in Ansari Faithful Answers

Ansari Faithful Answers uses the React MCP stack for its answer-card application. The public [destination integration PR](https://github.com/iaser-ai/ansari/pull/323) 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](https://github.com/timeleft--/ansari/blob/e4ded0ec3e39afb33292e311c8fe3a0669c1bb41/apps/ansari-faithful-answers/src/view/main.tsx) 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](https://github.com/MachineWisdomAI/saga-a2ui/pull/161) 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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/host-compatibility.md) 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](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/examples/pack1-consumer/src/notes-react-view.tsx) is a complete stack composition. It uses the [notes layout](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/examples/pack1-consumer/src/notes-surface.ts) and [React catalog](https://github.com/MachineWisdomAI/saga-a2ui/blob/fb4126cc5a9ac2e7f9cd916c377d24b237d00bb8/examples/pack1-consumer/src/notes-react-catalog.tsx), 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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/actions.md#trace-one-press-allowed-and-denied) 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](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/getting-started.md#create-a-new-project) describes; the starter holds the same public status, protected account and local Clear action as the Lit starters. Use [public installation](https://docs.saga-a2ui.dev/docs/releases/0.11.0-rc.1/guides/public-install.md) 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:

```ts theme={null}
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.

```tsx theme={null}
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):

```yaml theme={null}
overrides:
  '@a2ui/markdown-it': '0.2.1'
```

For npm, use root `package.json`:

```json theme={null}
{"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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.