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.


ToolPurpose
ggui_handshakeNegotiate the wire surface before rendering — returns handshakeId + a routed suggestion.
ggui_renderMaterialize the UI; mints sessionId.
ggui_consumeLong-poll buffered user events on one GguiSession.
ggui_updateMutate props on a delivered GguiSession in place.
ggui_emitPush a delivery onto a declared streamSpec channel.
ggui_get_sessionRead GguiSession state + activity timestamps.
ggui_list_sessionsEnumerate GguiSessions by host conversation (resume flows).
ggui_list_gadgetsFetch the renderer-side gadget catalog before authoring a contract.
ggui_list_themesList the theme presets usable via ggui_render({themeId}).
ggui_list_featured_blueprintsEnumerate builder-curated featured blueprints.
ggui_search_blueprintsSemantic search across this app’s blueprints.
ggui_render_blueprintResolve a registered blueprint id to its compiled bundle.
ggui_discoverPlatform capability discovery (hosted-only, coming soon).
ggui_request_credentialOAuth 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:

FieldTypeRequiredDescription
intentstringYesConcise semantic identity — same intent across calls = same component reused. Example: "Gmail inbox for email triage".
blueprintDraftobjectYesSingle-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.
forceCreatebooleanNoSkip 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? }

FieldTypeDescription
handshakeIdstringStable id — pass to ggui_render. Records are SINGLE-USE and expire after 10 minutes.
actionenumOne of create / reuse / update / replace / declined.
suggestionobjectRouted suggestion. Carries origin: 'cache' | 'agent' | 'synth', an always-present provisional blueprintMeta (incl. blueprintId), and conditional amendments (synth-only) / validationFindings (soft on cache).
nextStepobjectWire-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": {} } }
FieldTypeRequiredDescription
handshakeIdstringYesFrom a prior ggui_handshake response.
propsobjectYesRuntime 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.
themeIdstringNoPer-render theme preset override — wins over App.defaultThemeId for THIS render. Discover ids via ggui_list_themes. Omit to inherit the app theme.
infraobjectNo{model?} — provider-prefixed per-render model override (e.g. anthropic/claude-haiku-4-5). Strict — unknown keys are rejected.
overrideobjectNoOmit to ACCEPT the suggestion as-is. Provide {contract?, variance?} to re-aim: override.contract re-drafts the contract (STRICT — must already conform) and cold-gens; override.variance re-aims the variant axis.

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

FieldTypeDescription
sessionIdstringGlobally-unique id (UUID) for the delivered render. Use for ggui_consume / ggui_update.
resourceUristringSpec-canonical MCP-Apps entry point — ui://ggui/render/<id>, mirrored on the tool result’s _meta.ui.resourceUri. A host mounts the render from this; there is no clickable URL on the wire.
actionenumOne of create / reuse / update / replace / declined.
contractHashstringCanonical hash of the rendered data contract (shape only — fields, types, specs). Same hash ⟺ same data flow.
blueprintIdstringOpaque id of the materialised component. Equal across two renders ⟺ the same cached component was served (a fresh gen mints a new id).
variantKeystringCanonical hash of the design-time variance. With contractHash it forms the reuse key.
cacheobjectReuse outcome — { hit, similarity?, cachedBlueprintId?, llmCallsAvoided, kind?, reason? }.
nextStepobjectEmitted ONLY when the rendered contract has a non-empty actionSpec. Points at ggui_consume({sessionId}) for the inbound action loop. Pure-display renders get no nextStep.

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

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

FieldTypeRequiredDescription
sessionIdstringYesRender to consume from. Globally unique.
timeoutnumberNoLong-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": { } }
FieldTypeRequiredDescription
sessionIdstringYesThe render to mutate (UUID from ggui_render response).
kindenumYes'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).
propsobjectIf replaceFull replacement props map. Required when kind: 'replace'.
patchobjectIf mergeRFC 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.

FieldTypeRequiredDescription
sessionIdstringYesRender to stream to. Server enforces app-ownership.
channelstringYesChannel name declared on the render’s streamSpec. Undeclared channels reject.
payloadunknownYesDelivery payload. Validated against streamSpec[channel].schema.
completebooleanNoTerminal-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).

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

FieldTypeRequiredDescription
hostNamestringNoFilter by host identifier (claude.ai, sample, …). Pair with hostSessionId to target one conversation.
hostSessionIdstringNoThe host’s opaque conversation-grouping key (e.g. a claude.ai thread id). Typically paired with hostName.
limitnumberNoMax 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 0.3.0) is the structural floor; gadgets declared in ggui.json#app.gadgets layer on top (declared wins on a package collision).

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

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

FieldTypeRequiredDescription
querystringYesNatural-language description of the UI you’re looking for.
limitnumberNoMax 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? }

FieldTypeDescription
protocolVersionstringggui protocol revision (prelaunch drafts use draft-YYYY-MM-DD; first frozen release will be 1.0.0).
contentTypesstring[]Bundle content types this deployment serves (e.g. application/javascript+react).
shellTypesstring[]Available shell flavors (chat, fullscreen, spatial).
adapterTypesstring[]Adapter families wired on this deployment (voice, camera, location, bluetooth).
componentCapabilitiesstring[]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.
appobject?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.

FieldTypeRequiredDescription
serviceIdstringYesOAuth service identifier (matches an McpServiceConfig entry — e.g. "bashdoor", "ubot").
reasonstringNoOne-line rationale shown to the user inside the consent overlay.
sessionIdstringNoExisting render to surface the consent UI into. Required to actually surface the overlay.

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

FieldTypeDescription
grantedbooleanWhether 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.
reasonstringDenial / 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.

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

TypeCategoryDescription
data:submitDataUser 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.

CodeNameDescription
-32700PARSE_ERRORInvalid JSON in request
-32600INVALID_REQUESTNot a valid JSON-RPC object
-32601METHOD_NOT_FOUNDUnknown method name
-32602INVALID_PARAMSMissing or invalid tool arguments
-32603INTERNAL_ERRORServer-side failure
-32001UNAUTHORIZEDInvalid token or app ID
-32002SESSION_NOT_FOUNDSession expired or deleted
-32003APP_NOT_FOUNDApp ID does not exist
-32004PRODUCTION_FAILEDUI production failed
-32005CAPABILITY_DENIEDRequested capability not granted
-32010GENERATION_QUOTA_EXCEEDEDPlatform: generation quota exhausted
-32011APP_LIMIT_EXCEEDEDPlatform: app-count ceiling reached
-32012CONCURRENT_SESSION_LIMITPlatform: too many live sessions
-32013RATE_LIMIT_EXCEEDEDPlatform: reserved rate-limit code
-32020CONTRACT_VIOLATIONPlatform: contract validation failed

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.