Skip to content
Sneak peek — you found hosted ggui early · official launch soon

Claude Agent

read as .md

This is the canonical Claude pattern for ggui. You wire ggui as an MCP server in Claude Agent SDK’s mcpServers config, hand Claude the ggui posture prompt (GGUI_AGENT_SYSTEM_PROMPT), and Claude decides when to call ggui_handshake / ggui_render / ggui_consume based on the tool descriptions it discovers via tools/list. Zero ggui SDK code in your app — Claude drives the protocol directly.

A runnable version lives at samples/agents/claude-agent-sdk/. It runs the same query() loop, wrapped in an HTTP agent backend via @ggui-ai/agent-server — so it reads GGUI_MCP_URL / GGUI_MCP_BEARER from the environment instead of hard-coding the endpoint the way the snippet below does.

Terminal window
npm install @anthropic-ai/claude-agent-sdk @ggui-ai/protocol
Terminal window
export ANTHROPIC_API_KEY="sk-ant-..."
claude-agent.ts
import { query } from "@anthropic-ai/claude-agent-sdk";
import { GGUI_AGENT_SYSTEM_PROMPT } from "@ggui-ai/protocol";
// 1. Wire ggui as an MCP server. Claude Agent SDK will issue `tools/list`
// against this URL on session start and discover every `ggui_*` tool.
// `Bearer dev` authenticates because `ggui serve --dev-allow-all` accepts
// any bearer — local dev only.
const mcpServers = {
ggui: {
type: "http" as const,
url: "http://127.0.0.1:6781/mcp",
headers: { Authorization: "Bearer dev" },
},
};
// 2. Whitelist the ggui tools Claude is allowed to call. The SDK auto-namespaces
// every tool as `mcp__<serverName>__<toolName>`, so the prefix is `mcp__ggui__`.
const GGUI_ALLOWED_TOOLS = [
"mcp__ggui__ggui_handshake",
"mcp__ggui__ggui_render",
"mcp__ggui__ggui_update",
"mcp__ggui__ggui_amend",
"mcp__ggui__ggui_emit",
"mcp__ggui__ggui_consume",
"mcp__ggui__ggui_get_session",
];
async function chat(userPrompt: string) {
console.log(`\nUser: ${userPrompt}\n`);
// 3. `query()` returns an AsyncGenerator of SDKMessage events. The SDK runs
// the full tool-use loop internally — you just consume the stream.
for await (const message of query({
prompt: userPrompt,
options: {
// Any current Claude id works here — `claude-fable-5-1` for the most
// capable model, `claude-haiku-4-5-20251001` for the cheapest.
model: "claude-sonnet-5",
mcpServers,
allowedTools: GGUI_ALLOWED_TOOLS,
// The posture-only canonical ggui prompt. Tells Claude *when* to reach
// for a UI; tool descriptions tell it *how*.
systemPrompt: GGUI_AGENT_SYSTEM_PROMPT,
},
})) {
if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type === "text") {
process.stdout.write(block.text);
} else if (block.type === "tool_use") {
console.log(`\n[tool_use] ${block.name}`);
}
}
} else if (message.type === "user") {
// Tool results flow back as user-role messages from the SDK's perspective.
for (const block of message.message.content) {
if (block.type === "tool_result") {
console.log(`[tool_result] ${block.tool_use_id}`);
}
}
} else if (message.type === "result") {
console.log(`\n\n[done] subtype=${message.subtype}`);
}
}
}
chat("I want to book a restaurant reservation for this weekend").catch(console.error);
Terminal window
npx tsx claude-agent.ts

query() yields a stream of SDKMessage events. The shapes you’ll observe in a typical ggui turn:

  • system (init) — opening event listing the model, allowed tools, and MCP servers Claude discovered.
  • assistant — Claude’s response chunks. content is an array of blocks:
    • text — natural-language reply (streamed across multiple events).
    • tool_use — Claude invoking a ggui tool (e.g. mcp__ggui__ggui_handshake). The input field contains the tool arguments Claude generated.
    • thinking — extended-thinking blocks when reasoning is on.
  • user — tool results flowing back into the loop. content[].type === "tool_result" with tool_use_id pointing at the matching tool_use.
  • result — terminal event with subtype (success, error_max_turns, error_during_execution, …), usage totals, and total_cost_usd.

A reservation booking typically streams: systemassistant(text + tool_use:ggui_handshake) → user(tool_result) → assistant(tool_use:ggui_render) → user(tool_result with sessionId + resourceUri — the host mounts the UI from the MCP-Apps resource) → [user submits the form] → assistant(tool_use:ggui_consume) → user(tool_result with submission data) → assistant(confirmation text) → result.

  • Zero Agent Code. Your file imports query and GGUI_AGENT_SYSTEM_PROMPT and lists tool names — nothing else. The protocol lives behind MCP; Claude reads tool descriptions and drives it directly.
  • GGUI_AGENT_SYSTEM_PROMPT is posture, not instructions. It tells Claude when to reach for a UI (structured input, choices, visual presentation). Per-tool semantics ship from the server in tools/list — you don’t restate them.
  • Tool namespace is structural. mcp__<server>__<tool> is non-negotiable; it’s how the SDK routes calls. Match the prefix exactly in allowedTools.
  • Whitelist explicitly. The agent endpoint mounts two audiences — ggui_* agent tools and ggui_runtime_* tools the rendered iframe calls on its own behalf. (Operator and spec tools — ggui_ops_*, ggui_protocol_* — are not here; they live on the separate control plane.) Omit allowedTools and Claude can reach the runtime tools too, which is never what you want. The list above is the minimal agent-facing surface; the runnable sample adds ggui_list_sessions plus the blueprint and discovery tools. allowedTools matches exact names — there is no mcp__ggui__* wildcard, so each tool must be listed.
  • Prompt caching is automatic when the SDK detects a stable systemPrompt + mcpServers prefix across turns. No cache_control plumbing needed in your code.