Connect an agent
The catalog prompt
Section titled “The catalog prompt”catalogPrompt() writes the system prompt that teaches a model the catalog: every component, prop and enum in one sentence each, the style whitelist and the token names. It is budgeted and gated in CI:
| Variant | Components | Size (tokens, estimated) |
|---|---|---|
catalogPrompt() |
91 | ≈ 8,748 |
{ lite: true } |
63 | ≈ 6,414 |
{ terse: true } |
91 | ≈ 6,181 |
{ sections: ["rules", "index", "functions", "style"] } |
91 (one line each) | ≈ 3,006 |
{ sections: ["rules"] } |
– | ≈ 515 |
componentPrompt(name) |
1 | ≈ 86 on average |
The prompt is sectioned: sections picks what to print, only narrows the component blocks to the ones a task needs, and componentPrompt(name) is one component’s block on demand. A host serves the rules and the index first and answers a model’s request for a component with its block, which is how the ergonomics eval measured the cheapest prompt. Pass extensions to include yours. The playground shows every variant in full.
A2UI over MCP
Section titled “A2UI over MCP”This MCP server has one tool whose description is the catalog prompt. A valid answer goes back as A2UI over MCP: a resource with the mime type application/json+a2ui holding the JSONL messages. The user’s actions come back as a tools/call of a2ui_event.
// An MCP server (JSON-RPC over HTTP, Node 24: `node server.ts`) whose one// tool lets a model show UI. The tool's description IS the catalog prompt;// every answer is validated before it reaches a client, and a valid surface// goes back as A2UI over MCP: an `application/json+a2ui` resource.import { createServer } from "node:http"import { CORE_CATALOG_ID, ICON_NAMES, reduceSurface } from "@exponential_app/eui"import { catalogPrompt } from "@exponential_app/eui/prompt"import { coreSchema } from "@exponential_app/eui/schema"import type { FlatComponent } from "@exponential_app/eui"
const tool = { name: `show_ui`, description: `Show the user a UI. ${catalogPrompt()}`, // the catalog ({terse: true} = smaller) inputSchema: { type: `object`, properties: { components: { type: `array`, items: { type: `object` } } }, required: [`components`] },}
function showUi(components: FlatComponent[]) { // The reducer never throws: unknown components become placeholders, bad props issues. const { issues } = reduceSurface(components, { catalogId: CORE_CATALOG_ID }) if (issues.length) return { isError: true, content: [{ type: `text`, text: issues.map((i) => `${i.id}: ${i.message}`).join(`\n`) }] } // the model retries const messages = [ { version: `v0.9`, createSurface: { surfaceId: `main`, catalogId: CORE_CATALOG_ID } }, { version: `v0.9`, updateComponents: { surfaceId: `main`, components } }, ] const text = messages.map((m) => JSON.stringify(m)).join(`\n`) // JSONL return { content: [{ type: `resource`, resource: { uri: `a2ui://surface/main`, mimeType: `application/json+a2ui`, text } }] }}
function handle(method: string, params: any): unknown { if (method === `initialize`) return { protocolVersion: `2025-06-18`, capabilities: { tools: {} }, serverInfo: { name: `ui-agent`, version: `1.0.0` } } if (method === `tools/list`) return { tools: [tool] } if (method === `tools/call` && params?.name === `show_ui`) return showUi(params.arguments?.components ?? []) if (method === `tools/call` && params?.name === `a2ui_event`) return { content: [{ type: `text`, text: `ok` }] } // actions come back here return {}}
createServer(async (req, res) => { // The JSON Schema of the catalog, for structured outputs or a validator. if (req.method === `GET` && req.url === `/catalog.schema.json`) return void res.end(JSON.stringify(coreSchema(ICON_NAMES))) let body = `` for await (const chunk of req) body += chunk const { id, method, params } = JSON.parse(body || `{}`) if (id === undefined) return void res.writeHead(202).end() // a notification res.setHeader(`content-type`, `application/json`) res.end(JSON.stringify({ jsonrpc: `2.0`, id, result: handle(method, params) }))}).listen(Number(process.env.PORT ?? 4300), () => console.log(`MCP on http://localhost:${process.env.PORT ?? 4300}`))On the client, McpTransport unpacks those resources into surfaces and sends actions back as tool calls, so any host from the render guides can show what this agent draws.
Validate
Section titled “Validate”Treat a model’s JSON as untrusted. reduceSurface(components, { catalogId }) always returns: the tree and a list of issues (an unknown component, a bad prop, a missing child). Send the issues back as a tool error and the model retries. For structured outputs or a validator of your own, coreSchema() is the whole catalog as JSON Schema, with the core catalog id https://ui.exponential.at/catalogs/core/v1.
A Motion carries a whole animation: motionPropsProblems(props) from @exponential_app/eui/motion/server checks it with the animation engine itself (the dialect compiles, Lottie has no expressions, the limits hold) and names every problem with its JSON pointer. See Animate with Motion.
Compiled in CI
Section titled “Compiled in CI”bun packages/exponential-ui/guides/check.ts agent builds guides/agent, starts the server and asks it for a valid and an invalid surface over MCP.