Skip to content

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

  1. Render hostinguseMcpAppsChat + <AppRenderer>. The canonical path: drive an MCP-Apps agent backend and mount its renders. Start here.
  2. Chat persistenceuseChatThread / 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.

Terminal window
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>

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.

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.


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.

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.


@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.


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.