Skip to content

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 (coming soon) will use OAuth 2.0 with Dynamic Client Registration — Claude Desktop and other MCP-Apps hosts run the ceremony for you; raw-HTTP callers present the issued bearer token on every call. See OAuth on mcp.ggui.ai 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 (mints sessionId)
5. ggui_consume → Long-poll for user events (keyed by sessionId)
6. ggui_update → Mutate props in place (never re-render)
7. ggui_emit → Optional: push frames on a streamSpec channel

Renders decay implicitly via TTL — there is no explicit close ceremony. ggui_update and ggui_consume are keyed by sessionId (globally unique); the server tenancy-checks via the bearer token.

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; mints sessionId.
ggui_consume Long-poll buffered user events on one GguiSession.
ggui_update Mutate props on a delivered GguiSession in place.
ggui_emit Push a delivery onto a declared streamSpec channel.
ggui_get_session Read GguiSession state + activity timestamps.
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, coming soon).
ggui_request_credential OAuth consent proxy (hosted-only, coming soon).

Negotiate the wire surface for a UI. Call BEFORE ggui_render. The agent posts a draft; the server runs blueprint-search + contract-validation in parallel and returns a routed suggestion the agent then accepts or overrides on render.

Top-level fields:

Field Type Required Description
intent string Yes Concise semantic identity — same intent across calls = same component reused. Example: "Gmail inbox for email triage".
blueprintDraft object Yes Single-field draft wrapping the agent’s contract (required) plus optional variance and optional generator slug hint. The contract drives blueprint-search embed/structural axes; variance feeds the variance axis.
forceCreate boolean No Skip blueprint-search and route straight to validation + agent-mode suggestion against the draft. Use after a prior handshake returned an unwanted cache suggestion.

Returns: { handshakeId, action, suggestion, nextStep? }

Field Type Description
handshakeId string Stable id — pass to ggui_render. Records are SINGLE-USE and expire after 10 minutes.
action enum One of create / reuse / update / replace / declined.
suggestion object Routed suggestion. Carries origin: 'cache' | 'agent' | 'synth', an always-present provisional blueprintMeta (incl. blueprintId), and conditional amendments (synth-only) / validationFindings (soft on cache).
nextStep object Wire-shape recovery hint — {tool: 'ggui_render', example} worked-literal of the next call.

The agent branches the paired ggui_render on suggestion.origin: any origin can be accepted (reuse the provisional blueprintId verbatim) or overridden (mint fresh against a new draft).

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?} — provider-prefixed per-render model override (e.g. anthropic/claude-haiku-4-5). Strict — unknown keys are rejected.
override object No Omit to ACCEPT the suggestion as-is. Provide {contract?, variance?} to re-aim: override.contract re-drafts the contract (STRICT — must already conform) and cold-gens; override.variance re-aims the variant axis.

Returns: { sessionId, resourceUri?, action, contractHash, blueprintId, variantKey, cache, error?, nextStep? }

Field Type Description
sessionId string Globally-unique id (UUID) for the delivered render. Use for ggui_consume / ggui_update.
resourceUri string Optional — present iff the render is mountable (absent on failure). Spec-canonical MCP-Apps entry point — ui://ggui/render/<id>, mirrored on _meta.ui.resourceUri; no clickable URL on the wire.
action enum One of create / reuse / update / replace / declined.
contractHash string Canonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow.
blueprintId string Opaque id of the materialised component. Equal across two renders ⟺ the same cached component was served (a fresh gen mints a new id).
variantKey string Canonical hash of the design-time variance. With contractHash it forms the reuse key.
cache object Reuse outcome — { hit, similarity?, cachedBlueprintId?, llmCallsAvoided, kind?, reason? }.
nextStep object Emitted ONLY when the rendered contract has a non-empty actionSpec. Points at ggui_consume({sessionId}) for the inbound action loop. Pure-display renders get no nextStep.
error object Optional — present iff the tool result is isError: true. { code, message } with the closed enum PRODUCTION_FAILED | VALIDATION_ERROR | NO_PLATFORM_KEY | NO_CREDENTIALS. Failed renders carry no resourceUri and no _meta.

The render consumes the handshake record. Bootstrap credentials (wsUrl, wsToken, expiresAt) reach the iframe via the _meta["ai.ggui/render"] slice, not via this response.

