Generic MCP / Raw HTTP
read as.md Use ggui from any language or framework — through the official MCP TypeScript client, or raw HTTP with JSON-RPC. There is no ggui-specific wrapper SDK: every endpoint is a vanilla MCP server, so any spec-compliant client works.
Building on top of the Claude Agent SDK instead? See Claude Agent SDK example — it wires ggui in as a stock MCP server with no extra glue.
Using the MCP client library (TypeScript)
Section titled “Using the MCP client library (TypeScript)”The official @modelcontextprotocol/sdk package speaks the MCP wire end-to-end. Point its StreamableHTTPClientTransport at http://127.0.0.1:6781/mcp and call ggui’s tools by name.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "my-agent", version: "0.1.0" }, {});
// `Bearer dev` authenticates because `ggui serve --dev-allow-all` accepts// any bearer — local dev only.await client.connect( new StreamableHTTPClientTransport(new URL("http://127.0.0.1:6781/mcp"), { requestInit: { headers: { Authorization: "Bearer dev", }, }, }));
// Discover the tool surface.const { tools } = await client.listTools();console.log( "Available tools:", tools.map((t) => t.name));// → ['ggui_handshake', 'ggui_render', 'ggui_consume',// 'ggui_update', 'ggui_get_session', 'ggui_list_sessions',// 'ggui_list_gadgets', 'ggui_list_themes', 'ggui_emit',// 'ggui_list_featured_blueprints', 'ggui_search_blueprints',// 'ggui_render_blueprint']
// handshake → render at the raw MCP layer.const handshakeResult = await client.callTool({ name: "ggui_handshake", arguments: { intent: "feedback form", blueprintDraft: { contract: { /* DataContract — propsSpec, actionSpec, contextSpec, streamSpec */ }, }, },});const handshake = JSON.parse(handshakeResult.content[0].text) as { handshakeId: string; suggestion: { origin: "cache" | "agent" | "synth"; blueprintMeta: { blueprintId?: string; contractHash: string }; };};
const renderResult = await client.callTool({ name: "ggui_render", arguments: { handshakeId: handshake.handshakeId, props: {}, },});const { sessionId, resourceUri } = JSON.parse(renderResult.content[0].text) as { sessionId: string; resourceUri: string;};console.log("Render:", sessionId, "→", resourceUri);The three-noun model: a tool is what the agent calls (the MCP methods above); a gadget is a renderer-side capability the LLM may opt into when generating the UI; a blueprint is a cached recipe the handshake returns as a suggestion (with origin: 'cache' | 'agent' | 'synth') — accepting it on render reuses the provisional blueprintId and skips regeneration.
Need a higher-level Claude-flavored shortcut? The Claude Agent SDK example registers ggui as an MCP server and lets the agent loop drive ggui_handshake / ggui_render / ggui_consume on its own.
Using raw HTTP (no SDK)
Section titled “Using raw HTTP (no SDK)”Call MCP over JSON-RPC from any language. The flow is initialize → ggui_handshake → ggui_render → ggui_consume (poll, keyed by sessionId). Renders decay implicitly via TTL — no explicit close.
Hosted vs self-hosted — what to swap
Section titled “Hosted vs self-hosted — what to swap”Every curl below targets a local ggui serve --dev-allow-all. To drive the hosted endpoint (coming soon) instead, swap two values:
| What | Self-hosted (ggui serve) — default | Hosted (mcp.ggui.ai) — coming soon |
|---|---|---|
| Endpoint URL | http://127.0.0.1:6781/mcp | https://mcp.ggui.ai/apps/<appId> |
Authorization | Bearer dev (requires ggui serve --dev-allow-all; local dev only) | Bearer ggui_user_... |
Step 1 — Initialize the MCP session
Section titled “Step 1 — Initialize the MCP session”curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "clientInfo": { "name": "my-agent", "version": "1.0" }, "capabilities": {} } }'protocolVersion here is the MCP transport spec date, not the ggui protocol draft. The Accept header is mandatory — the server rejects requests that don’t accept both application/json and text/event-stream. Responses come back as a single SSE event (event: message + data: {...}); parse the data: line as JSON — the # → {...} comments below show that parsed payload.
Step 2 — Negotiate a handshake
Section titled “Step 2 — Negotiate a handshake”curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Accept: application/json, text/event-stream" \ -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": { "properties": { "fields": { "schema": { "type": "array", "items": {} } } } }, "actionSpec": { "submit": { "label": "Submit", "schema": { "type": "object" } } } } } } } }'# → { handshakeId, action, suggestion, nextStep? }The returned suggestion.origin is the routing discriminator: 'cache' (a cached blueprint matched — render with {handshakeId, props} and omit override for a cheap cache delivery), 'agent' (gen against the draft on render), or 'synth' (gen against a server-amended contract). suggestion.blueprintMeta is always present; it carries a blueprintId when the server matched or pre-minted one. See ggui_handshake for the full input + output schemas.
Step 3 — Render the UI
Section titled “Step 3 — Render the UI”curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "ggui_render", "arguments": { "handshakeId": "h_...", "props": {} } } }'# → { sessionId, resourceUri, action, contractHash, blueprintId, variantKey, cache, nextStep? }props is REQUIRED (pass {} when the contract declares no propsSpec); omit override to accept the suggestion as-is, or pass override: {contract?, variance?} to re-aim. The render comes back as an MCP-Apps resource (resourceUri) a host mounts — there is no URL to hand the user; you poll for their actions with sessionId.
Step 4 — Poll for events
Section titled “Step 4 — Poll for events”curl -X POST http://127.0.0.1:6781/mcp \ -H "Authorization: Bearer dev" \ -H "Accept: application/json, text/event-stream" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "ggui_consume", "arguments": { "sessionId": "4f6b2c0e-…", "timeout": 20 } } }'# → { events: ConsumeEventEntry[], status: "active" | "expired" }Consume is keyed by the sessionId from step 3. timeout (seconds): an integer in [0, 25]; 0 = immediate. Values outside that range reject with INVALID_PARAMS (-32602) — the cap dodges infrastructure kill windows (API-gateway 30s HTTP limits). Pick 5–15s typically, 25 max; to wait longer, re-call ggui_consume in a loop — a longer wait is your loop, not a bigger timeout. For push-style delivery, prefer the WebSocket Protocol over polling; raw HTTP callers loop ggui_consume per render. Each entry has {type: 'action', sessionId, intent, actionData, uiContext, actionId, firedAt}. See Envelopes for the wire shape.
Renders decay implicitly via TTL — there is no explicit close ceremony.
Python example
Section titled “Python example”A complete end-to-end run with the standard library and requests:
import jsonimport timeimport requests
API_URL = "http://127.0.0.1:6781/mcp"HEADERS = { "Authorization": "Bearer dev", # ggui serve --dev-allow-all; local dev only "Accept": "application/json, text/event-stream", "Content-Type": "application/json",}
request_id = 0
def parse_sse(body: str) -> dict: # Responses come back as a single SSE event; the JSON-RPC payload is # the `data:` line of the `message` event. for line in body.splitlines(): if line.startswith("data:"): return json.loads(line[len("data:"):].strip()) raise RuntimeError(f"no SSE data line in response: {body[:200]}")
def mcp_request(method: str, params: dict | None = None) -> dict: global request_id request_id += 1 payload = {"jsonrpc": "2.0", "id": request_id, "method": method} if params: payload["params"] = params return parse_sse(requests.post(API_URL, headers=HEADERS, json=payload).text)
def call_tool(name: str, arguments: dict) -> dict: result = mcp_request("tools/call", {"name": name, "arguments": arguments}) if "error" in result: raise RuntimeError(f"MCP error: {result['error']}") return json.loads(result["result"]["content"][0]["text"])
# 1. Initialize the MCP session.mcp_request("initialize", { "protocolVersion": "2025-11-25", "clientInfo": {"name": "python-agent", "version": "1.0"}, "capabilities": {},})# Notifications carry no `id` (JSON-RPC), so post this one directly.requests.post(API_URL, headers=HEADERS, json={ "jsonrpc": "2.0", "method": "notifications/initialized",})
# 2. Negotiate the contract.handshake = call_tool("ggui_handshake", { "intent": "Product feedback form", "blueprintDraft": { "contract": { "propsSpec": {"properties": { "rating": {"schema": {"type": "number"}}, "comments": {"schema": {"type": "string"}}, }}, "actionSpec": {"submit": {"label": "Submit feedback", "schema": {"type": "object"}}}, }, },})
# 3. Render — accept the handshake's suggestion as-is (omit `override`).result = call_tool("ggui_render", { "handshakeId": handshake["handshakeId"], "props": {},})session_id = result["sessionId"]print(f"resource: {result['resourceUri']}")
# 4. Poll for events — keyed by sessionId.while True: consume = call_tool("ggui_consume", {"sessionId": session_id, "timeout": 20}) for entry in consume["events"]: # ConsumeEventEntry: {type: 'action', intent, actionData, uiContext, ...} print(f"intent={entry['intent']} data={entry['actionData']}") if consume["status"] == "expired": break if consume["events"]: # Got the gesture we needed — exit on first non-empty payload. break time.sleep(2)
# Render decays implicitly via TTL — no explicit close.For long-lived UIs, prefer the WebSocket channel (ws://127.0.0.1:6781/ws self-hosted; wss://mcp.ggui.ai/ws hosted, coming soon) over polling — see WebSocket Protocol.
See also
Section titled “See also”- MCP Protocol Reference — every method, every argument
- Claude Agent SDK example — higher-level loop that drives these same tools
- WebSocket Protocol — push events instead of polling
- Envelopes —
ActionEnvelope,StreamEnvelope - OSS Quick Start — run your own
ggui servein minutes