MCP Client SDK
The MCP Client SDK lets your AI agent push interactive UIs to users, collect responses, stream data, and manage sessions — all through a typed TypeScript API over the ggui MCP protocol.
Installation
Section titled “Installation”npm install @ggui-ai/mcp-clientQuick Example
Section titled “Quick Example”import { GguiClient } from "@ggui-ai/mcp-client";
const ggui = new GguiClient({ apiKey: "ggui_sk_...", appId: "app_...",});
// Push a UI to the userconst { sessionId, url } = await ggui.push({ story: { intent: "Collect user feedback", prompt: "Show a form with a 1-5 star rating and a comments field", },});console.log("User can access:", url);
// Wait for the user to submitconst events = await ggui.waitForCompletion(sessionId);console.log("User submitted:", events);
// Clean upawait ggui.close(sessionId);GguiClient
Section titled “GguiClient”The main client class for interacting with ggui from an AI agent. The client is stateful: after the first push() it remembers the active sessionId so subsequent calls can omit it, and it can be primed with shellType / interfaceContext so the generator adapts to the rendering environment.
Constructor
Section titled “Constructor”new GguiClient(config: GguiClientConfig)GguiClientConfig:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
apiKey | string | Yes | — | API key for authentication (must start with ggui_sk_) |
appId | string | Yes | — | Your ggui app ID |
url | string | No | https://mcp.guuey.com/mcp | MCP endpoint URL |
streamingUrl | string | No | — | Optional SSE endpoint for long-poll consume() (timeouts > 25s). Omit to disable SSE routing. |
timeout | number | No | 60000 | Request timeout in milliseconds (accommodates long-poll consume) |
retry | RetryConfig | No | { maxRetries: 3, baseDelay: 1000, maxDelay: 30000 } | Retry configuration for transient errors |
RetryConfig:
| Field | Type | Default | Description |
|---|---|---|---|
maxRetries | number | 3 | Maximum retry attempts |
baseDelay | number | 1000 | Base delay in ms for exponential backoff |
maxDelay | number | 30000 | Maximum delay in ms between retries |
Retries apply only to transient errors: network failures, timeouts, HTTP 5xx, and HTTP 429. Auth errors and MCP protocol errors are never retried.
State accessors
Section titled “State accessors”The client holds a small amount of state so agents can fire-and-forget subsequent calls without re-passing the same arguments.
| Accessor | Type | Description |
|---|---|---|
sessionId | string | null | Active session ID. Auto-set after the first push(). Used as the default for subsequent push / consume / send / close calls. |
shellType | ShellType | null | Active shell (chat / fullscreen / spatial). Auto-included in push() so the generator picks the right layout. |
interfaceContext | InterfaceContext | null | Full viewport + device + shell context. Auto-included in push() so the generator adapts to the client. |
All three are read/write properties. Set them explicitly to use a session created externally (e.g. the platform invoke flow).
// Adopt a session handed off by the platform invokeggui.sessionId = "sess_abc";ggui.interfaceContext = platformContext;
// Reset so the next push() creates a new sessionggui.newSession();Methods
Section titled “Methods”push(input)
Section titled “push(input)”Materialise a UI emission. Creates a new session if none is supplied. Accepts two shapes:
- Handshake-consumed:
{ handshakeId, props? }— execute a decision already computed byhandshake(). - Direct path:
{ story, session?, rendering?, infra?, shortcuts? }— the push performs any internal handshake itself.
async push(input: GguiPushInput): Promise<GguiPushOutput>GguiPushInput — direct path:
| Field | Type | Required | Description |
|---|---|---|---|
story | PushStory | Yes | What the UI is for (see below). |
session | PushSession | No | { id?: string; message?: string } — reuse an existing session or show a thinking-indicator message. |
rendering | PushRendering | No | { shellType?; interfaceContext? } — rendering preferences. Auto-filled from client state if not provided. |
infra | PushInfra | No | { ttl?: number; model?: string } — session TTL (60–604800 seconds) and LLM model override. |
shortcuts | PushShortcuts | No | { blueprintId?; props?; autoCommit? } — skip negotiation/generation when you know what to render. |
PushStory:
| Field | Type | Required | Description |
|---|---|---|---|
intent | string | Yes | Concise purpose. Same intent = component reuse (e.g. "Gmail inbox for email triage"). |
prompt | string | No | Additional instructions for this specific push. |
data | Record<string, unknown> | No | Structured data to display. |
context | string | Record<string, unknown> | No | Agent reasoning, user situation, background. |
sourceTools | string[] | No | MCP tools that produced the data (provenance + blueprint matching). |
wiredTools | string[] | No | MCP tools the UI can dispatch to the agent (absorbs both user-triggered action dispatches and runtime-invoke tools). |
GguiPushOutput:
| Field | Type | Description |
|---|---|---|
sessionId | string | Session ID (new or existing). |
pageId | string | ID of the stack page this push produced. |
shortCode | string | Short code for the render URL. |
url | string | Full render URL (e.g. https://render.guuey.com/abc123). |
action | 'create' | 'reuse' | 'update' | 'replace' | 'compose' | What the negotiator decided to do. |
codeReady | boolean | Whether the rendered component is already compiled. false means generation is still in progress. |
handshakeId | string | Echoed when push performed an internal handshake. |
decision | NegotiatorDecision | Full negotiator decision — set only when this push consumed a handshakeId. |
contract | DataContracts | Data contract — convenience pullout from decision.contract. |
interaction | 'display' | 'collect' | 'converse' | 'broadcast' | 'flow' | Interaction mode — convenience pullout from decision.contract.interaction. |
contractHash | string | Deterministic cache key for pool lookup — stable across retries. |
Example — direct path:
const { sessionId, url } = await ggui.push({ story: { intent: "Collect product feedback", prompt: "Show a feedback form with rating (1-5) and comments", context: { userName: "Alice", product: "Widget Pro" }, }, rendering: { shellType: "chat" },});Example — stacking on an existing session:
// Client remembers the session from the first push, but you can be explicitawait ggui.push({ story: { intent: "Show thank-you message with feedback summary", data: collectedData, }, session: { id: sessionId },});Example — handshake-consumed:
const hs = await ggui.handshake({ story: { intent: "Collect product feedback" },});
await ggui.push({ handshakeId: hs.handshakeId });handshake(input)
Section titled “handshake(input)”Optional preflight negotiation. Returns a rich handshake result (match confidence, plan, cost estimate, user-facing hint). Use when you want progressive UX before committing to a push — pass the returned handshakeId into push() / update() to execute the decision without re-negotiating.
If you already know what to render, skip handshake and call push() directly.
async handshake(input: GguiHandshakeInput): Promise<GguiHandshakeOutput>update(input)
Section titled “update(input)”Mutate an existing session page. Accepts:
- Handshake-consumed:
{ handshakeId, patch }— execute a handshake-decided update. - Direct path:
{ sessionId, pageId, patch }— when you already know exactly what to patch.
async update(input: GguiUpdateInput): Promise<GguiUpdateOutput>pop(sessionId)
Section titled “pop(sessionId)”Remove the top UI card from the session stack.
async pop(sessionId: string): Promise<GguiPopOutput>Returns: { poppedId: string | null; stackSize: number }
consume(sessionId, timeout?)
Section titled “consume(sessionId, timeout?)”Poll for buffered action envelopes. Envelopes are cleared from the server after being returned.
async consume(sessionId: string, timeout?: number): Promise<GguiConsumeOutput>async consume<const T extends DataContracts>( sessionId: string, timeout?: number,): Promise<GguiConsumeOutput>Parameters:
| Parameter | Type | Description |
|---|---|---|
sessionId | string | Session to consume envelopes from. |
timeout | number (optional) | Long-poll timeout in seconds (0 = immediate, max 30). When set, the server waits for envelopes before returning empty. Recommend 30 for chat agents to save tokens. |
When timeout > 25 and streamingUrl is configured, the client automatically routes through the SSE streaming endpoint.
Returns: { events: ActionEnvelope[]; status: 'active' | 'completed' | 'expired' }
Typed form: pass a contract type parameter to narrow envelope payloads by action name:
const contract = defineContract({ ...yourContract } as const);const { events } = await ggui.consume<typeof contract>(sessionId, 25);send(sessionId, emission)
Section titled “send(sessionId, emission)”Emit a new delivery on a declared channel of the active stack item’s streamSpec. Canonical wrapper around the ggui_stream MCP tool.
async send( sessionId: string, emission: { channel: string; payload: JsonValue; complete?: boolean; pageId?: string; },): Promise<GguiStreamOutput>
async send<const T extends DataContracts>( sessionId: string, emission: TypedStreamEvent<T>,): Promise<GguiStreamOutput>Returns: { accepted: boolean; seq?: number }. Acceptance is at the server boundary; no-subscriber is not an error. seq is the server-assigned monotonic outbound sequence (set on implementations with a SessionStreamBuffer; omitted on the hosted cloud today, required on OSS @ggui-ai/mcp-server).
mode is intentionally not on the emission — it’s derived server-side from streamSpec[channel].mode. Agents that want a different mode should change the spec, not the emission.
Example — untyped:
await ggui.send(sessionId, { channel: "message", payload: { text: "Hello!", sender: "agent" },});Example — typed via contract:
const contract = defineContract({ intent: "Broadcast chat messages to subscribers", streamSpec: { message: { schema: { type: "object", properties: { text: { type: "string" } } } }, },} as const);
await ggui.send<typeof contract>(sessionId, { channel: "message", payload: { text: "Hello!" },});getSession(sessionId)
Section titled “getSession(sessionId)”Get the full state of a session without consuming envelopes.
async getSession(sessionId: string): Promise<GguiGetSessionOutput>Returns: SessionView — { id, appId, status, stack, currentStackIndex, eventSequence, endUserIdentity?, adapterPermissions?, createdAt, lastActivityAt, expiresAt }.
getStack(sessionId)
Section titled “getStack(sessionId)”Get lightweight stack navigation metadata (no component code). Cheaper than getSession when you only need stack-level info.
async getStack(sessionId: string): Promise<GguiGetStackOutput>Returns: { sessionId, stackSize, currentIndex, items: StackItemSummary[], canGoBack, canGoForward, status }.
close(sessionId)
Section titled “close(sessionId)”Close and delete a session, freeing all associated resources.
async close(sessionId: string): Promise<GguiCloseOutput>Returns: { success: boolean }
waitForCompletion(sessionId, options?)
Section titled “waitForCompletion(sessionId, options?)”Wait for the session to complete (status 'completed') or a timeout. Polls consume() internally (default) or uses long-poll subscribe, and accumulates every envelope it sees.
async waitForCompletion( sessionId: string, options?: WaitOptions,): Promise<ActionEnvelope[]>WaitOptions:
| Field | Type | Default | Description |
|---|---|---|---|
pollInterval | number | 1000 | Poll interval in milliseconds (ignored when mode: 'subscribe'). |
maxWait | number | no limit | Maximum time to wait in milliseconds. |
mode | 'poll' | 'subscribe' | 'poll' | 'poll' calls consume() every pollInterval ms. 'subscribe' uses a long-poll subscription for lower-latency envelope delivery. |
pushAndWait(input, options?)
Section titled “pushAndWait(input, options?)”Push a UI and wait for completion in one call. Combines push() + waitForCompletion().
async pushAndWait( input: GguiPushInput, options?: WaitOptions,): Promise<{ sessionId: string; url?: string; events: ActionEnvelope[] }>url is optional — it’s populated from the push() response when available.
Example:
const { url, events } = await ggui.pushAndWait({ story: { intent: "Confirm shipping address", data: { address: currentAddress }, },});const submit = events.find((e) => e.type === "data:submit");subscribe(sessionId, options)
Section titled “subscribe(sessionId, options)”Subscribe to session envelopes in real time via callbacks. Uses long-poll consume internally with automatic reconnection. Returns a handle to stop the subscription.
subscribe(sessionId: string, options: SubscribeOptions): EventSubscriptionHandleSubscribeOptions:
| Field | Type | Default | Description |
|---|---|---|---|
onEvent | (envelope: ActionEnvelope) => void | — | Callback invoked for each envelope received. |
onComplete | () => void | — | Callback invoked when the session completes (status = 'completed'). |
onError | (error: Error) => boolean | — | Callback invoked on errors. Return true to retry, false to stop. |
pollTimeout | number | 25 | Long-poll timeout per consume call in seconds. |
maxDuration | number | no limit | Maximum total subscription duration in ms. |
EventSubscriptionHandle: { unsubscribe(): void; readonly active: boolean }
Example:
const sub = ggui.subscribe(sessionId, { onEvent: (envelope) => { if (envelope.type === "data:submit") { console.log("User submitted:", envelope.payload); } }, onComplete: () => console.log("Session done"), onError: (err) => { console.error(err.message); return true; // retry },});
sub.unsubscribe();newSession()
Section titled “newSession()”Clear the client’s cached sessionId. The next push() without an explicit session will create a new one.
newSession(): voiddiscover()
Section titled “discover()”Discover platform capabilities and app configuration. Returns supported content types, shell types, adapters, and component capabilities. Call this first to understand what features are available.
async discover(): Promise<GguiDiscoverOutput>listTools()
Section titled “listTools()”List all available MCP tools on the server.
async listTools(): Promise< Array<{ name: string; description: string; inputSchema?: Record<string, unknown> }>>callTool(name, args)
Section titled “callTool(name, args)”Call any ggui MCP tool by name. Escape hatch for generic tool execution in agentic loops — prefer the typed methods (push, send, consume, …) when the tool has one.
async callTool<T = unknown>(name: string, args: JsonObject): Promise<T>Auto-includes sessionId for session-scoped tools if not explicitly provided, and auto-includes shellType / interfaceContext for push tools.
ping()
Section titled “ping()”Verify connectivity to the ggui API. Internally calls listTools to exercise auth + transport.
async ping(): Promise<boolean>Returns true on success; throws GguiAuthError / GguiConnectionError / GguiTimeoutError otherwise.
getConnectionInfo()
Section titled “getConnectionInfo()”Synchronous diagnostic getter — returns connection metadata without any network calls.
getConnectionInfo(): ConnectionInfoConnectionInfo:
| Field | Type | Description |
|---|---|---|
url | string | MCP endpoint URL |
appId | string | App ID |
initialized | boolean | Whether the MCP session has been initialized |
protocolVersion | string | MCP protocol version used by this client |
sdkVersion | string | SDK package version |
McpHttpTransport
Section titled “McpHttpTransport”Low-level HTTP transport for MCP protocol. Used internally by GguiClient. Available for advanced scenarios that need direct protocol access — most users should stick to GguiClient.
import { McpHttpTransport } from "@ggui-ai/mcp-client";
const transport = new McpHttpTransport({ url: "https://mcp.guuey.com/mcp", apiKey: "ggui_sk_...", appId: "app_...",});
const tools = await transport.listTools();const result = await transport.callTool<GguiPushOutput>("ggui_push", { story: { intent: "Collect feedback" },});Error Classes
Section titled “Error Classes”All errors extend GguiError which extends Error.
| Error Class | Code | Description |
|---|---|---|
GguiError | — | Base error class |
GguiTimeoutError | — | Request timed out |
GguiAuthError | -32001 | Invalid or expired API key |
GguiConnectionError | — | Network connection failed |
GguiRateLimitError | 429 | Rate limited (includes retryAfter in seconds) |
GguiSessionNotFoundError | -32002 | Session expired or deleted |
McpProtocolError | varies | MCP protocol-level error |
Error handling example:
import { GguiClient, GguiAuthError, GguiConnectionError, GguiRateLimitError, GguiSessionNotFoundError, GguiTimeoutError,} from "@ggui-ai/mcp-client";
try { const result = await ggui.push({ story: { intent: "Collect feedback" } });} catch (error) { if (error instanceof GguiAuthError) { console.error("Invalid API key. Check your ggui_sk_... key."); } else if (error instanceof GguiTimeoutError) { console.error("Request timed out. Try a simpler prompt or raise timeout."); } else if (error instanceof GguiConnectionError) { console.error("Network error:", error.cause); } else if (error instanceof GguiRateLimitError) { console.error(`Rate limited. Retry after ${error.retryAfter}s`); } else if (error instanceof GguiSessionNotFoundError) { console.error("Session expired or deleted."); } else { throw error; }}Shared Types
Section titled “Shared Types”Re-exported from @ggui-ai/protocol for convenience:
import type { ActionEnvelope, // { sessionId, type, payload?, stackIndex?, pageId?, clientSeq? } GguiPushInput, // union of handshake-consumed / direct-path shapes GguiPushOutput, GguiPopOutput, GguiConsumeOutput, GguiGetSessionOutput, // = SessionView GguiGetStackOutput, GguiCloseOutput, GguiStreamOutput, GguiDiscoverOutput, NegotiatorDecision, NegotiatorAlternative, Action, DataContracts, InteractionMode,} from "@ggui-ai/mcp-client";
// Contract type inference — zero parallel type definitionsimport { defineContract, type TypedStreamEvent, type TypedAction, type InferProps, type InferActionNames, type InferActionPayload, type InferStreamNames, type InferStreamPayload, type ContractTypeMap,} from "@ggui-ai/mcp-client";