Auth-Gated UI
read as.md ggui is a renderer, not an identity provider. Your app already knows who the user is. There are two separate auth concerns, and only the first is yours to wire:
- Authenticating your app → agent backend. Every request
useMcpAppsChatmakes (prompt POST, snapshot GET, the iframe → MCP tool-call relay) carries a bearer token your app supplies. The backend gates chat ownership on the principal that token identifies. This is the recipe below. - OAuth consent for tools the agent calls. When a tool the agent calls needs the end-user to authorize a third-party service (Google, Slack), that consent flow is not ggui’s responsibility — it belongs to MCP’s OAuth standard, the hosting agent, and your client app. ggui only surfaces an optional signal. See OAuth tool-consent is the client’s job.
Gate the chat behind a bearer token
Section titled “Gate the chat behind a bearer token”useMcpAppsChat takes two auth hooks:
getAuthToken()— called before every request; return the bearer token to send asAuthorization: Bearer <token>(orundefinedfor none).onUnauthenticated()— called on a401; refresh/re-mint your token, returntrueto retry the request once,falseto surface the error.
The ggui-basic-web sample uses these for a guest-token flow: it mints an anonymous token from the backend’s POST /auth/guest mount, caches it, and re-mints on 401. Swap that mint for your own user-session token and the same wiring gates renders behind a signed-in user:
import { useCallback, useRef } from "react";import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers";
function Chat({ agentEndpoint }: { agentEndpoint: string }) { const { user, getAccessToken, refreshSession } = useAuth(); // your auth provider
// Read the current token per-request (kept in a ref so the callback // always sees the latest after a refresh). const tokenRef = useRef<string | undefined>(getAccessToken()); const getAuthToken = useCallback(() => tokenRef.current, []);
// 401 → refresh once, then retry. Return false to give up. const onUnauthenticated = useCallback(async () => { const fresh = await refreshSession(); if (!fresh) return false; tokenRef.current = fresh; return true; }, [refreshSession]);
const { entries, sessions, send, handleAppMessage } = useMcpAppsChat({ chatEndpoint: `${agentEndpoint}/agent`, snapshotEndpoint: `${agentEndpoint}/agent`, getAuthToken, onUnauthenticated, });
// render `entries` + mount `sessions` with <AppRenderer> as usual}The token rides every hook request automatically. The backend authenticates it and scopes the conversation (its chatId snapshot + resume state) to that principal — clear the session and you’re a different principal with different chats.
Mount-time gate
Section titled “Mount-time gate”Don’t open the chat until your provider says the user is signed in — a plain conditional render, exactly how the sample guards on guestTokenReady:
function App({ agentEndpoint }: { agentEndpoint: string }) { const { isAuthenticated, isLoading } = useAuth(); if (isLoading) return <p>Loading…</p>; if (!isAuthenticated) return <LoginPage />; return <Chat agentEndpoint={agentEndpoint} />;}The tool-call relay carries the token too
Section titled “The tool-call relay carries the token too”When the iframe dispatches a tool call, your host relays it to the agent backend’s POST /agent (kind: "tool-call") inside <AppRenderer onCallTool>. Send the same bearer token on that relay so the MCP call runs as the signed-in user — the sample’s onCallTool reads getAuthToken() and sets Authorization: Bearer <token> on the relay fetch. Don’t trust anything the iframe puts in the payload as identity; the token on the relay is the credential.
OAuth tool-consent is the client’s job
Section titled “OAuth tool-consent is the client’s job”A different situation: the agent wants to call a tool that needs the end-user to authorize a third-party service first (read their Google Calendar, post to Slack). That consent flow is deliberately outside the ggui protocol. It is owned by:
- MCP’s OAuth standard — the spec defines how an MCP server advertises that a tool needs authorization and how the consent/token exchange happens.
- The hosting agent (e.g.
@anthropic-ai/claude-agent-sdk) — follows that spec to handle tool authorization. - Your end-user client app — the surface actually talking to the agent backend pops its own consent UI and drives the redirect. Consent is a client decision, not something ggui’s runtime or protocol renders.
- Guuey’s MCP proxy — captures an OAuth credential once so multiple agents can reuse it (coming soon — part of the hosted Guuey platform, not the open ggui protocol).
So ggui itself renders no consent screen. What it does provide is an optional helper signal: when the server emits a system frame with action: "auth_required", the iframe-runtime projects it to a typed AuthRequiredEvent on the ggui:observe postMessage channel ({ type: "ggui:observe", event }). A host that wants to surface a consent prompt can listen for it and open authUrl in a popup — but rendering that prompt, and the OAuth exchange itself, are your client’s job:
import type { AuthRequiredEvent } from "@ggui-ai/react";
// AuthRequiredEvent shape (kind: "auth-required"):// provider — canonical service id ("google", "slack")// authUrl — URL to open to start the OAuth consent flow// displayName? — human-readable service name// scopes? — requested OAuth scopes// message? — why access is neededIt’s a projection of the protocol’s SystemPayload, not a new ggui surface — surfacing it is opt-in, and ggui draws nothing itself. See Error Handling → renderer-side faults for the full observability catalog.
See also
Section titled “See also”- React SDK —
useMcpAppsChat(getAuthToken/onUnauthenticated) +<AppRenderer> ggui-basic-websample — the runnable guest-token reference (Chat.tsx)- Error Handling — HTTP
401, JSON-RPC auth codes, and theauth-requiredobservability event - Getting Started — the agent-side handshake → render → consume loop