Guide 9 of 9

Connect an agent

The catalog prompt, A2UI over MCP, and validating what the model sends.

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:

VariantComponentsSize (tokens, estimated)
catalogPrompt()70≈ 6,191
{ lite: true }50≈ 4,486
{ terse: true }70≈ 4,495

Pass extensions to include yours. The playground shows the prompt in full.

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.

guides/agent/server.ts
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, catalogPrompt, coreSchema, reduceSurface } from "@exponential-at/ui"
import type { FlatComponent } from "@exponential-at/ui"

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

Never trust a model's JSON. reduceSurface(components, { catalogId }) never throws: it 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.

Compiled in CI

Every file on this page is a real file in apps/ui-site/guides/agent. CI copies it into an empty directory and builds it from scratch, the way you would, so the example cannot drift from the SDK:

shell
bun apps/ui-site/guides/check.ts agent

For a complete app on this platform, see the samples: four hosts rendering the same surface from a local A2UI server, in a third-party theme, with a custom extension component.