This is the abridged developer documentation for ggui # Introduction > ggui is an open, MCP-native protocol for AI agents to render interactive UI on demand. GGUI Preview — self-hosted via `ggui serve`. **ggui** is an open protocol that lets AI agents render interactive UI on the fly. Your agent describes what it needs in natural language; ggui compiles a typed component and returns it as an MCP-Apps resource (`ui://ggui/render/`) that your host mounts. The UI reports back typed events the next time your agent calls `ggui_consume`. You run the whole protocol yourself with **`ggui serve`** — no account, no cloud, no API key. A managed hosted endpoint that speaks the same wire is coming after the preview. [Quickstart — local in 5 minutes](/oss-quickstart/) [How ggui works](/how-it-works/) [Reading as an LLM?](/agents/) ## Choose your path [Section titled “Choose your path”](#choose-your-path) ### Agent Builder — wire ggui into an MCP server [Section titled “Agent Builder — wire ggui into an MCP server”](#agent-builder--wire-ggui-into-an-mcp-server) Start with the [Quickstart](/oss-quickstart/) (5 min, local). Then read the [MCP protocol reference](/api/mcp-protocol/), browse the [Cookbook](/cookbook/feedback-form/), or copy a worked [example agent](/examples/claude-agent/). ### Host — connect a client to your server [Section titled “Host — connect a client to your server”](#host--connect-a-client-to-your-server) [Connect Claude Desktop](/clients/claude-desktop/) to a `ggui serve` you run yourself. [Other MCP hosts](/clients/connect-other-hosts/) use the same self-hosted endpoint with their own config shape. ### Operator — self-host the stack [Section titled “Operator — self-host the stack”](#operator--self-host-the-stack) [`ggui serve`](/cli/serve/) is the local deployment guide. [Reference deploys](/self-hosted/reference-deploys/) covers Docker, Fly.io, and Render. The [Self-hosted Registry](/sdk/self-hosted-registry/) is the artifact layer for private gadgets and blueprints. ### Agentic App Builder — make your SaaS agent-drivable [Section titled “Agentic App Builder — make your SaaS agent-drivable”](#agentic-app-builder--make-your-saas-agent-drivable) If you ship a SaaS or webapp and want agents to drive it without rewriting the frontend, see [Agentic App Builders](/agentic-app-builders/). ### LLM agent reading docs [Section titled “LLM agent reading docs”](#llm-agent-reading-docs) Every page is also raw markdown at the same slug. Start with [`/llms.txt`](/llms.txt) (index, per the [llms.txt convention](https://llmstxt.org/)), `/llms-full.txt` (single-file dump of the whole site), or `/llms-small.txt` (compact variant). See [the LLM-agent track](/agents/) for the full machine-readable surface. ## How ggui works [Section titled “How ggui works”](#how-ggui-works) A typical exchange is four moments — **handshake → render → interact → consume**. See [How ggui works](/how-it-works/) for the full walk-through with code. ## Key concepts [Section titled “Key concepts”](#key-concepts) A few terms recur across the docs: * **GguiSession** — one rendered UI, minted by `ggui_render` (*render* is the verb; the object it creates is a GguiSession). Each GguiSession carries a stable `sessionId`. * **Contract** — the typed agreement between agent and renderer for one GguiSession. * **Tool** — an agent-side action (`ggui_render`, `ggui_consume`, …) — the MCP surface. * **Gadget** — a renderer-side capability (Leaflet map, Stripe Checkout, …) the LLM can compose with. * **Blueprint** — a cached recipe — a UI promoted from one-shot to “use this exact screen next time.” → [Glossary](/glossary/) for everything else. ## What’s on this site [Section titled “What’s on this site”](#whats-on-this-site) * **[How ggui works](/how-it-works/)** — narrative walk-through for builders * **[Quickstart](/oss-quickstart/)** — zero to a running local server in 5 minutes * **Protocol** — [Overview](/protocol/overview/) · [Envelopes](/protocol/envelopes/) · [Bootstrap](/protocol/bootstrap-handshake/) · [Conformance](/protocol/conformance/) · [Version policy](/protocol/version-policy/) * **API** — [MCP](/api/mcp-protocol/) · [WebSocket](/api/websocket-protocol/) · [MCP Apps](/api/mcp-apps/) · [OAuth (self-hosted)](/api/oauth/) · [Ops MCP](/api/ops-mcp/) · [Rate limits](/api/rate-limits/) * **SDK** — [React](/sdk/react/) · [Gadgets](/sdk/gadgets/) · [Marketplace](/sdk/marketplace/) · [Self-hosted Registry](/sdk/self-hosted-registry/) * **CLI** — [Overview](/cli/) · [`ggui dev`](/cli/dev/) · [`ggui serve`](/cli/serve/) * **Connect a host** — [Claude Desktop](/clients/claude-desktop/) · [Other MCP hosts](/clients/connect-other-hosts/) * **Self-hosted** — [Pair a client app](/self-hosted/pairing/) · [Reference deploys](/self-hosted/reference-deploys/) * **Cookbook** — [Feedback form](/cookbook/feedback-form/) · [Multi-step wizard](/cookbook/multi-step-wizard/) · [Real-time dashboard](/cookbook/real-time-dashboard/) · [Auth-gated UI](/cookbook/auth-gated-ui/) · [Theming](/cookbook/custom-theming/) · [Error handling](/cookbook/error-handling/) · [Chat](/cookbook/chat-own-storage/) · [Testing](/cookbook/testing/) * **Architecture** — [Overview](/architecture/overview/) · [Agent backend](/architecture/agent-backend/) · [Audience routes](/architecture/audience-routes/) · [MCP services](/architecture/mcp-services/) · [Event System](/architecture/event-system/) · [UI Generator](/architecture/ui-generator/) * **Design System** — [Design Tokens](/design/tokens/) * **Examples** — [Claude Agent](/examples/claude-agent/) · [OpenAI](/examples/openai-agent/) · [Gemini](/examples/gemini-agent/) · [OpenClaw](/examples/openclaw-agent/) · [Generic MCP](/examples/generic-mcp/) * **Glossary** — [terminology reference](/glossary/) * **Troubleshooting** — [common errors and what they mean](/troubleshooting/) # 404 — page not found > Nothing lives at this URL. Try the home page, the glossary (gadget / tool / blueprint), or the search box in the sidebar. # For Agentic App Builders > Make your existing SaaS or webapp something an AI agent can drive — without rewriting it. Vision page + waitlist for the agentic-app-builders track on ggui. Vision page This describes a direction, not a shipped product. If you’re building agentic apps **today**, take the [Agent Builder track](/oss-quickstart/) — that path is live. The waitlist at the bottom is for early access to this track when its first adapter lands. ## The shift [Section titled “The shift”](#the-shift) AI agents drive existing SaaS apps through three increasingly unreliable layers: 1. **Reading screenshots** — vision-language models reason about rendered pixels. Brittle, slow, no typed I/O. 2. **Browser automation** — Playwright / Puppeteer scripts wrapping flows. Snap the moment a button moves; the agent has no way to know the contract changed. 3. **Reading official APIs** — when they exist, and only for the slice the API covers. Most production apps keep a third of their behavior in the UI, not the API. What’s missing: a **typed contract** between the agent and an existing app’s surface, so the agent can drive flows safely without parsing pixels or scraping markup. That’s the gap this track fills. ## What gguifying an app looks like [Section titled “What gguifying an app looks like”](#what-gguifying-an-app-looks-like) You add a small ggui adapter to your existing app. The adapter: 1. Exposes navigable routes as **renders** with typed contracts 2. Mirrors form fields as **`actionSpec`** (typed inbound actions that drive turns) 3. Mirrors visible state as **`contextSpec`** (read-only observable state the agent reacts to) 4. Wraps any browser-only library you use (Stripe Checkout, Mapbox, calendar pickers) as **gadgets** so the LLM knows how to compose with them The agent then drives your app the same way it drives a freshly-generated ggui UI — via `ggui_handshake`, `ggui_render`, `ggui_consume`. Same MCP wire. Same contract guarantees. **Critically:** the human-facing UI doesn’t change. End-users keep clicking your buttons. Agents get a parallel typed surface onto the same flows. ## What you’d write [Section titled “What you’d write”](#what-youd-write) The provisional shape is a `ggui.app.json` next to your existing app config: ```json { "name": "my-saas", "routes": [ { "path": "/invoices/new", "intent": "create an invoice", "agentCapabilities": { "tools": { "createInvoice": { "toolInfo": { "inputSchema": { "$ref": "./schemas/invoice.json" } } } } }, "actionSpec": { "submit": { "label": "Create invoice", "schema": { "$ref": "./schemas/invoice.json" }, "nextStep": "createInvoice" } }, "contextSpec": { "currentDraft": { "schema": { "$ref": "./schemas/invoice-draft.json" } } } } ] } ``` Your app code stays as-is. An agent runtime can now discover `/invoices/new` from the adapter’s catalog, `ggui_handshake` against its contract, and `ggui_render` with a typed payload — no clicking around. (Exact wire shape for app-catalog lookup is part of the SDK’s in-design surface.) ## Three scenarios this unlocks [Section titled “Three scenarios this unlocks”](#three-scenarios-this-unlocks) 1. **Your support agent drives the app for the customer.** “Refund last month’s invoice for customer X” → agent navigates to `/invoices/`, finds the row, calls `refund` with the typed payload. No human-in-the-loop browsing. 2. **Your sales engineer runs an agent-narrated demo.** During a prospect call, the agent narrates while driving the form in real-time. The prospect sees the same UI any user sees, but the agent’s typed actions land like a polished guided tour. 3. **Power users hand work off to their AI.** “I have 40 of these to fill in — can my AI do it?” The AI has a typed surface to drive, not pixels to scrape. ## What’s shipping when [Section titled “What’s shipping when”](#whats-shipping-when) | Component | Status | | --------------------------------------------------------- | --------- | | ggui protocol (this site) | Live | | Agent Builder track (build a ggui-native agent) | Live | | Agentic App SDK (wrap an existing app) | In design | | Hosted gguifier service on [guuey.com](https://guuey.com) | In design | | Reference adapters for Next.js / Rails / Django | Planned | | Reference adapters for legacy stacks (Java EE, Drupal, …) | Planned | No dates. Real engineering work, not a marketing roadmap. ## Waitlist [Section titled “Waitlist”](#waitlist) If this is the track you actually need, drop your email. We’ll write when the first adapter ships and we have an early-access slot to fill. Email Join waitlist Opens your mail client with a pre-filled message. We'll wire up a real intake form before the first adapter lands — until then this stays a direct mailto so nothing intermediates the signal. ## Related [Section titled “Related”](#related) * [How ggui works](/how-it-works/) — the protocol’s four moments. The gguify pattern reuses the same handshake → render → interact → consume loop, just driven by your app’s routes instead of LLM-generated UI. * [Glossary](/glossary/) — `actionSpec`, `contextSpec`, `gadget`, `tool`, `blueprint`. * [For LLM agents](/agents/) — machine-readable resources, including this page at [`/agentic-app-builders.md`](/agentic-app-builders.md). # For LLM agents > Machine-readable resources for LLMs and coding-assistant devtools reading ggui docs programmatically — aggregated dumps, per-page .md companions, stable anchors, wire schemas. This page is for **non-human readers** — LLM agents, coding-assistant devtools (Claude Code, Cursor, Cline, Continue, Codeium), evaluators, and scrapers. Humans, the rest of the site is for you. ## What’s available [Section titled “What’s available”](#whats-available) | Resource | URL | When to fetch | | --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | Site index | [`/llms.txt`](/llms.txt) | First contact. Every page in [llms.txt format](https://llmstxt.org/) with one-line summaries. | | Whole-site dump | [`/llms-full.txt`](/llms-full.txt) | One-shot context loading. \~400 KB. Drop into your window for cross-topic tasks. | | Compact dump | [`/llms-small.txt`](/llms-small.txt) | Smaller one-shot context when `/llms-full.txt` is too big. Custom subsets live at `/_llms-txt/.txt`. | | Per-page raw markdown | `/.md` | Reading one specific page. No HTML, no chrome. | | Stable anchors | `/#` | Deep-linking to a section. Every H2/H3 has a Starlight-derived id. | ## Per-page `.md` companions [Section titled “Per-page .md companions”](#per-page-md-companions) Every page is also served as raw markdown at the same slug with a `.md` extension. Examples: * [`/how-it-works.md`](/how-it-works.md) — the narrative walk-through * [`/api/mcp-protocol.md`](/api/mcp-protocol.md) — the wire reference * [`/protocol/envelopes.md`](/protocol/envelopes.md) — live-channel envelope shapes * [`/glossary.md`](/glossary.md) — terminology lookup * [`/cli/serve.md`](/cli/serve.md) — `ggui serve` command reference The `.md` response is the source markdown with a small `---\ntitle: ...\n---` envelope and **no other transformation**. Fetch from any origin: ```bash curl https://docs.ggui.ai/protocol/envelopes.md ``` ```ts const res = await fetch("https://docs.ggui.ai/protocol/envelopes.md"); const body = await res.text(); ``` CORS is open (`Access-Control-Allow-Origin: *`); `Cache-Control` permits 5-minute CDN caching. ## Search docs via MCP [Section titled “Search docs via MCP”](#search-docs-via-mcp) Any LLM agent will be able to connect to `mcp.ggui.ai/docs` (coming soon) and search / read these docs programmatically — no auth required. See [Docs MCP route](/api/mcp-docs/) for the tool catalog and connection details. ## When to use which [Section titled “When to use which”](#when-to-use-which) ```plaintext You want → Fetch ─────────────────────────────────────────── ────────────────────────────────── Orient quickly, plan a session /llms.txt Drop everything into context (one-shot) /llms-full.txt Read one specific page /.md Deep-link to a section in conversation //# ``` ## Stable anchors [Section titled “Stable anchors”](#stable-anchors) Every H2 and H3 has a stable `id` derived by Starlight from its text. Anchors don’t change between releases unless the heading text changes. Examples: * [`/glossary/#gguisession-a-render`](/glossary/#gguisession-a-render) — definition of the GguiSession (the “render”) * [`/protocol/envelopes/#actionenvelope`](/protocol/envelopes/#actionenvelope) — inbound live-channel envelope * [`/api/mcp-protocol/#ggui_render`](/api/mcp-protocol/#ggui_render) — the `ggui_render` MCP method The same anchors work on the `.md` companions: [`/protocol/envelopes.md#actionenvelope`](/protocol/envelopes.md#actionenvelope) — markdown clients with anchor-scroll support honor them. ## Wire schemas (roadmap) [Section titled “Wire schemas (roadmap)”](#wire-schemas-roadmap) The wire envelopes (`ActionEnvelope`, `StreamEnvelope`) and the MCP method shapes (`ggui_handshake`, `ggui_render`, `ggui_consume`, …) live in TypeScript in `@ggui-ai/protocol`. Protocol version: see `PROTOCOL_VERSION` in `@ggui-ai/protocol` (currently `draft-2026-06-12`). Standalone JSON-Schema endpoints at `/api/schemas/.json` are planned but not yet shipped. Until then, the canonical wire shapes live at: * [`/protocol/envelopes/`](/protocol/envelopes/) — envelope shapes, fields, validation rules * [`/api/mcp-protocol/`](/api/mcp-protocol/) — MCP methods, request/response shapes * [`/api/websocket-protocol/`](/api/websocket-protocol/) — live-channel framing For machine-parseable types, install the npm package: ```bash npm install @ggui-ai/protocol ``` ## Site conventions [Section titled “Site conventions”](#site-conventions) Things to know if you’re writing code against these docs: * **`ggui`** = the open protocol. Self-host with `ggui serve`; a hosted endpoint at `mcp.ggui.ai` is coming soon. Open source at `github.com/ggui-ai/ggui`. Documented on this site. * **`guuey`** = a separate SaaS platform at `guuey.com`. Different surface, different docs. Don’t conflate the two. * **`gadget`** = renderer-side capability — a wrapped 3rd-party library (`Leaflet`, `Stripe`, …). Formerly called `clientLibraries`. * **`tool`** = agent-side action (an MCP tool the agent invokes). * **`blueprint`** = cached UI recipe (matched at `ggui_handshake` by intent + contract similarity). These three nouns are not interchangeable; mixing them in generated code will confuse readers. ## Page-level metadata [Section titled “Page-level metadata”](#page-level-metadata) Every page has frontmatter with at least `title` and `description`. Most also carry `audience` (one of `agent-builder`, `host`, `operator`, `llm-agent`, `agentic-app-builder`, `all`) and optionally `prereqs`. The `.md` companion re-emits `title` and `description` at the top of the response; other frontmatter fields (`audience`, `prereqs`) are only in the repo source. ## Telemetry [Section titled “Telemetry”](#telemetry) The site sends pageview events to PostHog and tags requests from known LLM-agent user-agents (`claude-bot`, `gptbot`, `perplexitybot`, `cursor`, `cline`, `continue`, `codeium`, …) with `is_bot: true`. No PII, no fingerprinting. Set a useful user-agent and you’ll show up in the bot-traffic dashboard — that helps us prioritize docs machine readers actually use. ## Reporting issues [Section titled “Reporting issues”](#reporting-issues) Found a page that’s hard for LLM consumption, or want a `.md` companion that’s missing something? Open an issue at [`github.com/ggui-ai/ggui/issues`](https://github.com/ggui-ai/ggui/issues) tagged `docs/llm-readability`. # MCP Apps support > How ggui implements the io.modelcontextprotocol/ui capability so hosts like Claude Desktop, claude.ai, Goose, and VS Code Copilot render generative UIs inline. [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) is the protocol extension that lets MCP servers ship interactive UI alongside structured data, and lets MCP hosts render those UIs inline in the chat surface. The OSS [`@ggui-ai/mcp-server`](/oss-quickstart/) implements the wire format on both sides — as will the hosted ggui server (`mcp.ggui.ai`, coming soon) — so generative UIs render directly in chat instead of forcing a “click this link” detour to a browser tab. This page documents the protocol pieces ggui implements. For end-user setup, see [Connect Claude Desktop](/clients/claude-desktop/). For the underlying transport, see [WebSocket protocol](/api/websocket-protocol/). ## What MCP Apps adds [Section titled “What MCP Apps adds”](#what-mcp-apps-adds) Without MCP Apps, an MCP tool that produces UI has to choose between: 1. Returning structured data and hoping the host formats it (no interactivity), or 2. Returning a URL the user clicks out to (interactive, but chat and UI live in separate windows). MCP Apps adds a third option: declare a UI resource alongside the tool result, the host sandboxes it in an iframe inside the chat, and a WebSocket channel carries data both ways — host to UI for live updates, UI to server for actions. ## What ggui ships [Section titled “What ggui ships”](#what-ggui-ships) When the server boots with `mcpApps` enabled, three things happen: 1. **`io.modelcontextprotocol/ui` is advertised** in the server’s `initialize` capabilities (under `experimental`). MCP-Apps-aware hosts read this and switch on inline rendering. 2. **`ui://ggui/render` is served** as a resource via `resources/read` — a minimal HTML shell that loads [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime) and opens the WebSocket channel. 3. **Every `ggui_render` tool result carries the `_meta["ai.ggui/render"]` slice** — `sessionId`, `appId`, `runtimeUrl`, `wsUrl`, a short-TTL `wsToken`, and `expiresAt` (ISO 8601 string) as top-level fields, alongside capability + render-state fields, plus theme fields (`themeId`, `themeMode`, `theme` — a validated `--ggui-*` CSS-variable overlay the iframe applies at `:root`), a `pollingUrl` fallback for WS-blocked environments, and `lastSequence` to seed replay cursors. The iframe consumes the slice, opens the WebSocket, and trades the `wsToken` for a longer-lived `sessionToken` for reconnects. ## Capability declaration [Section titled “Capability declaration”](#capability-declaration) On `initialize`, ggui returns: ```json { "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": false, "listChanged": false }, "experimental": { "io.modelcontextprotocol/ui": {} } } } ``` Hosts that recognize `io.modelcontextprotocol/ui` flip into inline-render mode. Hosts that don’t simply ignore the capability and skip inline rendering — the render is still delivered as a resource (`ui://ggui/render/` on `_meta.ui.resourceUri`), but without MCP-Apps support there is nothing to mount it. There is no agent-returned URL to open instead. ## Tool result shape [Section titled “Tool result shape”](#tool-result-shape) Every UI-producing tool (today: `ggui_render`) declares a meta-resource on the result so the host knows where to load the UI from: ```json { "content": [{ "type": "text", "text": "Created render render_abc123" }], "_meta": { "ui": { "resourceUri": "ui://ggui/render/render_abc123" }, "ai.ggui/render": { "sessionId": "render_abc123", "appId": "app_abc", "runtimeUrl": "https://your-server.example.com/_ggui/iframe-runtime.js", "wsUrl": "wss://your-server.example.com/ws", "wsToken": "btkn_…", "expiresAt": "2099-01-01T00:00:00.000Z" } } } ``` The tool **declaration** (returned by `tools/list`) is what carries `_meta.ui.visibility: ["model"]` — the MCP Apps signal that this tool ships a renderable UI surface. Each per-call **result** stamps `_meta.ui.resourceUri` (the per-render URI) plus the `_meta["ai.ggui/render"]` slice. The host fetches `ui://ggui/render` (or the per-render form `ui://ggui/render/`) once, sandboxes it in an iframe, and forwards the `_meta["ai.ggui/render"]` slice to it. ## The shell at `ui://ggui/render` [Section titled “The shell at ui://ggui/render”](#the-shell-at-uigguirender) Reading the resource returns a small HTML document — paper-themed, full-bleed, no chrome — whose only job is: 1. Receive the `ai.ggui/render` slice from the host (via `postMessage`). 2. Dynamically load `runtimeUrl` (the iframe-runtime bundle). 3. Hand the slice to the runtime, which opens the WebSocket and starts rendering. The shell is intentionally minimal. The actual rendering work — component resolution, contract validation, action dispatch, gadget loading — lives in [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime), which the shell loads on demand. This keeps the shell payload tiny and lets the runtime version-bump independently of host caches. ## Bootstrap token exchange [Section titled “Bootstrap token exchange”](#bootstrap-token-exchange) The `wsToken` is short-lived (default 180s) and reusable within its TTL, so a transient WebSocket drop reconnects without a fresh handshake. The iframe trades it for a longer-lived `sessionToken` (default 4h) on the first wsToken-authed `subscribe` frame, then uses the session token (`sessionToken`) for reconnects. After the wsToken expires, the iframe swaps the envelope via `ggui_runtime_refresh_ws_token` (within the refresh window) or re-bootstraps. Consequences: * Hosts can cache the resource document, but the bootstrap is per-call — every render mints a fresh token. * An iframe that loses connection reconnects with its session token (`sessionToken`) without re-fetching the resource or re-running OAuth. * A leaked bootstrap token is useless after the TTL expires. The bootstrap is HMAC-signed with a server-side `wsTokenSecret`. Multi-pod deployments MUST share a deterministic secret (typically from a secrets manager) so any pod accepts any other pod’s tokens. The handshake details are documented in [Bootstrap handshake](/protocol/bootstrap-handshake/). ## Self-hosted: enabling MCP Apps in your own server [Section titled “Self-hosted: enabling MCP Apps in your own server”](#self-hosted-enabling-mcp-apps-in-your-own-server) ```typescript import { createGguiServer } from "@ggui-ai/mcp-server"; const server = createGguiServer({ // ... renderChannel: true, // required — MCP Apps needs the WS channel mcpApps: { wsUrl: "wss://your-server.example.com/ws", }, runtime: true, // serve the iframe-runtime bundle wsTokenSecret: process.env.WS_TOKEN_SECRET, // required for multi-pod }); ``` For local dev, `wsUrl: "ws://127.0.0.1:6781/ws"` is the conventional loopback URL. Hosted ggui (coming soon) will use `wss://mcp.ggui.ai/ws`. What each option does: * **`renderChannel: true`** — mounts the live-channel WebSocket at `/ws`. MCP Apps requires it; the iframe has nowhere to connect without one. * **`mcpApps.wsUrl`** — the publicly-reachable WebSocket URL written onto every bootstrap. Don’t ship `ws://localhost:…` to internet-accessible servers — clients must be able to reach it. * **`runtime: true`** (default when `mcpApps` is on) — mounts the iframe-runtime bundle at `/_ggui/iframe-runtime.js`. Pass `runtime: { url: "https://your-cdn/…" }` to point at an externally-hosted bundle. * **`wsTokenSecret`** — HMAC secret. If omitted, the server mints a random secret at boot — fine for single-process dev, wrong for multi-pod (pods would reject each other’s tokens). `mcpApps` requires `renderChannel: true`; the factory throws at construction if you enable one without the other. ## Compatibility matrix [Section titled “Compatibility matrix”](#compatibility-matrix) Host capabilities below describe what each MCP host supports when connected to your self-hosted server (a hosted ggui connector is coming soon): | Host | OAuth | MCP Apps | Notes | | ------------------- | ----- | -------- | ------------------------------------------------------------------------------------------------- | | Claude Desktop | Yes | Yes | Inline rendering, full UX. ([install](/clients/claude-desktop/)) | | claude.ai (web) | Yes | Yes | Same as Desktop. | | Goose | Yes | Yes | Inline rendering in TUI mode varies by terminal. | | VS Code Copilot | Yes | Yes | UI renders in a side panel. | | Cursor | Yes | Partial | OAuth works; MCP Apps support depends on version. | | Generic MCP runtime | No | No | Static `Authorization: Bearer …`; no inline render — resolve the `resourceUri` resource yourself. | If your host doesn’t yet implement MCP Apps, the underlying render still works — you just lose inline rendering. Each render is delivered as an MCP-Apps resource (`ui://ggui/render/` on `_meta.ui.resourceUri`); resolve it with `resources/read`. There is no render-viewer URL the agent receives. ## Reference [Section titled “Reference”](#reference) * MCP Apps protocol: * ggui server factory: [`@ggui-ai/mcp-server`](https://www.npmjs.com/package/@ggui-ai/mcp-server) * Iframe runtime: [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime) * Wire envelopes: [Envelopes](/protocol/envelopes/) * Glossary terms: [gadget, tool, blueprint](/glossary/) # Docs MCP service > Anonymous MCP service at mcp.ggui.ai/docs — three tools (docs_search, docs_read, docs_list) for any LLM to query ggui documentation programmatically. Coming soon This page describes a **managed hosted surface** (`mcp.ggui.ai/docs`), which is **not yet live** — it is not part of GGUI Preview 0.1.0. The self-hosted path is available today: start with the [Quickstart](/oss-quickstart/). This page is kept as forward documentation of the wire surface and goes live when hosted ggui ships. The Docs MCP service is a public, anonymous-auth Model Context Protocol surface that lets any LLM agent query the ggui documentation corpus from inside its tool loop. Three read-only tools, one in-memory index, no token required. One clarification on what’s open vs. hosted: the `McpService` mount primitive this service is built on **is OSS** (it ships in `@ggui-ai/mcp-server` — you can mount your own anonymous docs-style service today), but the hosted docs corpus service itself launches with `mcp.ggui.ai`. If you’re building a coding assistant, an IDE plugin, or any agent that needs to ground its answers in ggui’s docs, point it at `https://mcp.ggui.ai/docs` and the model gets `docs_search` / `docs_read` / `docs_list` for free. ## What it is [Section titled “What it is”](#what-it-is) A first-party MCP service mounted at `https://mcp.ggui.ai/docs` exposing three tools that wrap an in-memory index of the docs corpus: * `docs_search` — keyword search, ranked by occurrence count with a 3× title weighting. * `docs_read` — fetch the raw markdown body of one doc by path. * `docs_list` — enumerate every doc in the corpus, optionally filtered by path prefix. The service is built on the same `McpService` primitive that hosts the main agent API at `mcp.ggui.ai`. See [MCP services architecture](/architecture/mcp-services/) for the anonymous-mode mechanics and how multiple services are composed into one `createGguiServer` boot. ## Why anonymous [Section titled “Why anonymous”](#why-anonymous) The ggui docs corpus is public — every page on this site is reachable without a login, and every byte the service returns is something a browser could fetch by following a URL. There is no per-user state, no rate-shaped quota tied to identity, no privileged content behind the corpus. Forcing OAuth on a read-only public surface adds setup friction (Dynamic Client Registration ceremony, token refresh, secret storage) for zero security gain. Anonymous mode is the intentional default for surfaces that meet **all** of: * The data is already public. * The service has no side effects (no writes, no mutating tool calls, no external API spend). * Per-user state would be a lie (the same query produces the same answer for everyone). The agent surface at `mcp.ggui.ai` does **not** meet these criteria — sessions, BYOK credentials, and per-app rate limits all require an identified caller. The docs surface does, so it skips auth. ## Endpoint [Section titled “Endpoint”](#endpoint) ```plaintext POST https://mcp.ggui.ai/docs ``` Served as a **separate** MCP server, not folded into `/mcp`. The two surfaces have different auth modes (anonymous vs. OAuth) and different tool catalogs; keeping them on distinct paths means a docs-only client never has to discover or skip past agent-loop tools, and the agent loop never has to surface read-only docs tools alongside its session lifecycle. No `Authorization` header is required. `Content-Type: application/json` and the JSON-RPC 2.0 envelope are still required — this is MCP over HTTP, identical wire shape to `/mcp`, only the auth handshake is skipped. The same three tools are also co-hosted on the unified `/dev` developer endpoint (coming soon with the hosted platform), alongside the executable `ggui_protocol_*` tools — one `.mcp.json` line for learn + author + do. ## The three tools [Section titled “The three tools”](#the-three-tools) ### `docs_search` [Section titled “docs\_search”](#docs_search) Keyword search over the corpus. The query is tokenized on whitespace and lowercased; matches are case-insensitive. Each hit’s score is the sum of token-occurrence counts in the doc — title occurrences are weighted **3×**, body occurrences **1×**. The server clamps results to a maximum of **50 hits** regardless of the requested `limit`. Default `limit` is 10. | Field | Type | Required | Description | | ------- | -------- | -------- | ------------------------------------------------------- | | `q` | `string` | Yes | Search terms — whitespace-separated; case-insensitive. | | `limit` | `number` | No | Cap on returned hits. Default 10. Server-clamped to 50. | **Returns:** `{ hits: SearchHit[] }` ```ts interface SearchHit { path: string; // corpus-relative path, e.g. "principles/strict-typing.md" title: string; // first H1, or filename if none summary: string; // first ~200 chars of prose after the title score: number; // keyword score, higher is better } ``` Hits are sorted by `score` descending, with a stable secondary sort on `path` ascending so identical-score hits land in deterministic order. Follow up with `docs_read` to fetch the full body of any hit. ### `docs_read` [Section titled “docs\_read”](#docs_read) Fetch the full markdown body of one doc by its corpus-relative path. Pair with `docs_search` (search → read top hit) or `docs_list` (browse → read). | Field | Type | Required | Description | | ------ | -------- | -------- | ------------------------------------------------------------------------------------------------ | | `path` | `string` | Yes | Path relative to the corpus root, forward slashes. Leading `./` or `/` is trimmed before lookup. | **Returns:** `{ found, path, title, summary, body, bytes }` ```ts interface DocsReadOutput { found: boolean; // false when the path isn't in the corpus path: string; title: string; summary: string; body: string; // full raw markdown, including any frontmatter bytes: number; // utf-8 byte length of body } ``` When the path doesn’t match any doc, the tool returns `found: false` with empty `body` / `title` / `summary` rather than throwing — agents see a clean “no such doc” result and can fall back to `docs_search` or ask the user to refine. ### `docs_list` [Section titled “docs\_list”](#docs_list) Enumerate every doc in the corpus, optionally filtered by a path prefix. Returns metadata only (path / title / summary); bodies are not included. | Field | Type | Required | Description | | -------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `prefix` | `string` | No | Case-sensitive path-prefix filter. When set, only entries whose `path` starts with this prefix are returned. | **Returns:** `{ entries: DocMeta[], total }` ```ts interface DocMeta { path: string; title: string; summary: string; } ``` Entries are sorted by `path` ascending. `total` is the count after the prefix filter — use it to detect a typo’d prefix (e.g. `total: 0` when you expected dozens) without scanning `entries`. The full unfiltered list weighs roughly 50 KB for the current corpus, comfortably under any sane MCP response budget. Use `prefix` to scope to one section (`principles/`, `protocol/`, `architecture/`, etc.) when you want a focused slice. ## Corpus loading [Section titled “Corpus loading”](#corpus-loading) The corpus is loaded once at service boot via `loadDocsCorpus(rootDir)`, which walks the docs directory recursively, reads every `.md` file into RAM, extracts the title (first `# Heading` or filename fallback) and a short prose summary, and returns an immutable `DocsCorpus` handle. ```ts interface DocsCorpus { list(): readonly DocMeta[]; read(docPath: string): DocEntry | null; search(query: string, limit?: number): readonly SearchHit[]; } ``` Total weight: roughly **10 MB across \~400 files**, all held in process memory. Every request is served from RAM — no filesystem hit per call, no vector DB, no Algolia. Symlinks are skipped (prevents loops), non-`.md` files are skipped (no images, no frontmatter sidecars, no generated artifacts). The `DocsCorpus` interface is the upgrade seam. A future v2 can drop in an embedding-backed implementation — precomputed vectors, semantic ranking — without changing the tool surface or the wire shape. Agents written against today’s keyword search keep working; quality improves under their feet. ## Connecting from agents [Section titled “Connecting from agents”](#connecting-from-agents) ### Claude Agent SDK [Section titled “Claude Agent SDK”](#claude-agent-sdk) Add the service to the `mcpServers` block of your agent config: ```ts import { query } from "@anthropic-ai/claude-agent-sdk"; const result = await query({ prompt: "How do I write a blueprint for a contact form?", options: { mcpServers: { docs: { type: "http", url: "https://mcp.ggui.ai/docs", }, }, // No `authToken` needed — service is anonymous. }, }); ``` Tools surface to the model as `mcp__docs__docs_search`, `mcp__docs__docs_read`, and `mcp__docs__docs_list`. The SDK auto-discovers them on the first turn and injects them into the model’s tool list. ### Raw `@modelcontextprotocol/sdk` [Section titled “Raw @modelcontextprotocol/sdk”](#raw-modelcontextprotocolsdk) For non-Claude agents (Gemini, GPT, local models) talking MCP directly: ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "my-agent", version: "1.0.0" }); const transport = new StreamableHTTPClientTransport(new URL("https://mcp.ggui.ai/docs")); await client.connect(transport); const tools = await client.listTools(); // ["docs_search", "docs_read", "docs_list"] const hits = await client.callTool({ name: "docs_search", arguments: { q: "audience routes principle", limit: 5 }, }); ``` ### cURL [Section titled “cURL”](#curl) For one-off probes or wiring into a shell pipeline: ```bash # Search curl -X POST https://mcp.ggui.ai/docs \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"docs_search","arguments":{"q":"blueprint first","limit":5}}}' # Read one doc curl -X POST https://mcp.ggui.ai/docs \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"docs_read","arguments":{"path":"principles/blueprint-first-architecture.md"}}}' # List one section curl -X POST https://mcp.ggui.ai/docs \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"docs_list","arguments":{"prefix":"principles/"}}}' ``` ## What it pairs with [Section titled “What it pairs with”](#what-it-pairs-with) The Docs MCP service is the **runtime** half of ggui’s LLM-agent surface. The static half lives at [Agents track](/agents/) — narrative guides, recipe cookbooks, and step-by-step walkthroughs of agent patterns. Use the static docs to learn the protocol; use the Docs MCP service inside your agent to look up the specifics on demand. It also pairs naturally with the main agent API at [`/mcp`](/api/mcp-protocol/). An agent that’s already authenticated for session work can add the docs service as a second `mcpServers` entry — the two are independent endpoints with independent auth. ## Limits [Section titled “Limits”](#limits) * **Search hit cap: 50.** Any `limit` above 50 is silently clamped server-side. For corpus-wide enumeration use `docs_list`, not a huge `docs_search` limit. * **No streaming.** All three tools are request/response. There is no `tools/streaming` variant — bodies are small enough that streaming would add latency without benefit. * **No live refresh.** The corpus is loaded once at service boot and held immutable for the life of the process. Docs changes land in the corpus at the **next deploy** of the `@ggui-private/mcp-docs` service — typically when `main` ships to `mcp.ggui.ai`. Expect a lag of minutes-to-hours between a docs PR merging and the new content appearing in tool responses. * **Markdown only.** The corpus walker reads `.md` files. Images, code samples in separate files, and any non-markdown content are not indexed. The agent gets the markdown the docs site renders from; it does **not** get screenshots, generated diagrams, or compiled HTML. * **Anonymous.** Anyone can call these tools without identifying themselves. Do not assume there is a per-user audit trail on this surface — there isn’t. (Rate limiting at the edge still applies; behavior under high load is best-effort.) ## See also [Section titled “See also”](#see-also) * [MCP services architecture](/architecture/mcp-services/) — how anonymous services compose with auth’d services on one host * [MCP Protocol Reference](/api/mcp-protocol/) — the main agent API at `mcp.ggui.ai` * [LLM agents](/agents/) — narrative track for building agents against ggui # MCP Protocol Reference > Wire-level reference for the ggui MCP HTTP API — tools, inputs, return shapes, and a worked curl flow against self-hosted `ggui serve`. The ggui MCP API is [Model Context Protocol](https://modelcontextprotocol.io/) over HTTP with JSON-RPC 2.0. This page is the wire reference: tool names, input shapes, return shapes, error codes, and a complete curl walkthrough. Three-noun vocab in play on this page: **blueprint** (a cached recipe routed by `BlueprintSearch` over the draft’s contract + variance axes), **tool** (an agent-side MCP method the LLM invokes), **gadget** (a renderer-side capability the generated component imports). If those terms are new, skim the [glossary](/glossary/) first. ## Endpoint [Section titled “Endpoint”](#endpoint) **Self-hosted (`ggui serve`):** ```plaintext POST http://127.0.0.1:6781/mcp ``` ## Authentication [Section titled “Authentication”](#authentication) Local dev: start the server with `ggui serve --dev-allow-all` and any bearer (conventionally `dev`) authenticates as the `builder` identity: ```plaintext Authorization: Bearer dev Content-Type: application/json ``` Default `ggui serve` is **strict** — only pairing-minted bearers authenticate `/mcp`. Pair a key via the pair code the server prints at boot, or mint one locally with `ggui keys create --keys-file ` (the same file `ggui serve --keys-file` reads). The bare `createGguiServer` factory defaults to dev-allow-all until you pass `auth` — swap in a real `AuthAdapter` before exposing the port beyond `127.0.0.1`. Hosted ggui (coming soon) will use OAuth 2.0 with Dynamic Client Registration — Claude Desktop and other MCP-Apps hosts run the ceremony for you; raw-HTTP callers present the issued bearer token on every call. See [OAuth on mcp.ggui.ai](/api/oauth/) for the ceremony. ## Render Lifecycle [Section titled “Render Lifecycle”](#render-lifecycle) ```plaintext 1. initialize → MCP handshake (one-shot per connection) 2. ggui_list_gadgets → Optional: fetch the gadget catalog 3. ggui_handshake → Negotiate the wire (handshakeId + suggestion) 4. ggui_render → Materialize the UI (mints sessionId) 5. ggui_consume → Long-poll for user events (keyed by sessionId) 6. ggui_update → Mutate props in place (never re-render) 7. ggui_emit → Optional: push frames on a streamSpec channel ``` Renders decay implicitly via TTL — there is no explicit close ceremony. `ggui_update` and `ggui_consume` are keyed by `sessionId` (globally unique); the server tenancy-checks via the bearer token. ## Rendering Pipeline [Section titled “Rendering Pipeline”](#rendering-pipeline) The rendering decision is made during `ggui_handshake`. The negotiator runs `BlueprintSearch` plus contract validation in parallel and returns a routed `suggestion` whose `origin` tag tells the agent which branch fired: 1. **`origin: 'cache'`** — exact or semantic match against a registered blueprint. Free, deterministic reuse on the paired render. 2. **`origin: 'agent'`** — no cache hit, but the agent’s draft passed validation. Gen runs against the agent’s contract verbatim. 3. **`origin: 'synth'`** — no cache hit AND validation surfaced amendments. The server amends the draft and gen runs against the amended contract. The handshake response carries `handshakeId`, `action`, and `suggestion` (always with a provisional `blueprintMeta`). The agent then calls `ggui_render` with the `handshakeId` plus `props`: omit `override` to reuse the suggestion’s provisional `blueprintId` as-is, or pass `override: {contract?, variance?}` to mint a fresh `blueprintId` against a re-aimed draft. *** ## Agent system prompt [Section titled “Agent system prompt”](#agent-system-prompt) The canonical posture-only system prompt for any agent calling these tools is exported as a string constant: ```ts import { GGUI_AGENT_SYSTEM_PROMPT } from "@ggui-ai/protocol"; ``` Use it as-is. It teaches the wire flow (handshake → render → consume), the three rendering origins, and when to call which tool — without baking in any product-specific persona. Per-tool `description` strings on each `ggui_*` MCP tool reinforce the same flow at the tool layer, so the agent has two consistent signals during planning. Roll your own system prompt only when you have a domain-specific persona to layer on top. In that case, **concatenate**, don’t replace: keep `GGUI_AGENT_SYSTEM_PROMPT` first, then append your additions. Replacing it removes the wire-flow teaching, and the agent will misuse the toolset. The prompt source lives at `packages/protocol/src/recommended-prompts.ts` and ships with `@ggui-ai/protocol` for every consumer language the protocol package targets. *** ## Tools [Section titled “Tools”](#tools) | Tool | Purpose | | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | [`ggui_handshake`](#ggui_handshake) | Negotiate the wire surface before rendering — returns `handshakeId` + a routed `suggestion`. | | [`ggui_render`](#ggui_render) | Materialize the UI; mints `sessionId`. | | [`ggui_consume`](#ggui_consume) | Long-poll buffered user events on one GguiSession. | | [`ggui_update`](#ggui_update) | Mutate props on a delivered GguiSession in place. | | [`ggui_emit`](#ggui_emit) | Push a delivery onto a declared `streamSpec` channel. | | [`ggui_get_session`](#ggui_get_session) | Read GguiSession state + activity timestamps. | | [`ggui_list_sessions`](#ggui_list_sessions) | Enumerate GguiSessions by host conversation (resume flows). | | [`ggui_list_gadgets`](#ggui_list_gadgets) | Fetch the renderer-side gadget catalog before authoring a contract. | | [`ggui_list_themes`](#ggui_list_themes) | List the theme presets usable via `ggui_render({themeId})`. | | [`ggui_list_featured_blueprints`](#ggui_list_featured_blueprints) | Enumerate builder-curated featured blueprints. | | [`ggui_search_blueprints`](#ggui_search_blueprints) | Semantic search across this app’s blueprints. | | [`ggui_render_blueprint`](#ggui_render_blueprint) | Resolve a registered blueprint id to its compiled bundle. | | [`ggui_discover`](#ggui_discover) | Platform capability discovery (hosted-only, coming soon). | | [`ggui_request_credential`](#ggui_request_credential) | OAuth consent proxy (hosted-only, coming soon). | ### `ggui_handshake` [Section titled “ggui\_handshake”](#ggui_handshake) Negotiate the wire surface for a UI. Call BEFORE `ggui_render`. The agent posts a draft; the server runs blueprint-search + contract-validation in parallel and returns a routed `suggestion` the agent then accepts or overrides on render. **Top-level fields:** | Field | Type | Required | Description | | ---------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `intent` | `string` | Yes | Concise semantic identity — same intent across calls = same component reused. Example: `"Gmail inbox for email triage"`. | | `blueprintDraft` | `object` | Yes | Single-field draft wrapping the agent’s `contract` (required) plus optional `variance` and optional `generator` slug hint. The contract drives blueprint-search embed/structural axes; variance feeds the variance axis. | | `forceCreate` | `boolean` | No | Skip blueprint-search and route straight to validation + agent-mode suggestion against the draft. Use after a prior handshake returned an unwanted cache suggestion. | **Returns:** `{ handshakeId, action, suggestion, nextStep? }` | Field | Type | Description | | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `handshakeId` | `string` | Stable id — pass to `ggui_render`. Records are SINGLE-USE and expire after 10 minutes. | | `action` | `enum` | One of `create` / `reuse` / `update` / `replace` / `declined`. | | `suggestion` | `object` | Routed suggestion. Carries `origin: 'cache' \| 'agent' \| 'synth'`, an always-present provisional `blueprintMeta` (incl. `blueprintId`), and conditional `amendments` (synth-only) / `validationFindings` (soft on cache). | | `nextStep` | `object` | Wire-shape recovery hint — `{tool: 'ggui_render', example}` worked-literal of the next call. | The agent branches the paired `ggui_render` on `suggestion.origin`: any origin can be accepted (reuse the provisional `blueprintId` verbatim) or overridden (mint fresh against a new draft). ### `ggui_render` [Section titled “ggui\_render”](#ggui_render) Materialize the UI. Step 3 of the three-step handshake protocol. `handshakeId` and `props` are REQUIRED. Commit relative to the handshake’s suggestion by PRESENCE of `override`: omit it to ACCEPT the suggestion as-is, or provide `override: {contract?, variance?}` to re-aim (PATCH semantics). ```json // ACCEPT the suggestion as-is { "handshakeId": "h_…", "props": {…} } // re-draft the contract (cold-gen) { "handshakeId": "h_…", "props": {…}, "override": { "contract": {…} } } // re-aim the variant axis { "handshakeId": "h_…", "props": {…}, "override": { "variance": {…} } } ``` | Field | Type | Required | Description | | ------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `handshakeId` | `string` | Yes | From a prior `ggui_handshake` response. | | `props` | `object` | Yes | Runtime prop values for THIS render. Validated against the effective contract’s `propsSpec`; failures fail the render with a recoverable `ContractViolationError`. Pass `{}` when the contract declares no `propsSpec`. | | `themeId` | `string` | No | Per-render theme preset override — wins over `App.defaultThemeId` for THIS render. Discover ids via `ggui_list_themes`. Omit to inherit the app theme. | | `infra` | `object` | No | `{model?}` — provider-prefixed per-render model override (e.g. `anthropic/claude-haiku-4-5`). Strict — unknown keys are rejected. | | `override` | `object` | No | Omit to ACCEPT the suggestion as-is. Provide `{contract?, variance?}` to re-aim: `override.contract` re-drafts the contract (STRICT — must already conform) and cold-gens; `override.variance` re-aims the variant axis. | **Returns:** `{ sessionId, resourceUri, action, contractHash, blueprintId, variantKey, cache, nextStep? }` | Field | Type | Description | | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sessionId` | `string` | Globally-unique id (UUID) for the delivered render. Use for `ggui_consume` / `ggui_update`. | | `resourceUri` | `string` | Spec-canonical MCP-Apps entry point — `ui://ggui/render/`, mirrored on the tool result’s `_meta.ui.resourceUri`. A host mounts the render from this; there is no clickable URL on the wire. | | `action` | `enum` | One of `create` / `reuse` / `update` / `replace` / `declined`. | | `contractHash` | `string` | Canonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow. | | `blueprintId` | `string` | Opaque id of the materialised component. Equal across two renders ⟺ the same cached component was served (a fresh gen mints a new id). | | `variantKey` | `string` | Canonical hash of the design-time variance. With `contractHash` it forms the reuse key. | | `cache` | `object` | Reuse outcome — `{ hit, similarity?, cachedBlueprintId?, llmCallsAvoided, kind?, reason? }`. | | `nextStep` | `object` | Emitted ONLY when the rendered contract has a non-empty `actionSpec`. Points at `ggui_consume({sessionId})` for the inbound action loop. Pure-display renders get no `nextStep`. | The render consumes the handshake record. Bootstrap credentials (`wsUrl`, `wsToken`, `expiresAt`) reach the iframe via the `_meta["ai.ggui/render"]` slice, not via this response. ### `ggui_consume` [Section titled “ggui\_consume”](#ggui_consume) Long-poll for buffered user events on one render. Events drain on read (consume-once semantics). Call this right after every `ggui_render` whose response carries `nextStep.tool === 'ggui_consume'`. | Field | Type | Required | Description | | ----------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sessionId` | `string` | Yes | Render to consume from. Globally unique. | | `timeout` | `number` | No | Long-poll seconds — integer in `[0, 25]`; `0` = immediate (default). Values outside `[0, 25]` reject `INVALID_PARAMS`. Returns on the first event or at timeout; re-call on empty to keep waiting — a longer wait is your loop, not a bigger timeout (pick 5–15s typical, 25 max). | **Returns:** `{ events: ConsumeEventEntry[], status: "active" | "expired", client? }` Keyed by `sessionId`. THE LOOP: when `events` is non-empty, react (commonly via `ggui_update` to refresh the iframe), then re-call `ggui_consume`. The render stays `active` until its TTL elapses — `status: "expired"` means no more events will arrive, so the long-poll loop terminates. Exit when you have the events you need, or when `status` is `expired`. The optional `client` field echoes mid-render host observations (window resize, fullscreen toggle, etc.) without forcing a fresh handshake. ### `ggui_update` [Section titled “ggui\_update”](#ggui_update) Mutate props on a delivered render in place. Targets `sessionId` directly. Discriminated on `kind`. ```json // FULL replacement { "sessionId": "…", "kind": "replace", "props": { … } } // RFC 7396 JSON Merge Patch { "sessionId": "…", "kind": "merge", "patch": { … } } ``` | Field | Type | Required | Description | | ----------- | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sessionId` | `string` | Yes | The render to mutate (UUID from `ggui_render` response). | | `kind` | `enum` | Yes | `'replace'` — the `props` map IS the new state. `'merge'` — apply RFC 7396 JSON Merge Patch (null deletes a key; arrays fully replace, NOT element-wise). | | `props` | `object` | If replace | Full replacement props map. Required when `kind: 'replace'`. | | `patch` | `object` | If merge | RFC 7396 patch. Required when `kind: 'merge'`. | **Returns:** `{ sessionId, updated, resourceUri }` `resourceUri` is unchanged from the initial render — the same `ui://ggui/render/{sessionId}` the mount stamped. Both modes validate the final props state (post-merge for `merge`) against the render’s `propsSpec` and reject on violation. Use for partial UI state changes; for a structurally different surface, handshake + render a fresh one. Post-update the iframe receives the new props via the live-channel `props_update` WS frame. ### `ggui_emit` [Section titled “ggui\_emit”](#ggui_emit) Emit a new delivery on a declared `streamSpec` channel of the render. The agent describes WHAT new data exists; the server stamps the canonical `StreamEnvelope` (mode derived from `streamSpec[channel].mode`, `seq` + `timestamp` server-assigned). Validates `payload` against the channel’s declared `schema` and rejects undeclared channels at call time. | Field | Type | Required | Description | | ----------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sessionId` | `string` | Yes | Render to stream to. Server enforces app-ownership. | | `channel` | `string` | Yes | Channel name declared on the render’s `streamSpec`. Undeclared channels reject. | | `payload` | `unknown` | Yes | Delivery payload. Validated against `streamSpec[channel].schema`. | | `complete` | `boolean` | No | Terminal-delivery marker. Only valid when the channel was declared with `complete: true` on the streamSpec; setting it on a non-completable channel rejects. | **Returns:** `{ accepted }` `accepted: true` means the server validated and enqueued the envelope at the boundary. No-subscriber is NOT an error — buffered retention and live fan-out happen independently. The server-assigned `seq` is observable on the delivered `StreamEnvelope` (the live-channel `data` WS frame), not on this tool result. ### `ggui_get_session` [Section titled “ggui\_get\_session”](#ggui_get_session) Retrieve GguiSession state — id, appId, event sequence, activity timestamps. Bumps the activity heartbeat on every successful read. Omits `componentCode` + `sourceCode` (those live on the renderable surface, not the agent-visible one). | Field | Type | Required | Description | | ----------- | -------- | -------- | ----------------------- | | `sessionId` | `string` | Yes | GguiSession to inspect. | **Returns:** `{ id, appId, eventSequence, createdAt, lastActivityAt, expiresAt }` `createdAt` / `lastActivityAt` / `expiresAt` are epoch milliseconds (numbers). ### `ggui_list_sessions` [Section titled “ggui\_list\_sessions”](#ggui_list_sessions) Enumerate this app’s GguiSessions by host conversation — the lookup behind resume flows. Matches on the `_meta["ai.ggui/host-session"]` pair (`hostName` + `hostSessionId`) captured at render creation; sessions created without that slice never match host-scoped queries. | Field | Type | Required | Description | | --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `hostName` | `string` | No | Filter by host identifier (`claude.ai`, `sample`, …). Pair with `hostSessionId` to target one conversation. | | `hostSessionId` | `string` | No | The host’s opaque conversation-grouping key (e.g. a claude.ai thread id). Typically paired with `hostName`. | | `limit` | `number` | No | Max rows, 1–200. Default 50. Newest-last ordering matches the conversation timeline. | **Returns:** `{ sessions: [{ sessionId, hostName?, hostSessionId?, createdAt, lastActivityAt, status, wsToken?, wsTokenExpiresAt? }] }` `createdAt` / `lastActivityAt` are ISO 8601 strings here. The `wsToken` pair is present only when the deployment wires a `mintWsToken` seam — resume flows use it to remount each iframe without a fresh handshake. ### `ggui_list_gadgets` [Section titled “ggui\_list\_gadgets”](#ggui_list_gadgets) Return the catalog of renderer-side **gadgets** the UI may import via the package-keyed `clientCapabilities.gadgets` map of a `DataContract`. Call this BEFORE authoring a contract so the catalog you seed only references gadgets the renderer will actually serve. Returns the per-app catalog: the 7-hook stdlib package (`@ggui-ai/gadgets` 0.3.0) is the structural floor; gadgets declared in `ggui.json#app.gadgets` layer on top (declared wins on a package collision). | Field | Type | Required | Description | | ------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `appId` | `string` | No | The app whose catalog to fetch. Defaults to the caller-resolved appId from the auth header. Explicit mismatch surfaces as `app_access_denied`. | **Returns:** `{ gadgets: GadgetDescriptor[] }` Each `GadgetDescriptor` is a gadget PACKAGE: `{ package, version, exports: GadgetExport[], … }` — package-level identity plus transport metadata (`bundleUrl` / `bundleHost` / `bundleSri` / `styleUrl` / `connect` / `requires` / `typesUrl` / `typesSri` for non-stdlib packages). Each `GadgetExport` is a field-presence-discriminated union — a hook export `{ hook, description?, usage?, example?, gotchas?, permission?, required? }` or a component export `{ component, description?, usage?, example?, gotchas?, permission?, required? }`. Full entry shape: [SDK gadgets guide](/sdk/gadgets/). ### `ggui_list_themes` [Section titled “ggui\_list\_themes”](#ggui_list_themes) Return the theme presets an agent may apply per render via `ggui_render({ themeId })`. When the app configures an `availableThemeIds` allowlist, the catalog is filtered to it (catalog order preserved; unregistered ids silently dropped). | Field | Type | Required | Description | | ------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `appId` | `string` | No | The app whose theme catalog to fetch. Defaults to the caller-resolved appId from the auth header. Explicit mismatch surfaces as `app_access_denied`. | **Returns:** `{ themes: [{ id, name, description, modes }] }` `modes` lists the variants each preset ships (`light` / `dark`). ### `ggui_list_featured_blueprints` [Section titled “ggui\_list\_featured\_blueprints”](#ggui_list_featured_blueprints) Enumerate the builder-curated featured blueprints declared via the server’s blueprint catalog (typically `ggui.json#blueprints.include` for OSS deployments). Returns an empty list when no catalog is wired. **Inputs:** none. **Returns:** `{ blueprints: BlueprintEntry[], total }` Pair with `ggui_search_blueprints` for semantic lookup or `ggui_render_blueprint` to materialize one directly. ### `ggui_search_blueprints` [Section titled “ggui\_search\_blueprints”](#ggui_search_blueprints) Semantic search across this app’s blueprints — both manifest-declared UIs (`ggui.json#blueprints.include`) and previously cached generations. Matches by name/description against the manifest source and by cosine similarity against the semantic vector index; results merge + dedupe by id (manifest wins on collision) and sort by score descending. | Field | Type | Required | Description | | ------- | -------- | -------- | ---------------------------------------------------------- | | `query` | `string` | Yes | Natural-language description of the UI you’re looking for. | | `limit` | `number` | No | Max results. Default 10. Maximum 100. | **Returns:** `{ results, total, query }` Each result row carries `{ id, name, description, category, props, callbacks, featured, relevance: 'match', score }`. `score` is 0–1 (cosine similarity for semantic hits; `1.0` for exact manifest-name matches, `0.7` for manifest substring matches). Agents use `score` to decide whether to reuse a blueprint or generate from scratch. ### `ggui_discover` [Section titled “ggui\_discover”](#ggui_discover) Return platform capabilities (protocol version, supported content types, shell types, adapter types, component-capability catalog) and — when the bearer token resolves to a known app — that app’s enabled adapters / granted capabilities / auth mode / rate limit. Call BEFORE the first handshake when you need to branch on what this deployment supports. **Inputs:** none. **Returns:** `{ protocolVersion, contentTypes, shellTypes, adapterTypes, componentCapabilities, app? }` | Field | Type | Description | | ----------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `protocolVersion` | `string` | ggui protocol revision (prelaunch drafts use `draft-YYYY-MM-DD`; first frozen release will be `1.0.0`). | | `contentTypes` | `string[]` | Bundle content types this deployment serves (e.g. `application/javascript+react`). | | `shellTypes` | `string[]` | Available shell flavors (`chat`, `fullscreen`, `spatial`). | | `adapterTypes` | `string[]` | Adapter families wired on this deployment (`voice`, `camera`, `location`, `bluetooth`). | | `componentCapabilities` | `string[]` | Informational capability vocabulary. The load-bearing per-app grant lives on the operator-registered `GadgetExport.permission` in `App.gadgets` (registry side) — not on the contract wire. | | `app` | `object?` | Present when the bearer token resolves to a known app. `{ enabledAdapters?, grantedCapabilities?, defaultShellType?, authMode?, rateLimitPerMinute? }`. | ### `ggui_request_credential` [Section titled “ggui\_request\_credential”](#ggui_request_credential) Request OAuth consent from the end user via the Portal’s consent overlay. Blocks up to 25 seconds polling for the user’s choice (Allow once / Always allow / Deny). Short-circuits with `granted: true` when a prior grant already exists for this user + app + service. | Field | Type | Required | Description | | ----------- | -------- | -------- | --------------------------------------------------------------------------------------------- | | `serviceId` | `string` | Yes | OAuth service identifier (matches an `McpServiceConfig` entry — e.g. `"bashdoor"`, `"ubot"`). | | `reason` | `string` | No | One-line rationale shown to the user inside the consent overlay. | | `sessionId` | `string` | No | Existing render to surface the consent UI into. Required to actually surface the overlay. | **Returns:** `{ granted, mode?, service?, reason? }` | Field | Type | Description | | --------- | -------------------- | ----------------------------------------------------------------------- | | `granted` | `boolean` | Whether the user (or a prior grant) approved. | | `mode` | `'once' \| 'always'` | Grant mode when `granted: true`. Absent on denial / timeout. | | `service` | `{ name, icon }` | Display info pulled from `McpServiceConfig`. Echoed back for UI parity. | | `reason` | `string` | Denial / timeout / error rationale when `granted: false`. | ### `ggui_render_blueprint` [Section titled “ggui\_render\_blueprint”](#ggui_render_blueprint) Resolve a registered blueprint id to its compiled JS bundle, inline. The OSS handler reads the manifest entry via the server’s `UiRegistry`, compiles on demand from the colocated TSX (`@ggui-ai/dev-stack::LocalUiRegistry` is the reference impl), and returns the bundle as a single JSON field. Fails with a clear error when the id is unknown or no bundle is available. Only registered when the server boots with a `UiRegistry` seam — otherwise the tool is omitted from `tools/list` entirely. | Field | Type | Required | Description | | ------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------- | | `blueprintId` | `string` | Yes | Stable blueprint id declared via `ggui.ui.json#id`. Must match an entry in this server’s UI registry. | **Returns:** `{ blueprintId, blueprintName, code, contentType }` `code` is the compiled JS bundle as a string (ESM `export default` producing the component to mount). `contentType` is typically `'application/javascript+react'` — pinned by the server’s compile pipeline. The caller mounts `code` directly; no second round-trip is required. *** ## Events [Section titled “Events”](#events) Two distinct shapes are in play. Don’t conflate them: * **`ActionEnvelope`** — live-channel inbound on the WebSocket subscribe seam. Used by browser/SDK consumers that listen to live events (e.g. `@ggui-ai/wire`’s `useRender`). See [WebSocket Protocol](/api/websocket-protocol/). * **`ConsumeEventEntry`** — per-gesture row on the render-keyed consume pipe, returned by `ggui_consume`. This is what agents read. ### `ActionEnvelope` (live-channel inbound) [Section titled “ActionEnvelope (live-channel inbound)”](#actionenvelope-live-channel-inbound) ```typescript interface ActionEnvelope { sessionId: string; type: EventType; payload?: TPayload; // For `data:submit`: { action, data?, tool? } clientSeq?: number; // client-monotonic, for at-least-once dedup } ``` ### `ConsumeEventEntry` (consume pipe) [Section titled “ConsumeEventEntry (consume pipe)”](#consumeevententry-consume-pipe) ```typescript interface ConsumeEventEntry { readonly type: "action"; readonly sessionId: string; readonly intent: string; // which actionSpec[*] fired readonly actionData: JsonValue | null; // matches actionSpec[intent].schema readonly uiContext: JsonObject; // contextSpec slot snapshot at gesture time readonly actionId: string; // 8-hex FNV-1a correlation id readonly firedAt: string; // ISO 8601 UTC } ``` Both envelopes are flat — no nested `event` / `context` / `meta` blocks. Diagnostic render metadata (device info, interface context) lives on the render at subscribe time, not per-delivery. ### Event Types [Section titled “Event Types”](#event-types) `EventType` has exactly one member, `data:submit`. | Type | Category | Description | | ------------- | -------- | ---------------------------------------- | | `data:submit` | Data | User gesture surfaced as a consume event | The pre-actionSpec multi-event vocabulary (`data:change`, `lifecycle:*`, `interaction:*`, `error:*`) was deleted in draft-2026-06-12 — it never had a first-party producer. Today’s actionSpec-driven flow surfaces every user gesture as a `data:submit` `ConsumeEventEntry`. There is no other event vocabulary — agent code reads from `ggui_consume` and only needs to recognize the `data:submit` shape. *** ## Error Codes [Section titled “Error Codes”](#error-codes) Names mirror the `MCP_ERROR_CODES` / `PLATFORM_ERROR_CODES` constants exported from `@ggui-ai/protocol`. The `-32010` range is the ggui platform-extension block, not part of the core protocol. | Code | Name | Description | | -------- | --------------------------- | ------------------------------------ | | `-32700` | `PARSE_ERROR` | Invalid JSON in request | | `-32600` | `INVALID_REQUEST` | Not a valid JSON-RPC object | | `-32601` | `METHOD_NOT_FOUND` | Unknown method name | | `-32602` | `INVALID_PARAMS` | Missing or invalid tool arguments | | `-32603` | `INTERNAL_ERROR` | Server-side failure | | `-32001` | `UNAUTHORIZED` | Invalid token or app ID | | `-32002` | `SESSION_NOT_FOUND` | Session expired or deleted | | `-32003` | `APP_NOT_FOUND` | App ID does not exist | | `-32004` | `PRODUCTION_FAILED` | UI production failed | | `-32005` | `CAPABILITY_DENIED` | Requested capability not granted | | `-32010` | `GENERATION_QUOTA_EXCEEDED` | Platform: generation quota exhausted | | `-32011` | `APP_LIMIT_EXCEEDED` | Platform: app-count ceiling reached | | `-32012` | `CONCURRENT_SESSION_LIMIT` | Platform: too many live sessions | | `-32013` | `RATE_LIMIT_EXCEEDED` | Platform: reserved rate-limit code | | `-32020` | `CONTRACT_VIOLATION` | Platform: contract validation failed | *** ## Supported Models [Section titled “Supported Models”](#supported-models) There is no fixed model menu. The operator sets the per-app default via `ggui.json#generation.model` — any provider-prefixed route, written `provider:model` (canonical) or LiteLLM-style `provider/model`, e.g. `anthropic:claude-haiku-4-5-20251001`. Providers on the self-hosted BYOK path: `anthropic`, `openai`, `google`, `openrouter` (`bedrock` routes are hosted-runtime-only and rejected by `ggui serve`). Agents can override the model per render via `ggui_render({ infra: { model } })` with a provider-prefixed id (e.g. `anthropic/claude-haiku-4-5`). *** ## Example: Full Render Flow (curl) [Section titled “Example: Full Render Flow (curl)”](#example-full-render-flow-curl) This walkthrough runs against self-hosted `ggui serve` (started with `--dev-allow-all`). Hosted `mcp.ggui.ai` (coming soon) will speak the same wire — only the URL and bearer change. ```bash # 1. Initialize curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "clientInfo": { "name": "curl", "version": "1.0" }, "capabilities": {} } }' # 2. Handshake — negotiate the wire surface # (blueprintDraft carries the agent's contract) curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "ggui_handshake", "arguments": { "intent": "Contact form", "blueprintDraft": { "contract": { "propsSpec": {}, "actionSpec": {} } } } } }' # 3. Render — accept the handshake suggestion verbatim (mints sessionId) curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "ggui_render", "arguments": { "handshakeId": "hs_…", "props": {} } } }' # 4. Poll for events (keyed by sessionId from step 3) curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "ggui_consume", "arguments": { "sessionId": "", "timeout": 15 } } }' # Renders decay implicitly via TTL — no explicit close. ``` *** ## See Also [Section titled “See Also”](#see-also) * [Protocol overview](/protocol/overview/) — the three channels at a glance * [WebSocket Protocol](/api/websocket-protocol/) — live-channel live events * [MCP Apps](/api/mcp-apps/) — inline rendering inside MCP hosts * [OAuth on mcp.ggui.ai](/api/oauth/) — hosted auth ceremony (coming soon) * [Ops MCP route](/api/ops-mcp/) — operator agent tools at `/ops` * [Docs MCP route](/api/mcp-docs/) — anonymous docs search at `/docs` * [Playground · Todos](/clients/playground-todos/) — hosted demo service at `/playground/todos` (coming soon) * [Playground · MDH](/clients/playground-mdh/) — hosted demo service at `/playground/mdh` (coming soon) * [MCP services](/architecture/mcp-services/) — mounting standalone services on one server * [Glossary](/glossary/) — gadget / tool / blueprint, plus every other term # OAuth (self-hosted) > OAuth 2.1 + PKCE + Dynamic Client Registration wire format for a self-hosted @ggui-ai/mcp-server (enable with `oauth: true` / `ggui serve --oauth`), with the access-token-IS-the-API-key trade-off explained. A self-hosted [`@ggui-ai/mcp-server`](/oss-quickstart/) — enabled with `oauth: true` on the factory or `ggui serve --oauth` — implements [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1) with [PKCE](https://datatracker.ietf.org/doc/html/rfc7636), [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591) (RFC 7591), the [Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728) discovery profile (RFC 9728), and the [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707) extension (RFC 8707) for per-app token scoping. MCP-aware hosts can connect with just a server URL — no manually-issued `client_id`, no shared secret. To onboard a remote host (Claude Desktop, claude.ai, Goose) against your local server, front it with a tunnel (ngrok / cloudflared) and pass `--public-base-url=` so the discovery URLs resolve from the host’s browser. This page is the wire reference. If you only want to **connect a client** ([Claude Desktop](/clients/claude-desktop/), claude.ai, Goose, VS Code Copilot), the host handles everything — skip this page. Read on if you’re building a host, debugging a custom client, or operating a deployment of `@ggui-ai/mcp-server`. ## The flow at a glance [Section titled “The flow at a glance”](#the-flow-at-a-glance) ```plaintext Client Server User │ │ │ │── GET /mcp (no auth) ─────────→│ │ │← 401 WWW-Authenticate: │ │ │ Bearer realm="mcp", │ │ │ resource_metadata="" ──│ │ │ │ │ │── GET /.well-known/ │ │ │ oauth-protected-resource ───→│ │ │← { authorization_servers } ───│ │ │ │ │ │── GET /.well-known/ │ │ │ oauth-authorization-server ─→│ │ │← { authorize_endpoint, ... } ─│ │ │ │ │ │── POST /oauth/register ───────→│ (RFC 7591 DCR — no client_secret) │← { client_id } ───────────────│ │ │ │ │ │── open /oauth/authorize ──────→│── in-process consent form ───→│ │ │ │── paste key / pair code + Approve │ │← form POST {api_key, params} ─│ │← 302 → redirect_uri?code=… ───│ │ │ │ │ │── POST /oauth/token │ │ │ {code, code_verifier} ──────→│ │ │← { access_token: } ─│ │ │ │ │ │── GET /mcp │ │ │ Authorization: Bearer ─→│ │ │← MCP session ─────────────────│ │ ``` ## The access token IS the API key [Section titled “The access token IS the API key”](#the-access-token-is-the-api-key) The simplest possible bridge between MCP’s OAuth-required client UX and ggui’s existing API-key model: * **No parallel token table.** The `access_token` returned at `/oauth/token` is verbatim the bearer key your `AuthAdapter` accepts on `/mcp` — pasted (or, with `ggui serve`, exchanged from the terminal pair code) during consent. * **No translation in the request hot path.** An authenticated `/mcp` request looks identical to a static-bearer request — `Authorization: Bearer `. * **No refresh dance.** The access token TTL equals the API key TTL. Revoke the key in your store and the client’s next request returns `401`. Re-auth is one OAuth round trip — one paste from the user. * **One audit surface.** Every connected client appears as a single row in your keys list, labelled with the `client_name` from DCR. The trade-off: there’s no “this token came from OAuth” flag — the key works identically whether minted by your operator tooling or the OAuth flow. If that distinction matters to your operator policy, plug in your own `OAuthStorage` + `AuthAdapter` (see [Storage seam](#storage-seam) below). ## Endpoints [Section titled “Endpoints”](#endpoints) ### `GET /.well-known/oauth-protected-resource` (RFC 9728) [Section titled “GET /.well-known/oauth-protected-resource (RFC 9728)”](#get-well-knownoauth-protected-resource-rfc-9728) Tells the client where to find the authorization server. Same origin in our case — your server (here a tunnel exposing the local `ggui serve`) is both the resource and the auth server. ```json { "resource": "https://your-mcp.example.com/mcp", "authorization_servers": ["https://your-mcp.example.com"], "bearer_methods_supported": ["header"], "resource_documentation": "https://modelcontextprotocol.io/extensions/apps/overview" } ``` ### `GET /.well-known/oauth-authorization-server` (RFC 8414) [Section titled “GET /.well-known/oauth-authorization-server (RFC 8414)”](#get-well-knownoauth-authorization-server-rfc-8414) Authorization-server metadata. ```json { "issuer": "https://your-mcp.example.com", "authorization_endpoint": "https://your-mcp.example.com/oauth/authorize", "token_endpoint": "https://your-mcp.example.com/oauth/token", "registration_endpoint": "https://your-mcp.example.com/oauth/register", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none"], "scopes_supported": ["mcp"] } ``` `token_endpoint_auth_methods_supported: ["none"]` is intentional — PKCE is the only supported client authentication, no `client_secret` is ever issued. ### `POST /oauth/register` (RFC 7591) [Section titled “POST /oauth/register (RFC 7591)”](#post-oauthregister-rfc-7591) Dynamic Client Registration. Issues a random `client_id`, no secret. Accepts arbitrary `redirect_uris` from the client without an allowlist — the trade-off matches the MCP spec’s pragmatism: any client willing to do PKCE + paste-key gets registered. ```bash curl -X POST https://your-mcp.example.com/oauth/register \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["http://localhost:33418/callback"], "client_name": "Claude Desktop" }' ``` ```json { "client_id": "mcp_client_AbCdEf...", "redirect_uris": ["http://localhost:33418/callback"], "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "none", "client_name": "Claude Desktop" } ``` ### `GET /oauth/authorize` [Section titled “GET /oauth/authorize”](#get-oauthauthorize) The user-facing approval entry point. Required query params: | Param | Value | | ----------------------- | ---------------------------------------------------------------------------------------------------------- | | `response_type` | `code` | | `client_id` | from DCR | | `redirect_uri` | one of the URIs registered with DCR | | `code_challenge` | base64url(SHA256(verifier)) | | `code_challenge_method` | `S256` | | `state` | opaque, echoed back on redirect (CSRF defense) | | `scope` | `mcp` (advertised; not gated server-side today) | | `resource` | optional — RFC 8707 indicator naming the target MCP endpoint (per-app routing); omit for universal scoping | With no `consentUrl` configured (the typical self-hosted posture), the server renders an unbranded paste-key HTML form in-process — no external origin needed. When the operator does configure a `consentUrl`, the server 302s the browser there with every OAuth param forwarded plus an `mcp_origin` param the consent page uses to know where to POST back. (The hosted endpoint will point `consentUrl` at its own console — coming soon.) ### `POST /oauth/authorize` (form-encoded) [Section titled “POST /oauth/authorize (form-encoded)”](#post-oauthauthorize-form-encoded) Called by the consent UI (the in-process form by default) after the user approves. Same OAuth params echoed as hidden inputs, plus one of two credential fields: * `pair_code` — the 6-digit code printed on the terminal banner by `ggui serve`. The server exchanges it through `PairingService.completePairing` to mint a per-server bearer. This is the easiest self-hosted path. * `api_key` — a freshly-minted (or pasted) bearer key your `AuthAdapter` accepts. The server validates the resulting key against the same `AuthAdapter` that gates `/mcp`, mints a 5-minute auth code, and 302s to the client’s `redirect_uri?code=…&state=…`. ### `POST /oauth/token` [Section titled “POST /oauth/token”](#post-oauthtoken) Exchange the auth code for an access token. ```bash curl -X POST https://your-mcp.example.com/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:33418/callback&client_id=mcp_client_…&code_verifier=ORIGINAL_VERIFIER" ``` ```json { "access_token": "", "token_type": "Bearer", "scope": "mcp" } ``` No `refresh_token`, no `expires_in` — the access token’s lifetime is the underlying API key’s lifetime. Re-auth to get a new one. If the client passed a `resource` at `/authorize` and includes one at `/token`, the two MUST match (RFC 8707 §2.2); mismatch returns `400 invalid_target`. Omitting `resource` at `/token` is tolerated even when the auth code captured one — RFC 8707 only mandates the constraint when the client opts in. ## Resource indicators (RFC 8707) [Section titled “Resource indicators (RFC 8707)”](#resource-indicators-rfc-8707) The server supports per-app token scoping via the optional `resource` query parameter at `/oauth/authorize`. Two shapes are accepted: * **Universal** — `${issuer}` (cloud bare root) or `${issuer}/mcp` (OSS default path). Tokens bind to the user’s full app surface. * **Per-app** — `${issuer}/apps/` where `` matches the deployment’s app-id pattern. The auth code, and any token minted from it, is captured against that single app target. Per-app deployments also surface their own `/.well-known/oauth-protected-resource` at the app-scoped path so RFC 9728 discovery resolves correctly when a client starts from the per-app URL. Absent `resource`, universal scoping applies. Unknown resources are rejected at `/authorize` with `invalid_target` (RFC 8707 §2) before the consent step so the user sees the failure before pasting a key. OSS deployments can plug a `validateResource(issuer, resource)` callback into the server factory to gate which targets are accepted. The captured resource is snapshotted onto the DCR client record (`lastResource`) so an operator console can label rows “Connected to: \” rather than “Universal”. ## OSS deployment notes [Section titled “OSS deployment notes”](#oss-deployment-notes) **Running the CLI?** `ggui serve --oauth` mounts this whole surface — the `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` discovery endpoints plus `/oauth/{authorize,token,register}`. It’s required for OAuth-discovery hosts (claude.ai / ChatGPT “Add custom connector”); pure-bearer clients work without it. The paste-key form accepts either a `ggui_user_*` key or the 6-digit pair code from the serve banner. If you’re embedding [`@ggui-ai/mcp-server`](/oss-quickstart/) programmatically, pass `oauth: true` (or an `OAuthConfig`) to the server factory: ```typescript import { createGguiServer } from "@ggui-ai/mcp-server"; const server = createGguiServer({ // ... oauth: { issuerUrl: "https://your-mcp.example.com", consentUrl: "https://your-console.example.com/oauth/consent", // optional storage: new InMemoryOAuthStorage(), // or your own }, auth: yourAuthAdapter, }); ``` ### Storage seam [Section titled “Storage seam”](#storage-seam) Auth codes and DCR clients live in `OAuthStorage`. The default `InMemoryOAuthStorage` is fine for single-replica dev and any deployment with sticky sessions on the load balancer. Multi-replica deployments without sticky sessions need a shared backend — Redis or DynamoDB both work. The interface is two collections (auth codes, DCR clients) with `put` / `consume` / `get` methods; see `packages/mcp-server/src/oauth.ts` in the [public repo](https://github.com/ggui-ai/ggui) for the full type. ### Consent UI [Section titled “Consent UI”](#consent-ui) `consentUrl` delegates the user-facing approval step to a separate origin. The consent UI sees both the user’s session (whatever auth model it uses) and the freshly-minted API key in plaintext — same trust boundary as the MCP server itself. Validate the `mcp_origin` param against an allowlist before posting back, otherwise an attacker can craft a URL that exfiltrates a key to a third-party origin. Without `consentUrl`, the server renders an in-process paste-key HTML form. Functional but unbranded — fine for OSS deployers who don’t want to stand up a separate origin. ### Disable OAuth entirely [Section titled “Disable OAuth entirely”](#disable-oauth-entirely) `oauth: false` (or simply omitting the option) skips the OAuth surface — `/oauth/*` and the well-known endpoints return 404, and `/mcp` falls back to the bearer-only `AuthAdapter` model. Hosts that don’t speak OAuth-DCR can still authenticate by setting a static `Authorization: Bearer …` header. ## Hosted ggui (coming soon) [Section titled “Hosted ggui (coming soon)”](#hosted-ggui-coming-soon) The managed endpoint at `mcp.ggui.ai` will run this identical flow with two deployment-specific choices: `consentUrl` points at its own console’s consent page (so approval happens on a branded origin that mints a `ggui_user_*` key for you), and the universal MCP endpoint mounts at the bare root rather than `/mcp`. Neither is live yet — hosted ggui is not part of GGUI Preview 0.1.0, and nothing on this page depends on it. ## Non-goals [Section titled “Non-goals”](#non-goals) * **Not refresh-token compatible.** The OAuth ceremony is one-time per credential. Adding a refresh-token grant requires decoupling the access token from the API key (separate token store + lifetime). * **Not OIDC.** No ID tokens, no userinfo endpoint, no nonce. The MCP spec doesn’t ask for them, and the access-token-IS-the-API-key model doesn’t need them. * **Not a replacement for static API keys.** If your client can hold a static `Authorization: Bearer ` header (CLI agents, server-side runtimes), keep doing that. For local dev, `Authorization: Bearer dev` works against `ggui serve --dev-allow-all`. OAuth exists for hosts that need DCR + browser-mediated approval. # Ops MCP route > MCP tools on the /ops route — operator actions (apps, orgs, connector keys, coupons, blueprints, provider keys, credits) exposed for operator agents. The `/ops` route surfaces operator-class MCP tools — the same actions the [console UI](/clients/console/) (coming soon) will expose to a human (create an app, rename it, mint a connector key, redeem a coupon, …), exposed as MCP tools so an LLM acting as an operator agent can perform them on the user’s behalf. This page is the wire reference for the 24 ops-audience handlers across seven domains. The agent-loop surface (handshake / render / consume / …) lives on [`/mcp`](/api/mcp-protocol/) and is documented separately — `/ops` is a strictly disjoint route with no overlap. ## What’s on /ops [Section titled “What’s on /ops”](#whats-on-ops) `/ops` is the destination for an **operator agent** — an LLM acting as the console’s hands. Typical caller: a Claude conversation that the user opens from `console.ggui.ai` (coming soon) and gives natural-language instructions like “create a new app called Inbox Triage and lock a connector key to it.” The agent calls `ggui_ops_create_app` followed by `ggui_ops_issue_connector_key`, never touching the AppSync GraphQL layer directly. Every tool here mirrors a UI action the console (coming soon) will expose. The handler files in `@ggui-ai/mcp-server-handlers` are pure over typed seams (`AppsSource`, `OrgsSource`, `OrgInvitesSource`, `ConnectorKeysSource`, `CouponRedeemSource`) — the cloud pod binds AppSync-backed adapters; OSS deployments leave the seams unwired and the surface stays narrow. ## Endpoint [Section titled “Endpoint”](#endpoint) ```plaintext POST http://127.0.0.1:6781/ops ``` Self-hosted servers register tools on `/ops` when the operator seams are wired into `createGguiServer({opsApps, opsOrgs, opsConnectorKeys, opsCoupon})` (the ops-blueprint family additionally hangs off the `opsBlueprint` dep bundle on `defaultHandlers`). With nothing wired, the route still mounts but `tools/list` rejects with JSON-RPC `Method not found` — no tools capability is advertised when zero handlers are registered. Hosted ggui (coming soon) will serve the same route at `https://mcp.ggui.ai/ops`. ## Authentication [Section titled “Authentication”](#authentication) Identical to [`/mcp`](/api/mcp-protocol/#authentication) — bearer-token auth via the same upstream `AuthAdapter`: ```plaintext Authorization: Bearer dev Content-Type: application/json ``` Self-hosted: with `ggui serve --dev-allow-all`, any bearer authenticates as the `builder` identity; default serve requires a pairing-minted bearer. Hosted ggui (coming soon) will run the OAuth 2.0 Dynamic Client Registration ceremony (see [OAuth on mcp.ggui.ai](/api/oauth/)). The bearer presented on `/ops` is the same bearer presented on `/mcp` — there is no separate “ops token”. ## Identity model [Section titled “Identity model”](#identity-model) Every handler resolves the calling identity through a single helper: ```typescript function resolveOwnerSub(toolName: string, ctx: HandlerContext): string { const sub = ctx.userId ?? ctx.appId; if (!sub) throw new Error(`${toolName}: missing caller identity`); return sub; } ``` * **Hosted (multi-tenant):** `ctx.userId` is the caller’s Cognito sub, populated by the upstream auth adapter. * **OSS (single-tenant):** `ctx.userId` is undefined; `ctx.appId` (resolved by the auth adapter via `defaultAppIdFromIdentity` — typically `workspaceId ?? userId` for kind=user identities) serves as the identity. * **Neither set:** the handler throws — that means an unauthenticated caller slipped past auth, surfaced as a 5xx rather than masked as an empty list. ### Tenancy posture [Section titled “Tenancy posture”](#tenancy-posture) Every read and write is scoped by the resolved identity at the seam layer (`AppsSource.list(ownerSub)` returns only the caller’s rows; `AppsSource.get` returns `null` for foreign rows). Cross-tenant probes never reveal whether a given id exists on another user’s account — the handlers translate “row exists but you don’t own it” to the same shape as “no such row”: | Operation | Cross-tenant probe | | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | `ggui_ops_list_apps` | Returns only caller’s rows; foreign rows invisible. | | `ggui_ops_rename_app` / `ggui_ops_set_default_app` / `ggui_ops_update_app_system_prompt` | Throws `app_not_found` — same as a genuinely missing id. | | `ggui_ops_delete_app` | Returns `{deleted: true}` without touching the foreign row. Uniform with “row didn’t exist.” | | `ggui_ops_invite_to_org` / `ggui_ops_revoke_invite` | Throws `org_invite_access_denied` for orgs the caller doesn’t administer. | | `ggui_ops_revoke_connector_key` | Throws `connector_key_access_denied` for keys owned by other users. | | `ggui_ops_redeem_coupon` | Throws `coupon_access_denied` for `targetOrgId` orgs the caller isn’t a member of. | ### One-time secret reveal [Section titled “One-time secret reveal”](#one-time-secret-reveal) Plaintext keys appear exactly once `ggui_ops_issue_connector_key` returns the plaintext `ggui_user_*` secret on its result — this is the **only call** that ever surfaces it. The adapter persists `sha256(plaintext)` hex plus the first \~8 plaintext characters (`apiKeyPrefix`); the plaintext is not stored anywhere. Subsequent `ggui_ops_list_connector_keys` responses carry the prefix and the metadata but never the full secret. The MCP caller (Claude Desktop conversation, console) is responsible for surfacing the plaintext to the user immediately. There is no recovery if it’s lost — the user must revoke and reissue. *** ## Tools by domain [Section titled “Tools by domain”](#tools-by-domain) Seven domains, 24 handlers total. Each domain is optional: the four console-style domains (apps / orgs / connector keys / coupons) hang off `CreateGguiServerOptions`; ops-blueprint hangs off the `opsBlueprint` dep bundle on `defaultHandlers`; provider-keys and credits are bound by the hosted cloud pod (coming soon). Leaving a domain unwired removes its tools from `tools/list` at registration time. ## Apps (`ops-apps`, 6 handlers) [Section titled “Apps (ops-apps, 6 handlers)”](#apps-ops-apps-6-handlers) Operator actions on `GguiApp` rows — the rows the universal MCP route resolves per-request to scope sessions. Each row carries `appId` (server-minted base62), `displayName`, optional `systemPrompt` override, `createdAt`, `updatedAt`. Bound on the cloud pod via the AppSync `provisionGguiApp` mutation + the `GguiApp` model. ### `ggui_ops_list_apps` [Section titled “ggui\_ops\_list\_apps”](#ggui_ops_list_apps) Enumerate every `GguiApp` row owned by the calling user. Returns metadata only — same data the console’s Apps section renders. Use to discover ids before calling the mutating tools. **Inputs:** none. **Returns:** `{ apps: AppRecord[] }` ```typescript interface AppRecord { readonly appId: string; readonly displayName: string; readonly systemPrompt?: string; readonly createdAt: string; readonly updatedAt: string; } ``` **Tenancy:** scope is `ownerSub` from the bearer token. Cross-user listings are impossible by construction. ### `ggui_ops_create_app` [Section titled “ggui\_ops\_create\_app”](#ggui_ops_create_app) Provision a fresh `GguiApp` owned by the calling user. Wraps the cloud’s `provisionGguiApp` mutation — opaque base62 `appId` is minted server-side; argument-supplied `appId` is NEVER honored (tenant-takeover vector). | Field | Type | Required | Description | | ------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `displayName` | `string` (1–120 chars) | No | Human-friendly label. Defaults to `'My ggui app'` when absent — matches the auto-create path in `useGguiUser`. | **Returns:** the full `AppRecord` shape as above. **Follow-up:** call `ggui_ops_set_default_app({appId})` to promote the new app to the user’s default. ### `ggui_ops_rename_app` [Section titled “ggui\_ops\_rename\_app”](#ggui_ops_rename_app) Update an existing app’s `displayName`. The target app MUST be owned by the calling user. | Field | Type | Required | Description | | ------------- | ---------------------- | -------- | ------------------------------------------------------------ | | `appId` | `string` | Yes | Target `GguiApp.appId`. Discover via `ggui_ops_list_apps`. | | `displayName` | `string` (1–120 chars) | Yes | New display name. Cap matches the cloud provisioning Lambda. | **Returns:** the updated `AppRecord`. **Errors:** | Code | When | | --------------- | ---------------------------------------------------------------------------------------- | | `app_not_found` | The id doesn’t exist OR exists under another tenant (uniform shape — no existence leak). | ### `ggui_ops_delete_app` [Section titled “ggui\_ops\_delete\_app”](#ggui_ops_delete_app) Hard-delete an app owned by the calling user. Idempotent — a second delete of the same id resolves cleanly. The cloud adapter additionally cascades per-app keys / blueprints / sessions (orchestrated below the seam). | Field | Type | Required | Description | | ------- | -------- | -------- | ----------------------- | | `appId` | `string` | Yes | Target `GguiApp.appId`. | **Returns:** `{ deleted: true }` **Tenancy:** cross-tenant probes return the success shape without touching the foreign row. Uniform with “row didn’t exist.” ### `ggui_ops_set_default_app` [Section titled “ggui\_ops\_set\_default\_app”](#ggui_ops_set_default_app) Set the calling user’s `GguiUser.defaultAppId` — the universal MCP route resolves this on every request to scope the session. The handler first verifies the caller owns the target `appId` before writing `User.defaultAppId`. | Field | Type | Required | Description | | ------- | -------- | -------- | ----------------------------------------- | | `appId` | `string` | Yes | Target app — must be owned by the caller. | **Returns:** `{ defaultAppId: string }` **Errors:** | Code | When | | --------------- | ----------------------------------------------------------- | | `app_not_found` | Target `appId` doesn’t exist OR is owned by another tenant. | ### `ggui_ops_update_app_system_prompt` [Section titled “ggui\_ops\_update\_app\_system\_prompt”](#ggui_ops_update_app_system_prompt) Set or clear the per-app system-prompt override. Empty-string input clears the field — the pod’s per-app system-prompt resolution then falls back to the universal default. | Field | Type | Required | Description | | -------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `appId` | `string` | Yes | Target `GguiApp.appId`. | | `systemPrompt` | `string` (≤10,000 chars) | Yes | Replacement text. Pass `""` to clear the override. Cap bounds the response payload and matches a reasonable agent-authored prompt length. | **Returns:** the updated `AppRecord` (with `systemPrompt` omitted when cleared). **Errors:** | Code | When | | --------------- | ----------------------------------------------------------- | | `app_not_found` | Target `appId` doesn’t exist OR is owned by another tenant. | *** ## Orgs (`ops-orgs`, 4 handlers) [Section titled “Orgs (ops-orgs, 4 handlers)”](#orgs-ops-orgs-4-handlers) Operator actions on `GguiOrg` + `GguiOrgMember` + `GguiOrgInvite` rows. Orgs are the unit of multi-user collaboration; each row carries `orgId` (ULID), `name`, `ownerUserId`, plus per-membership role on the join rows. Bound on the cloud pod via the `provisionGguiOrg` / `fetchMyOrgs` / `issueOrgInvite` / `revokeOrgInvite` AppSync mutations. ### `ggui_ops_list_orgs` [Section titled “ggui\_ops\_list\_orgs”](#ggui_ops_list_orgs) Enumerate every org the calling user belongs to — owner + admin + member memberships in a single list, each row carrying the caller’s role. **Inputs:** none. **Returns:** `{ orgs: OrgMembershipRecord[] }` ```typescript interface OrgMembershipRecord { readonly orgId: string; readonly name: string; readonly ownerUserId: string; readonly role: "owner" | "admin" | "member"; readonly joinedAt: string; } ``` Mirrors the AppSync `fetchMyOrgs` custom resolver. Use to discover `orgId` before calling the invite tools. ### `ggui_ops_create_org` [Section titled “ggui\_ops\_create\_org”](#ggui_ops_create_org) Provision a fresh `GguiOrg` owned by the calling user. Wraps the cloud’s `provisionGguiOrg` mutation — ULID `orgId` minted server-side; an owner membership row and a zero-balance credit row are inserted atomically via TransactWrite. | Field | Type | Required | Description | | ------ | ---------------------- | -------- | ------------------------------------------------------------------------------------ | | `name` | `string` (1–120 chars) | Yes | Human-friendly display name. Required (no default — orgs are intentional creations). | **Returns:** ```typescript interface CreateOrgOutput { readonly orgId: string; readonly name: string; readonly ownerUserId: string; readonly createdAt: string; readonly updatedAt: string; } ``` ### `ggui_ops_invite_to_org` [Section titled “ggui\_ops\_invite\_to\_org”](#ggui_ops_invite_to_org) Issue an `admin`- or `member`-role invite to a `GguiOrg` the caller can administer. The invite link in the recipient’s email points at the console (coming soon): `console.ggui.ai/invites/`. | Field | Type | Required | Description | | ------- | --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `orgId` | `string` | Yes | Target org — caller must own or administer it. Discover via `ggui_ops_list_orgs`. | | `email` | `string` (RFC 5322) | Yes | Recipient email — the invite link is sent here. | | `role` | `'admin' \| 'member'` | Yes | Role the recipient holds once they accept. Owner can’t be granted via invite — ownership transfer is a separate flow. | **Returns:** ```typescript interface InviteToOrgOutput { readonly inviteId: string; readonly orgId: string; readonly email: string; readonly role: "admin" | "member"; readonly inviterUserId: string; readonly status: "pending" | "accepted" | "revoked" | "expired"; readonly expiresAt: string; readonly createdAt: string; readonly reused: boolean; } ``` **Anti-double-issue:** an existing pending invite for the same `(orgId, email)` is reused — no new row, no second email. `reused: true` flags the dedup. **Errors:** | Code | When | | -------------------------- | -------------------------------------------- | | `org_invite_access_denied` | Caller is not owner/admin of the target org. | ### `ggui_ops_revoke_invite` [Section titled “ggui\_ops\_revoke\_invite”](#ggui_ops_revoke_invite) Invalidate a pending org invite — the bearer-secret link in the recipient’s email stops working immediately. | Field | Type | Required | Description | | ---------- | -------- | -------- | ---------------------------------------------------------------- | | `inviteId` | `string` | Yes | Target invite — must belong to an org the caller can administer. | **Returns:** ```typescript interface RevokeInviteOutput { readonly inviteId: string; readonly status: "pending" | "accepted" | "revoked" | "expired"; readonly alreadyRevoked: boolean; } ``` **Concurrency:** the adapter flips `status` from `pending` → `revoked` via a CAS `ConditionExpression`. A racing accept surfaces a clear conflict instead of silently overwriting. Already-revoked invites return `alreadyRevoked: true`; already-accepted invites reject. **Errors:** | Code | When | | -------------------------- | ------------------------------------------------------ | | `org_invite_access_denied` | Caller is not owner/admin of the invite’s org. | | `org_invite_not_found` | The id doesn’t exist OR isn’t reachable by the caller. | *** ## Connector keys (`ops-connector-keys`, 3 handlers) [Section titled “Connector keys (ops-connector-keys, 3 handlers)”](#connector-keys-ops-connector-keys-3-handlers) Operator actions on `GguiUserApiKey` rows — the user-facing `ggui_user_*` API key strings that Claude Desktop (and other Connectors) present to call the MCP routes on the user’s behalf. Bound on the cloud pod via the `issueGguiUserApiKey` AppSync mutation + the `apiKeysByUserId` GSI + raw DDB `UpdateItem` for revoke. ### `ggui_ops_list_connector_keys` [Section titled “ggui\_ops\_list\_connector\_keys”](#ggui_ops_list_connector_keys) Read the calling user’s `ggui_user_*` connector keys. **Metadata only — NEVER plaintext.** **Inputs:** none. **Returns:** `{ keys: ConnectorKeySummary[] }` ```typescript interface ConnectorKeySummary { readonly id: string; // stable id for revoke readonly apiKeyPrefix: string; // first ~8 chars of the secret (human re-identification) readonly name?: string; // user-supplied label readonly appId?: string; // optional FK — when set the key locks to that app readonly status: "active" | "revoked"; readonly createdAt: string; readonly lastUsedAt?: string; // from the last successful auth lookup readonly expiresAt?: string; // past timestamp ⇒ adapter rejects auth } ``` The hash itself is never returned on any tool. ### `ggui_ops_issue_connector_key` [Section titled “ggui\_ops\_issue\_connector\_key”](#ggui_ops_issue_connector_key) Mint a fresh `ggui_user_*` connector key. | Field | Type | Required | Description | | ----------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` (1–120 chars) | No | Optional label, e.g. `'MacBook Claude Desktop'`. Surfaces on `ggui_ops_list_connector_keys`. | | `appId` | `string` | No | Lock the key to one app. When set, sessions opened with this key scope to the named app and meta-tools (`ggui_ops_open_app`, `ggui_ops_list_apps`) are NOT exposed. Absent ⇒ universal key (scopes to `User.defaultAppId` per request). | | `expiresAt` | `string` (ISO 8601) | No | Optional expiry. Past timestamps reject auth from the start. | **Returns:** ```typescript interface IssueConnectorKeyOutput { // metadata — same shape as a list row readonly id: string; readonly apiKeyPrefix: string; readonly name?: string; readonly appId?: string; readonly status: "active" | "revoked"; readonly createdAt: string; readonly lastUsedAt?: string; readonly expiresAt?: string; // ONE-TIME REVEAL — never returned again readonly plaintextKey: string; } ``` One-time reveal `plaintextKey` is the `ggui_user_` secret. It appears on this response and never again — the adapter persists `sha256(plaintextKey)` hex plus `apiKeyPrefix`, and the plaintext is not stored. The caller MUST surface it to the user immediately. ### `ggui_ops_revoke_connector_key` [Section titled “ggui\_ops\_revoke\_connector\_key”](#ggui_ops_revoke_connector_key) Soft-revoke a `GguiUserApiKey` row. The adapter sets `status='revoked'`; the auth path rejects revoked keys regardless of hash match. Rows are kept for audit (age-based sweep handles cleanup). | Field | Type | Required | Description | | ------- | -------- | -------- | ------------------------------------------------------------------------------------------ | | `keyId` | `string` | Yes | Stable id of the row (NOT the secret string). Discover via `ggui_ops_list_connector_keys`. | **Returns:** ```typescript interface RevokeConnectorKeyOutput { readonly id: string; readonly status: "active" | "revoked"; readonly alreadyRevoked: boolean; } ``` **Errors:** | Code | When | | ----------------------------- | ------------------------------------ | | `connector_key_access_denied` | The key belongs to another user. | | `connector_key_not_found` | No such key reachable by the caller. | Idempotent — re-revoking returns `alreadyRevoked: true`. *** ## Coupons (`ops-coupon`, 1 handler) [Section titled “Coupons (ops-coupon, 1 handler)”](#coupons-ops-coupon-1-handler) Operator action on `GguiCoupon` rows — bearer-secret promo codes that credit user or org wallets. Bound on the cloud pod via the `redeemCoupon` AppSync mutation. ### `ggui_ops_redeem_coupon` [Section titled “ggui\_ops\_redeem\_coupon”](#ggui_ops_redeem_coupon) Redeem a `cpn_*` coupon code, crediting the caller’s wallet (default) or a target org’s wallet. The adapter runs an atomic three-leg `TransactWrite`: 1. Flip `GguiCoupon.status` from `issued` → `activated`. 2. Credit the wallet (user or org). 3. Insert a ledger row. Failure of any leg rolls all back — no half-credit, no double-spend. | Field | Type | Required | Description | | ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `couponCode` | `string` | Yes | The bearer-secret code in format `cpn_<8 chars>`. One-time redemption. | | `targetOrgId` | `string` | No | When set, credits the named org’s wallet instead of the caller’s personal wallet. Caller MUST be a member of the org. | **Returns:** ```typescript interface RedeemCouponOutput { readonly couponCode: string; readonly creditCents: number; readonly redeemedByPrincipalType: "user" | "org"; readonly redeemedByPrincipalId: string; readonly activatedAt: string; } ``` **Errors:** | Code | When | | ------------------------- | ---------------------------------------------------------------------- | | `coupon_not_found` | The code doesn’t exist. | | `coupon_already_redeemed` | The code was previously activated (one-time semantics). | | `coupon_expired` | The code is past its expiry. | | `coupon_access_denied` | `targetOrgId` was provided but the caller is not a member of that org. | *** ## Blueprints (`ops-blueprint`, 5 handlers) [Section titled “Blueprints (ops-blueprint, 5 handlers)”](#blueprints-ops-blueprint-5-handlers) Operator blueprint authorship — generate, register, list, update, delete cached blueprints for the calling app. Unlike the four console-style domains, this family registers on the OSS server via the `opsBlueprint` dep bundle on `defaultHandlers` (registry + blueprint store + search; `generate` additionally requires the `resolveLlm` + `blueprints` deps the render generation path reads). ### `ggui_ops_generate_blueprint` [Section titled “ggui\_ops\_generate\_blueprint”](#ggui_ops_generate_blueprint) Author a blueprint via the bound generator (LLM generation + validation). | Field | Type | Required | Description | | ---------------------- | --------- | -------- | -------------------------------------------------------------- | | `contract` | `object` | Yes | The `DataContract` to generate against. | | `generator` | `string` | No | Generator slug. Unknown slug fails with `generator_not_found`. | | `persona` | `string` | No | Variance axis — normalized lowercase + trimmed. | | `aesthetic` | `string` | No | Variance axis. | | `context` | `string` | No | Variance axis. | | `seedPrompt` | `string` | No | Variance axis. | | `setAsOperatorDefault` | `boolean` | No | Promote the result to the operator default for its contract. | **Returns:** `{ blueprintId, codeHash?, validatorScore?, source }` — `validatorScore` (0–1) only on the advanced generator path; `source` is the stamped provenance `{ kind: 'llm', generator, model }` from the engine’s own metadata stamp. **Errors:** `generator_not_found`; `missing_credentials` (BYOK fix: `ggui_ops_set_provider_key`); generation failure. ### `ggui_ops_register_blueprint` [Section titled “ggui\_ops\_register\_blueprint”](#ggui_ops_register_blueprint) Register pre-built component code verbatim — no LLM, no validator. Operator entry point for fixture seeding and export/reimport round-trips. | Field | Type | Required | Description | | --------------- | -------- | -------- | ----------------------------------------------------- | | `contract` | `object` | Yes | The `DataContract` the code implements. | | `componentCode` | `string` | Yes | The component code to register verbatim (min 1 char). | Plus the same optional `generator` / `persona` / `aesthetic` / `context` / `seedPrompt` / `setAsOperatorDefault` fields as `ggui_ops_generate_blueprint`. **Returns:** `{ blueprintId, codeHash, source }` — `source` is always `{ kind: 'user' }`; hand-supplied bytes carry no engine claim, so none is recorded. ### `ggui_ops_list_blueprints` [Section titled “ggui\_ops\_list\_blueprints”](#ggui_ops_list_blueprints) | Field | Type | Required | Description | | ---------------- | ---------- | -------- | ----------------------------------------------------- | | `contractHash` | `string` | No | Filter by canonical contract hash. | | `generator` | `string` | No | Filter by generator slug. | | `persona` | `string` | No | Dispatches semantic search. | | `intentKeywords` | `string[]` | No | Dispatches semantic search. Filters are AND-composed. | **Returns:** `{ blueprints: Blueprint[] }` ### `ggui_ops_update_blueprint` [Section titled “ggui\_ops\_update\_blueprint”](#ggui_ops_update_blueprint) | Field | Type | Required | Description | | ------------------- | -------------- | -------- | ----------------------------------------------------------------- | | `blueprintId` | `string` | Yes | Target blueprint. | | `isOperatorDefault` | `literal true` | No | Promote to operator default. | | `variance` | `object` | No | Partial-merge of variance axes; `{persona: ""}` clears the field. | **Returns:** `{ blueprintId, updatedAt }` ### `ggui_ops_delete_blueprint` [Section titled “ggui\_ops\_delete\_blueprint”](#ggui_ops_delete_blueprint) | Field | Type | Required | Description | | ------------- | -------- | -------- | ----------------- | | `blueprintId` | `string` | Yes | Target blueprint. | **Returns:** `{ deleted: true }` — idempotent. *** ## Provider keys (`provider-keys`, 3 handlers) — BYOK [Section titled “Provider keys (provider-keys, 3 handlers) — BYOK”](#provider-keys-provider-keys-3-handlers--byok) Operator actions on the caller’s BYOK LLM provider keys. Provider enum: `'anthropic' | 'openai' | 'google' | 'openrouter'`. The handler factories ship in `@ggui-ai/mcp-server-handlers`; they are bound today by the hosted cloud pod (coming soon), which validates keys against the provider and encrypts at rest. ### `ggui_ops_set_provider_key` [Section titled “ggui\_ops\_set\_provider\_key”](#ggui_ops_set_provider_key) | Field | Type | Required | Description | | -------------- | -------- | -------- | -------------------------------------------------------------- | | `provider` | `enum` | Yes | One of `anthropic` / `openai` / `google` / `openrouter`. | | `plaintextKey` | `string` | Yes | The provider API key (min 1 char). Re-set replaces (rotation). | | `label` | `string` | No | Human label. | **Returns:** `{ provider, label?, lastFour, createdAt?, lastUsedAt? }` — never echoes the key. ### `ggui_ops_list_provider_keys` [Section titled “ggui\_ops\_list\_provider\_keys”](#ggui_ops_list_provider_keys) **Inputs:** none. **Returns:** `{ keys: [{ provider, label?, lastFour, createdAt?, lastUsedAt? }] }` ### `ggui_ops_remove_provider_key` [Section titled “ggui\_ops\_remove\_provider\_key”](#ggui_ops_remove_provider_key) | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------- | | `provider` | `enum` | Yes | Provider to remove. | **Returns:** `{ deleted, provider }` *** ## Credits (`credits`, 2 handlers) [Section titled “Credits (credits, 2 handlers)”](#credits-credits-2-handlers) Read-only views over the caller’s prepaid credit wallet. Bound by the hosted cloud pod (coming soon); self-hosted deployments have no credit plane. ### `ggui_ops_get_credit_balance` [Section titled “ggui\_ops\_get\_credit\_balance”](#ggui_ops_get_credit_balance) **Inputs:** none. **Returns:** `{ balanceCents, lifetimeGrantedCents, lifetimeSpentCents, updatedAt }` ### `ggui_ops_list_credit_transactions` [Section titled “ggui\_ops\_list\_credit\_transactions”](#ggui_ops_list_credit_transactions) | Field | Type | Required | Description | | -------- | -------- | -------- | ------------------ | | `limit` | `number` | No | 1–100, default 20. | | `cursor` | `string` | No | Pagination cursor. | **Returns:** `{ transactions: [{ transactionId, kind, deltaCents, balanceAfterCents, reason, createdAt, relatedSessionId? }], nextCursor? }` — `kind` is one of `free_credit` / `render_charge` / `topup` / `refund`. *** ## OSS vs hosted [Section titled “OSS vs hosted”](#oss-vs-hosted) The four console-style domains are wired through optional fields on `CreateGguiServerOptions` (ops-blueprint hangs off the `opsBlueprint` dep bundle on `defaultHandlers`; provider-keys + credits are cloud-pod-bound): ```typescript interface CreateGguiServerOptions { readonly opsApps?: { readonly apps: AppsSource; readonly userDefaultApp: UserDefaultAppSource; }; readonly opsOrgs?: { readonly orgs: OrgsSource; readonly invites: OrgInvitesSource; }; readonly opsConnectorKeys?: { readonly connectorKeys: ConnectorKeysSource; }; readonly opsCoupon?: { readonly coupons: CouponRedeemSource; }; } ``` * **Hosted (`mcp.ggui.ai`, coming soon):** the cloud pod binds all four — AppSync-backed adapters wrap the corresponding mutations — plus the provider-keys and credits families. The full ops surface is registered on `/ops`. * **OSS (`ggui serve`):** every field is `undefined` by default. The route still mounts but `tools/list` rejects with `Method not found` — no tools capability is advertised when zero handlers are registered. Operator tools only make sense alongside a data model to operate on; the ops-blueprint family is the one most self-hosters wire (via the `opsBlueprint` dep bundle). * **Partial wiring:** omit individual fields to drop their tools. A self-hosted deployment with its own `AppsSource` can register `ggui_ops_*_app` only and leave orgs / connector keys / coupons unwired. The seam interfaces (`AppsSource`, `OrgsSource`, `OrgInvitesSource`, `ConnectorKeysSource`, `CouponRedeemSource`) are exported from `@ggui-ai/mcp-server-handlers` — implementing them against your own backend is the integration path for downstream forks. ## Console parity [Section titled “Console parity”](#console-parity) The [console UI](/clients/console/) (coming soon) will mirror these tools 1:1 — every tool corresponds to one UI action: | Tool | Console surface | | ----------------------------------- | ----------------------------------------------------- | | `ggui_ops_list_apps` | Apps section — main list. | | `ggui_ops_create_app` | Apps section — “New app” button. | | `ggui_ops_rename_app` | Apps section — inline rename. | | `ggui_ops_delete_app` | Apps section — row menu → Delete. | | `ggui_ops_set_default_app` | Apps section — “Set as default” toggle. | | `ggui_ops_update_app_system_prompt` | Apps section → System Prompt editor. | | `ggui_ops_list_orgs` | Orgs section — main list. | | `ggui_ops_create_org` | Orgs section — “New org” button. | | `ggui_ops_invite_to_org` | Orgs section → Members → Invite. | | `ggui_ops_revoke_invite` | Orgs section → Members → pending invite row → Revoke. | | `ggui_ops_list_connector_keys` | Account → Connector Keys list. | | `ggui_ops_issue_connector_key` | Account → Connector Keys → “Issue new key”. | | `ggui_ops_revoke_connector_key` | Account → Connector Keys → row menu → Revoke. | | `ggui_ops_redeem_coupon` | Billing → Redeem coupon. | The MCP surface and the UI surface are siblings over the same seam — they call the same `AppsSource.create`, the same `OrgInvitesSource.issue`, etc. There’s no privileged path on either side. *** ## Example: curl [Section titled “Example: curl”](#example-curl) This walkthrough targets a self-hosted server with the ops seams wired (started with `--dev-allow-all` for the `Bearer dev` shortcut). On hosted ggui (coming soon), the same calls will go to `https://mcp.ggui.ai/ops` with an OAuth bearer. ```bash # 1. Initialize curl -X POST http://127.0.0.1:6781/ops \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"curl","version":"1.0"},"capabilities":{}}}' # 2. Enumerate the caller's apps curl -X POST http://127.0.0.1:6781/ops \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ggui_ops_list_apps","arguments":{}}}' # 3. Create a fresh app curl -X POST http://127.0.0.1:6781/ops \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ggui_ops_create_app","arguments":{"displayName":"Inbox Triage"}}}' # 4. Promote the new app to default (use the appId from step 3's response) curl -X POST http://127.0.0.1:6781/ops \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"ggui_ops_set_default_app","arguments":{"appId":""}}}' # 5. Issue a connector key locked to the new app # The response carries `plaintextKey` — surface it to the user immediately. curl -X POST http://127.0.0.1:6781/ops \ -H "Authorization: Bearer dev" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ggui_ops_issue_connector_key","arguments":{"name":"MacBook Claude Desktop","appId":""}}}' ``` The same calls can be made through the [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) client by pointing the transport at `/ops` instead of `/mcp` — the tool registration shapes are standard. *** ## See Also [Section titled “See Also”](#see-also) * [Console](/clients/console/) — the human-facing surface for the same actions (coming soon). * [Audience Routes](/architecture/audience-routes/) — the `agent` / `runtime` / `protocol` / `ops` tag model and how it projects to routes. * [MCP Protocol Reference](/api/mcp-protocol/) — the sibling agent-loop surface on `/mcp`. # Rate limits > How ggui surfaces rate limits — the in-band `rate_limited` tool error on `ggui_render`, the HTTP 429 + Retry-After contract, and how host SDKs handle backoff. ggui rate limiting is operator-configured: a `RateLimiter` seam the deployment wires (or doesn’t). This page covers the self-hosted defaults, the two enforcement layers and their wire shapes, and how to layer retry on top of whichever MCP host SDK you’re using. ## Self-hosted defaults [Section titled “Self-hosted defaults”](#self-hosted-defaults) * Default (strict) `ggui serve` wires **no** generation limiter — `ggui_render` is unlimited for paired callers. * `ggui serve --public-demo` binds a per-remote-IP fixed-window limiter to `ggui_render`: **30 generations / 10 minutes / IP** (operator-pays posture for public demos). * Library users wire their own `RateLimiter` into the render handler deps — the seam and the typed `RateLimitedError` live in `@ggui-ai/mcp-server-core`. ## Two enforcement layers [Section titled “Two enforcement layers”](#two-enforcement-layers) ### Tool-level: `ggui_render` rejects in-band [Section titled “Tool-level: ggui\_render rejects in-band”](#tool-level-ggui_render-rejects-in-band) When a rate limiter is wired into the render handler, a limited `ggui_render` call rejects with an MCP **tool error** (an `isError` tool result), not an HTTP 429. The error carries the code `rate_limited` and the retry decision (`retryAfterMs`). Catch it in your agent loop like any other tool error and back off before re-calling. ### HTTP-level: 429 on auth/pairing endpoints [Section titled “HTTP-level: 429 on auth/pairing endpoints”](#http-level-429-on-authpairing-endpoints) The pairing/login routes enforce limits at the HTTP transport layer. Every limited request returns: | Field | Value | | -------------------- | ---------------------------------------------------------------------------------------------- | | HTTP status | `429` | | `Retry-After` header | Seconds before the next attempt is permitted. Optional — absent means use exponential backoff. | | Body | JSON `{ "error": { "code": "rate_limited", "message": "...", "retryAfter": } }`. | `Retry-After` is the authoritative signal. When present, honor it verbatim — the server has already computed the appropriate wait. The `retryAfter` field in the body mirrors the header for convenience when only the body is observable (e.g. some transport wrappers). ## Retry is the host SDK’s job [Section titled “Retry is the host SDK’s job”](#retry-is-the-host-sdks-job) ggui has no first-party client SDK to wrap retries — your MCP host owns that loop. The pattern is the same regardless of host: catch the 429, read `Retry-After`, sleep, retry, cap attempts. For `ggui_render`, additionally check the tool result’s `isError` flag for the in-band `rate_limited` error and back off the same way. ### Claude Agent SDK [Section titled “Claude Agent SDK”](#claude-agent-sdk) The Claude Agent SDK’s `query()` already retries transient transport errors (including 429) using the standard Anthropic SDK retry config. You generally don’t need to do anything — bursts within the retry window never surface to your code. To tune, pass `maxRetries` through the SDK’s options. See [Examples → Claude Agent](/examples/claude-agent/) for a runnable scaffold. ### `@modelcontextprotocol/sdk` (generic MCP) [Section titled “@modelcontextprotocol/sdk (generic MCP)”](#modelcontextprotocolsdk-generic-mcp) The official MCP SDK throws on HTTP errors without retrying. Wrap `callTool` (or whichever method you invoke) yourself: ```typescript import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "my-agent", version: "1.0.0" }); await client.connect( new StreamableHTTPClientTransport(new URL("http://127.0.0.1:6781/mcp"), { requestInit: { headers: { Authorization: "Bearer dev" } }, }) ); async function callWithRetry( fn: () => Promise, { maxRetries = 3, baseDelayMs = 1000, maxDelayMs = 30000 } = {} ): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err) { // The MCP SDK surfaces HTTP errors with status + headers attached. const status = (err as { status?: number }).status; if (status !== 429 || attempt === maxRetries) throw err; const retryAfter = Number((err as { headers?: Record }).headers?.["retry-after"]) || undefined; const waitMs = retryAfter != null ? retryAfter * 1000 : Math.min(baseDelayMs * 2 ** attempt, maxDelayMs); await new Promise((r) => setTimeout(r, waitMs)); } } throw new Error("unreachable"); } const result = await callWithRetry(() => client.callTool({ name: "ggui_handshake", arguments: { /* ... */ }, }) ); ``` Tune `maxRetries` per workload: lower on interactive (user-blocking) paths so failures bubble up fast; raise on background batch paths where backoff is cheaper than re-queuing. Note that a rate-limited `ggui_render` on a `--public-demo` server does NOT throw an HTTP error — it resolves with `isError: true` and a `rate_limited` message; check the result before treating the call as a success. ## Raw HTTP [Section titled “Raw HTTP”](#raw-http) If you’re hitting the server directly without an MCP SDK, implement the same loop against `fetch`: 1. Read the `Retry-After` header on every 429. 2. If present, sleep that many seconds, then retry. 3. If absent, sleep `min(baseDelay * 2^attempt, maxDelay)`, then retry. 4. Cap retries (3–5 is reasonable for interactive workloads, more for batch). 5. Stop retrying on non-429 4xx (those won’t resolve with backoff). The [generic MCP example](/examples/generic-mcp/) walks through raw-HTTP usage end-to-end. ## See also [Section titled “See also”](#see-also) * [Examples → Claude Agent](/examples/claude-agent/) — runnable scaffold with the host SDK’s native retry. * [Cookbook → Error handling](/cookbook/error-handling/) — retry, surfacing, and dead-letter patterns. * [Troubleshooting](/troubleshooting/) — common error patterns. * [MCP Protocol](/api/mcp-protocol/) — full JSON-RPC method reference. # WebSocket Protocol > Live-channel wire format — the live session plane between a ggui client and your self-hosted ggui serve (hosted ggui coming soon). The live channel — the **live session plane** — runs over a WebSocket between a ggui server and a connected client. It carries agent `render` notifications, outbound `StreamEnvelope` deliveries, and canonical inbound `ActionEnvelope` user actions. | Deployment | URL | | -------------------------------- | ------------------------------------------------ | | Self-hosted (`ggui serve`) | `ws://127.0.0.1:6781/ws` (default; configurable) | | Hosted (`ggui.ai`) — coming soon | `wss://mcp.ggui.ai/ws` | ## Frame catalog [Section titled “Frame catalog”](#frame-catalog) | Direction | Type | Payload | Purpose | | --------------- | ----------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Client → Server | `subscribe` | `SubscribePayload` | Bind the connection to a GguiSession. MUST be first. | | Client → Server | `action` | `ActionEnvelope` | Canonical inbound user action. | | Client → Server | `ping` | — | Heartbeat; server answers `pong`. | | Client → Server | `channel_subscribe` | channel name | Subscribe to a `streamSpec[*].source.tool` channel; server polls the tool. | | Client → Server | `channel_unsubscribe` | channel name | Cancel a `channel_subscribe` (idempotent). | | Client → Server | `host_context_observed` | host-context projection | Iframe echoes the MCP-Apps host context. | | Server → Client | `ack` | `AckPayload` | Acknowledges `subscribe`; seeds resume cursors. | | Server → Client | `pong` | — | Heartbeat response. | | Server → Client | `error` | `ErrorPayload` | Transport / auth / subscribe-level error. | | Server → Client | `render` | `RenderPayload {session, matchType?}` | The agent committed a new GguiSession. | | Server → Client | `props_update` | `{sessionId, props}` | `ggui_update` fan-out — full props replacement. | | Server → Client | `data` | `StreamEnvelope` | Outbound delivery on a declared `streamSpec` channel. Generation-pipeline progress also flows here as a `{type:'data'}` delivery on the reserved `_ggui:lifecycle` channel — there is no dedicated progress frame. | | Server → Client | `render_event` | `GguiSessionEvent` | Event-ledger replay when `subscribe.sinceSequence` is set. | | Server → Client | `drain_ack` | `DrainAckPayload` | `ggui_consume` drained an action; iframe cancels its claim timer. | | Server → Client | `channel_payload` | channel frame | `source.tool` result for a subscribed channel. | | Server → Client | `channel_error` | channel error frame | Channel subscribe rejected / poll failed / tool errored. | | Server → Client | `system` | system payload | System-level events (auth, credentials). | ## Client → Server [Section titled “Client → Server”](#client--server) ### `subscribe` [Section titled “subscribe”](#subscribe) Bind the connection to a GguiSession. MUST be the first message. ```json { "type": "subscribe", "payload": { "sessionId": "ses_abc123", "appId": "app_myapp", "wsToken": "btkn_…" } } ``` | Field | Required | Description | | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `sessionId` | Yes | The GguiSession to bind. | | `appId` | No | Tenancy scope — when present MUST match the session’s bound app; when omitted the server resolves the identity-default appId. | | `wsToken` | No | Short-TTL auth credential from the `_meta["ai.ggui/render"]` slice. Required unless the connection authenticated by bearer (see below). | | `fromSeq` | No | Per-channel stream cursor — replay outbound `StreamEnvelope`s with `seq > N` before the live tail begins (needs a `GguiSessionStreamBuffer`). | | `sinceSequence` | No | Event-ledger replay cursor — replays `GguiSessionEvent`s with `sequence > N` as `render_event` frames. Independent of `fromSeq`. | | `role` | No | `'user'` or `'agent'`. | | `supportedVersions` | No | Protocol-version handshake — see below. | `fromSeq` and `sinceSequence` are two independent replay cursors over two ledgers: per-channel stream replay vs the render-level event ledger. When both are set, `sinceSequence` events replay first. #### Authentication [Section titled “Authentication”](#authentication) The load-bearing credential is the `wsToken` minted at `ggui_render` and delivered on the `_meta["ai.ggui/render"]` slice. Clients thread it twice: as `?wsToken=` on the WebSocket upgrade URL AND inside `SubscribePayload.wsToken`. It is opaque, validated server-side against `sessionId` + `appId`, short-TTL, and reusable within its TTL for reconnects. On a successful wsToken-authed subscribe the server mints `AckPayload.sessionToken` — a longer-lived reconnect credential passed on the standard bearer path (`Authorization: Bearer ` or `?token=`) on later connections. ### `action` [Section titled “action”](#action) A canonical [`ActionEnvelope`](/protocol/envelopes/#actionenvelope) — flat, no nested blocks. ```json { "type": "action", "payload": { "sessionId": "ses_abc123", "type": "data:submit", "payload": { "action": "submit", "data": { "rating": 5 } }, "clientSeq": 1 } } ``` The `sessionId` identifies the render the action originated from; the server rejects envelopes whose `sessionId` doesn’t match the subscriber’s bound render. *** ## Server → Client [Section titled “Server → Client”](#server--client) ### `ack` [Section titled “ack”](#ack) Acknowledges the `subscribe` and seeds resume cursors. ```json { "type": "ack", "payload": { "sequence": 42, "timestamp": 1716130000000, "streamSeq": 12, "session": null } } ``` | Field | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | `sequence` | Inbound event-ledger position. | | `timestamp` | Epoch milliseconds. | | `session` | Current `GguiSession` snapshot when one is already committed; `null` / absent otherwise. | | `streamSeq` | Highest outbound `StreamEnvelope.seq` sent — seeds `fromSeq` on the next subscribe. | | `replayTruncated` | `true` when a requested `fromSeq` predates the server’s buffer window — the client got the live tail but missed history. | | `sessionToken` | Longer-lived reconnect credential, minted on the first wsToken-authed subscribe (see Authentication above). | | `serverVersion` | Server’s `PROTOCOL_SCHEMA_VERSION` stamp — see the protocol-version handshake below. | ### `render` [Section titled “render”](#render) The agent committed a new GguiSession for this `sessionId`. The payload is `{ session: GguiSession, matchType? }` — the frame discriminator `type: "render"` stays verb-named; the object inside is the `GguiSession`. ```json { "type": "render", "payload": { "session": { "id": "ses_xyz", "componentCode": "/* compiled JS */", "propsSpec": { /* … */ }, "actionSpec": { /* … */ }, "streamSpec": { /* … */ }, "contextSpec": { /* … */ } } } } ``` ### `props_update` [Section titled “props\_update”](#props_update) The `ggui_update` fan-out — the agent mutated props on this GguiSession. `props` is the FULL replacement state (post-merge for `kind: "merge"` updates), not a patch. Persistence is the source of truth; this frame is the latency optimization. ```json { "type": "props_update", "payload": { "sessionId": "ses_abc123", "props": { "rating": 5 } } } ``` ### `render_event` [Section titled “render\_event”](#render_event) One `GguiSessionEvent` from the per-session event ledger, replayed when `subscribe.payload.sinceSequence` is set. Same ledger as `GET /api/sessions/:sessionId/events` — two transports, one cursor. ### `data` [Section titled “data”](#data) An outbound [`StreamEnvelope`](/protocol/envelopes/#streamenvelope). `channel` names the `streamSpec[name]` it belongs to; `mode` is `append` or `replace`. ```json { "type": "data", "payload": { "sessionId": "ses_abc123", "channel": "message", "mode": "append", "payload": { "text": "Found 3 flights.", "sender": "agent" }, "seq": 7 } } ``` ### `error` [Section titled “error”](#error) A server-side error — transport, auth, subscribe rejection, or an inbound action that failed contract validation. An inbound action that fails contract validation surfaces here as a typed `error` frame with code `CONTRACT_VIOLATION` (numeric `-32020`); nothing reaches the consume buffer. The earlier `_ggui:contract-error` reserved channel, its `ContractErrorPayload`, and the `ContractErrorCode` union were deleted in draft-2026-06-11 — contract failures now surface on the call that caused them, not on a side channel. Canonical `code` values exported from `@ggui-ai/protocol`: | Code | Meaning | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UPGRADE_REQUIRED` | Protocol-version handshake mismatch (see below). Servers default to `versionPolicy: 'reject'` and close the connection after emitting this frame. | | `CONTRACT_VIOLATION` | An inbound action failed validation against the render’s `actionSpec` (undeclared name or schema-rejected payload); nothing reaches the consume buffer. | Other codes are free-form strings. Server-emitted today: `SESSION_MISMATCH`, `BOOTSTRAP_SESSION_MISMATCH`, `SESSION_CREATE_FAILED`, `REPLAY_HORIZON_PASSED`; the `channel_error` frame uses `CHANNEL_UNKNOWN`, `CHANNEL_NOT_LOCAL`, `SESSION_NOT_FOUND`, `SUBSCRIBE_UNAUTHORIZED`, `POLL_FAILED`. See [Envelopes](/protocol/envelopes/). *** ## Protocol-version handshake [Section titled “Protocol-version handshake”](#protocol-version-handshake) Both peers advertise their schema version on the wire so version mismatch surfaces explicitly instead of silently corrupting state. * `subscribe.payload.supportedVersions?: string[]` — client declares the versions it accepts (first-party clients populate from `CLIENT_SUPPORTED_VERSIONS`). * `ack.payload.serverVersion?: string` — server stamps its `PROTOCOL_SCHEMA_VERSION` on every ack. Mismatch policy: * **Server-side** — if `subscribe.supportedVersions` is present and the server’s `PROTOCOL_SCHEMA_VERSION` isn’t a member, the server replies with `error { code: 'UPGRADE_REQUIRED' }`. Default `versionPolicy: 'reject'` also closes the socket; `versionPolicy: 'advisory'` keeps it open (controlled-migration opt-out only). * **Client-side** — if `ack.serverVersion` is absent from the client’s `CLIENT_SUPPORTED_VERSIONS`, the client surfaces `UPGRADE_REQUIRED` on its error channel. Absent declarations on either side are legacy-pass-through (version-agnostic) — preserves pre-handshake behavior for older peers. *** ## Lifecycle [Section titled “Lifecycle”](#lifecycle) ```plaintext 1. Connect to ws[s]:///ws?wsToken= 2. Send "subscribe" (sessionId + appId + wsToken, optional fromSeq / sinceSequence) 3. Receive "ack" (carries streamSeq + initial session) 4. Loop: receive "render" / "props_update" / "data" / "error" send "action" (generation-pipeline progress arrives as a {type:'data'} delivery on the reserved _ggui:lifecycle channel) 5. Close on session end or client disconnect ``` ### Reconnection [Section titled “Reconnection”](#reconnection) `@ggui-ai/react` reconnects automatically with exponential backoff (1s → 30s, capped at 10 attempts). To resume the outbound stream without gaps, track the last observed `StreamEnvelope.seq` and pass it as `fromSeq` on the next `subscribe`. The server replays buffered envelopes (where supported) before re-entering the live tail. ### Status (SDK) [Section titled “Status (SDK)”](#status-sdk) | Status | Meaning | | -------------- | ---------------------------------- | | `connecting` | Initial socket handshake in flight | | `connected` | Subscribed; `ack` received | | `disconnected` | Socket closed, no retry pending | | `reconnecting` | Backoff in progress after a drop | # The agent backend > How a ggui-aware agent is hosted — @ggui-ai/agent-server (a brand-agnostic Hono backend) plus a thin per-SDK AgentAdapter. The three channels, the user-action doorbell, and what "Zero Agent Code" means. A ggui-aware agent needs an HTTP backend the user’s chat client can talk to. You do **not** hand-roll that backend. The OSS reference implementation is **[`@ggui-ai/agent-server`](https://www.npmjs.com/package/@ggui-ai/agent-server)** — a brand-agnostic [Hono](https://hono.dev) server that owns every ggui-coupled host concern — plus a thin **`AgentAdapter`** that maps your LLM SDK’s event stream to a normalized message envelope. The split is the point: the agent-server has **zero LLM-SDK knowledge** and the adapter has **zero ggui awareness**. The protocol lives entirely between the two. ## Three parties [Section titled “Three parties”](#three-parties) ```plaintext ┌─────────────────────────┐ │ host / chat client │ MCP-Apps host: claude.ai, ChatGPT, │ (owns the chat UI, │ or the sample chat app. Forwards │ forwards ui/message) │ ui/message text to the model. └───────────┬─────────────┘ chat │ (HTTP POST /agent → SSE) ▼ ┌─────────────────────────┐ │ agent backend │ @ggui-ai/agent-server (Hono) │ agent-server + a thin │ + your AgentAdapter (per-SDK glue). │ AgentAdapter │ └───────────┬─────────────┘ MCP │ (Streamable HTTP) ▼ ┌─────────────────────────┐ │ ggui MCP server │ ggui_handshake / ggui_render / │ (GguiSessions + state │ ggui_update / ggui_consume / ggui_emit, │ + the iframe runtime) │ plus the iframe runtime served per session. └─────────────────────────┘ ``` The **iframe** (running [`@ggui-ai/iframe-runtime`](https://www.npmjs.com/package/@ggui-ai/iframe-runtime)) is the rendered surface that lives inside the host. It is not a fourth party — it is the ggui MCP server’s UI, mounted in the host. ## Three channels [Section titled “Three channels”](#three-channels) | Channel | Between | Transport | Status | Carries | | -------- | ------------------------------- | ------------------------ | --------- | ------------------------------------------------------------------------------------- | | **chat** | host ↔ agent backend | HTTP `POST /agent` → SSE | Mandatory | `{kind: 'chat', prompt, chatId?}` in; a stream of `NormalizedMessage`s out | | **MCP** | agent backend ↔ ggui MCP server | MCP Streamable HTTP | Mandatory | `ggui_handshake` / `ggui_render` / `ggui_update` / `ggui_consume` / `ggui_emit` calls | | **live** | ggui MCP server ↔ iframe | WebSocket (+ fallback) | Optional | declared `streamSpec` deliveries (`ggui_emit` fan-out) and `props_update` frames | **chat** is how a turn starts. `POST /agent` is one kind-discriminated endpoint: `{kind: 'chat', prompt, chatId?}` opens the SSE stream — the first event is always `chat-allocated`, carrying the server-allocated `chatId`; subsequent frames are `message` events. The same endpoint also accepts `{kind: 'tool-call', name, arguments}` — the iframe-issued `tools/call` relay, answered as plain JSON rather than SSE. `GET /agent?chatId=X` replays the server-authoritative snapshot through the same handler for rehydration (each recorded tool result is re-inlined fresh so the snapshot reflects current server state), and `GET /` serves a small public manifest the frontend reads `sandboxProxyUrl` from. **MCP** is the agent loop — the adapter’s LLM calls `ggui_render` / `ggui_consume` / etc. against the ggui MCP server using the URL + bearer the library threads on every call. **live** is the first-party fast path for streaming UI updates into the iframe over WebSocket (gated by a `wsToken`). The spec-compliant cross-host fallback is **tool-result inlining**: agent-server’s interceptor reads `_meta.ui.resourceUri` on each tool result, issues a `resources/read`, and inlines the iframe HTML under `_meta.ui.resource` — so the iframe mounts on the first SSE frame with no extra round-trip. ## What agent-server owns [Section titled “What agent-server owns”](#what-agent-server-owns) The library owns **every ggui-coupled host concern**, and nothing else: * HTTP routing (Hono) + SSE streaming * MCP discovery / routing + bearer threading (`bearer` defaults to `GGUI_MCP_BEARER`, then `dev` — pairing with `ggui serve --dev-allow-all`) * **Tool-result resource inlining** (`interceptToolResult`) — mounting iframes from `_meta.ui.resourceUri` * Server-allocated chat ids (`mintChatId`) — the frontend never mints ids client-side * Guest + bearer **auth** with chat-ownership gating * The second-origin **sandbox proxy** boot (per the MCP Apps spec; defaults to `port + 1000`) * The snapshot / rehydration path * **Cross-framework tool identity**: with `crossFramework` on (the `startAgentServer` default), the library declares each tool’s canonical `serverInfo` to ggui once per process via `ggui_runtime_declare_tool_catalog`, so blueprint reuse stays identity-stable across agent frameworks Crucially, it is a **pure prompt forwarder**: the prompt is fed to the adapter verbatim, and the server synthesizes no directive and special-cases no key. The user-gesture directive that tells the model to call `ggui_consume` is authored in the iframe’s `ui/message` text and passes straight through (see [the user-action flow](#the-user-action-flow) below). ## What the AgentAdapter implements [Section titled “What the AgentAdapter implements”](#what-the-agentadapter-implements) A thin per-SDK adapter implements **one** async-iterable method: ```ts import { startAgentServer, type AgentAdapter } from "@ggui-ai/agent-server"; const adapter: AgentAdapter = { name: "my-sdk", async *run(input) { // input.prompt — the string the LLM should see (verbatim) // input.chatId — server-allocated stable id for this conversation // input.mcpServers — { name → { url, bearer } } map (e.g. { ggui: {…} }) // input.systemPrompt — three-way: undefined = adapter default, // null = explicitly none, string = override // input.abortSignal — fires on client disconnect; stop the LLM call // input.agentCapabilities — canonical tool catalog (from live MCP // initialize + tools/list) to stamp into the // handshake's blueprintDraft contract // // Drive your SDK's tool loop and yield NormalizedMessage values: // assistant text · tool_use · tool_result (carrying the full MCP // CallToolResult as `tool_use_result`) · result }, }; await startAgentServer({ port: 6790, mcpServers: { ggui: { url: "http://localhost:6781/mcp" } }, // a `ggui` entry is required adapter, // optional: auth (default createGuestTokenAuth()), sandboxProxyPort // (default port + 1000), systemPrompt, bearer, chatStore, crossFramework }); ``` Adapters **must stay brand-agnostic**: no imports of `@ggui-ai/protocol/integrations/mcp-apps`, no awareness of `sessionId` / `host-session` / `_meta.ui` keys. The adapter maps its native SDK event stream onto the `NormalizedMessage` envelope; ggui mechanics stay in agent-server. Reference adapters ship for the Claude Agent SDK (`claude-agent-sdk`), the OpenAI Agents SDK (`openai-agents-sdk`), and Google ADK (`google-adk`). ## Frontend pairing — `@ggui-ai/react/chat-helpers` [Section titled “Frontend pairing — @ggui-ai/react/chat-helpers”](#frontend-pairing--ggui-aireactchat-helpers) On the browser side, agent-server pairs with the **`useMcpAppsChat`** hook from [`@ggui-ai/react/chat-helpers`](/sdk/react/). It: * opens the SSE stream to `POST /agent` and parses `chat-allocated` then `message` frames into one wire; * walks each tool result’s `tool_use_result` for `_meta.ui.resourceUri` (and any inlined `_meta.ui.resource`) and surfaces the result as `sessions` entries your app mounts with `` (imported directly from `@mcp-ui/client` — ggui doesn’t wrap or re-export it); * replays `GET /agent?chatId` through the same pipeline for rehydration; * runs the guest-token client flow (`POST /auth/guest` → store → `Bearer` on every request → retry once on `401`); * forwards an iframe `ui/message`’s text as the next prompt via `handleAppMessage` and carries its `_meta` **opaquely** as `data.meta` — it never reads a key inside. ## The user-action flow [Section titled “The user-action flow”](#the-user-action-flow) When a user interacts with a rendered UI, the gesture travels back to the agent through the **GguiSession’s pending-event pipe**, which is the single source of truth: 1. **Gesture in the iframe** → the iframe runtime calls `ggui_runtime_submit_action` via the host’s `tools/call` relay (postMessage per the MCP Apps spec; in the sample stack, the host relays it as `POST /agent {kind: 'tool-call'}`). 2. The ggui server **appends the gesture to the GguiSession’s pending-event pipe** and returns `{ok, consumerPresent}`. 3. `consumerPresent` is computed by an active-consumer registry: `ggui_consume` registers itself while long-polling, so `submit_action` knows whether a consume loop is currently listening. 4. **If a `ggui_consume` long-poll is listening**, it unblocks in-turn and returns the event `{intent, actionData, uiContext, actionId, firedAt}` to the agent. 5. **If nobody is listening** (`consumerPresent: false` — e.g. the user reloaded the page after the agent’s turn ended), the iframe emits a **userAction doorbell** on a `ui/message`. The host forwards the message text to the model, which wakes a fresh turn and calls `ggui_consume({sessionId})` to drain the already-enqueued gesture. The agent retrieves the gesture **exclusively** via `ggui_consume`, so it fires **exactly once**. The doorbell is a **pure pointer** — `_meta["ai.ggui/userAction"]` (`GguiUserActionMeta`) carries only `{kind: 'user-action', description, sessionId, actionId, submittedAt, intent, nextStep: {tool: 'ggui_consume', args: {sessionId}}}`, never the action payload. Carrying the payload in the doorbell would risk a double-trigger. ## ”Zero Agent Code”, redefined [Section titled “”Zero Agent Code”, redefined”](#zero-agent-code-redefined) [Zero Agent Code](/protocol/overview/) now means an agent builder writes only: 1. **MCP server config** naming the ggui MCP endpoint, 2. a **system prompt** (start from `GGUI_AGENT_SYSTEM_PROMPT`, exported by `@ggui-ai/protocol`), and 3. a few lines wiring a thin **`AgentAdapter`** into `startAgentServer()`. No polling loops, no event handlers, no protocol parsing, no `sessionId` / `host-session` awareness. All of that lives inside `@ggui-ai/agent-server`. The adapter is intentionally brand-agnostic SDK-mapping glue — not ggui logic. ## Auth posture (Preview) [Section titled “Auth posture (Preview)”](#auth-posture-preview) agent-server ships two `AuthAdapter` implementations: * **`createGuestTokenAuth` (default)** — stateless signed bearer tokens (signing secret from `GUEST_TOKEN_SECRET`, ephemeral with a warning if omitted) that work across browser / React Native / CLI. Mounts `POST /auth/guest`, `GET /auth/me`, `POST /auth/logout`. * **`createBearerTokenAuth`** — static operator-configured tokens for sample apps, CI, and small self-hosts. Mounts `GET /auth/me` only. Every chat row is stamped with an `ownerId`; reads and appends are ownership-gated (`200` owner / `403` other / `404` unknown), overridable via `authorizeChat` for team / org semantics. Richer JWT / JWKS / OAuth + PKCE flows are deferred to a future `@ggui-ai/agent-server-auth-extras` (same `AuthAdapter` contract, no handler rewrites). For the Preview, the bundled guest-token + static-bearer paths are the supported surface. ## See also [Section titled “See also”](#see-also) * [How ggui works](/how-it-works/) — the handshake → render → interact → consume loop * [Architecture overview](/architecture/overview/) — the wire pipeline at a glance * [Event System](/architecture/event-system/) — the pending-event pipe + the consume model * [React SDK](/sdk/react/) — `useMcpAppsChat` and `` on the frontend * [MCP Protocol reference](/api/mcp-protocol/) — `ggui_render` / `ggui_consume` / `ggui_update` / `ggui_emit` # Audience routes > Every MCP tool carries an audience tag that determines which route it surfaces on — /mcp, /protocol, or /ops. This page explains the four audiences and the routing rules. A single ggui server exposes several distinct MCP surfaces, not one. Each tool the server registers carries an **audience tag** that decides which HTTP route the tool appears on. The agent runtime sees one slice of the tool set; design-time clients see another; operators see a third. The audience tag is the structural mechanism that keeps those slices honest. This page explains the four audiences, the three routes they map to, and the placement rules for every new handler. ## Why audiences exist [Section titled “Why audiences exist”](#why-audiences-exist) Different callers connect to a ggui server for different reasons: * An **LLM agent** in the middle of a chat turn needs `ggui_render`, `ggui_handshake`, blueprint search, and not much else. * The **view runtime** — the iframe-runtime, relayed through the host’s `tools/call` — and first-party backend libraries (e.g. `@ggui-ai/agent-server`) need callbacks like `ggui_runtime_sync_context` and `ggui_runtime_submit_action`. * A **design-time client** authoring blueprints needs static spec/discovery tools like `ggui_protocol_describe_blueprint_format` once, then never again. * An **operator** managing apps, keys, and orgs needs administrative tools that should never appear in an LLM’s `tools/list`. Putting all of those tools on one `/mcp` surface burns agent context on tools the agent will never call, and exposes operator surfaces to runtimes that shouldn’t see them. The audience tag splits the surface so each caller’s `tools/list` is exactly the tools that caller cares about. ## The four audiences [Section titled “The four audiences”](#the-four-audiences) | Audience | Surfaces on | Wire-name prefix | Who calls | | ---------- | ----------- | ----------------- | ----------------------------------------------------------------------------------------------------- | | `agent` | `/mcp` | `ggui_*` | The LLM agent itself during a chat turn (render, handshake, blueprint search) | | `runtime` | `/mcp` | `ggui_runtime_*` | The view runtime — iframe-runtime (via the host’s `tools/call` relay) + first-party backend libraries | | `protocol` | `/protocol` | `ggui_protocol_*` | Design-time spec/discovery clients (conformance suites, registry browsers) | | `ops` | `/ops` | `ggui_ops_*` | Operator agents — an LLM acting as a console operator, dashboards, CI | Each tag answers exactly one question: **who is calling this tool, and on what time-scale?** ### `agent` [Section titled “agent”](#agent) Surfaced on `/mcp`. The LLM agent calls these tools live, inside a chat turn. They typically mutate render state, render contracts, or look up blueprints by intent. Wire-name prefix: bare `ggui_*` (e.g. `ggui_render`, `ggui_handshake`, `ggui_update`). The bare prefix is reserved for the canonical agent route — these are the tools an agent calls most often, and they don’t need a route disambiguator. ### `runtime` [Section titled “runtime”](#runtime) Surfaced on `/mcp` alongside `agent` tools. Called by the view runtime — the iframe-runtime, relayed through the host’s MCP-Apps `tools/call` — and by first-party backend libraries (e.g. `@ggui-ai/agent-server` declaring the per-app tool catalog via `ggui_runtime_declare_tool_catalog`) — not by the LLM directly. They handle things like syncing renderer state back to the server or submitting user actions. Wire-name prefix: `ggui_runtime_*` (e.g. `ggui_runtime_sync_context`, `ggui_runtime_submit_action`). ### `protocol` [Section titled “protocol”](#protocol) Surfaced on `/protocol`. Static design-time tools that describe the protocol itself — example blueprints, format references, schema validators. A client calls these **once** while authoring against the protocol, not during runtime. Wire-name prefix: `ggui_protocol_*` (e.g. `ggui_protocol_describe_blueprint_format`, `ggui_protocol_validate_blueprint`, `ggui_protocol_get_example_blueprints`). The litmus test: would the result change if the same caller invoked the tool again five minutes later? If no — the tool returns a static format reference or immutable example set — it belongs on `/protocol`. If yes, it’s a runtime lookup and belongs on `/mcp`. ### `ops` [Section titled “ops”](#ops) Surfaced on `/ops`. Operator-facing tools an LLM operator (or dashboard, or CI script) uses to manage apps, register blueprints, issue connector keys, redeem coupons, list orgs. Never visible to the agent runtime, never invoked from inside a rendered UI. Wire-name prefix: `ggui_ops_*` (e.g. `ggui_ops_create_app`, `ggui_ops_list_orgs`, `ggui_ops_issue_connector_key`). ## Routes table [Section titled “Routes table”](#routes-table) The four audiences map onto three HTTP routes: | Route | Audiences mounted | Typical caller | Auth posture | | ----------- | ------------------- | --------------------------------------- | ---------------------------- | | `/mcp` | `agent` ∪ `runtime` | LLM agent + view runtime | Bearer token or session auth | | `/protocol` | `protocol` | Conformance suites, design-time clients | Same auth chain as `/mcp` | | `/ops` | `ops` | Operator agents, dashboards, CI | Same auth chain as `/mcp` | The mounting logic reads each handler’s `audience` array and includes it on every route whose audience set intersects the handler’s tags. A handler tagged `audience: ['agent']` lands on `/mcp` only. A handler tagged `audience: ['agent', 'runtime']` also lands on `/mcp` (the union doesn’t change membership). A handler tagged `audience: ['ops']` lands on `/ops` only. When per-app routing is configured, the same `agent` ∪ `runtime` surface also mounts at a per-app path (`/apps/` on hosted deployments); the audience model is identical — only the tenancy resolution differs. Caution A handler with **no** audience tag is mounted on `/mcp` by default. This is a backward-compatibility behavior — handlers added before audience tagging existed default to `agent`. New handlers should always declare an explicit `audience` array. ## Placement decision tree [Section titled “Placement decision tree”](#placement-decision-tree) When you add a new MCP handler, walk this tree before picking an audience: 1. **Does the LLM agent invoke this during a chat turn?** Yes → `audience: ['agent']`. 2. **Is this a tool declared with `_meta.ui.visibility: ['app']`** that the rendered view invokes through the host’s MCP-Apps `tools/call` relay? Yes → `audience: ['runtime']`. 3. **Is this a static spec or discovery tool a client reads once while authoring against the protocol?** Yes → `audience: ['protocol']`. 4. **Is this an administrative operation a human, dashboard, CI, or operator agent performs out-of-band?** Yes → `audience: ['ops']`. The placement test is exclusive — if more than one branch fires, pick the **most-frequent caller** and tag that audience. Multi-audience tags are rare; see below. ### The “is this a runtime lookup?” trap [Section titled “The “is this a runtime lookup?” trap”](#the-is-this-a-runtime-lookup-trap) Tools like `ggui_search_blueprints` *sound* like spec/discovery — they discover blueprints. But their results change per-app and per-session: the agent calls them at chat time to decide what to build, not to learn the protocol’s format. They are runtime lookups, tagged `agent`, surfaced on `/mcp`. The litmus test repeats: **would the result change if the same caller invoked this tool again five minutes later?** * Yes → runtime lookup → `agent` (or `runtime` if called from the iframe). * No → static spec/discovery → `protocol`. ## Wire-name prefix discipline [Section titled “Wire-name prefix discipline”](#wire-name-prefix-discipline) The prefix encodes the audience at the wire-name level so a tool reader can infer the route without consulting documentation. An LLM scanning `tools/list` on `/protocol` sees `ggui_protocol_describe_blueprint_format` and immediately understands the routing. | Prefix | Implied route | Implied audience | | ----------------- | ------------- | ---------------- | | `ggui_*` (bare) | `/mcp` | `agent` | | `ggui_runtime_*` | `/mcp` | `runtime` | | `ggui_protocol_*` | `/protocol` | `protocol` | | `ggui_ops_*` | `/ops` | `ops` | Bare `ggui_*` is the exception, not the rule. It is reserved for agent runtime essentials — the canonical chat-turn tools that don’t need a route disambiguator. Every other audience requires the explicit prefix. Caution The prefix and the `audience` tag must agree. A handler named `ggui_protocol_foo` with `audience: ['ops']` is a discipline violation — the prefix promises `/protocol`, the tag mounts it on `/ops`, and any agent scanning `/protocol` for `ggui_protocol_*` tools will miss it. Keep them in sync. ## How a handler declares its audience [Section titled “How a handler declares its audience”](#how-a-handler-declares-its-audience) The `audience` field lives on every `SharedHandler`. It is a `ReadonlyArray<'agent' | 'runtime' | 'protocol' | 'ops'>` — an array because multi-audience tagging is structurally permitted (see below). ```ts import type { SharedHandler } from '@ggui-ai/mcp-server-handlers'; export function createListOrgsHandler(): SharedHandler<…> { return { name: 'ggui_ops_list_orgs', title: 'List organizations', audience: ['ops'], description: 'Enumerate orgs visible to the calling operator.', inputSchema: { /* … */ }, outputSchema: { /* … */ }, async handler(input, ctx) { /* … */ }, }; } ``` That’s the entire contract. Once a handler is registered through the normal channel (the `handlers` array passed to `createGguiServer`), the route mounter reads the `audience` field at compose time and decides which routes the handler appears on. There is no separate route-registration step. ## Multi-audience handlers [Section titled “Multi-audience handlers”](#multi-audience-handlers) The `audience` field is an array, not a scalar, because a handler can legally surface on more than one route. In practice this is rare and intentional: ```ts export function createSomeBoundaryToolHandler(): SharedHandler<…> { return { name: 'ggui_some_boundary_tool', audience: ['agent', 'runtime'], // … }; } ``` Such a handler appears in both the agent’s `tools/list` and the iframe runtime’s view. The canonical example is a tool that has to land on the agent’s wire AND accept calls from inside the rendered UI — the runtime essentials that genuinely span both callers. Multi-audience is a tool that lives on multiple routes simultaneously. If you find yourself reaching for it, double-check the placement test first — most “multi-audience” tools are actually two tools wearing a trench coat, and splitting them sharpens both surfaces. ## Relation to MCP services [Section titled “Relation to MCP services”](#relation-to-mcp-services) The audience model governs **shared** routes — `/mcp`, `/protocol`, `/ops` — where handlers from different sources are aggregated under audience filtering. A separate concept, **MCP services**, lets a server expose isolated, complete MCP servers at their own HTTP paths (e.g. `https://your-server/docs`, `https://your-server/playground/todos`). | Concept | Routing mechanism | Tool isolation | When to use | | ------------ | ---------------------------------- | --------------------- | ------------------------------------------- | | **Audience** | Tag filters tool onto shared route | Tools share namespace | Tool belongs alongside ggui-native tools | | **Service** | Path mounts an isolated MCP server | Per-path namespace | Tool set is conceptually its own MCP server | A service handler **must not** set an `audience` tag — the path IS the audience. The compose-time validator rejects services with audience-tagged handlers loudly: services bypass audience filtering entirely, so a tag would be silently meaningless. → See [MCP services](/architecture/mcp-services/) for the full service model. ## Placement anti-patterns [Section titled “Placement anti-patterns”](#placement-anti-patterns) Audience tagging makes it cheap to add new tools, which means the *placement* discipline matters more than ever. Some patterns to avoid: * **`ops`-audience tools that mutate non-tenant data.** The `/ops` route is operator-bounded but still scoped to the calling operator’s tenant. A tool that lets one operator probe another tenant’s apps is a confused-deputy bug, not a feature. * **Cross-tenant probing.** `ggui_ops_list_orgs` should enumerate orgs the caller can see, not all orgs. If a tool needs admin-level visibility, gate it on an explicit role check at the handler boundary, not at the route boundary. * **`protocol`-audience tools that return runtime data.** If a result depends on which session is calling, it is a runtime lookup. Move it to `agent`. * **`runtime`-audience tools that the agent should also call.** If an LLM agent needs the data, tag it `agent` (or `['agent', 'runtime']` if the iframe legitimately calls it too). Tagging it `runtime`-only hides it from the agent’s `tools/list`. * **Untagged handlers.** The default-to-`agent` behavior is a backward-compatibility convenience, not a recommendation. Always declare `audience` explicitly on new handlers. ## See also [Section titled “See also”](#see-also) * [MCP services](/architecture/mcp-services/) — isolated per-path MCP servers * [MCP protocol](/api/mcp-protocol/) — the `/mcp` surface in detail * [Ops MCP](/api/ops-mcp/) — the `/ops` surface in detail * [Architecture overview](/architecture/overview/) — three channels and the capability model # Benchmark methodology > How benchmarks.ggui.ai measures UI-generation quality, latency, and cost — the model matrix, the five aesthetic dimensions, the three-provider judge panel, the corpus, and how to reproduce a run locally. [benchmarks.ggui.ai](https://benchmarks.ggui.ai) is the public dashboard for ggui’s generation quality. It runs nightly and publishes per-cell **quality**, **latency**, and **cost** across a three-tier model matrix. This page is the methodology behind those numbers — what is measured, how it is scored, and how to reproduce a run yourself. ## What it measures [Section titled “What it measures”](#what-it-measures) Every night the harness generates UI for a fixed corpus of prompts across a **three-tier model matrix** — `fast`, `balanced`, and `premium` capability tiers, each instantiated on the three providers ggui supports (`claude`, `openai`, `google`). Every matrix cell records three things: * **Quality** — the aesthetic score (below), 0–100. * **Latency** — wall-clock time to a compiled, contract-typed component. * **Cost** — provider spend for the generation, in USD. The dashboard publishes **per-cell** results. It is not a provider leaderboard — see [Judge panel](#judge-panel). ## Quality scoring [Section titled “Quality scoring”](#quality-scoring) Quality is the mean of **five aesthetic dimensions**, each weighted equally at 20% and scored 0–100: | Dimension | What it captures | | ----------------- | ------------------------------------------------------- | | Layout | Spacing, alignment, structure, responsive behavior | | Design tokens | Correct use of `@ggui-ai/design` tokens over ad-hoc CSS | | Hierarchy | Visual weighting — what reads first, second, third | | Polish | States, affordances, finish; the absence of rough edges | | Data presentation | How clearly the contract’s data is rendered | The **pass threshold is 70**. A cell at or above 70 is a pass; below is a fail. The five-dimension breakdown is published alongside the composite so a regression can be traced to the dimension that moved. ## Judge panel [Section titled “Judge panel”](#judge-panel) Quality is **not** scored by a single model. Each generation is judged by a **three-provider panel** — `claude`, `openai`, and `google` — all run at **temperature 0** for determinism. The published score is the **panel mean**, and the **per-cell spread** across the three judges is shown alongside it. The panel exists to neutralize single-model bias: **no model judges only its own output**, and a generous-to-self or harsh-to-rivals bias from any one judge is diluted by the other two. The visible spread is the honesty check — a wide spread on a cell is a signal that the judges disagree, not a number to trust blindly. This is why the dashboard publishes **per-cell scores, not a provider ranking**. The unit of truth is “this model, this tier, on this prompt” — rolling that up into a single “best provider” headline would discard exactly the per-cell, per-dimension detail the panel is designed to preserve. ## Corpus [Section titled “Corpus”](#corpus) The harness runs against a **fixed set of generation prompts** — representative UI shapes that exercise the contract surface: `weather-card`, `survey-form`, `kanban-board`, and others, **plus gadget commits** (renderer-side capability flows). The corpus is fixed so that night-over-night movement reflects model and triad changes, not a shifting set of prompts. ## Reproducibility [Section titled “Reproducibility”](#reproducibility) The benchmark is **source-available** — the entire harness ships in the public repo. To run it yourself: ```bash git clone https://github.com/ggui-ai/ggui cd ggui pnpm install pnpm --filter @ggui-ai/benchmark bench … ``` You need a provider API key (set the relevant provider environment variable; the harness reads it the same way the live dashboard does). The benchmark **dataset is licensed CC-BY-4.0** — reuse it, cite it, build on it. ## See also [Section titled “See also”](#see-also) * [UI Generator](/architecture/ui-generator/) — the harness the benchmark exercises. * [benchmarks.ggui.ai](https://benchmarks.ggui.ai) — the live dashboard. # Event System > How user gestures travel from the renderer back to the agent — EventType vocabulary, ActionEnvelope shape, subscription rules, and the two consumer paths. User actions originate in the renderer (on the live channel), land in the server, and reach the agent through one of two read paths. This page covers the closed vocabulary of events, the canonical envelope, how subscriptions gate what gets delivered, and how an agent or React SDK consumer actually reads them. For the full wire grammar of the envelope itself, see [Envelopes](/protocol/envelopes/). For the channel topology, see the [Architecture overview](/architecture/overview/). ## Event vocabulary [Section titled “Event vocabulary”](#event-vocabulary) The protocol recognizes exactly **one** event type. Every user gesture that drives a turn is a `data:submit`. The earlier `data:change` / `lifecycle:*` / `interaction:*` / `error:*` vocabulary was deleted (draft-2026-06-12) — those types never had a producer. ```typescript type EventType = "data:submit"; // the only member ``` A `data:submit` is schema-validated against the render’s `actionSpec`. ## The envelope [Section titled “The envelope”](#the-envelope) Every user gesture arrives as a flat `ActionEnvelope`: ```typescript interface ActionEnvelope { sessionId: string; // bound at subscribe time; server rejects mismatches type: EventType; payload?: TPayload; // for `data:submit`: { action, data?, tool? } clientSeq?: number; // client-monotonic dedup hint schemaVersion?: string; // producer's PROTOCOL_SCHEMA_VERSION (advisory) } ``` The envelope is intentionally flat — no nested `event` / `context` / `meta` blocks. Render-level diagnostics (device info, interface context, user identity) are captured **once** at subscribe time on the server, not per-delivery. See [Envelopes — “Fields intentionally NOT on the envelope”](/protocol/envelopes/#fields-intentionally-not-on-the-envelope) for the rationale. ## Subscription gating [Section titled “Subscription gating”](#subscription-gating) Delivery gating falls out of the contract’s `actionSpec`: every declared action emits a `data:submit` envelope the agent reads via `ggui_consume`. The old per-event `EventSubscription` filter (an allowlist of event types at render time) and the `DEFAULT_SUBSCRIPTION` constant were deleted from the protocol (no shims) — there is no wire-level subscribe object on render, and `data:submit` is now the only event type. The current `ggui_render` input is `{handshakeId, props, themeId?, infra?, override?}` — `props` is required (pass `{}` when the contract declares no propsSpec), and there is no `subscribe` field. ## Two reader paths, one envelope [Section titled “Two reader paths, one envelope”](#two-reader-paths-one-envelope) The same `ActionEnvelope` reaches consumers through two distinct seams. **Don’t conflate them.** | Consumer | Path | Shape returned | | -------------------------------------- | ----------------------------------------- | ------------------- | | **Agent** (server-side, LLM-driven) | Long-polls `ggui_consume` over MCP | `ConsumeEventEntry` | | **Renderer SDK** (browser, e.g. React) | Live tail on the WebSocket subscribe seam | `ActionEnvelope` | `ggui_consume` is the agent’s read path. It’s render-keyed, consume-once, and the row shape (`ConsumeEventEntry`) carries a tiny bit of extra context the LLM needs to route the gesture. The WebSocket subscribe seam is what the iframe-runtime uses to deliver interaction events into the rendered component. See [MCP Protocol — Events](/api/mcp-protocol/#events) for both shapes. ### Host relay + the `ai.ggui/userAction` doorbell [Section titled “Host relay + the ai.ggui/userAction doorbell”](#host-relay--the-aigguiuseraction-doorbell) In MCP-Apps hosts the gesture reaches the server via the host’s `ggui_runtime_submit_action` `tools/call` relay instead of the WS. If the response reports `consumerPresent: false` (no `ggui_consume` long-poll is draining — e.g. after a page reload), the iframe emits a `ui/message` whose text directs the agent to call `ggui_consume({sessionId})`, with an optional structured mirror on `content[0]._meta["ai.ggui/userAction"]` — a pure pointer; the gesture itself is only ever drained via `ggui_consume`. ## The MCP control surface [Section titled “The MCP control surface”](#the-mcp-control-surface) The agent never talks to the live channel directly — it renders UI, polls events, discovers gadgets, and browses the blueprint marketplace over MCP. The canonical agent-callable tool surface (lifecycle, capability discovery, stream emit, and blueprint marketplace) is enumerated in the [MCP Protocol](/api/mcp-protocol/) reference — that page is the single source of truth, with field-level shapes and return types. Linking out instead of duplicating here keeps this page from drifting as the tool surface evolves. The `ggui_consume` long-poll in particular is the agent’s event read path — its return shape (`ConsumeEventEntry`) is covered in [MCP Protocol — Events](/api/mcp-protocol/#events). ## Outbound traffic on the same channel [Section titled “Outbound traffic on the same channel”](#outbound-traffic-on-the-same-channel) `ActionEnvelope` is the inbound half of the live channel. Server-to-renderer traffic on the same WebSocket arrives as `StreamEnvelope` (one delivery per named `streamSpec` channel). Contract violations are not a separate channel: an invalid inbound action is answered with a typed `error` frame (`CONTRACT_VIOLATION`) on the live channel, and a `ggui_render` / `ggui_emit` validation failure rejects the agent’s own tool call. The former `_ggui:contract-error` channel and its `ContractErrorPayload` vocabulary were removed (draft-2026-06-11). Both surviving shapes are covered in [Envelopes](/protocol/envelopes/). ## Generation progress events [Section titled “Generation progress events”](#generation-progress-events) While the server is generating a fresh UI (no blueprint hit), it emits progress on the reserved `_ggui:lifecycle` channel (a `{type:'data'}` frame) that the renderer surfaces as a loading state. The canonical `GguiLifecyclePayload` vocabulary lives in `packages/protocol/src/types/lifecycle.ts`: ```plaintext handshake_started → handshake_completed → render_started → consume_polling ``` These feed the built-in progress UI inside the rendered iframe (the iframe-runtime) — your end-user sees real-time feedback instead of a frozen iframe. ## React SDK integration [Section titled “React SDK integration”](#react-sdk-integration) In the web consumer path the live-channel WebSocket — and the progress events above — are owned by the **iframe-runtime inside the sandboxed ``**, not by host code. The host doesn’t open the socket, set a `wsEndpoint`, or handle `ActionEnvelope`s directly: the `wsUrl` is server-stamped on the render’s `ai.ggui/render` slice, the iframe connects + subscribes + resumes on its own, and the progress UI animates inside the iframe. ```tsx import { AppRenderer } from "@mcp-ui/client"; import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers"; // Mount the render; the iframe-runtime drives the live channel + progress UI. const { sessions, handleAppMessage } = useMcpAppsChat({ chatEndpoint }); // ``` Structured signals the host may want (dispatch telemetry, subscribe failures, version mismatches, auth-required) surface on the `ggui:observe` postMessage channel. See [React SDK](/sdk/react/) for the host surface and [Error Handling → renderer-side faults](/cookbook/error-handling/#renderer-side-faults-stay-inside-the-iframe) for the observability events. ## Where to next [Section titled “Where to next”](#where-to-next) * [Envelopes](/protocol/envelopes/) — canonical live-channel wire grammar * [MCP Protocol](/api/mcp-protocol/) — agent-side control plane * [WebSocket Protocol](/api/websocket-protocol/) — renderer-side live channel * [Architecture overview](/architecture/overview/) — three-channel topology # MCP services > Multi-mount MCP servers — McpService primitive, path reservations, anonymous mode, and how it differs from McpServerMount. A ggui server is not a single MCP server. One Node process composes: * **The audience-filtered routes** (`/mcp`, `/protocol`, `/ops`) — ggui’s native control plane, optionally extended by `McpServerMount`s that aggregate external tools onto the same surface. * **Zero or more `McpService`s** — fully isolated MCP servers, each mounted at its own HTTP path with its own tool namespace. This page covers the second half. For the audience model that drives the shared routes, see [Audience routes](/architecture/audience-routes/). ## What an `McpService` is [Section titled “What an McpService is”](#what-an-mcpservice-is) An `McpService` is a complete, self-contained MCP server reachable at a single HTTP path you pick (`/docs`, `/playground/todos`, `/internal/billing`, …). A client connecting to that path sees exactly the tools the service declares — no ggui-native tools, no other service’s tools, no audience filtering. The path **is** the namespace boundary. Contrast with `McpServerMount`. A mount is a named bundle of `SharedHandler`s aggregated onto the audience-filtered routes. Mounted tools compose alongside ggui’s native tools (`ggui_render`, `ggui_handshake`, `ggui_consume`, …) so a single MCP session over `/mcp` enumerates the union of everything. Services do not compose; they isolate. ```plaintext ┌─────────────────────────────────┐ Agent (LLM-driven) ─────▶│ /mcp │ │ ggui-native tools │ │ + every mount's tools │ audience-filtered │ (audience=agent|runtime) │ └─────────────────────────────────┘ ┌─────────────────────────────────┐ Operator console ──────▶│ /ops │ audience-filtered │ ggui_ops_* │ (audience=ops) └─────────────────────────────────┘ ┌─────────────────────────────────┐ Docs client ──────▶│ /docs │ │ docs_search, docs_read, │ isolated service │ docs_list │ (own namespace) └─────────────────────────────────┘ ┌─────────────────────────────────┐ Playground user ──────▶│ /playground/todos │ │ todos_list, todos_add, │ isolated service │ todos_toggle, todos_delete │ (own namespace) └─────────────────────────────────┘ ``` ## When to use which [Section titled “When to use which”](#when-to-use-which) | You want | Reach for | | -------------------------------------------------------------------------------------------- | ---------------------------------- | | Tools that should appear alongside `ggui_render` / `ggui_consume` in a generative-UI session | `McpServerMount` | | Tools the LLM-driven agent should call as part of the same MCP connection that drives ggui | `McpServerMount` | | A complete, standalone MCP server at its own URL (`mcp.example.com/docs`, `…/billing`, …) | `McpService` | | First-touch public surface for unauthenticated clients (docs lookup, marketing demos) | `McpService` (+ `anonymous: true`) | | Conceptually distinct tool surfaces that should NOT collide on names with each other | `McpService` (one per surface) | Rubric: * **Composes with ggui’s core tools?** Mount. * **Replaces ggui’s core tools at its own URL?** Service. If you find yourself disabling ggui-native tools on a mount because they don’t make sense for the caller, you wanted a service. If you find yourself proxying ggui’s `tools/list` output through a service to bolt on extra tools, you wanted a mount. ## The `McpService` shape [Section titled “The McpService shape”](#the-mcpservice-shape) ```ts import type { SharedHandler } from "@ggui-ai/mcp-server-handlers"; import type { ZodRawShape } from "zod"; interface McpService { /** * Human-readable service identifier. Surfaced in validation * errors and telemetry. No uniqueness constraint across services * — only `path` must be unique. */ readonly name: string; /** * HTTP path the service mounts at (e.g. `/docs`, * `/playground/todos`). Validated at compose time. */ readonly path: string; /** * Tool handler bundle. Same `SharedHandler` shape ggui-native * handlers and mount handlers use. */ readonly handlers: ReadonlyArray>; /** * Auth-optional: a valid bearer still resolves to the real * identity; missing/invalid credentials fall back to a * synthesized anonymous builder. Default `false` — same auth * posture as `/mcp`. */ readonly anonymous?: boolean; } ``` Field-by-field: * **`name`** — diagnostic-only. Surfaces in `validateMcpServices` error messages and composition telemetry so an operator with several services can tell which one is misconfigured. Does NOT appear on the wire; tool names stay whatever each handler declares. * **`path`** — the HTTP path Express mounts the service handler at. Branded `ServicePath` after passing `validateServicePath`. Must be unique across the `mcpServices` array. * **`handlers`** — `SharedHandler[]`, the same canonical shape every ggui-native handler satisfies. The server registers each handler through `buildMcpServer`’s regular path; validation, logging, and output-schema parsing all happen uniformly. * **`anonymous`** — makes auth optional. Default `false`. See [Anonymous mode](#anonymous-mode) below. ## Path reservations [Section titled “Path reservations”](#path-reservations) Ten paths are reserved at validation time. Declaring a service at any of them throws at server-construction: | Path | Why reserved | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `/` | Root — must not be swallowed by a service router. | | `/mcp` | The agent-facing audience-filtered MCP route. | | `/protocol` | The protocol-discovery audience-filtered route. | | `/ops` | The operator-facing audience-filtered route. | | `/ws` | The live-channel WebSocket upgrade path. | | `/health` | Reserved for health probing — the live endpoint is `/ggui/health`; `/health` is held back so a service can never shadow a future short alias. | | `/.well-known` | RFC-reserved discovery prefix (OAuth metadata, security.txt, …). | | `/oauth` | OAuth dance endpoints (authorize / token / register). | | `/_ggui` | Internal ggui control surfaces (admin console, pairing, debug routes). | | `/ggui` | Same — public-facing `/ggui/*` endpoints. | A typo in a host config (`path: '/mpc'` instead of `/mcp`) can no longer silently shadow OAuth discovery, health, or per-app traffic. Caution Reservations apply to **exact equality**, not to prefixes you build on top. A service at `/.well-known/example` is rejected because of the `/.well-known` reservation, but a service at `/wellknown-example` is fine. Pick a distinct first segment. ## Path validation rules [Section titled “Path validation rules”](#path-validation-rules) `validateServicePath` enforces: 1. **Regex** `^/[a-zA-Z0-9_/-]+$` — must start with `/` and contain only letters, digits, `-`, `_`, and `/`. No whitespace, no `.`, no path traversal. 2. **No trailing slash** — `/docs/` is rejected; use `/docs`. Prevents trailing-slash variant collisions where `/docs` and `/docs/` would resolve to different routes. 3. **Non-empty after the leading slash** — at least one character after `/`. 4. **Not a reserved path** — see the table above. Throwing happens at server construction, before any request is served — misconfiguration cannot become a runtime mystery. ## Compose-time invariants [Section titled “Compose-time invariants”](#compose-time-invariants) `validateMcpServices(services)` walks the whole array and enforces: * **Non-empty `name`** on every entry. The name appears in every other error message; empty names defeat the diagnostic. * **`path` passes `validateServicePath`** — all the rules above. * **Service paths are unique** across the `mcpServices` array. Two services cannot mount at the same path. * **Every handler declares a non-empty `outputSchema`.** An empty `ZodRawShape` (`{}`) silently strips `structuredContent` at the MCP SDK boundary — the handler can return `{ items: [...] }` and the wire answer is `{}`. Operators hitting this see success-looking responses with missing data and no diagnostic. Rejected at compose time so the failure arrives with the service + tool names attached. * **No `audience` tag on service handlers.** Services bypass audience filtering entirely — the path IS the audience. An explicit `audience: ['ops']` on a service handler is silently meaningless. Rejected loudly. * **Tool names are unique within a service.** Two handlers declaring the same `name` inside one service collide; one would shadow the other at registration. Rejected. * **Cross-service tool-name collisions ARE allowed.** Services are isolated namespaces. A client connects to one path and only ever sees that path’s tools, so `docs_search` on `/docs` and `docs_search` on `/internal/docs` coexist without ambiguity. This is by design — collapsing into a global tool namespace would defeat the isolation that makes services worth having. ## Anonymous mode [Section titled “Anonymous mode”](#anonymous-mode) `anonymous: true` makes auth **optional**, not skipped. The server always attempts to resolve a presented bearer — a valid credential resolves to the real identity (letting one service mix public reads with authenticated capabilities); only a missing or invalid credential makes the binding layer fall back to the synthesized: ```ts { identity: { kind: 'builder' }, source: 'anonymous', } ``` Two things to note about the shape: * **`identity.kind` does NOT widen.** The `Identity` union still has its three variants (`'builder'` / `'user'` / `'app'`). Anonymous traffic collapses to `'builder'` so handlers that pattern-match on `kind` keep working without an extra arm. Adding a `'anonymous'` kind would force every consumer in the codebase to add a defensive case — the synthesized `'builder'` is the lighter touch. * **The signal lives on `source`.** `AuthResult.source` carries a dedicated `'anonymous'` variant. Handlers that need to distinguish “this request was authenticated” from “this request was let through anonymously” read `source` directly — pattern-matching on `identity.kind` alone cannot answer the question. ### When to use it [Section titled “When to use it”](#when-to-use-it) * **Read-only public surfaces.** Documentation lookup, public catalog browse, marketing demos. * **First-touch onboarding flows** where requiring a bearer token would block the use case (a brand-new visitor to `mcp.example.com/docs` has nothing to present). * **Server-to-server probes** that verify protocol shape without claiming an identity. ### When NOT to use it [Section titled “When NOT to use it”](#when-not-to-use-it) * **Any write path.** An anonymous caller has no accountability surface; you cannot audit who created the row. * **Anything tenant-scoped.** `ctx.appId` for an anonymous request collapses to the single builder. Reads will return data the caller has no business seeing, or writes will land in a tenant they don’t own. * **Anything sensitive enough that you’d want rate limits per-caller.** Anonymous mode pairs with per-IP / per-session rate limits (the `RateLimiter` seam composes the same way for authenticated and anonymous paths), but it cannot give you per-user attribution. ## Wiring it up [Section titled “Wiring it up”](#wiring-it-up) Pass the service array to `createGguiServer`: ```ts import { createGguiServer } from "@ggui-ai/mcp-server"; // monorepo-internal first-party services (not published): import { createDocsHandlers, loadDocsCorpus } from "@ggui-private/mcp-docs"; import { createPlaygroundTodosHandlers, createInMemoryTodoStore, } from "@ggui-private/mcp-playground-todos"; const corpus = await loadDocsCorpus("./docs"); const todoStore = createInMemoryTodoStore(); const server = await createGguiServer({ // ... ggui-native options (renderChannel, blueprintStore, …) mcpServices: [ { name: "docs", path: "/docs", handlers: createDocsHandlers({ corpus }), anonymous: true, // read-only doc lookup; no token required }, { name: "playground-todos", path: "/playground/todos", handlers: createPlaygroundTodosHandlers({ store: todoStore }), // no `anonymous: true` — handlers throw when ctx.userId is missing }, ], }); await server.listen(6781); ``` After `listen()`: * `POST /mcp` — ggui-native + every mount’s tools (audience-filtered). * `POST /docs` — `docs_search`, `docs_read`, `docs_list`. No auth required. * `POST /playground/todos` — `todos_list`, `todos_add`, `todos_toggle`, `todos_delete`. Auth-gated (handlers reject anonymous callers). The audience-filtered routes and the service routes coexist on the same Node process, the same Express app, the same WebSocket binding. There is no second server to run. ## Examples in the wild [Section titled “Examples in the wild”](#examples-in-the-wild) Three first-party services are built on this primitive for the hosted deployment (coming soon): * **`/docs`** — `@ggui-private/mcp-docs`. Three read-only tools (`docs_search`, `docs_read`, `docs_list`) over the ggui documentation corpus. Anonymous-mode; the canonical “public surface” example. See [MCP Docs Service](/api/mcp-docs/). * **`/playground/todos`** — `@ggui-private/mcp-playground-todos`. Four tools (`todos_list`, `todos_add`, `todos_toggle`, `todos_delete`) for the landing-page playground. Auth-gated per-user state (handlers reject anonymous callers). See [Playground · todos](/clients/playground-todos/). * **`/playground/mdh`** — `@ggui-private/mcp-playground-mdh`. Million-Dollar Homepage playground service. See [Playground · MDH](/clients/playground-mdh/). The hosted deployment’s unified `/dev` endpoint (coming soon) is the fullest example: one anonymous service co-hosting public docs + protocol tools with ops tools that re-impose auth per-tool — possible precisely because `anonymous` is auth-optional, so a presented connector key still resolves to the real identity. For the operator-facing audience-filtered route alongside these services, see [Ops MCP](/api/ops-mcp/). For running an analogous service stack under your own hostname, see [`ggui serve`](/cli/serve/). ## Where to next [Section titled “Where to next”](#where-to-next) * [Audience routes](/architecture/audience-routes/) — the audience model behind `/mcp` / `/protocol` / `/ops` * [Architecture overview](/architecture/overview/) — the three-channel topology services live inside * [Event System](/architecture/event-system/) — live-channel traffic is shared by every service on the same host * [MCP Docs Service](/api/mcp-docs/) — the canonical anonymous-mode example * [Ops MCP](/api/ops-mcp/) — the operator-facing audience-filtered route * [Playground · todos](/clients/playground-todos/) and [Playground · MDH](/clients/playground-mdh/) — authenticated-service examples * [`ggui serve`](/cli/serve/) — self-host an analogous service stack # Architecture > How ggui is wired — three channels, a symmetric capability model, a generation pipeline, and a four-tier artifact registry. Protocol-level; implementation-agnostic. This page is the **protocol’s architecture** — what every ggui implementation must do, independent of how it’s deployed. For deployment shapes, see [Self-Hosted](/self-hosted/pairing/) and [Reference deploys](/self-hosted/reference-deploys/). ## Three actors [Section titled “Three actors”](#three-actors) ```plaintext Agent (LLM-driven) │ │ MCP ▼ Server ◀───── WebSocket (live) ──────▶ Renderer (iframe / standalone page) │ │ │ bootstrap (bundle fetch) │ └────────────────────────────────────────────┘ ``` * **Agent** — your code with an LLM in the loop. Speaks to the server over MCP. * **Server** — speaks MCP outward, orchestrates generation, routes events. Hosts the artifact registry the renderer pulls from. Runs at your URL via `ggui serve` (hosted `mcp.ggui.ai` coming soon). * **Renderer** — an iframe (or standalone page) hosting the generated component. Sends user actions back to the server through the host’s `tools/call` relay. ## Three channels [Section titled “Three channels”](#three-channels) ggui’s wire is split across three orthogonal channels. Each has one job. | Channel | Direction | Purpose | | ------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Bootstrap** | Server → Renderer | One-shot fetch of the compiled component bundle when the iframe first loads. Any gadgets bound to the session load behind a `` SRI gate. | | **MCP** | Agent ↔ Server | Control plane. `ggui_handshake`, `ggui_render`, `ggui_update`, `ggui_consume`, `ggui_emit`, `ggui_get_session`. | | **Live** | Renderer ↔ Server | WebSocket at `ws://127.0.0.1:6781/ws` (self-hosted default; hosted `wss://mcp.ggui.ai/ws` coming soon). Server deliveries outbound (`StreamEnvelope`, props updates, drain acks); contract violations on an inbound action are answered with a typed `error` frame (`CONTRACT_VIOLATION`, code -32020) on this channel — nothing lands on the consume buffer. | The rendered view’s user actions reach the server through the host’s MCP-Apps `tools/call` relay to `ggui_runtime_submit_action` — the spec-canonical dispatch path; the server appends the gesture to the pipe `ggui_consume` drains. The live WebSocket’s job is server → renderer delivery (stream emits, props updates, drain acks). The channels are independent. The renderer can drop and reconnect the live channel without disturbing an agent’s MCP turn; the agent can `ggui_render` repeatedly without ever touching the live channel. → See [Protocol overview](/protocol/overview/) for the formal three-channel spec. ## Capability model [Section titled “Capability model”](#capability-model) ggui has two **symmetric** capability surfaces — one for what the agent can do, one for what the renderer can render. Both are operator-bounded and declared per-app. | | Renderer side | Agent side | | --------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | **Unit** | **gadget** | **tool** | | **Catalog** | `clientCapabilities.gadgets` | `agentCapabilities.tools` | | **Role** | Wraps a 3rd-party browser library (Leaflet, Mapbox, Chart.js) into an LLM-callable hook | Gives the agent a function to invoke (e.g. `searchContacts`, `createInvoice`) | | **Authored as** | `ggui.gadget.json` manifest | MCP tool code | | **Bound at** | iframe boot (SRI-verified) | session start | Plus a third primitive — **blueprints** — which aren’t capabilities but **cached recipes** (pre-composed UIs). A blueprint hit short-circuits fresh generation. → See [Gadgets SDK](/sdk/gadgets/), [MCP Protocol](/api/mcp-protocol/), [Marketplace](/sdk/marketplace/). ## Generation pipeline [Section titled “Generation pipeline”](#generation-pipeline) Rendering is a two-call flow — `ggui_handshake` negotiates, `ggui_render` commits: ```plaintext 0. NEGOTIATE ggui_handshake({intent, blueprintDraft}) searches the blueprint cache by contract shape (exact contractHash hit short-circuits; semantic similarity otherwise) and returns a suggestion 1. COMMIT ggui_render({handshakeId, props}) consumes the handshake; a cache hit reuses the stored blueprint (~100ms) 2. GENERATE Otherwise, run the server's UI generator (@ggui-ai/ui-gen): workflow → impl → check → derive 3. COMPILE TSX → JS via esbuild (~20-50ms) 4. DELIVER Renderer fetches the compiled bundle on the bootstrap channel ``` Published artifacts (gadgets, blueprints) on the marketplace registry pick up an author signature at *publish* time — Ed25519 (publisher keypair) for private artifacts, sigstore keyless (OIDC) for public ones — see [Marketplace](/sdk/marketplace/). Fresh generations are session-scoped and skip that step. The generator (step 2) is a bounded harness: pick a workflow (`single_pass`, `staged`, `staged-concurrent`), run the LLM-driven impl phase, run a check leg (typecheck, render-smoke, per-axis assertions), and on failure derive a revised harness and retry up to `maxIterations`. Output: a TypeScript-typed contract plus a compiled component module. → See [UI Generator](/architecture/ui-generator/) for the harness internals. ## Artifact registry [Section titled “Artifact registry”](#artifact-registry) Gadgets and blueprints resolve through a four-tier waterfall on every push: ```plaintext 1. App-local ggui.json#app.gadgets, ggui.json#blueprints.include (plus installed artifacts under .ggui/installed-blueprints/) 2. Per-org private operator's private registry (artifacts with visibility:"private") 3. Public registry.ggui.ai (marketplace) 4. Fall back fresh generation ``` A blueprint is keyed by a stable `blueprintId`; its `contractHash` — the RFC 8785 canonical-JSON hash of its `DataContract`, scoped per `(appId, contractHash)` — groups variants and is the cache lookup key. An exact contractHash hit short-circuits to score 1.0; otherwise a multi-axis semantic search (contract embedding, structural fingerprint, variance tags, intent) ranks candidates. Install the same `(scope, name, version)` on two different servers and the matcher returns the byte-identical UI on both. → See [Marketplace](/sdk/marketplace/), [Self-Hosted Registry](/sdk/self-hosted-registry/). ## Deployment shapes [Section titled “Deployment shapes”](#deployment-shapes) The same protocol runs in two shapes: | | Self-hosted | Hosted (coming soon) | | ---------------- | ------------------------------------------------------------------- | --------------------------------- | | **MCP endpoint** | your URL via `ggui serve` | `mcp.ggui.ai` | | **WS endpoint** | `ws://127.0.0.1:6781/ws` (or your URL) | `wss://mcp.ggui.ai/ws` | | **Auth** | pluggable `AuthAdapter` + optional OAuth 2.1 (`ggui serve --oauth`) | OAuth | | **Registry** | your registry (or none) | `registry.ggui.ai` | | **Generation** | in-process | managed | | **Pick if** | ”I need data residency, custom auth, or air-gapped" | "I just want to ship an agent UI” | Both speak the same wire. Switching between them is configuration, not code. → See [OSS Quick Start](/oss-quickstart/) to run it yourself. A managed hosted path at `mcp.ggui.ai` is coming soon. ## Where to next [Section titled “Where to next”](#where-to-next) * [Protocol overview](/protocol/overview/) — formal spec * [How ggui works](/how-it-works/) — narrative walk-through of the four moments * [Event System](/architecture/event-system/) — live-channel event flow in detail * [UI Generator](/architecture/ui-generator/) — the generator harness # UI Generator > The bounded harness that turns a data contract into a compiled React component — workflow, check, derive, repeat. The UI generator runs when a [blueprint match](/architecture/overview/#generation-pipeline) misses and ggui has to build a component from scratch. It takes a **data contract** (`PropsSpec` + `ActionSpec` + `StreamSpec` + `ContextSpec`) and returns a compiled, contract-typed React module — typically a handful of LLM turns; cache hits via blueprints are what deliver the \~100ms path. ## The harness [Section titled “The harness”](#the-harness) A **harness** is a per-contract execution plan derived from the contract’s risk and axes. It bundles three things: * **Workflow** — the topology of LLM phases and tasks. Today the dispatcher always picks `single_pass`; `staged` and `staged_concurrent` are registered as reserved-future topologies for risk-tier routing that has not yet shipped. * **Prompt + boilerplate** — the system prompt and fragment set that tell the LLM *how* to author against `@ggui-ai/design`, the active [gadgets](/glossary/#gadget-renderer-side-capability), and the contract. * **Check leg** — the post-conditions the output must satisfy. Generation runs the workflow to produce source, runs the check leg, and — if checks fail — derives a revised harness and re-runs, bounded by `maxIterations`. ```plaintext workflow → source → check → pass? ─► return │ └─ fail → derive (swap fragments / upgrade workflow / adjust prompt) → loop ``` ## Workflows [Section titled “Workflows”](#workflows) | Workflow | Status | Shape | | ------------------- | ----------------------------------- | ------------------------------------------- | | `single_pass` | **Live** — only dispatched workflow | One impl turn. | | `staged` | Reserved (registered, not routed) | Plan → execute. | | `staged_concurrent` | Reserved (registered, not routed) | Plan → parallel skeleton tasks → integrate. | `pickWorkflow` is deliberately conservative — every classification today routes to `single_pass`. The staged topologies are wired up so a future risk-tier router can dispatch to them without a schema change, but changing the picker is a first-class experiment with its own bench gate. All three feed the same check + derive loop; the workflow only changes how source is produced, not how it’s validated. ## Plain-text impl loop (no tool-call ceremony) [Section titled “Plain-text impl loop (no tool-call ceremony)”](#plain-text-impl-loop-no-tool-call-ceremony) The impl phase is structured so the LLM doesn’t waste turns on deterministic ceremony: 1. The LLM receives **everything pre-injected** — primitives docs, design tokens, the data contract with examples, gadget capability cards. 2. The LLM writes the component as **plain text**. No tool calls required. 3. The system **auto-runs** `self_check` and `compile_component` on the emitted source. 4. Failures are fed back as a structured diff for the next turn to fix. This is what keeps healthy generations to 3–5 turns. Turns ≥ 6 is a triad-misalignment signal, not a turn-budget problem. ## Check leg [Section titled “Check leg”](#check-leg) Every workflow runs the same checks before returning: * **TypeScript** — the emitted source is compiled against the real `@ggui-ai/design` type definitions via the TypeScript compiler API on a virtual filesystem. Catches wrong prop types, missing required props, invalid imports, and `strictNullChecks` violations. * **Render smoke** — `ReactDOMServer.renderToString()` with contract-derived sample props. Catches `undefined.toLowerCase()`-class runtime errors before the component reaches the renderer. * **Per-axis assertions** — axis-specific checks derived from the contract (e.g., does an `interactive` axis include a focusable element? does a `submit` action have a matching form?). If any leg fails, the harness derives a revised configuration and loops. If the final iteration still fails, generation returns `ok: false` with `reason: "max-iterations"` and the last source, compiled output, and check result attached for diagnostics — the caller decides whether to surface a fallback, retry with a different harness, or report the failure. ## Multi-provider transport [Section titled “Multi-provider transport”](#multi-provider-transport) The harness is provider-agnostic. Only the LLM transport differs: * **Claude** (Anthropic) — raw API or Claude Agent SDK. * **OpenAI** — Responses API or OpenAI Agents SDK. * **Google** (Gemini) — GenAI API or Google ADK. Workflow, check, and derive are identical across providers. Switching providers does not change what gets generated, only the per-turn cost and latency profile. A deployment pins its model in `ggui.json#generation.model` (`provider:model`, e.g. `anthropic:claude-haiku-4-5-20251001`); the `GGUI_GENERATION_MODEL` env var overrides the manifest (precedence: `GGUI_GENERATION_MODEL` env > `ggui.json#generation.model` > per-provider default), and an agent may override per render via `ggui_render({infra: {model}})`. `generation.keySource: 'own' | 'managed'` declares whose provider key funds generation — self-hosted deployments always use their own key. ## Output shape [Section titled “Output shape”](#output-shape) A successful generation returns: * A **compiled component module** (TSX source compiled to JS). * A **typed data contract** — the same `PropsSpec` / `ActionSpec` / `StreamSpec` / `ContextSpec` that was the input, now frozen as the runtime wire shape. * A **blueprint candidate** — the contract (hashed via RFC 8785 to `contractHash`) plus its variance (`variantKey`), which the registry stores under a stable `blueprintId` for the next handshake’s cache match. The contract is enforced again at the MCP handler boundary so that what the agent renders matches what the component was generated to render. → See [Architecture overview](/architecture/overview/) for where the generator fits in the render pipeline, and the [Glossary](/glossary/) for `harness`, `blueprint`, `gadget`, `contract`. # ggui dev > Inner-loop dev hub — local blueprint registry, devtools console, optional agent supervision, optional managed tunnel. `ggui dev` is the developer-facing inner loop. It loads `ggui.ui.json` blueprint manifests, serves the local registry + dev hub at `127.0.0.1:6780`, and (with `--agent `) supervises a local agent runtime in the same shell. Distinct from [`ggui serve`](/cli/serve/), the production-shaped self-host counterpart. ## Quick start [Section titled “Quick start”](#quick-start) ```bash ggui dev ``` Binds `127.0.0.1:6780`, indexes any `ggui.ui.json` blueprints declared in `ggui.json#blueprints.include`, and auto-opens the dev hub in your browser. To iterate against a local agent runtime in the same shell: ```bash ggui dev --agent ./agent.ts ``` ## What you get [Section titled “What you get”](#what-you-get) Port 6780 hosts the local blueprint registry + dev hub. The dev server exposes: | Path | What it serves | | ------------------------------------- | ------------------------------------------------------------------------------------ | | `/hub` | The dev dashboard (no auth — same-origin XHRs embed the bearer for everything else). | | `/hub/preview?ui=` | Iframe-mountable preview shell for one indexed blueprint. | | `/health` | Liveness probe (no auth). | | `/uis`, `/uis/:id`, `/uis/:id/bundle` | Discovered `ggui.ui.json` blueprints (Bearer auth). | | `/events` | Server-sent events for live reload (Bearer auth). | | `/runtime/status`, `/runtime/events` | Mounted when `--agent ` is set (Bearer auth). | All non-`/hub` endpoints require `Authorization: Bearer `. The token is taken from `GGUI_DEV_TOKEN` if set; otherwise a random one is generated and printed (with an `export` hint) on the boot banner. `ggui dev` also sets `GGUI_MODE=dev` in the process env. The dev stack itself does not run an MCP server — that’s [`ggui serve`](/cli/serve/)’s job — but a supervised `--agent` that composes `@ggui-ai/mcp-server` inherits the env and mounts its own `/devtools/*` console namespace. ## vs `ggui serve` [Section titled “vs ggui serve”](#vs-ggui-serve) | Concern | `ggui dev` | `ggui serve` | | ----------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------- | | Audience | Developer iterating on UIs | Operator running a self-hosted instance | | Default port | `6780` | `6781` | | Default mode | `GGUI_MODE=dev` (mounts `/devtools/*`) | Production-shaped (no devtools) | | Agent supervision | Opt-in via `--agent ` | Default-on, sourced from `ggui.json#agent.entry` | | Tunnel | Opt-in via `--tunnel` (provider seam; none bundled) | Bring your own (`cloudflared` etc.) + set `--public-base-url` | | Auth posture | Loopback bind + single bearer token (`GGUI_DEV_TOKEN`) | Strict-auth pairing by default; opt-down via `--dev-allow-all` / `--public-demo` | Both can run side-by-side without colliding (different ports). ## Flags [Section titled “Flags”](#flags) ```text ggui dev [options] ``` | Flag | Default | Purpose | | ----------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `--port ` | `6780` | Bind port. `0` = OS-assigned. | | `--host ` | `127.0.0.1` | Bind host. Loopback only by default. | | `--no-serve` | off | Load + discover and exit without binding. Useful for one-shot manifest validation / discovery dry-run. | | `--no-open` | off | Skip auto-opening the browser at the hub URL. Implied by non-TTY stdout, `BROWSER=none`, or `CI=1`. | | `--agent ` | none | Supervise a local agent runtime. See [Agent supervision](#agent-supervision) for extension routing. | | `--tunnel` | off | Opt into managed mode — open a managed tunnel above the local stack and print the remote URL. See [Managed mode](#managed-mode-tunnel). | ## Agent supervision [Section titled “Agent supervision”](#agent-supervision) `--agent ` points at the agent file you’re iterating on. Extension routing decides how it’s spawned: | Extension | Spawn | Notes | | --------------------- | --------------------------- | ------------------------------------------------------------ | | `.js`, `.mjs`, `.cjs` | `node ` | Plain Node | | `.ts`, `.tsx`, `.mts` | `node --import=tsx ` | `tsx` must be resolvable in your project (`pnpm add -D tsx`) | The dev-stack picks the agent’s port and forwards it via `PORT` env unless `--tunnel` is also set, in which case the CLI pre-allocates a free port and hands it down so the tunnel can forward inbound traffic to it. Bad `--agent` paths fail before the socket binds — the CLI validates the command mapping and exits 1 with a remediation hint. ## Managed mode tunnel [Section titled “Managed mode tunnel”](#managed-mode-tunnel) With `--tunnel`, once the local stack is listening `ggui dev` asks a `TunnelProvider` to open a managed tunnel above the host and prints the remote URL beside the local hub URL. The dev loop runs unchanged whether the tunnel resolves or not. Provider discovery reads `GGUI_TUNNEL_PROVIDER` — a module specifier exporting `createTunnelProvider()`. No provider is bundled; without the env var the banner prints `tunnel skipped: no tunnel provider configured` and local dev runs unchanged. Real providers (`cloudflared` bindings, `ngrok`) plug into this seam without changing the CLI surface. For a known-working public URL today, run `cloudflared tunnel --url http://localhost:6780` (or your provider of choice) in a sibling shell, then point claude.ai or your MCP client at the printed URL. For the production-shaped equivalent on `ggui serve`, see [`ggui serve` → Recommended setups](/cli/serve/#recommended-setups). ## Common workflows [Section titled “Common workflows”](#common-workflows) **Iterate on a blueprint manifest:** ```bash ggui dev # edit packages//ggui.ui.json # refresh the hub — the registry re-indexes on every load ``` **Iterate on an agent + blueprints together:** ```bash ggui dev --agent ./agent.ts # edit agent.ts → the supervised runtime restarts on file change (when the runtime supports it) # edit ggui.ui.json → the registry re-indexes ``` **Run a second `ggui dev` alongside the first (6780 is taken):** ```bash ggui dev --port 0 # 0 = OS-assigned; the actual port prints in the boot banner. # Pass `--port 6790` (or any free integer) if you need a stable URL. ``` **Validate manifests without binding a socket:** ```bash ggui dev --no-serve # loads + discovers + exits non-zero on any malformed ggui.ui.json # good in CI for catching manifest regressions ``` ## See also [Section titled “See also”](#see-also) * [`ggui` CLI overview](/cli/) — the full command surface. * [`ggui serve`](/cli/serve/) — production-shaped self-host counterpart. * [OSS Quick Start](/oss-quickstart/) — the bootstrap walkthrough. * [Glossary](/glossary/) — `gadget` / `tool` / `blueprint` definitions. # ggui CLI > Open command-line tool for the ggui protocol — local dev, self-host, marketplace authoring, and (coming soon) ggui.ai cloud provisioning. `ggui` is the open CLI for the ggui protocol, shipped as [`@ggui-ai/cli`](https://www.npmjs.com/package/@ggui-ai/cli). It does three jobs: 1. **Local protocol dev + self-host** — `ggui dev` (iterate gadgets and blueprints against a local registry + dev hub), `ggui serve` (run a self-hosted personal-mode app), `ggui keys … --keys-file` (mint local bearers, no account), and `ggui export-pool` (share blueprints across deployments). Account-free; runs entirely on your machine. **Available now.** 2. **Marketplace authoring** — `ggui gadget` / `ggui blueprint` author and publish marketplace artifacts; `ggui theme validate` checks DTCG theme files. 3. **ggui.ai cloud provisioning** *(Preview — managed cloud, coming soon)* — `ggui login` / `keys` / `create` / `deploy` / `push` / `provider-key` provision apps and `ggui_user_*` connector keys against the managed ggui.ai cloud. ## Install [Section titled “Install”](#install) ```bash npm install -g @ggui-ai/cli # or pnpm add -g @ggui-ai/cli ``` ## Commands at a glance [Section titled “Commands at a glance”](#commands-at-a-glance) | Command | Purpose | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ggui dev` | Boot the local UI registry server, open the dev hub, optionally supervise an agent | | `ggui serve` | Run the open self-hosted personal-mode app (MCP server + supervised agent) | | `ggui keys` | List / create / revoke connector keys. With `--keys-file `, keys are minted into a local JSON store (no account) that `ggui serve --keys-file` reads — the self-host path. Without it, keys target the ggui.ai cloud *(Preview — coming soon)*. Also registers publisher Ed25519 public keys (see [`ggui keys register`](/cli/keys-register/)) | | `ggui theme` | Validate a DTCG theme file against the protocol’s theme schema (`theme validate `) | | `ggui export-pool` | Export this deployment’s reusable blueprints as a directory artifact, loadable elsewhere via `ggui serve --seed-pool ` | | `ggui gadget` | Author and consume marketplace gadgets (`create` / `publish` / `install` / `search`) | | `ggui blueprint` | Author UI blueprints for the marketplace (`create` / `publish` / `install` / `uninstall` / `search`) | | `ggui login` | *(Preview — coming soon)* Sign into `api.ggui.ai` via the OAuth Device Authorization Grant | | `ggui logout` | *(Preview — coming soon)* Discard the local `api.ggui.ai` session | | `ggui whoami` | *(Preview — coming soon)* Print the authenticated user | | `ggui create` | *(Preview — coming soon)* Provision a ggui.ai cloud app (`create app`) | | `ggui deploy` | *(Preview — coming soon)* Idempotent ggui.ai cloud provisioning; `--push-keys` also pushes provider keys | | `ggui push` | *(Preview — coming soon)* Compile and upload local-pool blueprints to a ggui.ai cloud app | | `ggui provider-key` | *(Preview — coming soon)* Push an LLM provider key to a ggui.ai cloud app (`provider-key set`) | | `ggui --version` | Print the installed `@ggui-ai/cli` version | Run `ggui --help` for the per-command flag list. `ggui --help` prints the full surface. ## Local dev & self-host [Section titled “Local dev & self-host”](#local-dev--self-host) `ggui dev` and `ggui serve` are the two ways to run the protocol locally: * **[`ggui dev`](/cli/dev/)** — inner-loop dev hub. Loads `ggui.ui.json` manifests, serves the local gadget + blueprint registry, and (with `--agent `) supervises a local agent runtime. Default `127.0.0.1:6780`. Use while iterating. * **[`ggui serve`](/cli/serve/)** — production-shaped self-host. Boots an MCP server with a supervised agent (`ggui.json#agent.entry`), ready to put behind your own auth on a public URL. Default `127.0.0.1:6781`; clients connect over WebSocket at `ws://127.0.0.1:6781/ws` (or `wss:///ws` once tunneled). See [Reference deploys](/self-hosted/reference-deploys/) for Docker / Fly / Render manifests and [OSS Quick Start](/oss-quickstart/) for the bootstrap. Run `ggui dev --help` or `ggui serve --help` for the full flag list. ## Bearer keys — local first [Section titled “Bearer keys — local first”](#bearer-keys--local-first) The available-now path is account-free: `ggui keys list / create / revoke --keys-file ` mints bearer keys into a local JSON store — the same file format `ggui serve --keys-file` reads — so a locally minted bearer authenticates on the next serve boot. No account involved. ### ggui.ai auth & keys (Preview — coming soon) [Section titled “ggui.ai auth & keys (Preview — coming soon)”](#gguiai-auth--keys-preview--coming-soon) The managed ggui.ai cloud is coming soon. Once live, agent runtimes pointed at the universal MCP at `mcp.ggui.ai` authenticate with a `ggui_user_*` connector key, and `ggui login` signs the CLI into `api.ggui.ai` so you can mint and revoke those keys from the terminal — see [`ggui login`](/cli/login/) for the device-flow walkthrough plus `whoami`, `keys list / create / revoke`, and `logout`. ## Pairing a client app [Section titled “Pairing a client app”](#pairing-a-client-app) `ggui` has no `pair` subcommand. To pair a client to a `ggui serve` instance, follow [Self-hosted pairing](/self-hosted/pairing/) — tunnel setup, QR handshake, and the AuthAdapter swap-in for production. ## Configuration [Section titled “Configuration”](#configuration) Two environment variables control where the CLI talks and stores state: | Variable | Default | Purpose | | ----------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `GGUI_API_URL` | `https://api.ggui.ai` | Override the auth + key-management endpoint. Useful for sandbox / dev testing. | | `GGUI_CONFIG_DIR` | `~/.ggui` | Override the `~/.ggui` root (`auth.json`, BYOK `credentials.json`, embedding-model cache, publisher keypairs). Useful for isolated dev shells. | ## Versioning [Section titled “Versioning”](#versioning) ```bash ggui --version ``` Versions track the `@ggui-ai/cli` npm package. Protocol semantics are pinned to the `@ggui-ai/protocol` major version it bundles — see [Protocol version policy](/protocol/version-policy/) for what changes between major bumps. # ggui keys register > Register a publisher's Ed25519 public key with the ggui.ai marketplace registry so signed gadget and blueprint publishes validate. Coming soon This page describes the **managed hosted path** (`mcp.ggui.ai` / `console.ggui.ai` / the `registry.ggui.ai` marketplace), which is **not yet live** — it is not part of GGUI Preview 0.1.0. The self-hosted path is available today — start with the [Quickstart](/oss-quickstart/). This page is kept as a preview of the managed path and goes live when hosted ggui ships. `ggui keys register` ships your **publisher** Ed25519 public key to the marketplace registry’s `POST /author-keys` endpoint. After this, every subsequent `ggui gadget publish` / `ggui blueprint publish` whose bundle is signed by the matching private key validates against the registry’s stored row. ## When to use it [Section titled “When to use it”](#when-to-use-it) You run `ggui keys register` **after** `ggui gadget publish` (or `ggui blueprint publish`) has auto-generated a keypair on disk for a given scope. The publish flow does the keypair generation automatically on first run, then signs the artifact, then POSTs to `/publish`. The registry rejects a signed publish whose `publicKeyId` isn’t already registered for the caller’s identity — that’s the moment to run this command. The typical bootstrap sequence for a new publisher: ```bash # 1. First publish under @my-org auto-generates # ~/.ggui/keys/@my-org/{private,public}.key and signs the bundle. ggui gadget publish # → Error: unknown_key — public key not registered for this publisher. # 2. Register the public half with the registry. ggui keys register --scope @my-org # 3. Re-run the publish. The signature now verifies. ggui gadget publish ``` After step 2, every future publish under `@my-org` from this machine (and any other machine you copy `~/.ggui/keys/@my-org/private.key` to) validates without a second `register` call. ## Usage [Section titled “Usage”](#usage) ```text ggui keys register --scope <@scope> [--registry ] [--auth=bearer [--token ]] ``` | Flag | Required | Purpose | | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--scope` | yes | npm scope the key was generated under, prefixed with `@` (e.g. `--scope @my-org`). The CLI reads `~/.ggui/keys//public.key` — must already exist (see “When to use it” above). | | `--registry` | no | Override the registry URL. Same resolution chain as `ggui gadget publish`: `--registry` flag, then `GGUI_REGISTRY` env var, then `ggui.json#registry` walking up from CWD. No hard-coded default — pick a registry deliberately so a typo doesn’t accidentally register against prod. | | `--auth=bearer` | no | Send an explicit bearer token instead of the stored `ggui login` session — for self-hosted registries that authenticate with a static publish token. Pair with `--token ` or set `GGUI_REGISTRY_TOKEN`. Same flags the publish verbs take. | | `--token` | no | The bearer token for `--auth=bearer` (overrides `GGUI_REGISTRY_TOKEN`). | ## Auth [Section titled “Auth”](#auth) `ggui keys register` uses your stored **`ggui login` session** by default — the same credential `ggui gadget publish` sends to `/publish`, and the one the hosted registry’s `/author-keys` route authenticates (see [`Marketplace § Auth`](/sdk/marketplace/#auth)). The CLI reads `~/.ggui/auth.json`, refreshes the access token automatically when it has expired, and sends it as `Authorization: Bearer ` — the only identity surface; the request body carries only `publicKeyBase64`, never the caller’s user id. The server derives the publisher subject from the verified credential and the `keyId` from the raw public-key bytes. Self-hosted operators running their own [`@ggui-ai/registry-server`](/sdk/self-hosted-registry/) deployments pass `--auth=bearer --token ` (or set `GGUI_REGISTRY_TOKEN`) — the same escape hatch the publish flow takes (see [`ggui marketplace`](/sdk/marketplace/#auth) for the parallel). ## Output [Section titled “Output”](#output) Success — first-write (HTTP 201): ```bash $ ggui keys register --scope @my-org Registered publisher key for @my-org. registry: https://registry.ggui.ai subject: keyId: a1b2c3d4e5f60718 ``` Idempotent re-register (HTTP 200) — same public-key bytes for `(subject, keyId)` already on file: ```bash $ ggui keys register --scope @my-org Already registered publisher key for @my-org. registry: https://registry.ggui.ai subject: keyId: a1b2c3d4e5f60718 ``` Both exit `0`. Safe to run unconditionally in CI bootstrap scripts. ## Errors and exit codes [Section titled “Errors and exit codes”](#errors-and-exit-codes) | Code | Exit | Meaning | | --------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `no-registry` | 1 | No registry URL resolved. Pass `--registry`, set `GGUI_REGISTRY`, or add `registry` to `ggui.json`. | | `invalid-registry` | 1 | Resolved URL is malformed. | | `no-keypair` | 1 | No `~/.ggui/keys//public.key` on disk. Run `ggui gadget publish` or `ggui blueprint publish` under this scope first — first-publish generates the keypair as a side-effect. | | `auth_failed` | 1 | The stored login session expired (or its refresh was rejected). Run `ggui login` again. | | `auth_config_missing` | 1 | No stored login session (`~/.ggui/auth.json` missing or unreadable) — run `ggui login` first. Or `--auth=bearer` was passed without `--token` / `GGUI_REGISTRY_TOKEN`. | | `network-error` | 1 | `fetch` threw — DNS, TLS, or connection refused. | | `unauthorized` | 1 | Registry rejected the bearer credential (HTTP 401). Re-run `ggui login` (the session may have been revoked) — or, for self-hosted registries, check the `--auth=bearer` token. | | `invalid_request` | 1 | Registry refused the body (HTTP 400). The public-key file is corrupted or the wrong length — regenerate by deleting `~/.ggui/keys//` and re-running `ggui gadget publish`. | | `key_conflict` | 3 | Registry holds a different public key for the same `(subject, keyId)` tuple (HTTP 409). Vanishingly rare — a 64-bit `keyId` SHA-256 truncation collision OR a stale row from a previous owner. Exit `3` is distinct so scripts can detect it without parsing the message. | | `http-error` | 1 | Any other non-2xx response (typically 5xx). Check the message for the registry’s error string; retry transient failures. | | `bad-response` | 1 | Registry returned a 2xx with a malformed body, or any status with invalid JSON. Almost always a registry-side bug — re-run; if persistent, the registry is misconfigured. | The structured `error` field of the registry’s response body (closed enum: `unauthorized` / `invalid_request` / `key_conflict` / `server_error`) is preferred over status-code mapping when the response carries a well-formed body — `key_conflict` returned with a non-409 status still surfaces as `key_conflict` on the CLI side. ## Trust model [Section titled “Trust model”](#trust-model) The on-disk private key under `~/.ggui/keys//private.key` is mode `0o600` — treat it like a long-lived password. Copying it between machines lets you publish from CI without re-registering. Losing it means generating a new keypair under the same scope: when the new public key gets registered, both old + new keys are valid (publish flow pins the signing key onto each `ArtifactVersionRow` at publish time, so historical versions still verify under the previous key). To rotate out the old key entirely, register the new one + remove the old row server-side (operator-only). See [`Marketplace § Trust model`](/sdk/marketplace/#trust-model) for the full per-author-key + per-version pinning design and the install-time two-leg verification (SHA-384 + Ed25519). # ggui login > Sign the @ggui-ai/cli into ggui.ai via OAuth 2.0 Device Authorization Grant to manage hosted connector keys. Coming soon This page describes the **managed hosted path** (`api.ggui.ai` / `console.ggui.ai`), which is **not yet live** — it is not part of GGUI Preview 0.1.0. The self-hosted path is available today — start with the [Quickstart](/oss-quickstart/); for connector keys without an account, see [local keys](#no-account-local-keys) below. This page is kept as a preview of the managed path and goes live when hosted ggui ships. `ggui login` signs the open `@ggui-ai/cli` into [`api.ggui.ai`](https://api.ggui.ai) so you can manage `ggui_user_*` connector keys from the terminal — list, mint, and revoke — without leaving your shell. It uses the [OAuth 2.0 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628): the CLI prints a URL and a short code, you approve in any browser (even on a different machine), and tokens land on disk once approval completes. ## Install [Section titled “Install”](#install) ```bash npm install -g @ggui-ai/cli # or pnpm add -g @ggui-ai/cli ``` ## Sign in [Section titled “Sign in”](#sign-in) ```bash $ ggui login Endpoint: https://api.ggui.ai (default) Open this URL in your browser to approve: https://console.ggui.ai/cli-confirm/ABCD-EFGH Verification code: ABCD-EFGH (Confirm this matches what the browser shows.) Waiting for approval… Signed in. Tokens saved to ~/.ggui/auth.json. Try `ggui whoami` or `ggui keys list`. ``` Under the hood: 1. **Device code request** — the CLI POSTs `/v1/auth/device` and prints the user-readable code plus URL. 2. **Browser approval** — `console.ggui.ai/cli-confirm/` shows the same code, your account, and an `Approve` button. Confirm the codes match, then approve. 3. **CLI polls `/v1/auth/poll`** every few seconds until the server returns tokens. 4. **Tokens land on disk** — `~/.ggui/auth.json` (mode `0600`) holds the access bearer, the refresh bearer, and the API endpoint they were minted against. The approval window is \~10 minutes. If it expires, just run `ggui login` again. ## Flags [Section titled “Flags”](#flags) ```text ggui login [--name