The contract is the product
The popular mental model of generative UI is a code generator with taste. You ask an AI for an interface, it writes React, the React runs. Call it “AI writes React.” It is an appealing model because every piece of it already exists. Models write competent component code today. If that were the whole problem, generative UI would have shipped the day the first coding model did.
It did not ship that day, because the code was never the hard part. The hard part sits around the code. An agent has no DOM and no event loop, and it has never seen a browser. Something still has to put an interface in front of a person and carry back what that person did. “AI writes React” has no answer for this. A generated component with no wire back to the agent is a screenshot with extra steps.
This publication exists to argue for a different model. Generative UI is an agent describing a surface, and a host rendering that surface under a contract. The host is the application the person is already sitting in, such as a chat client or an IDE, and it mounts the generated component in a sandboxed iframe. The object worth studying is the description, and the machinery that holds both sides to it.
What actually crosses the wire
Watch what happens in GGUI, the open generative-UI protocol this
publication is built around, when an agent shows a user an interface.
The agent never emits JSX. It negotiates a
contract. That contract
declares the data the surface starts with, the deliveries it accepts
while live, the actions a user can take, and the state the agent may
observe. Four specs carry the data, and two catalogs sit beside them: agentCapabilities, the MCP tools the agent may call, and clientCapabilities, the browser helpers the component imports. Three of the four specs are flat maps keyed by
name,
and propsSpec nests its entries under properties:
| Spec | Direction | Answers |
|---|---|---|
propsSpec |
agent → client | what data the UI starts with |
streamSpec |
agent → client | what real-time deliveries it accepts |
actionSpec |
client → agent | what user interactions it emits |
contextSpec |
client → agent | what observable client state the LLM sees |
Here is what one declared interaction looks like on the wire, from a todo-list contract:
"actionSpec": {
"addTodo": {
"label": "Add todo",
"schema": {
"type": "object",
"properties": { "text": { "type": "string" } },
"required": ["text"]
},
"nextStep": "todo.add" // advisory hint; must name a tool in agentCapabilities.tools
}
}
Every action carries a label, which the schema requires. When the user clicks the button this action backs, the card, the rendered surface in the iframe, checks the payload against actionSpec.addTodo.schema and relays it through the host as a tool call. The server appends the event to the session’s consume buffer and tells the card the action arrived only once that append has succeeded. GGUI’s documentation says the server validates the payload against the contract’s actionSpec, and on the live channel it does. On this relay path it does not yet, so today the card’s check is the only one there. The agent
drains it with one tool call. The names are load-bearing: they appear in every envelope, and
they are how producer and consumer agree on which declared slot a
delivery targets. Every spec is optional. The server refuses a context snapshot carrying slots the contract never declared, and stream deliveries to channels it never declared. For actions, a missing spec means there is nothing to check them against. A contract with no propsSpec takes no props: ggui_render drops them when the agent accepts the proposal and refuses them on an override, and only ggui_update and ggui_amend skip the check.
The component code exists, of course. Something has to run in the
iframe. But notice its position in the system. The code is derived
output, generated to satisfy the contract, and cached and reused
whenever a new render’s contract and design variance both match a stored
pair. In GGUI’s render response the stable identifier is the
blueprintId, equal across renders that reused the same component. The
code is fungible; the contract is the load-bearing axis of its identity.
If you want to know what a generative UI system really is, ask what it
caches by.
The agent holds no rendering code either
The same inversion holds on the agent side. Point a host like Claude
Desktop at the server and a GGUI agent is an MCP configuration block and
a system prompt describing its role. Run your own backend instead and
the list grows by one item: a thin adapter for your agent SDK, wired into the agent server in a few lines. What stays absent either
way is the machinery you would expect to write. There is no event
handler and no polling loop. When the user acts and the server cannot confirm that a call is already waiting for it, the card’s own doorbell, a message it sends through the host, names the call to make next, and that call is
ggui_consume, a long-poll that returns on the first event or at
timeout.
The loop lives in the runtime, and the agent just follows the tool’s own
instruction. The
event that comes back carries actionData (what the user did) and
uiContext (the contract’s context slots at the moment they did it).
The agent reasons about that, in the names it agreed to in the contract,
never about DOM nodes.
A confession belongs here. An official @ggui-ai/mcp-client package
existed once. It was retired on 2026-05-19 because shipping a
brand-specific client library violated the principle it was meant to
serve. The protocol’s only required client contract is MCP itself, and any runtime that
speaks MCP over HTTP can point at the same endpoint and get the same
wire. Whether that
wire mounts as a surface in front of a person is a second question,
settled by whether the host implements MCP
Apps — the same protocol reaches
everywhere, the same rendered result does not. When your interop story
requires your SDK, you do not have an interop story; you have a product
with a long setup guide.
What keeps the contract small
A contract like this survives only if it refuses to absorb things that do not belong on it. GGUI’s first test for any candidate field is one question. Can the agent observe this from the wire alone? If yes, it is data, and it belongs on one of the four specs. If no, it is behavior, and it lives in the generated component code. Scroll position, focus rules, toasts, animations and clipboard writes all live there. The agent knows the list changed, and the component decides to scroll the new item into view. The agent receives the submission, and the component decides which invalid field to focus.
This split is what keeps the description renderer-neutral. A React renderer, a hand-written test harness, and a native renderer someone writes later can all read the same contract and choose their own behaviors. The moment a scroll command or a toast field leaks onto the wire, every renderer must honor it, and the agent is suddenly dictating presentation mechanics it cannot even observe. If the agent must know a behavior happened — say, the user pressed copy — the contract carries the signal as a declared action, and the clipboard write stays in the component. The few exceptions are declared rather than smuggled: an action can say it fires only once or asks for confirmation, and a context slot can set its own debounce, because only the author knows these things and generated code cannot be trusted to guarantee them.
“Contract” is a load-bearing word
Any system can print the word contract in its docs. The reason to take it seriously is enforcement, and this is where most generative UI framings go quiet. GGUI’s published bar for the word requires four things of every boundary that claims it. Named parties, explicit per-party obligations, a defined failure mode per obligation, and violations an operator can see without a debugger. Three of four fails the bar, and a pile of MUST strings in docstrings fails it too.
Concretely, on the render path:
- The props key set is closed at every level by default: the validator closes every object node the contract does not explicitly open. A render whose props
carry a key the contract never declared, an extra
idor a renamed field, fails with a typedcontract_violationthe agent receives on the call itself and can fix by correcting the values and retrying the same call. The contract stands. One leniency is written down: when the agent accepts a contract that carries nopropsSpec, stray props are dropped rather than rejected, and the drop appears in the operator’s log while the agent hears nothing about it. - Deliveries to undeclared stream channels are rejected by the server’s validators, and the caller is told.
- An action schema that does not fit its target tool’s input schema fails the render with a named error:
contract_validation_failedagainst the schema the contract declares, orschema_mismatch_errorwhen the tool registered on the server has drifted from it, before any user ever sees the surface. - The server owes the surface an honest answer about delivery. On the tool-call path GGUI’s runtime uses, a user’s action is appended to the session’s consume buffer before the server says it arrived, and an action the server cannot queue comes back as a refusal, which the surface shows the user as a “could not reach the agent” notice. A payload the card itself rejects never leaves the card, and today it is logged to the console only.
Every one of those is an obligation with a named failure you can observe from outside. That is the difference between a wire format and a contract. A wire format tells you what shapes exist. A contract tells you who owes what to whom, and what happens, visibly, when they don’t pay.
Why this is the product
Step back from GGUI and the claim generalizes. A generative UI system built on “AI writes React” is a feature. It lives inside one vendor’s renderer, its outputs are as trustworthy as this week’s model, and no third party can implement the other side of it, because there is no other side. A generative UI system built on a described surface under an enforced contract is a different kind of object. It can be specified and conformance-tested. It can be implemented twice, by parties who have never spoken, and where the implementations disagree a test kit adjudicates. This is also why MCP Apps matters more than any single renderer. It standardizes how a host mounts an agent-delivered surface, which lets a contract like this travel across MCP-Apps-compliant hosts instead of living inside one vendor’s chat window.
There is an economic reading too. Models will keep getting better at writing components, and every improvement makes the generated code cheaper and more disposable. The contract is the only part of the system that appreciates. Each new surface rendered against it, and each host that learns to mount it, makes the description worth more. Build the business on the part that appreciates.
That is also the honest yardstick for reading anything in this space, GGUI included. When you evaluate a generative UI system, skip the demo and ask two questions. Where is the contract written down, and what does each party owe? And when an obligation breaks, what does an operator see? If the answers are “the prompt,” “it’s flexible,” and “the model usually handles it,” you are looking at a code generator with taste. The taste may be excellent. It is still not the product.
Corrected on 26 September 2026. An earlier version of this post said
that a click travels to the server over the session’s live channel and
that the server validates it there. In GGUI’s runtime the card checks
the payload and the host relays it as a tool call. The server’s own check runs on the live channel and does not yet run on this path. The todo example lacked the
label every action requires, and it did not say that nextStep must
name a tool the contract declares, which the server enforces. The post
also overstated several things: it called the props closed at every
level without saying this is a default, and without the one leniency
for a contract with no propsSpec; it said the server owes the agent
delivery, where it owes the surface an answer; it said the doorbell
comes from the server, where it comes from the card; and it listed
debounce cadence as component code, where a context slot can declare
it. It also said one test keeps every behavior off the contract, where
a few declared exceptions exist. Smaller fixes cover the two catalogs
beside the four specs, what a missing spec means for each plane, which
error a schema mismatch raises, and the renderers that exist today.
This note covers the changes made on 26 September. An earlier revision
on 15 September added links to sources and corrected some claims
without a note.