/** * MCP Apps integration — outbound delivery types for ggui. * * This module is the **boundary** for MCP Apps outbound delivery. Anything * MCP-Apps-specific that ggui exposes to the rest of the codebase lives * here — never in `types/live-channel.ts`, `types/mcp.ts`, `types/render.ts`, * or any other core module. Consumers opt in via the subpath import: * * ```ts * import { * MCP_APPS_UI_CAPABILITY, * GGUI_RENDER_RESOURCE_URI, * parseMcpAppAiGguiRenderMeta, * type McpAppAiGguiRenderMeta, * } from '@ggui-ai/protocol/integrations/mcp-apps'; * ``` * * The root `@ggui-ai/protocol` barrel does NOT re-export this module. * That's the isolation rule: core protocol consumers that don't integrate * with MCP Apps pay none of its weight, and the blast radius of any spec * drift is bounded to callers that explicitly import from here. * * Core still carries two fields that make the bootstrap flow work — * `SubscribePayload.wsToken?: string` and `AckPayload.sessionToken?: * string`. Those are deliberately framed as **general transport bootstrap * credentials** (opaque strings), not MCP-Apps-specific. Any future * bootstrap mechanism (short-code auto-login, signed-URL bootstrap, etc.) * reuses the same slots. */ import type { JsonObject, JsonSchema, JsonValue } from '../types/data-contract.js'; import { type AppTheme } from '../schemas/app-theme.js'; import type { z } from 'zod'; import type { gguiSessionSummaryWireSchema } from '../schemas/mcp.js'; import type { DeepReadonly } from '../types/readonly.js'; /** * MCP capability name ggui servers advertise in their MCP `initialize` * response capabilities when they implement the MCP Apps outbound path. * Spec-canonical; MUST match the string the MCP Apps protocol publishes. */ export declare const MCP_APPS_UI_CAPABILITY: "io.modelcontextprotocol/ui"; /** * The single MCP Apps resource URI ggui exposes for outbound delivery. * `ggui_render` is the sole tool declaration that carries this in its * `_meta.ui.resourceUri`. No other ggui tool gets a resource URI — * `ggui_render` is the single outbound entry point. */ export declare const GGUI_RENDER_RESOURCE_URI: "ui://ggui/render"; /** * MIME type for the `ui://ggui/render` resource. Per MCP Apps spec, UI * resources carry the `text/html` base type with a `profile=mcp-app` * parameter so hosts that don't support MCP Apps don't accidentally * render them as plain HTML. */ export declare const GGUI_RENDER_RESOURCE_MIME: "text/html;profile=mcp-app"; export { composeEpochUri, parseEpochUri, EPOCH_URI_SEPARATOR, type ParsedEpochUri, } from './epoch-uri.js'; /** * The single `_meta.ui.resourceUri` value ggui uses across every MCP Apps * host surface. Exposed as a named constant so tool-declaration code, * resource-serving code, and tests all agree on one spelling. */ export declare const GGUI_RENDER_UI_META: { /** Resource URI hosts fetch via `resources/read` on a `ggui_render` tool call. */ readonly resourceUri: "ui://ggui/render"; /** Only `"model"` — outer agent can call, iframe views cannot. */ readonly visibility: readonly ["model"]; }; /** * Visibility tag carried in `_meta.ui.visibility` on a tool declaration. * Per MCP Apps spec, controls who can invoke the tool: * - `"model"` — outer agent can call (default in practice) * - `"app"` — only an MCP Apps view (iframe) can call, hidden from agent */ export type McpAppsToolVisibility = 'model' | 'app'; /** * Phase B render-identity collapse — the previously two-slice wire * (`ai.ggui/session` + `ai.ggui/stack-item`) is merged into ONE slice * (`ai.ggui/render`). Consumers parse with {@link parseMcpAppAiGguiRenderMeta} * and read fields directly off the {@link McpAppAiGguiRenderMeta} struct. * * Why the merge: every "session" wrapped exactly one stack item post- * Phase-A, so the two slices were always activated in lock-step. The * pair-holder added ceremony with no signal. Flat is the honest shape. */ /** * Precompiled, eval-free validators for a contract's runtime-validated * specs — served at `_meta["ai.ggui/contract"].validatorsUrl` as the * `default` export of a content-addressable ES module. * * Each value is the SOURCE TEXT of an ES module whose `default` export * is an Ajv validator function (`(data) => boolean`, carrying * `.errors` after a run) — the output of `compileValidatorModule`. * The iframe-runtime loads each via a `blob:` dynamic import. * * @public */ export interface CompiledContractValidators { /** Validator for inbound runtime props (`DataContract.propsSpec`). */ readonly props?: string; /** * Validators for outbound action envelopes, keyed by action name * (`DataContract.actionSpec`). */ readonly actions?: Readonly>; /** * Validators for inbound stream payloads, keyed by channel name * (`DataContract.streamSpec`). */ readonly streams?: Readonly>; /** * Validators for inbound context-slot values, keyed by slot name * (`DataContract.contextSpec`). */ readonly context?: Readonly>; } /** * Derives the PascalCase Context name from a contextSpec slot key. * E.g., `currentStep` → `CurrentStepContext`. Consumed by the server * (when populating bootstrap.contextSlots) and the iframe-runtime * boilerplate (when generating destructuring lines). * * Edge cases: * - Empty input → `'Context'` (caller-fault path; documented for * determinism). * - Single-character input → `Context` (e.g. `'a'` → `'AContext'`). * * @public */ export declare function deriveContextName(slotKey: string): string; /** * `_meta` key carrying the full render slice. Single source of truth * post-Phase-B: identity, boot wiring, live-channel auth, capability * advertisements, current render state, contract pointer, and component- * mode discriminator. * * @public */ export declare const MCP_APP_AI_GGUI_RENDER_META_KEY: "ai.ggui/render"; /** * Single entry in the {@link McpAppAiGguiRenderMeta.gadgets} catalog. * One per registered gadget package — the iframe-runtime * dynamic-imports each at boot and stores the loaded namespace under * `globalThis.__ggui__.gadgets[package]`. * * @public */ export interface McpAppGadgetRef { /** Bare npm package name (e.g. `@my-org/leaflet`). REQUIRED — it * is the registry key the iframe-runtime stores the loaded module * namespace under at `globalThis.__ggui__.gadgets[package]`, and * the bare-specifier load source when `bundleUrl` is absent. */ readonly package: string; /** ggui-hosted ESM bundle URL — preferred load source when present * (same-origin posture, CSP-friendly). The iframe `await import(this)`; * absent → the iframe imports the bare `package` specifier. */ readonly bundleUrl?: string; /** SHA-384 SRI hash of the bundle (`sha384-`). * When present alongside `bundleUrl`, iframe-runtime routes the * load through a `` gate so * the browser refuses execution on hash mismatch. */ readonly bundleSri?: string; } /** * Single entry in {@link McpAppAiGguiRenderMeta.contextSlots}. * One per `contextSpec` slot — the iframe-runtime synthesizes one * `React.createContext(default)` per entry at boot. * * @public */ export interface McpAppContextSlot { /** Slot key — camelCase JS identifier from `contextSpec`. */ readonly name: string; /** PascalCase Context name auto-derived from `name`. Used as the * registry key in `globalThis.__ggui__.contexts`. */ readonly contextName: string; /** JsonSchema for the slot value — used by the runtime observer to * validate Provider values before posting `ui/update-model-context`. */ readonly schema: JsonSchema; /** Initial Provider value. Always populated by the server. */ readonly default: JsonValue; /** Per-slot debounce override in milliseconds. Omitted → runtime * applies `DEFAULT_CONTEXT_DEBOUNCE_MS` (300). `0` = immediate. */ readonly debounceMs?: number; } /** * The full render slice — flat post-Phase-B. Identity + boot wiring + * live-channel auth + capability advertisements + render state + * contract pointer + component-mode discriminator. * * **Identity.** `sessionId` is the value an iframe's bootstrap meta and * every wire reference (props_update, consume, update) keys by. The * value is the same one stack items carried as `stackItemId` pre-Phase-B; * the rename reflects the conceptual collapse (no enclosing vessel). * * **Live-channel auth.** `wsUrl` + `wsToken` are paired — both present * or both absent; `expiresAt` is informational. `wsToken` is the opaque * WS auth credential the iframe threads on the WebSocket upgrade as * `?wsToken=` and inside `SubscribePayload.wsToken`. * * **Failover ladder (WS → SSE → polling).** `sseUrl` and `pollingUrl` * are the two HTTP fallback rungs the iframe-runtime descends when the * WS rung fails (host CSP block, proxy, network policy). Both are * stamped by the SERVER via {@link composeSessionApiUrls} — one * composer for every stamping surface — and consumed by the * IFRAME-RUNTIME. Auth for both is the embedded `wsToken` query * credential (`EventSource` cannot set request headers, so the query * string is the only channel — same posture on both rungs), which is * why each URL is stamped ONLY when the live trio was minted. * Absence semantics: `sseUrl` absent ⇒ no SSE rung (ladder is * WS → polling); `pollingUrl` absent ⇒ no polling rung. * * `pollingUrl` (server-stamped * `/api/sessions//events?wsToken=`): the * iframe-runtime composes per-tick `&sinceSequence=&limit=` * against this base; companion to `lastSequence` which seeds the * initial cursor. * * `sseUrl` (server-stamped * `/api/sessions//stream?wsToken=`): consumed via * `EventSource`. Server obligations — the endpoint serves * `text/event-stream` where each SSE message's `data:` is EXACTLY one * ChannelFrame JSON (`{type, payload}` — byte-same shape as the WS * push); a message's `id:` is the decimal event-ledger `seq` (the SAME * cursor space as `lastSequence`/`sinceSequence`), stamped ONLY on * ledger-backed `render_event` replay frames — live frames are id-less * (same ephemeral posture as WS) — so the browser's automatic * `Last-Event-ID` reconnect header IS the existing cursor replay; the * server emits a comment heartbeat (`: hb`, no `id:`) at least every * ~25s (proxy/ALB idle-timeout floor) and `retry: 3000` as the first * write after headers. Client obligations — append * `&sinceSequence=` on FIRST connect (before any * `Last-Event-ID` exists); on reconnect the header WINS over the * query. Failure modes mirror `/events`: 401 invalid token; 404 * evicted → stop; 410 expired / `REPLAY_HORIZON_PASSED` → * re-bootstrap (the horizon violation is an in-band error frame * carrying `resumeId: String(lastSequence)`, so the browser cursor * advances and the ack snapshot re-mounts state). * * **Mode discriminator.** At least one of `{ codeUrl, codeB64, kind, * wsUrl-with-token }` MUST be present for the iframe to mount. `kind` is * mutually exclusive with `codeUrl`/`codeB64` (kind = system-card mode; * codeUrl/codeB64 = static-component mode, fetched vs inline; * live-channel = absent all). * * @public */ export interface McpAppAiGguiRenderMeta { readonly sessionId: string; readonly appId: string; readonly runtimeUrl: string; readonly wsUrl?: string; readonly wsToken?: string; readonly expiresAt?: string; readonly pollingUrl?: string; readonly sseUrl?: string; readonly themeId?: string; readonly themeMode?: 'light' | 'dark'; /** * Resolved per-app theme overlay (ggui#987): the projection for BOTH * modes (`overlays.light` / `overlays.dark`), the mode-agnostic * `cssVariables` on top, per-mode keyframes, the `overlayHash` * attestation, an optional default `mode`, and (ggui#1093) the declared * assets — `fonts` (faces the host admits under `font-src` and the shell * inlines) and `imagery` (mark / hero / pattern), both outside the * attestation — snapshotted from `App.theme`. Distinct from `themeId` * (a compiled-theme reference) and `themeMode` (the bare light/dark * discriminator): the iframe injects `overlays[effectiveMode]` then * `cssVariables` as `:root` declarations. Absent ⇒ no per-app overlay; * the renderer applies its default theme. Parsed at the READ door * (`parseAppThemeAtReadDoor`, VERSION-POLICY §3.6): a top-level member * this release does not name is stripped and reported, never dropped * with the theme. */ readonly theme?: AppTheme; readonly gadgets?: ReadonlyArray; readonly publicEnv?: Readonly>; readonly streamWebSocketLocalTools?: readonly string[]; readonly permissionsPolicy?: readonly string[]; /** * Monotonic sequence number of the most-recent event applied to * this render's event ledger. Stamped on every emission (render, * update, `GET /api/sessions/:id/state` read, MCP `resources/read` * of `ui://ggui/render/`). Consumers use it to initialize * polling cursors aligned with the event ledger — see the * `/api/sessions/:id/events?sinceSequence=N` endpoint that reads * from a cursor. */ readonly lastSequence?: number; /** * History head epoch this mount belongs to (#483, SPEC §7.1.2.2). * `ggui_render` mints epoch 0; each `ggui_update` result carries its * new epoch; a pinned `#N` read carries N. The iframe reads it at * boot as its OWN epoch: when a later `props_update` frame carries a * HIGHER epoch, this mount has been superseded and freezes (the * freeze latch). Absent ⇒ 0. */ readonly epoch?: number; readonly propsJson?: string; readonly contextSlots?: ReadonlyArray; readonly contractHash?: string; readonly validatorsUrl?: string; readonly codeUrl?: string; readonly codeHash?: string; /** * Strict-CSP module variant of {@link codeUrl} (ggui#522 slice 2): * the SAME component bytes, server-side import-rewritten so every * bare specifier resolves to a static shim asset on the code origin * — directly `import()`able under a `script-src` that allows only * that origin (no `blob:`, no `data:`, no `'unsafe-eval'`). The * renderer tries this FIRST when present and falls back to the * codeB64/codeUrl blob ladder on any failure. Never a mode * discriminator on its own — always accompanies `codeUrl`/`codeB64` * (the raw bytes remain the fallback), so its presence adds a load * path without changing mountability. */ readonly codeModuleUrl?: string; /** * Base64-encoded compiled ES-module source of the component — the * fetch-free twin of {@link codeUrl}. Produced by servers whose * renders must mount inside hosts that forbid cross-origin fetches * at the iframe CSP layer (no `connect-src`, no external * `script-src`); consumed by the iframe-runtime's seed builder, * which decodes it instead of fetching. MAY coexist with `codeUrl` * (a consumer prefers the inline bytes; the URL remains for * cache-addressable consumers); mutually exclusive with `kind`. * Malformed base64 fails at decode time as a boot failure — the * parser here validates shape only (non-empty string). */ readonly codeB64?: string; readonly kind?: string; } /** * Discriminated result of {@link parseMcpAppAiGguiRenderMeta}. The parser * does structural slice-shape validation only — `MALFORMED_RENDER` * surfaces a structurally-invalid slice (wrong type, missing required * identity, paired fields half-present, mutually-exclusive fields both * present). Missing key entirely is NOT a failure; the "is the slice * required for THIS consumer" gate lives in consumer-side mount check. * * @public */ export type ParseMcpAppAiGguiRenderMetaResult = { readonly ok: true; readonly meta?: McpAppAiGguiRenderMeta; } | { readonly ok: false; readonly reason: 'MALFORMED_RENDER'; }; /** * Read the `ai.ggui/render` slice off a parsed JSON-RPC `_meta` object. * * Structural validation only. Missing key returns `{ok: true, meta: undefined}` * — not a failure. Required-fields gate (sessionId / appId / runtimeUrl) * fires only when the key is present. Field-level optional-field * defensive parsing (e.g. context-slot schema narrowing, expiresAt date * parse) lives downstream in the iframe-runtime's `validateMeta`. * * @public */ export interface ParseMcpAppAiGguiRenderMetaOptions { /** * Called when a `theme` is present but the read door refuses it * (`parseAppThemeAtReadDoor`: a reason the write door would refuse too) — * the slice drops it (tolerant degrade) but MUST NOT be silent * (ggui#987 §3.4): the caller logs or emits its observability event. */ readonly onInvalidTheme?: ((issues: readonly string[]) => void) | undefined; /** * Called when the read door KEPT the theme but stripped top-level members * this release does not name (ggui#1093 belt, VERSION-POLICY §3.6) — the * caller logs the keys (never theme content). The theme in the slice is * the stripped one; nothing else changes. */ readonly onStrippedThemeMembers?: ((keys: readonly string[]) => void) | undefined; } export declare function parseMcpAppAiGguiRenderMeta(meta: unknown, options?: ParseMcpAppAiGguiRenderMetaOptions): ParseMcpAppAiGguiRenderMetaResult; /** * Emitter convenience — wrap a server-built {@link McpAppAiGguiRenderMeta} * slice as the wire `_meta` envelope under the canonical key constant. * * @public */ export declare function toMcpAppEnvelope(render: McpAppAiGguiRenderMeta): Record; /** * Token-bearing session-API URL pair — the return shape of * {@link composeSessionApiUrls}. Field names deliberately match the * {@link McpAppAiGguiRenderMeta} slice fields they stamp, so callers * spread the pair straight into a slice literal. * * @public */ export interface SessionApiUrls { /** `${base}/api/sessions//events?wsToken=` */ readonly pollingUrl: string; /** `${base}/api/sessions//stream?wsToken=` */ readonly sseUrl: string; } /** * Compose the token-bearing session-API URL pair — ONE composer for * every stamping surface (render resultMeta, update resultMeta, the * self-contained shell, the `/api/sessions/:id/state` bootstrap). Same * move as `deriveRenderMeta`: because every surface routes through this * function, cross-transport drift in the URL shapes is structurally * impossible. * * Pure string composition, no I/O. `base` is the absolute public * origin the session API is served on (a trailing `/` is tolerated and * stripped); `sessionId` and `wsToken` are URL-encoded into path and * query. Callers MUST invoke this only when the live trio was minted — * both URLs embed the token, and a token-less URL can only 401 through * the fallback composers; omission of both fields is the honest * no-HTTP-fallback signal the slice contract defines. * * @public */ export declare function composeSessionApiUrls(base: string, sessionId: string, wsToken: string): SessionApiUrls; /** * `_meta` key carrying host-supplied conversation-grouping metadata on * every inbound `tools/call` request. Captured ONCE on the first call * that materializes a ggui render row and persisted as opt-in identity * for later rehydration. * * Hosts that don't set this key produce one-shot renders — they work * fine for a single chat turn but cannot be re-listed or restored after * the host closes the conversation surface. Opt-in is the whole design: * hosts that want resume thread their conversation id here; hosts that * don't get the simple write-only path. * * @public */ export declare const MCP_APP_AI_GGUI_HOST_SESSION_META_KEY: "ai.ggui/host-session"; /** * Host-supplied conversation-grouping slice. Sent on the request `_meta` * of the first `ggui_*` tool call that creates a ggui render; subsequent * calls naming the same render ignore the field — set-at-creation, * immutable. * * Opaque grouping key, NOT a credential. Auth still comes from the * caller's identity (API key, OAuth bearer, cookie). `hostSessionId` * scopes which renders the authenticated caller can rehydrate; it * does NOT itself authorize access. * * Both fields are required when the slice is present. A slice with a * missing/empty field is treated as absent (degrades to one-shot). * * @public */ export interface McpAppAiGguiHostSessionMeta { /** * Stable host identifier — e.g. `'sample'`, `'claude.ai'`, `'chatgpt'`. * Used to partition `hostSessionId` namespace so the same chat-id * across two different hosts cannot alias. */ readonly hostName: string; /** * Host's grouping key for "this conversation" — opaque to ggui. * Typically: claude.ai thread id, ChatGPT chat id, sample-agent * chatSessionId. The server treats it as an opaque string. */ readonly hostSessionId: string; } /** * Discriminated result of {@link parseMcpAppAiGguiHostSessionMeta}. * * Three outcomes: * - `ok: true, hostSession: ` — slice present + well-formed. * - `ok: true, hostSession: undefined` — slice absent (host opted out * of rehydration). Caller proceeds without it; the render it * creates is one-shot. * - `ok: false` — slice present but structurally invalid. Caller's * choice whether to reject the request or proceed as "absent". * The handler MAY log + proceed; this is host implementor error, * not a security boundary. * * @public */ export type ParseMcpAppAiGguiHostSessionMetaResult = { readonly ok: true; readonly hostSession?: McpAppAiGguiHostSessionMeta; } | { readonly ok: false; readonly reason: 'MALFORMED_HOST_SESSION'; }; /** * Read the `ai.ggui/host-session` slice off a parsed inbound `_meta` * object. Structural validation only — `hostName` + `hostSessionId` * both required and non-empty. Both missing entirely returns * `{ok: true, hostSession: undefined}` (the documented opt-out path). * * @public */ export declare function parseMcpAppAiGguiHostSessionMeta(meta: unknown): ParseMcpAppAiGguiHostSessionMetaResult; /** * Wire shape of one row in `ggui_list_sessions` output. Mirrors the * handler's Zod-described `sessions[*]`. Surfaced at the protocol level * so non-handler consumers (sample-agent's `/chat/restore` server, future * host SDK helpers) can import a single typed shape instead of * redeclaring it — preventing drift if the handler ever grows fields. * * `wsToken` + `wsTokenExpiresAt` are populated when the deployment * wired a `mintWsToken` seam on the handler — otherwise the lean * summary path returns them absent. * * Post-Phase-B: `stackItemId` → `sessionId`; the old `stackItemCount` is * dropped (every render is exactly one item — Phase B collapsed the * vessel). * * @public */ export type GguiSessionSummaryWire = DeepReadonly>; /** * CSP metadata copied from an MCP Apps resource declaration. * Spec-canonical field names — do NOT rename. */ export interface McpAppsCsp { readonly connectDomains?: string[]; readonly resourceDomains?: string[]; readonly frameDomains?: string[]; } /** * Permissions Policy metadata copied from an MCP Apps resource * declaration. Spec-canonical field names — do NOT rename. */ export interface McpAppsPermissions { readonly camera?: boolean; readonly microphone?: boolean; readonly geolocation?: boolean; readonly clipboardWrite?: boolean; } /** * Container dimensions hint passed to the embedded iframe via the * MCP Apps `ui/initialize` response. */ export interface McpAppsContainerDimensions { readonly height?: number; readonly width?: number; readonly maxHeight?: number; readonly maxWidth?: number; } /** * Locator for the source of an embedded MCP App. * * Persists STABLE identity (not a raw URL) so render state survives * source-server endpoint changes. The hosting runtime resolves * `connectorId` to the actual endpoint at render time. */ export interface McpAppsSource { /** Stable connector id declared in the app's connector registry. */ readonly connectorId: string; /** Source-server tool whose call produced this UI; scope for * `tools/call` proxying. */ readonly toolName: string; /** `ui://` resource URI declared on the source tool's * `_meta.ui.resourceUri`. */ readonly resourceUri: string; } /** * GguiSession variant: an embedded third-party MCP App iframe. * * **Locator-oriented, not content-oriented.** Persisted state carries * `source` (connector identity) + declared CSP/permissions/dimensions * metadata; resource BYTES are not stored in render state by default. * The `@ggui-ai/mcp-server` resource-proxy route fetches the bytes * on-demand via `resources/read` against the source server. * * **Union safety.** Fields that exist on the {@link ComponentGguiSession} * variant are declared here as `?: never` so consumers that access them * via optional chaining on `GguiSession` still typecheck cleanly. Those * fields semantically DO NOT exist on McpAppsGguiSession — the `?: never` * typing encodes the "structurally absent" guarantee. */ export interface McpAppsGguiSession { /** Discriminator — required on this variant. */ readonly type: 'mcpApps'; readonly id: string; readonly createdAt: string; /** * History head epoch (#483) — same semantics as * `GguiSessionBase.epoch` (absent ⇒ 0; advanced only by * `ggui_update`). Declared here too because this variant does not * extend the base. */ readonly epoch?: number; readonly prompt?: string; readonly description?: string; readonly message?: string; readonly source: McpAppsSource; readonly csp?: McpAppsCsp; readonly permissions?: McpAppsPermissions; readonly containerDimensions?: McpAppsContainerDimensions; /** * Optional integrity pin — sha256 of the resource bytes computed at * render time. The resource-proxy route verifies the re-fetched * content against this hash; a mismatch breaks the render LOUDLY * rather than silently serving mutated content. */ readonly resourceHash?: string; /** * Bounded dev/cache optimization. When present, the proxy route MAY * serve this inline instead of re-fetching via `resources/read`. NOT * the canonical carrier — metadata persists, bytes don't. Use only * for dev harnesses / offline replay. */ readonly resourceContent?: string; readonly componentCode?: never; readonly props?: never; readonly contentType?: never; readonly schema?: never; readonly subscription?: never; readonly capabilities?: never; readonly actions?: never; readonly quality?: never; readonly error?: never; readonly streamSpec?: never; readonly propsSpec?: never; readonly actionSpec?: never; readonly contextSpec?: never; readonly clientCapabilities?: never; } /** * Type guard: narrows a `GguiSession` (or unknown) to {@link McpAppsGguiSession}. * Uses the discriminator. */ export declare function isMcpAppsGguiSession(entry: unknown): entry is McpAppsGguiSession; /** * Structural validator for an `McpAppsGguiSession` — not a Zod schema * so we don't force a Zod dependency here. Returns null on failure * (caller maps to an appropriate error code). Required when accepting * one over the wire from an agent: the discriminator alone isn't * enough. */ export declare function validateMcpAppsGguiSession(input: unknown): McpAppsGguiSession | null; /** * Lifecycle states the renderer transitions through inside an MCP Apps * iframe. Closed union — adding a new state is a protocol-version- * eligible change. Hosts that don't recognise a state MUST treat it as * a no-op (don't mirror it, don't crash). * * State machine: * * ``` * ┌────────────┐ * (iframe boot) │ mounting │ * └─────┬──────┘ * │ bundle evaluated + * │ React tree mounted + * │ WS handshake completed * ▼ * ┌─────────────┐ * │ code-ready │◀────── (terminal happy state) * └──┬─────┬────┘ * │ │ * (WS close) │ │ (eval / mount / handshake throw) * ▼ ▼ * ┌──────────┐ ┌───────┐ * │disconnected│ │ error │ * └────────────┘ └───────┘ * ``` * * - `mounting` — emitted ASAP after iframe boot (before bundle eval). * A host that observes only `mounting` and never a follow-up state * has a renderer that crashed before posting code-ready/error. * - `code-ready` — happy-path terminal state. Bundle evaluated, React * tree mounted, WS connected, first render ack folded. Equivalent of * the in-iframe `data-ggui-status="connected"`. * - `error` — terminal failure. Pairs with the existing * `ggui:bootstrap-failed` postMessage envelope which carries the * typed reason; this lifecycle state is the COARSE outer-DOM signal * ("renderer is not going to come up — give up waiting"). * - `disconnected` — non-terminal. WebSocket closed after a successful * `code-ready`. The renderer MAY transition back to `code-ready` if * reconnection succeeds (subscribe.ts owns the reconnect ladder); * hosts that pin selectors on `code-ready` will re-resolve when it * does. * * @public */ export type McpAppLifecycleState = 'mounting' | 'code-ready' | 'error' | 'disconnected'; /** * Lifecycle event payload shape. Carried inside an * {@link McpAppLifecycleMessage} envelope (`type: 'ggui:lifecycle'`). * * Fields: * - `state` — required. The lifecycle state being entered. * - `sessionId` — optional. When present, the lifecycle pertains to a * specific render (per-card iframes via single-item mode). * Absent → whole-renderer lifecycle. * - `error` — optional, only meaningful when `state === 'error'`. * Mirrors the `ggui:bootstrap-failed` postMessage envelope's * `reason` + `message` so a single `ggui:lifecycle` listener can * surface both the coarse signal AND the typed cause without * subscribing to two envelopes. Producers SHOULD set this when * `state === 'error'`; it is OPTIONAL because legacy producers * emitted no lifecycle event at all and we don't want to require * a code change for the coarse signal alone. * * Producers MUST NOT add fields not enumerated here in this shape; * additive evolution requires a new optional key + a doc revision so * hosts know what they may observe. Consumers MUST ignore unknown * fields (shape-preserving extensibility). * * @public */ export interface McpAppLifecycleEvent { readonly state: McpAppLifecycleState; readonly sessionId?: string; readonly error?: { readonly code: string; readonly message: string; }; } /** * postMessage envelope the renderer posts to its parent on every * lifecycle transition. The string `'ggui:lifecycle'` is the protocol- * canonical envelope tag — hosts filter `event.data.type` to subscribe. * * **Named parties:** * - **Renderer** (producer) — running inside the MCP Apps iframe; * emits one envelope per state transition. * - **Host** (consumer) — running in the parent window (e.g., * ``); listens on `window.message`, narrows * `event.source` to the iframe's `contentWindow`, and mirrors * `event.state` onto the outer iframe element. * - **Observer** (downstream) — tests, accessibility scanners, dev * inspectors; read the host-mirrored attribute on the outer DOM * element. Observers DO NOT subscribe to postMessage directly — * the host is the protocol-defined mirror point. * * **Obligations:** * - Renderer MUST post `mounting` before evaluating the bundle. * - Renderer MUST post exactly one terminal state (`code-ready` * or `error`) for any successful boot attempt. * - Renderer MAY post `disconnected` after a `code-ready` and MAY * post `code-ready` again after a successful reconnect. * - Host MUST mirror the latest received state onto the outer * element via the `data-ggui-mcp-app-iframe-lifecycle=""` * attribute. Idempotent re-emission of the same state is a no-op. * - Host MUST narrow `event.source` to the iframe's `contentWindow` * before trusting the envelope (cross-frame postMessage is the * attack surface; envelopes from other windows MUST be dropped). * * **Defined failure modes:** * - Renderer never emits any lifecycle event → host's outer-element * attribute is never set, observers timeout waiting for a state. * This is the UN-INSTRUMENTED legacy case; not a violation. * - Renderer emits `mounting` then no terminal state → host's * attribute pins to `'mounting'`. Observers waiting for * `'code-ready'` see a stuck attribute and fail their own timeout * — the coarse-grained surfacing of "renderer crashed before * declaring ready". Hosts MAY layer a watchdog on top to * transition the attribute to a synthetic `'timeout'` state, but * that is host policy, not protocol obligation. * - Renderer emits `code-ready` and the WS later drops without a * subsequent `disconnected` → host's attribute remains * `'code-ready'`. This is shape-acceptable because reconnect * attempts are still in flight; observers that need finer- * grained connection state subscribe to `ggui:observe`'s * `subscribe-failed` events instead. * * **Observable violation:** * - The outer-element attribute. A renderer that posts envelopes * the host can't classify (wrong shape, wrong type tag) does NOT * update the attribute; the violation is observable as a stuck * attribute relative to the inferred WS / DOM state of the * iframe child. * * @public */ export interface McpAppLifecycleMessage { readonly type: 'ggui:lifecycle'; readonly event: McpAppLifecycleEvent; } /** * The closed set of valid lifecycle states. Exposed as a `readonly` * tuple so consumers (renderer host filters, conformance tests) can * iterate without re-typing the union literally. * * @public */ export declare const MCP_APP_LIFECYCLE_STATES: readonly McpAppLifecycleState[]; /** * Type guard for {@link McpAppLifecycleMessage}. Trust-boundary helper * — apps consuming raw postMessage data MUST narrow before reading * `event.state` to avoid reaching into untyped property bags. * * Validation rules (all required for `true`): * - Outer envelope is an object with `type === 'ggui:lifecycle'`. * - `event` is an object with `state` matching {@link * MCP_APP_LIFECYCLE_STATES}. * - If `sessionId` is present, it is a non-empty string. * - If `error` is present, it is an object with string `code` + * `message`. * * @public */ export declare function isMcpAppLifecycleMessage(message: unknown): message is McpAppLifecycleMessage; /** Envelope tag: renderer alive + bundle evaluated (pre-`ui/initialize`). */ export declare const MCP_APP_RENDERER_READY_TYPE = "ggui:renderer-ready"; /** Envelope tag: a boot-path failure (parse / initialize / handshake). */ export declare const MCP_APP_BOOTSTRAP_FAILED_TYPE = "ggui:bootstrap-failed"; /** Envelope tag: renderer-internal observability event (telemetry). */ export declare const MCP_APP_OBSERVE_TYPE = "ggui:observe"; /** Envelope tag: a user dismiss GESTURE, forwarded to the host as an intent (ggui#1109). */ export declare const MCP_APP_DISMISS_TYPE = "ggui:dismiss"; /** * Envelope tag: mount-lifecycle transition. Constant twin of the * literal on {@link McpAppLifecycleMessage} — the annotation ties the * two so they cannot drift. */ export declare const MCP_APP_LIFECYCLE_TYPE: McpAppLifecycleMessage['type']; /** * `ggui:renderer-ready` — posted by the renderer immediately after its * status DOM mounts, BEFORE `ui/initialize` fires. Optional * informational signal; hosts MAY surface a "renderer alive" * indicator. `version` is the renderer bundle's package version. */ export interface McpAppRendererReadyMessage { readonly type: typeof MCP_APP_RENDERER_READY_TYPE; readonly version: string; } /** * `ggui:bootstrap-failed` — posted by the renderer (or a pre-renderer * shell) on any boot-path failure. Hosts surface it on their error * callback. `reason` is extensibly-closed at the protocol layer * (emitters narrow it to their own closed reason unions, e.g. the * renderer's boot-failure reasons); hosts MUST tolerate reason codes * they don't recognise. */ export interface McpAppBootstrapFailedMessage { readonly type: typeof MCP_APP_BOOTSTRAP_FAILED_TYPE; readonly reason: string; readonly message: string; } /** * Discriminator for the user-action envelope delivered via * `ggui_runtime_submit_action` over the MCP Apps host-relay path * (postMessage `tools/call` → host MCP client → server). Every * user-driven `WireConfig` method emits this envelope so operators get * **uniform server-side observability** across every gesture kind * regardless of which user-visible effect the iframe already fired * locally (`ui/open-link` / `ui/request-display-mode`) before the audit. * * **Closed primary set, extensibly-closed forward-compat.** The three * primary kinds correspond 1:1 to the `WireConfig` methods that emit * gestures today. Forward additions land via the `(string & {})` slot * — handlers MUST treat unknown values gracefully (log under an * `'unknown'` bucket, never throw or hard-switch). Adding a new kind * is additive and does NOT bump the protocol version. * * | kind | primary host effect | payload shape | * | ----------------------- | -------------------------------- | ---------------------------------------------------------------------- | * | `dispatch` | pipe append (single source) | `{ intent: string, actionData: JsonValue \| null, uiContext: JsonObject }` | * | `openLink` | `ui/open-link` | `{ url: string }` | * | `requestDisplayMode` | `ui/request-display-mode` | `{ mode: 'fullscreen' \| 'pip' \| 'inline' }` | * * Audit is **fail-soft** at the client: if the `tools/call` envelope * fails to deliver (host rejects, postMessage on detached parent), the * primary host effect MUST still proceed. The audit miss surfaces as a * diagnostic on the operator side (gap in the RenderInspector activity * row), not as a user-facing failure. This mirrors today's `dispatch` * audit-fire posture so semantics stay uniform. * * Failure-mode note: a malformed envelope (unknown `kind` AND malformed * `payload`) is rejected by the `ggui_runtime_submit_action` handler * with `{ok: false, code: 'INVALID_ACTION_KIND'}` in * `structuredContent` — the iframe observes the rejection through the * host's `tools/call` relay response. */ export type SubmitActionKind = 'dispatch' | 'openLink' | 'requestDisplayMode' | (string & {}); /** * Per-kind payload schemas for {@link SubmitActionKind}. Keep this discriminated * union narrow — adding a new gesture means adding both a kind variant AND * its payload shape here, in lockstep, so the `ggui_runtime_submit_action` * handler's input parser can validate exhaustively. * * `payload` for the unknown `(string & {})` extension slot widens to * `Record` — handlers MUST validate shape against their * own schema before consuming, since the protocol type can't narrow it. */ export type SubmitActionEnvelope = { readonly kind: 'dispatch'; readonly payload: { /** `actionSpec[*]` key the iframe dispatched against. */ readonly intent: string; /** * Typed payload satisfying `actionSpec[intent].schema`. * `null` for no-payload gestures (bare button click). */ readonly actionData: JsonValue | null; /** * Iframe-local snapshot of the contract's `contextSpec` slot * values at the moment the user fired the gesture. Captured at * gesture time so the agent can reason about WHAT the user did * AND WHAT THEY WERE LOOKING AT atomically — without a second * round trip to read state from the rendered UI. * * Empty object `{}` when the contract has no `contextSpec` or * the iframe hasn't yet mirrored any slots. */ readonly uiContext: JsonObject; }; } | { readonly kind: 'openLink'; readonly payload: { readonly url: string; }; } | { readonly kind: 'requestDisplayMode'; readonly payload: { readonly mode: 'fullscreen' | 'pip' | 'inline' | (string & {}); }; } | { readonly kind: string; readonly payload: Record; }; /** * Canonical input contract for the `ggui_runtime_submit_action` MCP tool. * The iframe-runtime delivers this via the MCP Apps host-relay path * (postMessage `tools/call` → host MCP client → server) — the iframe * has no auth credential of its own, so the host is the protocol- * defined relay party (per `_meta.ui.visibility: ['app']` on the * tool declaration). * * Per-kind semantics: * * - `kind === 'dispatch'`: server appends a consume-entry onto the * render-keyed pending-events pipe (`{type:'action', sessionId, * intent, actionData, uiContext, actionId, firedAt}`) so the agent's * `ggui_consume` long-poll unblocks in the same chat turn. The * handler's response carries `consumerPresent` — whether a * `ggui_consume` long-poll is currently listening on this render's * pipe. When `consumerPresent === false` (no loop is listening — * e.g. the agent's persistent consume loop ended after a page * reload), the iframe-runtime ALSO emits a `ui/message` doorbell * carrying `content[0]._meta["ai.ggui/userAction"]` (see * {@link GguiUserActionMeta}) so a fresh agent turn calls * `ggui_consume({sessionId})` to drain the just-enqueued gesture. * The doorbell is a PURE POINTER — the gesture stays solely on the * pipe, making the action exactly-once. * - `kind ∈ {'openLink','requestDisplayMode'}`: pure audit — the * user-visible host effect already fired iframe-side via * `ui/open-link` / `ui/request-display-mode`. The server records * the gesture for the RenderInspector feed. * * Required fields: * - `sessionId` / `appId`: bootstrap-issued; server cross-checks. * - `actionId`: 8-hex correlation hash (FNV-1a of intent + data + firedAt * for `dispatch`, kind + payload + firedAt for the host-control kinds). * Lets the host LLM cross-verify a `[ggui:pending-action]` context entry * against a `ui/message` consent prompt by id. * - `firedAt`: ISO-8601 client-monotonic timestamp; useful for ordering * and replay diagnostics. Server uses its own clock for authoritative * log ordering. * * The discriminated `kind` + `payload` pair carries the actual gesture * shape — see {@link SubmitActionEnvelope}. */ export type GguiSubmitActionInput = SubmitActionEnvelope & { readonly sessionId: string; readonly appId: string; readonly actionId: string; readonly firedAt: string; }; /** * The three canonical gesture kinds — useful for exhaustiveness checks * in `switch (kind) { ... }` blocks. Frozen so consumers can safely use * `as const` against the readonly tuple. */ export declare const SUBMIT_ACTION_KINDS: readonly ["dispatch", "openLink", "requestDisplayMode"]; /** * Type guard narrowing an unknown value to {@link GguiSubmitActionInput}. * Validates the `kind` discriminator + the per-kind `payload` shape. * Used by the server-side `ggui_runtime_submit_action` handler to reject * malformed envelopes with `INVALID_ACTION_KIND` instead of silently * coercing. * * Unknown extension kinds are accepted at this guard layer (the * `(string & {})` slot is part of the type) but the per-kind payload * narrowing collapses to `Record` — extension-handlers * MUST validate shape before consuming. */ export declare function isGguiSubmitActionInput(value: unknown): value is GguiSubmitActionInput; /** * The `dispatch` member of {@link GguiSubmitActionInput} — the one kind * whose payload the pipe stores. Derived here, never restated in a * consumer (ggui#839): `env.kind === 'dispatch'` alone cannot narrow the * union, because the forward-compat extension member (`kind: string` with a * `Record` payload) also admits the literal at the type level. */ export type GguiSubmitDispatchInput = Extract; /** * Narrow a guarded envelope to its `dispatch` member. Sound because * {@link isGguiSubmitActionInput}'s closed-set `switch` validates the * `'dispatch'` payload before any extension kind can carry that literal. */ export declare function isGguiSubmitDispatchInput(env: GguiSubmitActionInput): env is GguiSubmitDispatchInput; /** * `content[0]._meta["ai.ggui/userAction"]` — a PURE DOORBELL. * * Spec-canonical extension point: MCP Apps closes `params._meta` via * `additionalProperties: false`, but each content block has its own * open `_meta` record (per the base MCP spec). The `ai.ggui/*` key * prefix matches our other protocol extensions * (`ai.ggui/render`, `ai.ggui/bootstrap`, etc.). * * Stamped by the iframe-runtime on a `ui/message` envelope when a user * gesture needs to wake the agent because no `ggui_consume` long-poll is * currently listening (the agent's persistent consume loop has ended — * e.g. after a page reload). The gesture itself was ALREADY enqueued onto * the render's server-side pending-event pipe by the iframe's * `ggui_runtime_submit_action` call (relayed by the host) BEFORE this * notification fired; this slice's only job is to make a fresh agent turn * call `ggui_consume({sessionId})` to drain it. * * SINGLE SOURCE OF TRUTH: the pending-event queue. This slice carries ONLY * a pointer to the render whose queue holds the gesture — never the action * payload. The agent retrieves the action EXCLUSIVELY via `ggui_consume`. * Carrying the payload here would let the agent both act on it AND drain * the queue = a double-trigger; the pointer-only shape makes the action * exactly-once. * * `intent` is metadata (which `actionSpec[*]` entry fired) — NOT the * actionable data. The agent can't react meaningfully on `intent` alone * (it lacks the `actionData` payload), so its presence doesn't tempt a * pre-consume action. * * **The directive lives in the `ui/message` TEXT, not here.** The * iframe-runtime authors a `ui/message` whose human-readable text * carries the full "call `ggui_consume`" directive — that text is what * EVERY host (claude.ai, chatgpt.com, ggui-aware SDKs) forwards to the * model. This `_meta` slice is the OPTIONAL structured mirror for * ggui-aware programmatic consumers; an `_meta`-agnostic host ignores * it and acts on the text alone. No part of the loop depends on a * server-side parse of this slice. * * @public */ export interface GguiUserActionMeta { readonly kind: 'user-action'; readonly description: string; readonly sessionId: string; readonly actionId: string; readonly submittedAt: string; readonly intent: string; readonly nextStep: { readonly tool: 'ggui_consume'; readonly args: { readonly sessionId: string; }; }; } /** * A mountable ggui render bootstrap: the runtime bundle URL (the one * field the host layer reads), plus the WHOLE `ai.ggui/render` slice, * verbatim, for {@link gguiShellHtml} to inline. * * `slice` is an open record ON PURPOSE — it is a pass-through of the * wire slice whose authority is the iframe-runtime's boot projector, * not a shaped value this layer owns. Typing it as * {@link McpAppAiGguiRenderMeta} would invite projection and imply * unchecked optional fields are known-good; the open record plus the * verbatim guarantee is the honest contract (see the section banner). * * @public */ export interface GguiRenderBootstrap { /** The slice's `runtimeUrl` — the ES-module bundle the shell loads. */ readonly runtimeUrl: string; /** The verbatim `ai.ggui/render` slice, unprojected. */ readonly slice: Readonly>; } /** * A `_meta` container → the ggui render bootstrap, or `undefined`. * * Two hard requirements, both of which the iframe-runtime's own boot * validator enforces as `MALFORMED_BOOTSTRAP`: a non-empty * `runtimeUrl`, AND at least one mode discriminator. A slice with * `runtimeUrl` alone has a bundle to load but nothing for it to mount * — the runtime would boot into a blank shell rather than a card, so * that shape is unmountable here too. * * Everything else on the slice is carried verbatim — see the section * banner for why this is deliberately NOT a projecting parse. * * @public */ export declare function asGguiRenderBootstrap(meta: unknown): GguiRenderBootstrap | undefined; /** * A spec-canonical `CallToolResult` → the ggui render bootstrap it * carries, or `undefined` for anything that is not a mountable ggui * render. Slices ride the result's top-level `_meta` (the MCP Apps * `ui/notifications/tool-result` delivery location). * * The input is `unknown` on purpose — hosts hold tool results in * their own SDK's types (or as persisted JSON), and this narrowing is * the boundary where those shapes meet ggui's wire contract. * * @public */ export declare function toolResultGguiRender(result: unknown): GguiRenderBootstrap | undefined; /** * CSS background the shell paints on its own `html`/`body` in the * default `'surface'` posture. Resolves to the active theme's exact * per-mode surface color once the iframe-runtime injects the `:root` * theme variables at boot; the static dark fallback covers the * pre-resolve first paint. * * The chain opens with `--ggui-shell-background` — unset in the * served bytes — as the runtime's override point. The shell stamps * its backdrop as an INLINE style, which no stylesheet `background` * rule can out-cascade; but because the inline value is a `var()`, * a stylesheet that sets the custom property re-resolves it in * place. The layer that knows whether the embedding host composites * its own chrome behind the document (the design system's * `ThemeProvider` in its transparent posture) sets * `--ggui-shell-background: transparent`; every other context keeps * the surface paint and its per-browser consistency. * * @public */ export declare const GGUI_RENDER_SHELL_SURFACE = "var(--ggui-shell-background, var(--ggui-color-ground, var(--ggui-shell-scheme-surface, #f9fafb)))"; /** * Inline `"; /** * Options for {@link gguiShellHtml}. * * @public */ export interface GguiShellHtmlOptions { /** * What the shell document paints behind the rendered component. * * - `'surface'` (default) — paint the theme surface * ({@link GGUI_RENDER_SHELL_SURFACE}). Right for standalone * served documents (`resources/read` shells, public render * pages): Safari renders a transparent iframe document's * backdrop as the opaque UA canvas color (white), so an * unpainted document diverges per-browser. * - `'transparent'` — let the host page composit behind the card. * Right for hosts that draw their own card chrome around the * iframe and accept the Safari canvas-color trade-off. */ readonly background?: 'surface' | 'transparent'; /** * Full source text of the iframe-runtime bundle to embed as an * inline `