---
title: "Actions and MCP"
description: "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."
section: "concepts"
status: "Preview"
spec: "mcp-actions"
canonical: "https://mainsdk.dev/docs/concepts/actions-and-mcp"
updated: "2026-10-28"
---

# 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: Preview · Canonical: https://mainsdk.dev/docs/concepts/actions-and-mcp · Updated: 2026-10-28

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

```ts title="app/actions/orders.ts"
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 });
  },
});
```

```mermaid
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](https://mainsdk.dev/docs/interfaces/action) page.

## Agents get a narrow, short-lived token

```yaml title="main.yaml"
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 `401` with a `WWW-Authenticate` header 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:read` can't call `orders.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](https://mainsdk.dev/docs/concepts/interfaces-and-drivers#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](https://mainsdk.dev/docs/concepts/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](https://mainsdk.dev/docs/getting-started/with-your-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`, `logs` and the `adopt.*` 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

- [Actions and agent tools](https://mainsdk.dev/docs/interfaces/action)
- [Policy as code](https://mainsdk.dev/docs/concepts/policy)
- [Use MainSDK with your coding agent](https://mainsdk.dev/docs/getting-started/with-your-agent)
