---
title: "AI agent app reference"
description: "MCP tools from actions, agent identity and budget, human confirmation for destructive calls, EU AI Act disclosure."
section: "guides"
status: "Planned for 0.7"
spec: "reference-architectures"
canonical: "https://mainsdk.dev/docs/guides/reference-ai-agent"
updated: "2026-10-28"
---

# AI agent app reference

> MCP tools from actions, agent identity and budget, human confirmation for destructive calls, EU AI Act disclosure.

Status: Planned for 0.7 · Canonical: https://mainsdk.dev/docs/guides/reference-ai-agent · Updated: 2026-10-28

<!-- generated by tools/build_reference.py from packages/cli/templates/mainsdk/ai-agent; edit the template, not this page -->

Start from it: `npx mainsdk init --template mainsdk/ai-agent`

The reference architecture for an agent that acts: app actions become MCP tools, the agent has its own identity and daily budget, and a destructive call waits for a person. Standard: EU AI Act (disclosure, human oversight, logging).

- `orders.read` is a read tool and runs. `orders.refund` is destructive: an MCP client or agent gets `MAIN-MCP-0005` until a person confirms, and the confirmed call is held as a pending approval that `agents.approve` releases.
- Every audit record of an agent call names the agent as the actor (`actor.kind: agent`).
- Models are called through `ai`, never a model SDK, so price, budget and logging apply. Every answer to a person carries the disclosure line (`disclose()`).

Runs with no account and no key: `npm run dev` uses the scripted model. `npm test` runs the tests. Every change and every MainSDK release runs `.github/workflows/reference.yml`: `mainsdk check`, `mainsdk assure --standard eu-ai-act-2024-1689`, `npm run bench` and `npm run chaos`. `npm run verify-ci` fails when a job is removed.

Coming from direct model SDK calls? `mainsdk migrate raw` ends here when most calls are model calls.

## Performance

The benchmark harness fails a change when p95 goes over 25 ms on local drivers (`npm run bench`).

## main.yaml

```yaml
schema: 1
project: prj_template_ai_agent
baseline: main27
access:
  roles:
    support: [orders.*]
    member: [orders.read]
agents:
  support-bot:
    budget: { spend: $5/day, calls: 500/day }
    approve: { risk: destructive, spend-over: $1 }
mcp:
  confirm: { write: false, destructive: true }
ai:
  models: ['fake/*']
logging:
  audit: true
  retention: 12m
  redact: [personal, sensitive]
network:
  allow: []
  default: deny
  mode: observe
```

## app.ts

```ts
// The AI agent reference architecture: app actions become MCP tools, an agent acts under its own identity and budget,
// a destructive call waits for a person, and every audit record names the agent. Models are called through `ai`, never
// a model SDK directly, and the answer carries the AI disclosure text.
import { action, app, s } from 'mainsdk'

/** The line every agent answer carries: the person knows they talk to an AI (EU AI Act, article 50). */
export const DISCLOSURE = 'You are talking to an AI assistant.'

const orderId = s.object({ orderId: s.string() })

/** Read one order: allowed for a member, no confirmation. */
export const readOrder = action({
  name: 'orders.read',
  intent: 'Read one order',
  input: orderId,
  risk: 'read',
  mcp: true,
  resource: (i) => ({ type: 'orders', id: i.orderId, owner: i.orderId.split(':')[0] ?? '' }),
  run: ({ input }) => ({ order: input.orderId, status: 'shipped' }),
})

/** Refund one order: destructive, so an agent or an MCP client waits for a person's yes. */
export const refundOrder = action({
  name: 'orders.refund',
  intent: 'Refund one order',
  input: orderId,
  risk: 'destructive',
  mcp: true,
  spend: () => '$0.50',
  run: ({ input }) => ({ refunded: input.orderId }),
})

/** The MCP endpoint of the running app: tools are the actions marked `mcp: true`. */
export function mcpHandler(
  identify: Parameters<ReturnType<typeof app>['mcpServer']>[0]['identify'],
) {
  const a = app()
  return a.mcpServer({
    actions: [readOrder, refundOrder] as never,
    ...(identify ? { identify } : {}),
    // The person's answer to "may the agent do this?"; without a person present the answer is no.
    elicit: (c) => Promise.resolve((c.user as { confirm?: boolean } | undefined)?.confirm === true),
    log: a.log,
  })
}

/** Add the disclosure to an answer the agent gives a person. */
export function disclose(text: string): string {
  return `${text}\n\n${DISCLOSURE}`
}
```
