MCP Protocol Reference
read as.mdThe GGUI MCP API is Model Context Protocol 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 first.
Endpoint
Section titled “Endpoint”Self-hosted (ggui serve):
POST http://127.0.0.1:6781/mcpAuthentication
Section titled “Authentication”Local dev: start the server with ggui serve --dev-allow-all and any bearer (conventionally dev) authenticates as the builder identity:
Authorization: Bearer devContent-Type: application/jsonDefault 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 <path> (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 uses 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 for the ceremony.
Render Lifecycle
Section titled “Render Lifecycle”1. initialize → MCP handshake (one-shot per connection)2. ggui_list_gadgets → Optional: fetch the gadget catalog3. ggui_handshake → Negotiate the wire (handshakeId + suggestion)4. ggui_render → Materialize the UI (outcome: rendered mints sessionId)5. ggui_consume → Long-poll for user events (keyed by sessionId)6. ggui_amend → Repaint the mounted card in place (the gesture loop)7. ggui_update → Render updated state as a NEW card (advances history)8. ggui_emit → Optional: push frames on a streamSpec channel9. ggui_get_render_source → Optional: read back the source you shippedThere is no explicit close ceremony, because there is nothing to close. A render is a durable record: the agent simply stops addressing it, and the card stays reopenable from chat history via resources/read on its ui:// locator. On hosted GGUI the default retention is effectively indefinite, and there is no per-render time limit.
ggui_amend, 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”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:
origin: 'cache'— a hit in the semantic cache for generative UI: an exact or semantic match against a registered blueprint. Free, deterministic reuse on the paired render.origin: 'agent'— no cache hit, but the agent’s draft passed validation. Gen runs against the agent’s contract verbatim.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”The canonical posture-only system prompt for any agent calling these tools is exported as a string constant:
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.
| Tool | Purpose |
|---|---|
ggui_handshake |
Negotiate the wire surface before rendering — returns handshakeId + a routed suggestion. |
ggui_render |
Materialize the UI; a rendered outcome mints sessionId. |
ggui_consume |
Long-poll buffered user events on one GguiSession. |
ggui_amend |
Repaint the mounted card in place — no new card, history number unchanged. |
ggui_update |
Render the updated state as a NEW card; advances the history number (epoch). |
ggui_emit |
Push a delivery onto a declared streamSpec channel. |
ggui_get_session |
Read the GguiSession wire projection — ids, event sequence, timestamps, plus contextSnapshot when present. |
ggui_get_render_source |
Read the generated source of the calling app’s own render. |
ggui_list_sessions |
Enumerate GguiSessions by host conversation (resume flows). |
ggui_list_gadgets |
Fetch the renderer-side gadget catalog before authoring a contract. |
ggui_list_themes |
List the theme presets usable via ggui_render({themeId}). |
ggui_list_featured_blueprints |
Enumerate builder-curated featured blueprints. |
ggui_search_blueprints |
Semantic search across this app’s blueprints. |
ggui_render_blueprint |
Resolve a registered blueprint id to its compiled bundle. |
ggui_discover |
Platform capability discovery (hosted-only). |
ggui_handshake
Section titled “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 purpose — same intent across calls = same component reused. Example: "Gmail inbox for email triage". The intent is stored with the generated interface and read again when later requests of this app are matched against it, so describe the task, never the end user: no names, emails or personal details. |
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, propsSchema?, propsSchemaHash?, propsSchemaProfile?, 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 (contractHash always; blueprintId, codeHash and source only on a cache hit; matchedIntent, the matched card’s stored intent at most 280 characters, only on a judged semantic hit from the app’s own pool), and conditional amendments (synth-only) / validationFindings (soft on cache). |
propsSchema |
object |
The exact JSON Schema the paired ggui_render enforces for this handshake (SPEC §2.3.2) — enum fields list their full legal vocabulary. Present when the agreed contract differs from your draft; absent means your draft’s propsSpec is agreed verbatim. |
propsSchemaHash |
string |
sha256 (lowercase hex) over the RFC 8785 canonical form of the enforced schema. On every non-declined handshake. A later contract_violation carries the hash of the schema it enforced — equal hashes mean the props were at fault. |
propsSchemaProfile |
string |
'grammar-safe' (every keyword in the SPEC’s enumerated core — compilable into a decoding grammar) or 'full' (read the schema as context). Treat unknown values as 'full'. |
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”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).
// 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?} — model route in either wire form — canonical anthropic:claude-haiku-4-5-20251001 or LiteLLM anthropic/claude-haiku-4-5; a value that parses in neither fails the handler input parse at infra.model. Strict: a typo like infra.modelId surfaces as a zod path instead of silently falling back to the default model. |
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 resolves at its key — reuse if a card exists there, else cold-gen; override.variance re-aims the variant axis. |
Returns: { outcome, … } — read outcome first; it decides which of the other fields exist.
| Field | Type | Description |
|---|---|---|
outcome |
enum |
REQUIRED, always present. rendered | failed | refused — the discriminant. rendered: an interface was produced. failed: generation ran and produced none. refused: the deployment declined before doing any work. See SPEC §7.1. |
sessionId |
string |
Globally-unique id (UUID) for the delivered render. Present iff outcome is rendered or failed. Use for ggui_consume / ggui_update. |
resourceUri |
string |
Present iff outcome is rendered — a failed render commits an error session but exposes no mount. Spec-canonical MCP-Apps entry point, mirrored on _meta.ui.resourceUri; no clickable URL on the wire. See Render locator grammar for the full URI shape. |
action |
enum |
One of create / reuse / update / replace / declined. Present iff outcome is rendered or failed. |
contractHash |
string |
Canonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow. Present iff outcome is rendered or failed. |
blueprintId |
string |
Opaque id of the materialised component. Equal across two renders means the same cached component was served — or, for two fresh generations at a key bound to other code, byte-identical code (that id is content-addressed and registered nowhere). A fresh generation at a free key mints a new id. Present iff outcome is rendered or failed. |
variantKey |
string |
Canonical hash of the design-time variance. With contractHash it forms the reuse key. Present iff outcome is rendered or failed. |
cache |
object |
Reuse outcome — { hit, similarity?, cachedBlueprintId?, llmCallsAvoided, kind?, reason? }. Present iff outcome is rendered or failed. |
nextStep |
object |
Emitted ONLY on a rendered result whose contract has a non-empty actionSpec. Points at ggui_consume({sessionId}) for the inbound action loop. Pure-display renders get no nextStep. |
error |
object |
Present iff outcome is failed (and the tool result is isError: true). { code, message } with the closed enum PRODUCTION_FAILED | VALIDATION_ERROR | NO_PLATFORM_KEY | NO_CREDENTIALS | GENERATION_QUEUE_OVERLOADED. Failed renders carry no resourceUri and no _meta. |
refusal |
object |
Present iff outcome is refused, and then it is the ONLY field beside outcome. { code, message, fix, retry, handshake: "intact", balanceCentsAtCheck? } — see Refused (pre-generation). |
The render consumes the handshake record — except on a refusal, which consumes nothing. Bootstrap credentials (wsUrl, wsToken, expiresAt) reach the iframe via the _meta["ai.ggui/render"] slice, not via this response.
Refused (pre-generation)
Section titled “Refused (pre-generation)”A refusal is the THIRD outcome, not a failure: the deployment declined the call before it did any work, so there is no session to report on. ggui_render returns a tools/call result with isError: true and structuredContent of exactly { outcome: "refused", refusal } — every identity field, error and nextStep are absent, and there is no _meta. Nothing was read, nothing was committed, no model spend — the SDK has already checked the call against the declared inputSchema, so this is not a claim that nothing was validated.
{ "outcome": "refused", "refusal": { "code": "insufficient_credit", "message": "The balance this app is funded from is exhausted (checked at render time).", "fix": "The app owner adds funds; the same handshakeId then renders.", "retry": "after-fix", "handshake": "intact", "balanceCentsAtCheck": 0 }}refusal.code comes from PRE_GENERATION_REFUSAL_CODES, the closed refusal registry in @ggui-ai/protocol; a code outside the render-gate subset fails the wire enum loudly. retry is one of after-fix | next-period | later | never, and each after-fix registry row names a fixBy — the party who can act. An agent MUST NOT auto-retry an after-fix refusal whose fixBy is not caller: the fix belongs to someone else and retrying does not perform it. refusal.handshake is always "intact" — the handshake was never read, so the same handshakeId is valid on a retry once the fix lands. Full envelope, hook rule and registry semantics: SPEC §7.1 “Refusal envelope”.
ggui_consume
Section titled “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?, nextStep? }
Keyed by sessionId. THE LOOP: when events is non-empty, react (commonly via ggui_amend to refresh the iframe), then re-call ggui_consume. A non-empty drain says so itself: the result carries nextStep: { tool: 'ggui_amend', description, example, args: { sessionId } }, and its text leads with the same copy-paste example, so the agent is told twice. It means repaint this card in place on the same sessionId, with kind: 'replace' and the full props or kind: 'merge' and a patch; a new render is the exception, for a milestone worth its own card in the transcript. nextStep is absent on an empty drain and on a late drain from an expired render, where the amend it would point at cannot succeed. Exit when you have the events you need — that is the real terminal condition. status: "expired" is the other exit and means no more events will arrive, but it only fires when a render’s TTL actually elapses; on hosted GGUI the default retention is effectively indefinite, so treat expired as a self-hosted case rather than something to wait for. 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”Render the session’s updated state as a new card in the conversation — a new history entry. The history number (epoch) advances by one; the previous card becomes a frozen history record. Use for milestones worth showing in the transcript, or when the original card is no longer visible or usable. Discriminated on kind.
// 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, epoch, warning?, propsSchemaHash?, propsSchemaProfile? } — resourceUri is the epoch-pinned URI of the new record (ui://ggui/render/{sessionId}#N); epoch is the head after the call. A no-op (updated: false + warning) mints nothing: bare head URI, epoch unchanged. propsSchemaHash + propsSchemaProfile (present when the session declares a propsSpec) attest the enforced schema this mutation was validated against — equal to the handshake’s propsSchemaHash under the session-continuity guarantee.
Both modes validate the final props state (post-merge for merge) against the render’s propsSpec and reject on violation. For a structurally different surface, handshake + render a fresh session instead.
ggui_amend
Section titled “ggui_amend”Repaint the already-mounted card in place — no new card appears and the history number does not advance. This is the default move in the gesture loop (consume → domain-tool → ggui_amend): the card the user is looking at updates without losing scroll position, focus, or uncommitted input. Same input union as ggui_update (SPEC §7.1.2.1).
Returns: { sessionId, updated, resourceUri, warning?, propsSchemaHash?, propsSchemaProfile? } — resourceUri is the bare live-head URI; there is no epoch field by construction. The two schema-attestation fields carry ggui_update’s exact semantics.
ggui_amend is not a UI-bound tool: its results carry no _meta, hosts never mint a view for them, and the mounted card receives the new props over the live channels (WS / SSE / polling / bridge-pull). Git reading: ggui_update = commit, ggui_amend = commit –amend.
ggui_emit
Section titled “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”Retrieve full render state — the wire projection — variant, ids, event sequence, lifecycle timestamps — plus the last-known contextSpec values (contextSnapshot) when the render has them. 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 (gguiGetSessionOutputSchema — the protocol owns the schema; nothing outside this table travels):
| Field | Type | Required | Description |
|---|---|---|---|
variant |
'render' | 'mcpApps' |
Yes | Which mount the session is; an MCP-Apps mount projects the same base fields from its store row. |
id |
string |
Yes | Render ID. |
appId |
string |
Yes | Owning app ID. |
eventSequence |
number |
Yes | Server event sequence for this render. |
createdAt |
number (epoch ms) |
Yes | Creation timestamp. |
lastActivityAt |
number (epoch ms) |
Yes | Last-seen activity timestamp (post-heartbeat). |
expiresAt |
number (epoch ms) |
Yes | Expiration timestamp (post-heartbeat). |
contextSnapshot |
object |
No | The last-known value of every contextSpec slot (via ggui_runtime_sync_context); present iff a component (render) mount’s row has one — never on an mcpApps mount. |
ggui_get_render_source
Section titled “ggui_get_render_source”Read the generated source of the calling app’s own render. This is the agent’s window into what it actually shipped — the TSX, the contract that came out of negotiation, and the live prop values. Unlike ggui_get_session, reading source is not an activity signal, so this call never bumps the TTL heartbeat.
| Field | Type | Required | Description |
|---|---|---|---|
sessionId |
string |
Yes | The render whose source you want to read. |
Returns: { sessionId, blueprint: { source, contract?, fixtureProps? } }
| Field | Description |
|---|---|
blueprint.source |
TSX with a default-exported React component. |
blueprint.contract |
Optional — the DataContract envelope reassembled from the render’s propsSpec / actionSpec / streamSpec / contextSpec, when any are present. |
blueprint.fixtureProps |
Optional — the render’s live prop values, when present. A natural preview-props snapshot. |
Only a component-variant render has source. Four cases answer a not-found or typed no-source error rather than an empty string: a render created by another app (cross-tenant and unknown sessionId surface identically, so existence is never leaked), an unknown sessionId, an MCP-Apps or system-card variant, and a render not yet committed.
ggui_list_sessions
Section titled “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”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) 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.
ggui_list_themes
Section titled “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”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”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”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_render_blueprint
Section titled “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”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’suseRender). See WebSocket Protocol.ConsumeEventEntry— per-gesture row on the render-keyed consume pipe, returned byggui_consume. This is what agents read.
ActionEnvelope (live-channel inbound)
Section titled “ActionEnvelope (live-channel inbound)”interface ActionEnvelope<TPayload = JsonValue> { 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)”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”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”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 |
-32007 |
UNAUTHORIZED |
Invalid token or app ID |
-32002 |
SESSION_NOT_FOUND |
Session expired or deleted |
-32003 |
APP_NOT_FOUND |
The endpoint no longer serves this app — unknown, or deprovisioned (then HTTP 403 with data.refusal; see below) |
-32006 |
MOUNT_UNAVAILABLE |
Render locator resolves to no mount |
-32014 |
GENERATION_OVERLOADED |
Platform: the generation queue is saturated — HTTP 503 with Retry-After; the server, not the caller, is the limit |
-32004 is retired-reserved: PRODUCTION_FAILED is now an in-result -32013 is retired-reserved likewise (GGUI#890): the render-rate caps are returned refusals (app_rate_limited, issuer_rate_limited) and no first-party server ever emitted a transport-level rate-limit error after #886. -32005, -32010, -32011, -32012 and -32020 are retired-reserved likewise (GGUI#910): no first-party server ever emitted them; quota and concurrency limits, where a deployment has them, are returned refusals or implementation-range codes, and a contract violation is the Plane-2 contract_violation result (§7.9) or the live channel’s CONTRACT_VIOLATION frame code.
tool-error code. A failed generation returns a tools/call result with
isError: true and schema-conformant structuredContent carrying
error: { code, message } (closed enum PRODUCTION_FAILED | VALIDATION_ERROR | NO_PLATFORM_KEY | NO_CREDENTIALS | GENERATION_QUEUE_OVERLOADED), with no
resourceUri and no _meta — a failed render is not mountable. The
error session is still committed, so the live channel and render
resource keep the failure message. See SPEC §7.9 Plane 3.
-32006 MOUNT_UNAVAILABLE answers a resources/read of a render
locator that identified the render but cannot return a mount for it —
the component behind it is gone, the server keeps no durable record to
restore from, or nothing can deliver it. The closed classification
(NOT_FOUND | BLUEPRINT_UNRESOLVABLE | NOT_SUPPORTED | NOT_MOUNTABLE)
rides on error.data.code; NOT_FOUND rides on -32002 instead. See
SPEC §7.10 for the read contract.
-32003 APP_NOT_FOUND is also the endpoint-level refusal — the one refusal that happens before any tool is called, at the per-app MCP endpoint’s authorization, on any JSON-RPC request from an authenticated caller bound to the app, initialize included. Bound means: for a federated identity, a signature-verified token whose aud equals the endpoint’s {appId} and whose iss equals the app’s federation issuer; for a native key, a key whose bound app (if any) equals {appId}. Identity comes first: an anonymous request is refused 401 with -32007 by the auth adapter before this arm and learns nothing about the app, and a caller bound to a different app gets the bare 403 before any lookup. The refusal is the app’s state, not the caller’s kind — a deprovisioned app answers it to every bound caller, federated or native, and answers it before the subject’s own deny status is read, so a revoked subject learns an id it already holds and nothing else. The answer is HTTP 403 with a JSON-RPC error whose code is -32003, whose message is App not found, and whose data.refusal is the registry projection { code, message, fix, retry, appId } — today exactly code: "app_deprovisioned", retry: "never". appId is non-empty and equals the endpoint path’s {appId} — the app the refused endpoint serves, the id the bound caller already holds — carried as data so a client’s repair loop keys on it instead of parsing the message. data is strict; fixBy never travels (a client reads it from the registry by code). Observable: the conformance kit’s transport-refusal catalog grades the projection (appId present and equal to its input); equality with the path is a route-level obligation the kit cannot see, observed by the deployment’s own route test; an emitter whose projection fails the strict parse degrades to the untyped arm and logs error_mapper_failed. An authorization failure that is not a registry state — no issuer, a foreign issuer, a denied subject on a live app — answers a bare 403 with -32007 and no data, indistinguishable among themselves by contract; a deprovisioned app is the one refusal with a tenant-side fix, which is why it is the one that must be legible. See SPEC §7.1, “Endpoint-level refusal”.
Supported Models
Section titled “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 model route in either wire form — canonical anthropic:claude-haiku-4-5-20251001 or LiteLLM anthropic/claude-haiku-4-5; a value that parses in neither fails the handler input parse at infra.model (a bare model id is one such value). The protocol registry is the allowlist: the current Anthropic lineup is claude-haiku-4-5-20251001, claude-sonnet-5, claude-opus-5, claude-opus-5-5, and claude-fable-5-1 (most capable, ~10× Haiku’s per-render cost), with claude-fable-5 and the still-served 4.6–4.8 generation also accepted; for OpenAI, the GPT-6 ids gpt-6-astra, gpt-6-sol and gpt-6-luna sit beside the GPT-5.6 tiers (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna) and the still-served GPT-5.4/5.5 ids; anything outside the registry is rejected at parse time. On hosted GGUI, a pool-funded render that names no model — no generation.model, no infra.model — runs on the pool default, openai/gpt-6-luna; any other model on the pool needs a rate row for your account, or your own provider key (see Premium models).
Example: Full Render Flow (curl)
Section titled “Example: Full Render Flow (curl)”This walkthrough runs against self-hosted ggui serve (started with --dev-allow-all). Hosted mcp.ggui.ai speaks the same wire — only the URL and bearer change.
# 1. Initializecurl -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": "<sessionId from step 3>", "timeout": 15 } } }'
# No explicit close — the render is a durable record; you just stop addressing it.See Also
Section titled “See Also”- Protocol overview — the three channels at a glance
- WebSocket Protocol — live-channel live events
- MCP Apps — inline rendering inside MCP hosts
- OAuth on mcp.ggui.ai — hosted auth ceremony
- Control MCP plane — design-time and operator agent tools at
/control - Docs MCP route — anonymous docs search at
/docs - Playground · Todos — hosted demo service at
/playground/todos - Playground · MDH — hosted demo service at
/playground/the-million-dollar-homepage - MCP services — mounting standalone services on one server
- Glossary — gadget / tool / blueprint, plus every other term