MainSDK
Search⌘K
How-to

AI agent app reference

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

Planned for 0.7

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

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}`
}

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet