Skip to content

Connect an agent

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.

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.

guides/agent/server.ts
// 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.

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.

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.