---
title: Feedback Form
description: Configure an agent to render a feedback form via ggui's MCP server, then read the typed result — the smallest end-to-end ggui pattern.
---

:::note[Self-hosted by default]
This recipe wires the Claude Agent SDK to a local `ggui serve` endpoint — `http://127.0.0.1:6781/mcp` with `Authorization: Bearer dev`, which pairs with `ggui serve --dev-allow-all`. See the [OSS Quick Start](/oss-quickstart/) for the local-first walkthrough. The single-tenant server scopes everything to one app, so the bearer alone identifies it.
:::

:::note[Hosted (coming soon)]
On the hosted platform, point the same `mcpServers` entry at the universal endpoint `https://mcp.ggui.ai/` or your per-app endpoint `https://mcp.ggui.ai/apps/<appId>` — note there is **no `/mcp` suffix** on the hosted URLs (that suffix is local-`ggui serve`-only) — with a `ggui_sk_*` API key in the `Authorization` header. The appId rides in the per-app URL path, not a header. The wire flow is identical.
:::

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.

## Canonical: Claude Agent SDK + ggui MCP

The developer's job: define the typed contract, configure the host, write a user-facing prompt. The LLM autonomously calls `ggui_handshake` → `ggui_render` → `ggui_consume` and surfaces the typed payload.

```typescript
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);
    }
  }
}
```

:::tip[Typed payloads]
`feedbackContract` is wrapped in `defineContract({...} as const)` so `InferActionPayload<typeof feedbackContract, 'submit'>` resolves to `{rating: number; comments: string}` — the same shape `actionSpec.submit.schema` validates server-side when `ggui_consume` returns. The contract is documentation for the developer and a schema for the runtime; the agent learns the shape from the MCP tool descriptions, not from importing this constant. See [`ggui_consume`](/api/mcp-protocol/#ggui_consume).
:::

For a complete runnable example (including streaming partial events, multi-turn refinement, and the host's MCP wiring), see [Examples → Claude Agent](/examples/claude-agent/).

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

```typescript
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
  }
}
```

:::caution[No `sleep()` between polls]
Pass a `timeout` to `ggui_consume` (recommended: `15` or `25` seconds for chat agents — `25` is the maximum; the server rejects anything above 25 with `INVALID_PARAMS`) so the server long-polls. A busy `while` loop without a timeout — or with a hand-rolled `setTimeout` between calls — burns budget and adds latency.
:::

## What the user sees

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

## Related

- [Examples → Claude Agent](/examples/claude-agent/) — full runnable Claude Agent SDK + ggui MCP example
- [MCP protocol reference](/api/mcp-protocol/) — wire-level `ggui_handshake` / `ggui_render` / `ggui_consume` shapes
- [Event system](/architecture/event-system/) — `actionSpec`-driven flow and the `ConsumeEventEntry` shape
- [Multi-step wizard](/cookbook/multi-step-wizard/) — chain forms across successive renders
- [Glossary](/glossary/) — `render`, `contract`, `envelope`, `blueprint`