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. 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.
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'— 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 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).
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 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.
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? }
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.
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 (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.
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