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 pathContents
@ggui-ai/reactProviders, legacy render primitives
@ggui-ai/react/chat-helpersuseMcpAppsChat + render/message helpers
@ggui-ai/react/chat-threadThread-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.

OptionTypeRequiredDescription
chatEndpointstringYesPOST endpoint; the hook opens an SSE stream and feeds each event: message frame.
snapshotEndpointstringNoGET endpoint for the server-authoritative snapshot (rehydration). Defaults to chatEndpoint.
chatIdstringNoStable conversation id. Omit for a fresh chat — the server allocates one.
onChatAllocated(chatId: string) => voidNoFires when the server mints a fresh id; stamp it into URL / localStorage for rehydration.
getAuthToken() => string | undefined | Promise<…>NoBearer token sent as Authorization: Bearer <token> per request. See Auth-Gated UI.
onUnauthenticated() => boolean | Promise<boolean>No401 handler — refresh the token, return true to retry once.

Returns { entries, sessions, hostDisplayMode, sending, send, handleAppMessage, abort }:

FieldTypeDescription
entriesReadonlyArray<ChatEntry>Render-ready chat log (user / assistant / tool-call / session / error / end). A session entry carries { session: GguiSessionRef }.
sessionsReadonlyArray<GguiSessionRef>Every MCP-Apps resource produced this conversation (latest last). Mount with <AppRenderer>.
hostDisplayMode'inline' | 'fullscreen' | 'pip' | undefinedMost-recent render’s _meta.ui.displayMode hint (MCP-Apps SEP-1865) for layout auto-switch.
sendingbooleanTrue 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() => voidAbort 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:

PropTypeRequiredDescription
threadIdstringYesIdentifier for the persisted thread
appIdstringYesYour ggui app ID
adapterMessageStorageAdapterYesPersistence backend (see Chat with your own storage)
bearerTokenstringNoAuth token forwarded to useInvoke
aiContextRecord<string, unknown>NoAmbient metadata stamped on persisted messages
loadingFallbackReactNodeNoShown while the initial history load is in flight

useChatThread options:

OptionTypeDescription
outboxStorageOutboxStorage | nullDurable outbox for offline sends. createKvOutboxStorage(localStorage) on web; defaults to disabled
bearerTokenstringOverrides provider-level auth
onToolUse(block: ToolUseBlock) => voidForwarded to useInvoke
aiContextunknownOverrides provider-level metadata
isOnlinebooleanExplicit 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.

ExportWas
<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.
useWebSocketLow-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.