Long-poll for buffered user events on one render. Events drain on read (consume-once semantics). Call this right after every ggui_render whose response carries nextStep.tool === 'ggui_consume'.

Field Type Required Description
sessionId string Yes Render to consume from. Globally unique.
timeout number No Long-poll seconds — integer in [0, 25]; 0 = immediate (default). Values outside [0, 25] reject INVALID_PARAMS. Returns on the first event or at timeout; re-call on empty to keep waiting — a longer wait is your loop, not a bigger timeout (pick 5–15s typical, 25 max).

Returns: { events: ConsumeEventEntry[], status: "active" | "expired", client? }

Keyed by sessionId. THE LOOP: when events is non-empty, react (commonly via ggui_update to refresh the iframe), then re-call ggui_consume. The render stays active until its TTL elapses — status: "expired" means no more events will arrive, so the long-poll loop terminates. Exit when you have the events you need, or when status is expired. The optional client field echoes mid-render host observations (window resize, fullscreen toggle, etc.) without forcing a fresh handshake.

Mutate props on a delivered render in place. Targets sessionId directly. 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 }

resourceUri is unchanged from the initial render — the same ui://ggui/render/{sessionId} the mount stamped. Both modes validate the final props state (post-merge for merge) against the render’s propsSpec and reject on violation. Use for partial UI state changes; for a structurally different surface, handshake + render a fresh one. Post-update the iframe receives the new props via the live-channel props_update WS frame.

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 GguiSession state — id, appId, event sequence, activity timestamps. Bumps the activity heartbeat on every successful read. Omits componentCode + sourceCode (those live on the renderable surface, not the agent-visible one).

Field Type Required Description
sessionId string Yes GguiSession to inspect.

Returns: { id, appId, eventSequence, createdAt, lastActivityAt, expiresAt }

createdAt / lastActivityAt / expiresAt are epoch milliseconds (numbers).

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

Request OAuth consent from the end user via the Portal’s consent overlay. Blocks up to 25 seconds polling for the user’s choice (Allow once / Always allow / Deny). Short-circuits with granted: true when a prior grant already exists for this user + app + service.

Field Type Required Description
serviceId string Yes OAuth service identifier (matches an McpServiceConfig entry — e.g. "bashdoor", "ubot").
reason string No One-line rationale shown to the user inside the consent overlay.
sessionId string No Existing render to surface the consent UI into. Required to actually surface the overlay.

Returns: { granted, mode?, service?, reason? }

Field Type Description
granted boolean Whether the user (or a prior grant) approved.
mode 'once' | 'always' Grant mode when granted: true. Absent on denial / timeout.
service { name, icon } Display info pulled from McpServiceConfig. Echoed back for UI parity.
reason string Denial / timeout / error rationale when granted: false.

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
-32001 UNAUTHORIZED Invalid token or app ID
-32002 SESSION_NOT_FOUND Session expired or deleted
-32003 APP_NOT_FOUND App ID does not exist
-32005 CAPABILITY_DENIED Requested capability not granted
-32010 GENERATION_QUOTA_EXCEEDED Platform: generation quota exhausted
-32011 APP_LIMIT_EXCEEDED Platform: app-count ceiling reached
-32012 CONCURRENT_SESSION_LIMIT Platform: too many live sessions
-32013 RATE_LIMIT_EXCEEDED Platform: reserved rate-limit code
-32020 CONTRACT_VIOLATION Platform: contract validation failed

-32004 is retired-reserved: PRODUCTION_FAILED is now an in-result 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), 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.


There is no fixed model menu. The operator sets the per-app default via ggui.json#generation.model — any provider-prefixed route, written provider:model (canonical) or LiteLLM-style provider/model, e.g. anthropic:claude-haiku-4-5-20251001. Providers on the self-hosted BYOK path: anthropic, openai, google, openrouter (bedrock routes are hosted-runtime-only and rejected by ggui serve).

Agents can override the model per render via ggui_render({ infra: { model } }) with a provider-prefixed id (e.g. anthropic/claude-haiku-4-5).


This walkthrough runs against self-hosted ggui serve (started with --dev-allow-all). Hosted mcp.ggui.ai (coming soon) will speak the same wire — only the URL and bearer change.

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 }
}
}'
# Renders decay implicitly via TTL — no explicit close.