Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/action-mcp-tool-and-important.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-native/core": patch
---

Add `mcpTool` and `important` to `defineAction`, so an action declares its external-agent exposure and its first-request tool slot beside itself instead of in a plugin-level name list. `mcpTool` defaults to `agentTool`, so hiding an action from the agent hides it from outside agents too; declaring it overrides that inheritance in both directions. `mcpTool: false` hides an action from every MCP tier and the direct A2A surface (including the `--full-catalog` opt-in) while the in-app agent keeps calling it, `mcpTool: true` is the action-owned form of `mcp.connectorCatalog` membership, and `agentTool: false` with `mcpTool: true` makes an action MCP-only — external agents get it, the app's own agent does not. `important: true` puts an action in the agent's first tool list and narrows the derived default to the marked actions, the action-owned form of `initialToolNames`. Both name lists keep working, so an app can migrate one action at a time.
47 changes: 43 additions & 4 deletions packages/core/docs/content/actions-access-control.mdx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
---
title: "Access & Authorization"
description: "Who can call an action: exposure flags (agentTool, toolCallable), ctx-based scoping, accessFilter/assertAccess, and the authorize guard."
description: "Who can call an action: exposure flags (agentTool, mcpTool, toolCallable), ctx-based scoping, accessFilter/assertAccess, and the authorize guard."
---

# Access & Authorization

Five settings decide who can call an action: the agent, the frontend, an extension, an outside API caller, or nobody without a human's approval first. Most actions never need to touch any of these, since the defaults already fit normal UI-and-agent use.
Six settings decide who can call an action: the agent, the frontend, an extension, an outside API caller, or nobody without a human's approval first. Most actions never need to touch any of these, since the defaults already fit normal UI-and-agent use.

### Exposure flags {#exposure-flags}

All five flags default to the permissive value, so you only set one when you need to tighten a specific surface. The table below is a quick summary. The sections after it add the one detail each flag needs.
All six default to the permissive value — `mcpTool` by inheriting `agentTool` — so you only set one when you need to tighten a specific surface. The table below is a quick summary. The sections after it add the one detail each flag needs.

| Flag | Default | Restrictive value → who can still call | Typical use |
| --------------- | ------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `agentTool` | `true` | `false` → UI, HTTP, and CLI only (hidden from the model, MCP, and A2A) | UI-only or programmatic actions that shouldn't take up a tool slot |
| `mcpTool` | `agentTool` | `false` → your own app's agent only (hidden from MCP and A2A) | Actions that need an in-app screen or session on the other end |
| `toolCallable` | `true` | `false` → everything **except** the sandboxed extension iframe bridge (403) | Sensitive account or org changes (delete account, change membership) |
| `publicAgent` | off (private) | `{ expose: true }` → adds the action to **public** MCP/A2A/OpenAPI surfaces | Safe read or ingest tools that don't require authentication |
| `needsApproval` | `false` | `true` → the agent **pauses**, and a human must approve the specific call | Consequential side effects (send email, charge a card, delete) |
| `authorize` | none | a guard function → only callers it accepts, on **every** surface | Operations restricted to a role (per-app RBAC, admin-only ops) |

