React host helpers (MCP Apps)
read as.md@ggui-ai/mcp-apps-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. - 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).
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. 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
Section titled “Installation”npm install @ggui-ai/mcp-apps-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/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>
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/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 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 — bring your own storage
Section titled “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.
useInvoke — the low-level chat stream
Section titled “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.
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.
<UiFeedback> — host-side feedback affordance
Section titled “<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
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. |
<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 — 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 (reconnect ladder: 1s → cap 60s, 10 attempts, never-opened fail-fast), reached in practice through @ggui-ai/iframe-runtime.