Actions and MCP
Write an action once and get an API endpoint, a server action, a CLI command and, if you choose, an MCP tool, all behind one policy.
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.
Apps are used two ways now: people click, and agents ask. An agent arrives through MCP with an intent ("refund my last order") and needs to act inside your app. MainSDK makes you write each action once, and every door into it runs the same checks.
One definition, every surface
import { action, db, pay } from "mainsdk";
import { s } from "mainsdk/schema";
export const refundOrder = action({
name: "orders.refund",
intent: "Refund an order the signed-in user owns",
input: s.object({ orderId: s.string(), reason: s.string() }),
risk: "destructive", // read | write | destructive
mcp: true, // opt in; default false
run: async ({ input, user }) => {
const order = await db.orders.for(user).get(input.orderId);
return pay.refund(order.paymentId, { reason: input.reason });
},
});flowchart LR H[HTTP<br/>POST /api/actions/orders.refund] --> C S[Server action] --> C L[CLI<br/>mainsdk run orders.refund] --> C M[MCP tool] --> C C[Same checks:<br/>identity, owner scope,<br/>rate limit, validation, policy] --> R[run] --> A[Audit event]
That one definition becomes:
- an HTTP endpoint,
POST /api/actions/orders.refund; - a server action for your UI;
- a CLI command,
mainsdk run orders.refund; - an MCP tool, only because it is marked
mcp: true; - an audit event for every call, recording the surface, user, decision and input hash.
There is no second door. A call through MCP can't skip any check the HTTP path runs. A user who doesn't own the order gets MAIN-DATA-0007 on every surface.
Full calls and options are on the action interface page.
Agents get a narrow, short-lived token
mcp:
expose: [orders.refund, orders.list] # or mark actions with mcp: true
confirm: { write: true, destructive: true }
auth: { issuer: auth, scopes: [orders:read, orders:refund], token-ttl: 10m }- The MCP endpoint follows the MCP authorization spec: OAuth 2.1 with PKCE and protected resource metadata. A request without a token gets
401with aWWW-Authenticateheader pointing to that metadata. - An agent acting for a person gets a short-lived token with narrow scopes through OAuth token exchange (RFC 8693). A token for
orders:readcan't callorders.refund. - The audit log records both the agent and the person who approved it.
A person says yes to risky actions
risk | What an agent needs |
|---|---|
read | Nothing extra. Read-only actions can also be exposed as MCP resources. |
write | A confirmation, by default (mcp.confirm.write: true) |
destructive | Always a person's yes, through MCP elicitation, before anything runs. A decline returns an error and nothing runs. |
Signed tool descriptions
A changed tool description can trick an agent (tool poisoning). MainSDK signs each tool description with the project's signing key and verifies it when tools are listed. A description edited after signing is left out and MAIN-MCP-0003 is logged. Each tool carries a schema version, so changes follow versioned contracts.
The MainSDK MCP server for your coding agent
Separate from your app's actions, mainsdk init registers the MainSDK MCP server with your coding agent. It lets the agent run the check, read logs and errors, and work through adopting an existing app (adopt.scan, adopt.report, adopt.lock, adopt.plan_module, adopt.verify_module, adopt.finish). See Use MainSDK with your coding agent.
Not decided yet
- Which key signs tool descriptions (the project key or a per-release key) and in what format.
- The full tool list of the coding-agent MCP server beyond
check,logsand theadopt.*tools.
Approval on the person's phone (OpenID CIBA) and signed approvals for confirmed actions arrive in phase 3. Agent-to-agent calls over A2A arrive in phase 5.
Next
Was this page helpful?