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

MCP Protocol Reference

read as .md

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

Self-hosted (ggui serve):

POST http://127.0.0.1:6781/mcp

Local dev: start the server with ggui serve --dev-allow-all and any bearer (conventionally dev) authenticates as the builder identity:

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

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 (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 channel
9. ggui_get_render_source → Optional: read back the source you shipped

There 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. Where a lifetime bound genuinely matters, a render carries an OPTIONAL per-render ttlSeconds; on hosted ggui the default retention is effectively indefinite.

ggui_amend, ggui_update, and ggui_consume are keyed by sessionId (globally unique); the server tenancy-checks via the bearer token.

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.


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

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, 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 (incl. blueprintId), 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).

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 cold-gens; 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 ⟺ the same cached component was served (a fresh gen 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.

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

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_amend to refresh the iframe), then re-call ggui_consume. 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 or explicit-ttlSeconds 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.

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.

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.

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.

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.

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.

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.

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.

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

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.

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.

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

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.


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.
  • ConsumeEventEntry — per-gesture row on the render-keyed consume pipe, returned by ggui_consume. This is what agents read.
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
}
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.

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.


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


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 (the hosted pool default), claude-sonnet-5, claude-opus-5, and claude-fable-5-1 (most capable, ~10× the default’s per-render cost), with claude-fable-5 and the still-served 4.6–4.8 generation also accepted; anything outside the registry is rejected at parse time.


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.

Terminal window
# 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": "<sessionId from step 3>", "timeout": 15 }
}
}'
# No explicit close — the render is a durable record; you just stop addressing it.