---
title: Generic MCP / Raw HTTP
description: Drive ggui from any MCP-compatible agent or via raw JSON-RPC over HTTP — official MCP client library, and language-agnostic curl/Python recipes.
---

:::note[One wire, two endpoints]
The wire format is identical across any self-hosted `ggui serve` and the hosted endpoint (coming soon). Only the URL and auth header change. Examples below default to a local `ggui serve --dev-allow-all`; see [Hosted vs self-hosted](#hosted-vs-self-hosted--what-to-swap) before the raw-HTTP walkthrough.
:::

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](/examples/claude-agent/) — it wires ggui in as a stock MCP server with no extra glue.

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

```typescript
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](/examples/claude-agent/) 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)

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

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_...`               |

:::caution[`--dev-allow-all` is for local dev only]
`--dev-allow-all` accepts any bearer — keep the default `127.0.0.1` bind and never expose it publicly. For real bearers use pair-minted ones (the strict default) — see [Pairing](/self-hosted/pairing/), and `--keys-file` to persist them across restarts.
:::

### Step 1 — Initialize the MCP session

```bash
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

```bash
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`](/api/mcp-protocol/#ggui_handshake) for the full input + output schemas.

### Step 3 — Render the UI

```bash
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

```bash
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](/api/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](/protocol/envelopes/) for the wire shape.

Renders decay implicitly via TTL — there is no explicit close ceremony.

---

## Python example

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

```python
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](/api/websocket-protocol/).

---

## See also

- [MCP Protocol Reference](/api/mcp-protocol/) — every method, every argument
- [Claude Agent SDK example](/examples/claude-agent/) — higher-level loop that drives these same tools
- [WebSocket Protocol](/api/websocket-protocol/) — push events instead of polling
- [Envelopes](/protocol/envelopes/) — `ActionEnvelope`, `StreamEnvelope`
- [OSS Quick Start](/oss-quickstart/) — run your own `ggui serve` in minutes