These flags are independent, so setting one doesn't change the others. `agentTool` controls what the model sees, see [Hide from the model](#agent-tool) below. `toolCallable` controls only the extension iframe, see [Block extension iframes](#tool-callable) below. `publicAgent` adds an opt-in public surface, and a public web route never implies public tool exposure, see [Expose to public agents](#public-agent) below. `needsApproval` gates execution after the call is already made, see [Require human approval](#needs-approval) below. `authorize` decides whether this caller may run the action at all, on every dispatch path, before `run()` is entered, see [Restrict callers by role](#authorize) below.
These flags are independent, with one exception: an undeclared `mcpTool` inherits `agentTool`. `agentTool` controls what the model sees, see [Hide from the model](#agent-tool) below. `mcpTool` narrows that to outside agents only, see [Hide from external agents](#mcp-tool) below. `toolCallable` controls only the extension iframe, see [Block extension iframes](#tool-callable) below. `publicAgent` adds an opt-in public surface, and a public web route never implies public tool exposure, see [Expose to public agents](#public-agent) below. `needsApproval` gates execution after the call is already made, see [Require human approval](#needs-approval) below. `authorize` decides whether this caller may run the action at all, on every dispatch path, before `run()` is entered, see [Restrict callers by role](#authorize) below.

## Hide from the model {#agent-tool}

Expand All @@ -39,6 +40,44 @@ export default defineAction({

Use this when you add a UI-only or purely programmatic action, or when the UI stops using an action that would otherwise stay exposed to the model.

## Hide from external agents {#mcp-tool}

`agentTool` is all-or-nothing: an action is a tool for every agent, or for none. `mcpTool` splits that in two, so an action can stay a normal tool for your own app's agent while never reaching Claude, Cursor, or a sibling app over MCP and A2A:

```ts
export default defineAction({
description: "Open the record inspector beside the current selection.",
mcpTool: false, // needs a live screen on the other end
schema: z.object({ recordId: z.string() }),
run: async ({ recordId }) => {
/* ... */
},
});
```

Use it for actions whose result only means something with your app's UI, an open session, or the current selection in front of the caller. `mcpTool` can only narrow `agentTool`, never widen it: an action with `agentTool: false` stays hidden everywhere. It governs the external tool surface itself, not what your own agent may do while answering an `ask_app` question — that run is your agent's, and it keeps its whole tool list.

Left undeclared, `mcpTool` follows `agentTool` rather than defaulting to a flat `true`, so hiding an action from the agent hides it from outside agents too and one flag stays one decision.

The opposite value has a second job. External agents are served a small, curated catalog by default rather than your whole action registry, and `mcpTool: true` is how an action declares itself part of it:

```ts
export default defineAction({
description: "List the databases connected to this workspace.",
mcpTool: true, // served to external agents, not just discoverable
http: { method: "GET" },
run: async () => {
/* ... */
},
});
```

This is the same catalog as the plugin's `mcp: { connectorCatalog: [...] }` list, declared on the action instead of in a separate array that has to be kept in sync as actions are renamed. Both forms work, and an app can move over one action at a time. Everything left out stays discoverable through `tool-search`.

Pairing the two flags the other way — `agentTool: false` with `mcpTool: true` — makes an action **MCP-only**: outside agents get it, your own app's agent never sees it. Use it for the export, sync, or handoff an outside tool needs and your own agent has no reason to call.

Membership is not permission. An external caller still has to pass the OAuth scope check, the `externalAgents` policy, and — for the direct, no-model A2A path — the `publicAgent` opt-in below.

## Block extension iframes {#tool-callable}

Extensions ([Alpine.js mini-apps in sandboxed iframes](/docs/extensions)) call actions through `appAction(name, params)`, running with the viewer's own permissions, secrets, and SQL scope. For sensitive operations, that's too much trust by default. Set `toolCallable: false` to make the extension bridge return a 403, while keeping the action callable from the UI, agent, CLI, MCP, and A2A:
Expand Down
16 changes: 16 additions & 0 deletions packages/core/docs/content/actions-defining.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ The framework auto-discovers every file in `actions/` and mounts it on startup.
description:
"Set false to hide from every agent tool list. See Access & Authorization.",
},
{
name: "mcpTool",
type: "boolean",
optional: true,
default: "agentTool",
description:
"Defaults to agentTool. Set false to hide from external agents over MCP and A2A, or true to declare curated catalog membership (with agentTool: false, MCP-only). See Access & Authorization.",
},
{
name: "important",
type: "boolean",
optional: true,
description:
"Set true to put the action in the agent's first tool list. See Keep the action surface small below.",
},
{
name: "toolCallable",
type: "boolean",
Expand Down Expand Up @@ -261,6 +276,7 @@ Every action the agent can see takes up a slot in the model's tool list, and a l
- Prefer **one CRUD-style `update` action** that takes a patch of optional fields, over many per-field actions like `update-name`, `update-order`, and `update-color`. The caller only sends the fields that changed.
- Before adding a new read action for every query or filter, consider a more general option first: the [provider API trio](/docs/template-dispatch-vault-integrations#provider-api) (`provider-api-catalog`, `provider-api-docs`, and `provider-api-request`) for provider data, or the dev `db-query` tool for app data.
- Mark UI-only or programmatic actions [`agentTool: false`](/docs/actions-access-control#agent-tool). They stay callable from the frontend or over HTTP, without taking a slot in the model's tool list.
- Mark the handful of actions the agent reaches for constantly `important: true`. Those are the schemas sent on the first model request; everything else loads on demand through `tool-search` when the agent needs it. Marking even one action narrows that first request to the marked set, so mark the whole starter set at once, not one action at a time. This is about what the first turn costs, not about access: an unmarked action is callable the moment `tool-search` returns it. It is the per-action form of the plugin's `initialToolNames` array, and an app can use either.
- Delete or hide actions the UI no longer uses, instead of leaving them exposed to the model.

The repo includes an advisory helper, `node scripts/audit-template-actions.mjs [template ...]` (alias `pnpm actions:audit`). It scans a template's `actions/` folder and flags actions the UI may no longer use, along with groups of per-field actions that could be combined into one. It always exits with code 0 and never fails CI, and its heuristics are conservative, so treat its output as a suggestion, not an error.
Expand Down
Loading
Loading