Skip to content
Sneak peek — you found hosted ggui early · official launch soon

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:

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

Terminal window
npm install @ggui-ai/mcp-apps-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/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.

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


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