---
title: React SDK
description: @ggui-ai/react — embed ggui-generated UIs in React via <AppRenderer> (@mcp-ui/client) + the useMcpAppsChat hook, with chat persistence.
---

:::tip[Web host: `<AppRenderer>` + `useMcpAppsChat`]
The web consumer surface is `<AppRenderer>` — imported **directly from `@mcp-ui/client`** (the spec-canonical MCP Apps host; ggui does not re-export it) — driven by ggui's `useMcpAppsChat` hook. `<AppRenderer>` mounts each ggui render in a sandboxed iframe; the iframe owns the WebSocket lifecycle + renderer bundle, so your host code never imports `GguiRender`, `GguiSessionRenderer`, or WebSocket internals.

React Native's equivalent host is `<McpAppIframe>` (from `@ggui-ai/react-native`) — RN-only; there is no `<McpAppIframe>` on web. The bootstrap envelope, postMessage events, and `BootstrapFailureReason` codes are spec'd in [Bootstrap handshake](/protocol/bootstrap-handshake/).
:::

`@ggui-ai/react` lets you embed ggui-generated UIs inside a React web app. It has two layers, used independently:

1. **Render hosting** — `useMcpAppsChat` + `<AppRenderer>`. The canonical path: drive an MCP-Apps agent backend and mount its renders. **Start here.**
2. **Chat persistence** — `useChatThread` / `useInvoke`. The invoke-protocol chat stream with pluggable storage, for apps that own their message history.

