React SDK
read as.md @ggui-ai/react lets you embed ggui-generated UIs inside a React web app. It has two layers, used independently:
- Render hosting —
useMcpAppsChat+<AppRenderer>. The canonical path: drive an MCP-Apps agent backend and mount its renders. Start here. - 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. New consumer code does not need them.
Installation
Section titled “Installation”npm install @ggui-ai/react @mcp-ui/clientreact / 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>
Section titled “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):
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 sample. Start there.
useMcpAppsChat(options)
Section titled “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. |
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
Section titled “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.
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) |
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 for the full MessageStorageAdapter interface, the offline-outbox pattern, and a worked adapter.
useInvoke — the low-level chat stream
Section titled “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.
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.
Theming
Section titled “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 for the two-layer model and the full token reference.
Legacy primitives (console-debugger only)
Section titled “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 — they’re documented as implementation, not a stable consumer contract.