---
title: React host helpers (MCP Apps)
description: @ggui-ai/mcp-apps-react — host-helper library for embedding GGUI views in React via <AppRenderer> (@mcp-ui/client) + the useMcpAppsChat hook. Not an SDK: rendering semantics live in the mounted view.
---

:::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 render internals or WebSocket machinery.

React Native's equivalent host is `<McpAppIframe>` (from `@ggui-ai/mcp-apps-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/mcp-apps-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. **Invoke stream helpers** — `useInvoke` + `chat-helpers`. The low-level invoke-protocol stream and content-group utilities, for apps that own their message history (see the [own-storage cookbook](/cookbook/chat-own-storage/)).

Two in-process render primitives (`<GguiSessionRenderer>` / `<DynamicComponent>`) also ship, retained solely for the `@ggui-ai/console` debugger and dev-stack preview tooling — see [Legacy primitives](#legacy-primitives-console-debugger-only). New consumer code never needs them: rendering semantics (freeze latch, history epochs, error surfaces) live in the mounted view itself, which is why an embedded render behaves exactly like claude.ai.

## Installation

```bash
npm install @ggui-ai/mcp-apps-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/mcp-apps-react`              | Providers, `useInvoke`, `<UiFeedback>`, tool-result helpers, error types, legacy render primitives |
| `@ggui-ai/mcp-apps-react/chat-helpers` | `useMcpAppsChat` + render/message helpers                                                          |

---

## 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/mcp-apps-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 — bring your own storage

The package deliberately ships NO chat-persistence framework. Own your message history with your own storage and drive the stream with `useInvoke` + the `chat-helpers` conversion utilities — the full pattern is the [Chat with your own storage](/cookbook/chat-own-storage/) cookbook.

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

`useInvoke` is the low-level stream primitive. 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/mcp-apps-react";

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

// …mounted inside <GguiProvider appId="<yourAppId>">…</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/mcp-apps-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/).

---

## `<UiFeedback>` — host-side feedback affordance

`<UiFeedback>` (from the package root) renders a thumbs-up/down affordance for the currently mounted render. It stays hidden entirely unless you wire its `onUiFeedback` callback — with no callback wired, it renders nothing.

An in-iframe twin of the same affordance lives in `@ggui-ai/iframe-runtime` and emits a `ui-feedback` event on the `ggui:observe` seam instead — it is **off by default**: the serving runtime must opt in via its boot options (`uiFeedback: true`). Wire **exactly one** of the two surfaces — the host-side `<UiFeedback onUiFeedback>` or the in-iframe twin — never both, or feedback submits twice.

Two tool-result helpers round out the root export: `buildAppRendererToolResult` builds the `<AppRenderer>` toolResult envelope from a raw MCP tool result, and `extractUiMoments` pulls `UiMoment` entries (feedback, interaction milestones) out of an invoke stream.

---

## 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`.  |
| `<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. |

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/mcp-apps-react`](https://github.com/ggui-ai/ggui/tree/main/packages/mcp-apps-react) — they're documented as implementation, not a stable consumer contract. There is no `useWebSocket` export — the package doesn't expose a low-level WS client of its own. The only remaining low-level WS client is `WSTransport` from [`@ggui-ai/live-channel`](https://www.npmjs.com/package/@ggui-ai/live-channel) (reconnect ladder: 1s → cap 60s, 10 attempts, never-opened fail-fast), reached in practice through `@ggui-ai/iframe-runtime`.