Skip to content

Feedback Form

read as .md

The smallest useful ggui interaction: an agent renders a form, the user submits, the agent reads the typed data. Following the Zero Agent Code principle, you don’t hand-call handshake / render / consume — you configure the agent host with ggui’s MCP server and the LLM autonomously drives the loop from a user prompt.

Two flavors below — the canonical LLM-driven shape via the Claude Agent SDK, and a manual orchestration variant using @modelcontextprotocol/sdk when you need imperative control.

The developer’s job: define the typed contract, configure the host, write a user-facing prompt. The LLM autonomously calls ggui_handshakeggui_renderggui_consume and surfaces the typed payload.

import { query } from "@anthropic-ai/claude-agent-sdk";
import { defineContract } from "@ggui-ai/protocol";
// Typed contract — `actionData` for `submit` narrows to {rating, comments}.
// The contract still lives in code: it's how you document the shape
// the agent should negotiate during `ggui_handshake`.
// NOTE: the intent string is NOT a contract field — it travels separately
// as the flat `intent` argument of `ggui_handshake` (see the manual
// orchestration variant below).
const feedbackContract = defineContract({
propsSpec: {
properties: {
userName: { schema: { type: "string" } },
product: { schema: { type: "string" } },
},
},
actionSpec: {
submit: {
label: "Submit feedback",
schema: {
type: "object",
properties: {
rating: { type: "number", minimum: 1, maximum: 5 },
comments: { type: "string" },
},
required: ["rating", "comments"],
},
},
},
} as const);
async function collectFeedback(userName: string, product: string) {
const result = query({
prompt: `Collect product feedback from ${userName} about ${product}.
Render a feedback form with a 1–5 star rating, a comments text
area, and a submit button. Greet the user by name. Wait for
their submission, then report the rating and comments back
to me as JSON: {"rating": number, "comments": string}.`,
options: {
mcpServers: {
ggui: {
type: "http",
url: "http://127.0.0.1:6781/mcp", // ggui serve --dev-allow-all
headers: {
Authorization: "Bearer dev",
},
},
},
allowedTools: [
"mcp__ggui__ggui_handshake",
"mcp__ggui__ggui_render",
"mcp__ggui__ggui_consume",
],
},
});
// The LLM drives ggui_handshake → ggui_render → ggui_consume on its own.
// The render surfaces as an MCP-Apps resource the host mounts;
// the typed payload comes back in the final message.
for await (const message of result) {
if (message.type === "assistant") {
console.log(message.message.content);
}
}
}

For a complete runnable example (including streaming partial events, multi-turn refinement, and the host’s MCP wiring), see Examples → Claude Agent.

Manual orchestration: @modelcontextprotocol/sdk

Section titled “Manual orchestration: @modelcontextprotocol/sdk”

Use this shape when you need imperative control — e.g. you’re building a non-LLM workflow, testing the protocol directly, or reacting to every action the user fires (not just the terminal submit). This calls ggui’s MCP tools directly via the official MCP SDK; no LLM in the loop.

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("http://127.0.0.1:6781/mcp"), {
requestInit: {
headers: { Authorization: "Bearer dev" },
},
});
const client = new Client({ name: "feedback-script", version: "1.0.0" });
await client.connect(transport);
async function collectFeedbackLive(userName: string, product: string) {
// 1. Handshake — negotiate the contract before rendering.
// Pre-launch, `ggui_render` is handshake-first only.
const hsResp = await client.callTool({
name: "ggui_handshake",
arguments: {
intent: "Collect product feedback (live)",
blueprintDraft: {
contract: feedbackContract,
variance: {
seedPrompt: `Show ${userName} a product feedback form for ${product}.`,
},
},
},
});
const { handshakeId } = JSON.parse((hsResp.content[0] as { type: "text"; text: string }).text);
// 2. Render — accept the handshake's suggestion (omit `override`).
// `ggui_render` mints the `sessionId`.
const renderResp = await client.callTool({
name: "ggui_render",
arguments: {
handshakeId,
props: { userName, product },
},
});
const { sessionId, resourceUri } = JSON.parse(
(renderResp.content[0] as { type: "text"; text: string }).text
);
console.log(`Render ${sessionId} ready (${resourceUri}) — a host mounts it.`);
// 3. Consume — long-poll until the user submits.
while (true) {
const consumeResp = await client.callTool({
name: "ggui_consume",
arguments: { sessionId, timeout: 25 },
});
const { events, status } = JSON.parse(
(consumeResp.content[0] as { type: "text"; text: string }).text
);
for (const entry of events) {
// Every consume entry has `type: 'action'`; the contract's
// `actionSpec` key fires on `entry.intent`.
if (entry.intent === "submit") {
console.log("Submitted:", entry.actionData);
return entry.actionData;
}
console.log(`Action ${entry.intent}:`, entry.actionData);
}
if (status !== "active") break; // 'expired' — render TTL elapsed
}
}

The agent renders the form into whatever MCP-Apps host the user is in — inline in claude.ai / Claude Desktop, or in your own app via <AppRenderer>. The user fills it in and submits; the agent reads the typed result off ggui_consume:

Rating: 4/5 — Great product, would love dark-mode support!

There is no URL to hand the user — the render is an MCP-Apps resource the host mounts. (For local development without an MCP-Apps host, the operator console bundled with ggui serve can display the render for debugging.)