AI agent app reference
MCP tools from actions, agent identity and budget, human confirmation for destructive calls, EU AI Act disclosure.
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.
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.readis a read tool and runs.orders.refundis destructive: an MCP client or agent getsMAIN-MCP-0005until a person confirms, and the confirmed call is held as a pending approval thatagents.approvereleases.- 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
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: observeapp.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?