Skip to content

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.

Terminal window
npm install @ggui-ai/mcp-client
import { GguiClient } from "@ggui-ai/mcp-client";
const ggui = new GguiClient({
apiKey: "ggui_sk_...",
appId: "app_...",
});
// Push a UI to the user
const { 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 submit
const events = await ggui.waitForCompletion(sessionId);
console.log("User submitted:", events);
// Clean up
await ggui.close(sessionId);

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.

new GguiClient(config: GguiClientConfig)

GguiClientConfig:

FieldTypeRequiredDefaultDescription
apiKeystringYesAPI key for authentication (must start with ggui_sk_)
appIdstringYesYour ggui app ID
urlstringNohttps://mcp.guuey.com/mcpMCP endpoint URL
streamingUrlstringNoOptional SSE endpoint for long-poll consume() (timeouts > 25s). Omit to disable SSE routing.
timeoutnumberNo60000Request timeout in milliseconds (accommodates long-poll consume)
retryRetryConfigNo{ maxRetries: 3, baseDelay: 1000, maxDelay: 30000 }Retry configuration for transient errors

RetryConfig:

FieldTypeDefaultDescription
maxRetriesnumber3Maximum retry attempts
baseDelaynumber1000Base delay in ms for exponential backoff
maxDelaynumber30000Maximum 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.

The client holds a small amount of state so agents can fire-and-forget subsequent calls without re-passing the same arguments.

AccessorTypeDescription
sessionIdstring | nullActive session ID. Auto-set after the first push(). Used as the default for subsequent push / consume / send / close calls.
shellTypeShellType | nullActive shell (chat / fullscreen / spatial). Auto-included in push() so the generator picks the right layout.
interfaceContextInterfaceContext | nullFull 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 invoke
ggui.sessionId = "sess_abc";
ggui.interfaceContext = platformContext;
// Reset so the next push() creates a new session
ggui.newSession();

Materialise a UI emission. Creates a new session if none is supplied. Accepts two shapes:

  • Handshake-consumed: { handshakeId, props? } — execute a decision already computed by handshake().
  • Direct path: { story, session?, rendering?, infra?, shortcuts? } — the push performs any internal handshake itself.
async push(input: GguiPushInput): Promise<GguiPushOutput>

GguiPushInput — direct path:

FieldTypeRequiredDescription
storyPushStoryYesWhat the UI is for (see below).
sessionPushSessionNo{ id?: string; message?: string } — reuse an existing session or show a thinking-indicator message.
renderingPushRenderingNo{ shellType?; interfaceContext? } — rendering preferences. Auto-filled from client state if not provided.
infraPushInfraNo{ ttl?: number; model?: string } — session TTL (60–604800 seconds) and LLM model override.
shortcutsPushShortcutsNo{ blueprintId?; props?; autoCommit? } — skip negotiation/generation when you know what to render.

PushStory:

FieldTypeRequiredDescription
intentstringYesConcise purpose. Same intent = component reuse (e.g. "Gmail inbox for email triage").
promptstringNoAdditional instructions for this specific push.
dataRecord<string, unknown>NoStructured data to display.
contextstring | Record<string, unknown>NoAgent reasoning, user situation, background.
sourceToolsstring[]NoMCP tools that produced the data (provenance + blueprint matching).
wiredToolsstring[]NoMCP tools the UI can dispatch to the agent (absorbs both user-triggered action dispatches and runtime-invoke tools).

GguiPushOutput:

FieldTypeDescription
sessionIdstringSession ID (new or existing).
pageIdstringID of the stack page this push produced.
shortCodestringShort code for the render URL.
urlstringFull render URL (e.g. https://render.guuey.com/abc123).
action'create' | 'reuse' | 'update' | 'replace' | 'compose'What the negotiator decided to do.
codeReadybooleanWhether the rendered component is already compiled. false means generation is still in progress.
handshakeIdstringEchoed when push performed an internal handshake.
decisionNegotiatorDecisionFull negotiator decision — set only when this push consumed a handshakeId.
contractDataContractsData contract — convenience pullout from decision.contract.
interaction'display' | 'collect' | 'converse' | 'broadcast' | 'flow'Interaction mode — convenience pullout from decision.contract.interaction.
contractHashstringDeterministic 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 explicit
await 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 });

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>

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>

Remove the top UI card from the session stack.

async pop(sessionId: string): Promise<GguiPopOutput>

Returns: { poppedId: string | null; stackSize: number }


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:

ParameterTypeDescription
sessionIdstringSession to consume envelopes from.
timeoutnumber (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);

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!" },
});

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


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 and delete a session, freeing all associated resources.

async close(sessionId: string): Promise<GguiCloseOutput>

Returns: { success: boolean }


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:

FieldTypeDefaultDescription
pollIntervalnumber1000Poll interval in milliseconds (ignored when mode: 'subscribe').
maxWaitnumberno limitMaximum 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.

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 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): EventSubscriptionHandle

SubscribeOptions:

FieldTypeDefaultDescription
onEvent(envelope: ActionEnvelope) => voidCallback invoked for each envelope received.
onComplete() => voidCallback invoked when the session completes (status = 'completed').
onError(error: Error) => booleanCallback invoked on errors. Return true to retry, false to stop.
pollTimeoutnumber25Long-poll timeout per consume call in seconds.
maxDurationnumberno limitMaximum 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();

Clear the client’s cached sessionId. The next push() without an explicit session will create a new one.

newSession(): void

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>

List all available MCP tools on the server.

async listTools(): Promise<
Array<{ name: string; description: string; inputSchema?: Record<string, unknown> }>
>

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.


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.


Synchronous diagnostic getter — returns connection metadata without any network calls.

getConnectionInfo(): ConnectionInfo

ConnectionInfo:

FieldTypeDescription
urlstringMCP endpoint URL
appIdstringApp ID
initializedbooleanWhether the MCP session has been initialized
protocolVersionstringMCP protocol version used by this client
sdkVersionstringSDK package version

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" },
});

All errors extend GguiError which extends Error.

Error ClassCodeDescription
GguiErrorBase error class
GguiTimeoutErrorRequest timed out
GguiAuthError-32001Invalid or expired API key
GguiConnectionErrorNetwork connection failed
GguiRateLimitError429Rate limited (includes retryAfter in seconds)
GguiSessionNotFoundError-32002Session expired or deleted
McpProtocolErrorvariesMCP 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;
}
}

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 definitions
import {
defineContract,
type TypedStreamEvent,
type TypedAction,
type InferProps,
type InferActionNames,
type InferActionPayload,
type InferStreamNames,
type InferStreamPayload,
type ContractTypeMap,
} from "@ggui-ai/mcp-client";