A set of legacy in-process render primitives (`<GguiRender>` / `<GguiSessionRenderer>` / `<DynamicComponent>`) also ships, retained for the `@ggui-ai/console` debugger — see [Legacy primitives](#legacy-primitives-console-debugger-only). New consumer code does not need them.

## Installation

```bash
npm install @ggui-ai/react @mcp-ui/client
```

`react` / `react-dom` (18 or 19) and `@modelcontextprotocol/sdk` are peer dependencies. `@mcp-ui/client` is a direct dependency you import `<AppRenderer>` from.

| Import path                   | Contents                                  |
| ----------------------------- | ----------------------------------------- |
| `@ggui-ai/react`              | Providers, legacy render primitives       |
| `@ggui-ai/react/chat-helpers` | `useMcpAppsChat` + render/message helpers |
| `@ggui-ai/react/chat-thread`  | Thread-backed chat (`useChatThread`)      |

---

## Render hosting — `useMcpAppsChat` + `<AppRenderer>`

An agent emits UI as MCP-Apps renders. You drive the conversation with `useMcpAppsChat` and mount each render's sandboxed iframe with `<AppRenderer>` (imported directly from `@mcp-ui/client`):

```tsx
import { AppRenderer } from "@mcp-ui/client";
import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers";

function Chat({ agentUrl, sandboxUrl }: { agentUrl: string; sandboxUrl: string }) {
  const { entries, sessions, send, handleAppMessage } = useMcpAppsChat({
    chatEndpoint: `${agentUrl}/agent`,
    snapshotEndpoint: `${agentUrl}/agent`,
  });

  // Relay callbacks — minimal versions of the wiring in ggui-basic-web:
  const callViaRelay = async ({ name, arguments: args }: { name: string; arguments?: Record<string, unknown> }) => {
    const r = await fetch(`${agentUrl}/agent`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ kind: "tool-call", name, arguments: args ?? {} }),
    });
    const { result } = await r.json();
    return result; // CallToolResult — see ggui-basic-web for auth + the error envelope
  };
  const readViaRelay = async ({ uri }: { uri: string }) => {
    // Never fires when `html` is supplied — the agent-server interceptor inlines
    // the iframe HTML. Add a real relay only for guest re-reads.
    throw new Error(`unexpected resources/read for ${uri}`);
  };

  // render `entries` as chat bubbles; call send(prompt) to talk to the agent
  const latest = sessions[sessions.length - 1];
  return latest ? (
    <AppRenderer
      toolName="ggui_render"
      sandbox={{ url: new URL(sandboxUrl) }}
      html={latest.inlinedResource?.text}
      onReadResource={readViaRelay}
      onCallTool={callViaRelay}
      onMessage={handleAppMessage}
      onError={(err) => console.warn("render error", err)}
    />
  ) : null;
}
```

`<AppRenderer>`'s sandbox + resource-read + tool-call relay wiring is non-trivial (it implements the MCP-Apps host contract: a second-origin sandbox proxy, plus `onReadResource` / `onCallTool` callbacks that relay through your agent backend). The complete runnable reference — including auth and the relay — is the [`ggui-basic-web`](https://github.com/ggui-ai/ggui/tree/main/samples/apps/ggui-basic-web) sample. **Start there.**

### `useMcpAppsChat(options)`

| Option              | Type                                      | Required | Description                                                                                                      |
| ------------------- | ----------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `chatEndpoint`      | `string`                                  | Yes      | POST endpoint; the hook opens an SSE stream and feeds each `event: message` frame.                               |
| `snapshotEndpoint`  | `string`                                  | No       | GET endpoint for the server-authoritative snapshot (rehydration). Defaults to `chatEndpoint`.                    |
| `chatId`            | `string`                                  | No       | Stable conversation id. Omit for a fresh chat — the server allocates one.                                        |
| `onChatAllocated`   | `(chatId: string) => void`                | No       | Fires when the server mints a fresh id; stamp it into URL / localStorage for rehydration.                        |
| `getAuthToken`      | `() => string \| undefined \| Promise<…>` | No       | Bearer token sent as `Authorization: Bearer <token>` per request. See [Auth-Gated UI](/cookbook/auth-gated-ui/). |
| `onUnauthenticated` | `() => boolean \| Promise<boolean>`       | No       | `401` handler — refresh the token, return `true` to retry once.                                                  |

**Returns** `{ entries, sessions, hostDisplayMode, sending, send, handleAppMessage, abort }`:

| Field              | Type                                                  | Description                                                                                   |
| ------------------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `entries`          | `ReadonlyArray<ChatEntry>`                            | Render-ready chat log (`user` / `assistant` / `tool-call` / `session` / `error` / `end`). A `session` entry carries `{ session: GguiSessionRef }`. |
| `sessions`         | `ReadonlyArray<GguiSessionRef>`                       | Every MCP-Apps resource produced this conversation (latest last). Mount with `<AppRenderer>`. |
| `hostDisplayMode`  | `'inline' \| 'fullscreen' \| 'pip' \| undefined`      | Most-recent render's `_meta.ui.displayMode` hint (MCP-Apps SEP-1865) for layout auto-switch.  |
| `sending`          | `boolean`                                             | True between `send()` and turn completion.                                                    |
| `send`             | `(prompt: string, opts?: { meta? }) => Promise<void>` | Post a user prompt; opens the SSE stream.                                                     |
| `handleAppMessage` | `(params) => Promise<Record<string, unknown>>`        | Drop-in for `<AppRenderer onMessage>` — forwards a guest `ui/message` to the agent.           |
| `abort`            | `() => void`                                          | Abort the in-flight stream.                                                                   |

A `GguiSessionRef` carries `{ resourceUri, action, toolUseId?, inlinedResource? }`. When `inlinedResource.text` is present (the `@ggui-ai/agent-server` interceptor pre-fetched the iframe HTML), pass it as `<AppRenderer html={…}>` and skip the `onReadResource` round-trip.

---

## Chat persistence — `useChatThread`

`useMcpAppsChat` rehydrates from a server snapshot. When you want to own message history (your own database, offline outbox, multi-device sync), use the chat-thread tier under the `@ggui-ai/react/chat-thread` subpath. It runs on the invoke-protocol stream (`useInvoke`) and persists through a `MessageStorageAdapter` you supply.

```tsx
import {
  ChatThreadProvider,
  useChatThread,
  type MessageStorageAdapter,
} from "@ggui-ai/react/chat-thread";

function ChatRoute({
  threadId,
  appId,
  adapter,
}: {
  threadId: string;
  appId: string;
  adapter: MessageStorageAdapter;
}) {
  return (
    <ChatThreadProvider threadId={threadId} appId={appId} adapter={adapter}>
      <Transcript />
    </ChatThreadProvider>
  );
}

function Transcript() {
  const { messages, send, isStreaming, error } = useChatThread();
  // …
}
```

`ChatThreadProvider` is the **outer loader** — it gates render until persisted history resolves, then mounts children with a frozen seed (it wraps `<GguiProvider>` for you). `useChatThread()` reads the seed, forwards it to `useInvoke.initialMessages`, persists finalized content groups via `adapter.appendMessage`, and merges persisted + pending-outbox + live messages into one timeline.

**`ChatThreadProvider` props:**

| Prop              | Type                      | Required | Description                                                                         |
| ----------------- | ------------------------- | -------- | ----------------------------------------------------------------------------------- |
| `threadId`        | `string`                  | Yes      | Identifier for the persisted thread                                                 |
| `appId`           | `string`                  | Yes      | Your ggui app ID                                                                    |
| `adapter`         | `MessageStorageAdapter`   | Yes      | Persistence backend (see [Chat with your own storage](/cookbook/chat-own-storage/)) |
| `bearerToken`     | `string`                  | No       | Auth token forwarded to `useInvoke`                                                 |
| `aiContext`       | `Record<string, unknown>` | No       | Ambient metadata stamped on persisted messages                                      |
| `loadingFallback` | `ReactNode`               | No       | Shown while the initial history load is in flight                                   |

**`useChatThread` options:**

| Option          | Type                            | Description                                                                                          |
| --------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `outboxStorage` | `OutboxStorage \| null`         | Durable outbox for offline sends. `createKvOutboxStorage(localStorage)` on web; defaults to disabled |
| `bearerToken`   | `string`                        | Overrides provider-level auth                                                                        |
| `onToolUse`     | `(block: ToolUseBlock) => void` | Forwarded to `useInvoke`                                                                             |
| `aiContext`     | `unknown`                       | Overrides provider-level metadata                                                                    |
| `isOnline`      | `boolean`                       | Explicit online override (otherwise read from `useNetworkState`)                                     |

See [Chat with your own storage](/cookbook/chat-own-storage/) for the full `MessageStorageAdapter` interface, the offline-outbox pattern, and a worked adapter.

### `useInvoke` — the low-level chat stream

`useChatThread` is built on `useInvoke`, which you can also use directly. It POSTs the user's message + history to `{endpointUrl}/invoke`, reads the SSE response, and accumulates assistant content blocks. It reads its endpoint + app id from the nearest `<GguiProvider>`, so it must be mounted inside one.

```tsx
import { GguiProvider, useInvoke } from "@ggui-ai/react";

function Chat() {
  const { messages, send, isStreaming, error } = useInvoke({ endpointUrl: "/api/chat" });
  // messages: ConversationMessage[] — { id, role, content: ContentBlock[], isStreaming }
}

// …mounted inside <GguiProvider appId="app_…">…</GguiProvider>
```

`send(text, { clientMessageId })` lets the caller control the user message id — the basis for retry-without-duplicates and cross-device continuity. The `@ggui-ai/react/chat-helpers` exports (`invokeMessageToContentGroups`, `contentGroupsToConversationMessages`, `useRafThrottled`, `extractRenderFromToolResult`) turn the stream into a durable persistence shape — see the [chat-own-storage cookbook](/cookbook/chat-own-storage/).

---

## Theming

`@ggui-ai/design`'s `<ThemeProvider>` themes your host chrome by injecting `--ggui-*` CSS variables; the agent's `ggui.json` `theme` preset themes the generated iframe content. The operator's per-app theme overlay (`ggui.json#theme`) is delivered on the render's bootstrap meta and applied inside the iframe automatically — your host code never injects variables into generated content. See [Custom Theming](/cookbook/custom-theming/) for the two-layer model and the full token reference.

---

## Legacy primitives (console-debugger only)

Before the MCP-Apps `<AppRenderer>` host, ggui rendered generated component code **directly in the React tree**. Those primitives still ship and are still exported — `@ggui-ai/console`'s render debugger imports them for in-process render visibility — but they are **not the consumer path**. New web code hosts renders with `<AppRenderer>` (above); generated component code runs inside the sandboxed iframe, not in your tree.

| Export                  | Was                                                                                   |
| ----------------------- | ------------------------------------------------------------------------------------- |
| `<GguiProvider>`        | App-config context. Still required as the ancestor for `useInvoke` / `useChatThread`. |
| `<GguiRender>`          | Single-render mount keyed by `sessionId`, managing the live WebSocket.                |
| `<GguiSessionRenderer>` | Renders a `GguiSession`'s compiled component code in-tree.                            |
| `<DynamicComponent>`    | Imports + mounts compiled ESM component code at runtime.                              |
| `<ProvisionalRenderer>` | Paints in-flight `_ggui:preview` envelopes while generation streams.                  |
| `<SelfRepairBoundary>`  | Error boundary that ships a fault report to a server-side repair pipeline.            |
| `useWebSocket`          | Low-level live-channel connection with `sendAction`.                                  |

These read against the live-channel WebSocket directly (`ws://127.0.0.1:6781/ws` for `ggui serve`). For their exact prop shapes, read the source in [`@ggui-ai/react`](https://github.com/ggui-ai/ggui/tree/main/packages/ggui-react) — they're documented as implementation, not a stable consumer contract.