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.