Skip to content

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.

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.


Call MCP over JSON-RPC from any language. The flow is initializeggui_handshakeggui_renderggui_consume (poll, keyed by sessionId). Renders decay implicitly via TTL — no explicit close.

Every curl below targets a local ggui serve --dev-allow-all. To drive the hosted endpoint (coming soon) instead, swap two values:

WhatSelf-hosted (ggui serve) — defaultHosted (mcp.ggui.ai) — coming soon
Endpoint URLhttp://127.0.0.1:6781/mcphttps://mcp.ggui.ai/apps/<appId>
AuthorizationBearer dev (requires ggui serve --dev-allow-all; local dev only)Bearer ggui_user_...
Terminal window
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.

Terminal window
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.

Terminal window
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.

Terminal window
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.


A complete end-to-end run with the standard library and requests:

import json
import time
import 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.