Event System
read as.md User actions originate in the renderer (on the live channel), land in the server, and reach the agent through one of two read paths. This page covers the closed vocabulary of events, the canonical envelope, how subscriptions gate what gets delivered, and how an agent or React SDK consumer actually reads them.
For the full wire grammar of the envelope itself, see Envelopes. For the channel topology, see the Architecture overview.
Event vocabulary
Section titled “Event vocabulary”The protocol recognizes exactly one event type. Every user gesture that drives a turn is a data:submit. The earlier data:change / lifecycle:* / interaction:* / error:* vocabulary was deleted (draft-2026-06-12) — those types never had a producer.
type EventType = "data:submit"; // the only memberA data:submit is schema-validated against the render’s actionSpec.
The envelope
Section titled “The envelope”Every user gesture arrives as a flat ActionEnvelope:
interface ActionEnvelope<TPayload = JsonValue> { sessionId: string; // bound at subscribe time; server rejects mismatches type: EventType; payload?: TPayload; // for `data:submit`: { action, data?, tool? } clientSeq?: number; // client-monotonic dedup hint schemaVersion?: string; // producer's PROTOCOL_SCHEMA_VERSION (advisory)}The envelope is intentionally flat — no nested event / context / meta blocks. Render-level diagnostics (device info, interface context, user identity) are captured once at subscribe time on the server, not per-delivery. See Envelopes — “Fields intentionally NOT on the envelope” for the rationale.
Subscription gating
Section titled “Subscription gating”Delivery gating falls out of the contract’s actionSpec: every declared action emits a data:submit envelope the agent reads via ggui_consume. The old per-event EventSubscription filter (an allowlist of event types at render time) and the DEFAULT_SUBSCRIPTION constant were deleted from the protocol (no shims) — there is no wire-level subscribe object on render, and data:submit is now the only event type.
The current ggui_render input is {handshakeId, props, themeId?, infra?, override?} — props is required (pass {} when the contract declares no propsSpec), and there is no subscribe field.
Two reader paths, one envelope
Section titled “Two reader paths, one envelope”The same ActionEnvelope reaches consumers through two distinct seams. Don’t conflate them.
| Consumer | Path | Shape returned |
|---|---|---|
| Agent (server-side, LLM-driven) | Long-polls ggui_consume over MCP | ConsumeEventEntry |
| Renderer SDK (browser, e.g. React) | Live tail on the WebSocket subscribe seam | ActionEnvelope |
ggui_consume is the agent’s read path. It’s render-keyed, consume-once, and the row shape (ConsumeEventEntry) carries a tiny bit of extra context the LLM needs to route the gesture. The WebSocket subscribe seam is what the iframe-runtime uses to deliver interaction events into the rendered component. See MCP Protocol — Events for both shapes.
Host relay + the ai.ggui/userAction doorbell
Section titled “Host relay + the ai.ggui/userAction doorbell”In MCP-Apps hosts the gesture reaches the server via the host’s ggui_runtime_submit_action tools/call relay instead of the WS. If the response reports consumerPresent: false (no ggui_consume long-poll is draining — e.g. after a page reload), the iframe emits a ui/message whose text directs the agent to call ggui_consume({sessionId}), with an optional structured mirror on content[0]._meta["ai.ggui/userAction"] — a pure pointer; the gesture itself is only ever drained via ggui_consume.
The MCP control surface
Section titled “The MCP control surface”The agent never talks to the live channel directly — it renders UI, polls events, discovers gadgets, and browses the blueprint marketplace over MCP. The canonical agent-callable tool surface (lifecycle, capability discovery, stream emit, and blueprint marketplace) is enumerated in the MCP Protocol reference — that page is the single source of truth, with field-level shapes and return types. Linking out instead of duplicating here keeps this page from drifting as the tool surface evolves.
The ggui_consume long-poll in particular is the agent’s event read path — its return shape (ConsumeEventEntry) is covered in MCP Protocol — Events.
Outbound traffic on the same channel
Section titled “Outbound traffic on the same channel”ActionEnvelope is the inbound half of the live channel. Server-to-renderer traffic on the same WebSocket arrives as StreamEnvelope (one delivery per named streamSpec channel). Contract violations are not a separate channel: an invalid inbound action is answered with a typed error frame (CONTRACT_VIOLATION) on the live channel, and a ggui_render / ggui_emit validation failure rejects the agent’s own tool call. The former _ggui:contract-error channel and its ContractErrorPayload vocabulary were removed (draft-2026-06-11). Both surviving shapes are covered in Envelopes.
Generation progress events
Section titled “Generation progress events”While the server is generating a fresh UI (no blueprint hit), it emits progress on the reserved _ggui:lifecycle channel (a {type:'data'} frame) that the renderer surfaces as a loading state. The canonical GguiLifecyclePayload vocabulary lives in packages/protocol/src/types/lifecycle.ts:
handshake_started → handshake_completed → render_started → consume_pollingThese feed the built-in progress UI inside the rendered iframe (the iframe-runtime) — your end-user sees real-time feedback instead of a frozen iframe.
React SDK integration
Section titled “React SDK integration”In the web consumer path the live-channel WebSocket — and the progress events above — are owned by the iframe-runtime inside the sandboxed <AppRenderer>, not by host code. The host doesn’t open the socket, set a wsEndpoint, or handle ActionEnvelopes directly: the wsUrl is server-stamped on the render’s ai.ggui/render slice, the iframe connects + subscribes + resumes on its own, and the progress UI animates inside the iframe.
import { AppRenderer } from "@mcp-ui/client";import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers";
// Mount the render; the iframe-runtime drives the live channel + progress UI.const { sessions, handleAppMessage } = useMcpAppsChat({ chatEndpoint });// <AppRenderer ... onMessage={handleAppMessage} />Structured signals the host may want (dispatch telemetry, subscribe failures, version mismatches, auth-required) surface on the ggui:observe postMessage channel. See React SDK for the host surface and Error Handling → renderer-side faults for the observability events.
Where to next
Section titled “Where to next”- Envelopes — canonical live-channel wire grammar
- MCP Protocol — agent-side control plane
- WebSocket Protocol — renderer-side live channel
- Architecture overview — three-channel topology