# MCP Servers

Expose a curated set of tools to agents running outside Twisp over the Model Context Protocol, with every request recorded as an execution and every call bounded by the requesting principal's and the server's policies.

An **MCP server** is an [action](https://www.twisp.com/docs/reference/actions.md) of kind `mcp` that offers some of your [tools](https://www.twisp.com/docs/reference/actions/tools.md) to agents running outside Twisp, such as a coding assistant or an operations bot, over the [Model Context Protocol](https://modelcontextprotocol.io/specification/2026-07-28). Every request an external agent makes is one [execution](https://www.twisp.com/docs/reference/actions/executions.md) of the server: what it asked, what it was shown, and everything its calls did are recorded and can be investigated like the work of an agent Twisp runs.

> **Note:**
>
> An MCP server grants nothing. Every operation a tool performs through it must be allowed by the principal the request's token resolves to, by the server's `policies`, and by the tool's own policies. A principal cannot do through an MCP server what it could not do itself, and the server's policies bound what any principal can do through it.

## Defining a server

An MCP server is created with `createAction`, like any action. It has kind `mcp`, an `mcp` block, a `session`, and optionally `tools`, `skills`, and `policies`. It has no `triggers`, `tool`, or `agent` block, and its `config` is `{}`.

```graphql
mutation {
  actions {
    createAction(
      id: "3c1e4d1a-7a1b-4bde-9b7e-6b8f3f0c5a21"
      input: {
        name: "fraud-ops"
        description: "Account review tools for the fraud operations team."
        kind: "mcp"
        config: {}
        mcp: {
          name: "fraud-ops"
          instructions: "Tools for reviewing account activity."
          session: {
            start: {
              input: { principal: "execution.input.principal", subject: "execution.input.subject" }
            }
          }
        }
        session: {
          inputSchema: {
            type: "object"
            properties: { principal: { type: "string" }, subject: { type: "string" } }
            required: ["principal", "subject"]
          }
          id: "string(size(session.input.principal)) + ':' + session.input.principal + ':' + session.input.subject"
        }
        tools: [
          { name: "get_account_context" }
          { name: "notify_human_ops", input: { requested_by: "execution.input.subject" } }
        ]
        policies: [{ effect: ALLOW, actions: [SELECT], resources: ["financial.*"] }]
      }
    ) {
      version
    }
  }
}
```

| `mcp` field    | Meaning                                                                                                                                                                                                                                                                                                                          |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`         | The server's name as MCP clients see it: 1 to 64 letters, digits, underscores, hyphens, or periods. Required.                                                                                                                                                                                                                    |
| `instructions` | Guidance for the external agent, returned by `server/discover`. At most 64 KiB.                                                                                                                                                                                                                                                  |
| `session`      | How each request selects its [session](https://www.twisp.com/docs/reference/actions/mcp.md#sessions). Required.                                                                                                                                                                                                                                                                     |
| `mode`         | `PREVIEW` or `LIVE`. See [preview mode](https://www.twisp.com/docs/reference/actions/mcp.md#preview-mode). Omitted resolves to `PREVIEW` when the definition is written, and is stored.                                                                                                                                                                                             |
| `intent`       | `REQUIRED`, `OPTIONAL`, or `NONE`. See [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent). Omitted is `REQUIRED`.                                                                                                                                                                                                                                   |
| `limits`       | `callsPerMinute`, the [rate limit](https://www.twisp.com/docs/reference/actions/mcp.md#rate-limits) of each session: 300 when omitted, at most 6,000.                                                                                                                                                                                                                               |
| `resources`    | [Resource templates](https://www.twisp.com/docs/reference/actions/mcp.md#resources), each with a `uriTemplate` (RFC 6570 level 1, such as `twisp://accounts/{account_id}`), a `name`, a `description`, and the `tool` that reads it. The tool must be [read-only](https://www.twisp.com/docs/reference/actions/mcp.md#annotations), and each template variable must name a top-level input property of the tool that accepts a string. |

- **Tools.** `tools` are ordinary [tool references](https://www.twisp.com/docs/reference/actions/tools.md#tool-references), with a `name`, an optional `version`, and fixed `input`. A reference can name a tool, a [callable agent](https://www.twisp.com/docs/reference/actions/agents.md#callable-agents), or a [native tool](https://www.twisp.com/docs/reference/actions/tools.md#native-tools): `graphql` and `cel`, and [`program`](https://www.twisp.com/docs/reference/actions/mcp.md#programs), [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke), [`get_task`](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), and [`cancel_task`](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), which only an MCP server can list. A server offers exactly the tools it lists. At most 128 tools. The names the external agent sees must be distinct at the versions the write reads. `approval` cannot be required.
- **Fixed input.** Expressions read `execution.input`, the [request](https://www.twisp.com/docs/reference/actions/mcp.md#requests), and `session.id` and `session.input`. That is how you stamp who asked into a tool's input, as `requested_by: "execution.input.subject"` does above. `event` is not available. Fixed values replace anything the external agent supplies.
- **Skills.** `skills` are skill references, at most 64, without `preload`, which the server offers as [skills and prompts](https://www.twisp.com/docs/reference/actions/mcp.md#skills). A skill's name must be a valid Agent Skills name: lowercase letters and digits, in words joined by single hyphens, at most 64 characters. Its `description` is required, and at most 1,024 characters.
- **Versions.** A reference with a `version` always calls that version. One without calls the tool's latest version, resolved when each request's execution starts. The server itself answers at its current version, and disabling it refuses every request at once.
- **Execution settings.** The server's `execution.responseTimeout` bounds each request: 1 minute by default, and at most 10 minutes. A tool called beneath it is also bounded by its own `responseTimeout`.

Creating a server requires permission to read and invoke each tool and skill it references, as for an agent.

## The endpoint

Each MCP server has its own endpoint, which is also the resource its tokens are issued for:

```
https://api.<region>.<environment>.twisp.com/mcp/v1/<account_id>/<name>
```

`<name>` is the action's registry `name`. The server speaks protocol revision `2026-07-28` over Streamable HTTP. It is stateless: every request carries its protocol version, client information, and capabilities, and is authorized by its own bearer token. Hosts that speak an earlier revision, `2025-11-25`, `2025-06-18`, or `2025-03-26`, can connect too: they get tools, resources, and prompts, but not [tasks](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), so they cannot call callable agents.

- **Signing in.** A person connects an MCP host by signing in to Twisp as the user they are, on the same page and with the same MFA as the console; [Connecting MCP hosts](https://www.twisp.com/docs/guides/mcp-hosts.md) has the step for each host. The host discovers the sign-in from the endpoint, and stores and refreshes its token itself. Each request is made as that user, with the permissions they have in the console.
- **Tokens.** A request carries the access token the host got from Twisp sign-in, with an `aud` claim naming the region's MCP endpoints, `https://api.<region>.<environment>.twisp.com/mcp`. A token for one MCP server works at the region's other MCP servers your user may reach, and nowhere else: not at the GraphQL API, and not in another region. A token from anywhere else, including Twisp's IAM token exchange, is not accepted. A missing, invalid, or wrongly addressed token gets `401` with a `WWW-Authenticate` header naming the server's Protected Resource Metadata and the scope to request, `openid`. The endpoint's account must be one of the user's accounts.
- **Connecting.** The principal needs permission to read the server's action and `INVOKE` on it. Without `INVOKE`, a request gets `403`. The principal needs no `INVOKE` on the server's tools: the server's references authorize those calls.
- **Discovery.** `server/discover` returns the supported protocol version, the capabilities the server offers, among them the [Tasks extension](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), and its `instructions`. `tools/list` returns one tool per reference that can be called, in reference order: a tool whose action or version is disabled or deleted is left out. Each tool's `inputSchema` is its input schema without the fixed input's properties and without any `x-mcp-header` annotation: the server reads arguments only from the request body, so a host sends no `Mcp-Param` headers. Both can be cached for 60 seconds.
- **Methods.** Every server answers `server/discover`, `tools/list`, and `tools/call`, and the Tasks extension's `tasks/get`, `tasks/update`, and `tasks/cancel`. A server with `skills` also offers resources, prompts, and the [Skills extension](https://modelcontextprotocol.io/extensions/skills/overview) (`io.modelcontextprotocol/skills`), and a server with resource templates offers resources; `server/discover` declares exactly what the server offers. A method of a capability the server does not offer, like any other method, gets `404` and `-32601`.

A request refused before its token is checked, such as one with mismatched protocol headers or an unsupported version, is not recorded.

## Requests

Each request that passes the token check is one execution of the server, whose input is the request, with its event type `mcp.request`. `ActionExecution.mcpRequest` reads it:

| Field                      | Meaning                                                                                                               |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `method`                   | The MCP method, such as `tools/list` or `tools/call`.                                                                 |
| `name`                     | The tool a `tools/call` named, the URI a `resources/read` or `skills/get` named, or the prompt a `prompts/get` named. |
| `arguments`                | The call's arguments as the external agent sent them, without its stated intent.                                      |
| `intent`                   | The call's [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent).                                                                           |
| `principal`                | The Twisp principal the token resolved to.                                                                            |
| `subject`                  | The token's `sub`: the signed-in user.                                                                                |
| `oauthClientId`            | The token's `client_id`: the MCP host application.                                                                    |
| `clientInfo`               | The client's name and version, as it reported them. They are not verified.                                            |
| `meta`, `metaOmitted`      | The request's `_meta` other than the protocol's own fields, such as trace context; not recorded over 16 KiB.          |
| `argumentsOmitted`         | The request was refused as too large to record, and its arguments were not recorded.                                  |
| `requestId`                | The JSON-RPC request ID.                                                                                              |
| `protocolVersion`, `tasks` | The protocol revision the client requested, and whether it declared the Tasks extension.                              |
| `taskId`                   | The [task](https://www.twisp.com/docs/reference/actions/mcp.md#tasks) a `tasks/get`, `tasks/update`, or `tasks/cancel` named.                                            |

The execution's `caller` is the principal, and its outcome is:

- **`SUCCEEDED`** when Twisp answered with a result, including a tool result with `isError`.
- **`FAILED`** when Twisp answered with a JSON-RPC error, such as for an unknown tool, a passed deadline, or a transaction that did not commit, and when the request was [refused](https://www.twisp.com/docs/reference/actions/mcp.md#refusals), including a `tools/call` in a closed session, which the external model sees as a tool error `session_closed`.
- **`CANCELLED`** when the client closed the request before its result.

A `tools/call` makes exactly one call, a child execution of the request, or, for a callable agent, starts one [task](https://www.twisp.com/docs/reference/actions/mcp.md#tasks). A call of a native tool, such as [`program`](https://www.twisp.com/docs/reference/actions/mcp.md#programs) or [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke), is the request's one call too: an execution of the native tool, whose own calls are its children. Its `invocation.output` records whether the result was an error, and the call records its effective input, with fixed input applied, and its output or error. A successful call's output is recorded as a `TOOL` [use](https://www.twisp.com/docs/reference/actions/skills.md#output-uses) of the request. A `tools/list` records, in its invocation's `tools`, the exact version of every tool it offered.

**Nothing leaves before it is recorded.** Twisp returns a request's result only after the execution that recorded it commits. If the commit fails, the client gets an error and no content.

The result is a fixed rendering of what was recorded:

| Call                                                 | `tools/call` result                                                                                |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Succeeded with a string output, and no output schema | One text content block with the string.                                                            |
| Succeeded with a string output, and an output schema | `structuredContent` with the string, and one text content block with the string.                   |
| Succeeded with any other output                      | `structuredContent` with the output, and one text content block with its canonical JSON.           |
| Failed                                               | `isError: true`, one text block `<code>: <message>`, and a second with the hint when there is one. |

The principal, subject, and OAuth client on the request come from the verified token, never from what the client sends.

Arguments that fail the tool's input schema, a policy denial, and any other call error are results with `isError`, so the external agent can correct itself. A policy denial's message names the operation, the resource, and the layer that denied it: the principal, the server, or the tool, with a hint. It leaves out the principal's identifier, the index of the denying policy, and action IDs, which the call's recorded error keeps for you. Every result carries the server's `name` and the action `version` that answered in `_meta["io.modelcontextprotocol/serverInfo"]`.

A callable agent runs only as a [task](https://www.twisp.com/docs/reference/actions/mcp.md#tasks).

## Tasks

A [callable agent](https://www.twisp.com/docs/reference/actions/agents.md#callable-agents) runs its whole loop, with its model exchanges and tool calls, often for minutes: longer than a request should be held open. So a call that runs one answers with a task: a `tools/call` naming a callable agent, and an [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke) of an agent. The server offers tasks through the MCP [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview), `io.modelcontextprotocol/tasks`, which `server/discover` declares, and to hosts without it through `get_task`. Every other tool answers its call at once.

- **Creating.** A `tools/call` naming a callable agent [starts](https://www.twisp.com/docs/reference/actions/tools.md#starting-a-tool) the agent with its arguments and the reference's fixed input. To a client that declares the extension in the request's client capabilities, it answers with the task: `resultType: "task"`, `taskId`, `status: "working"`, `createdAt` and `lastUpdatedAt`, `ttlMs` (24 hours), and `pollIntervalMs` (5 seconds). The task is the started execution: `taskId` is its ID, and its `startedBy` names the request. The answer is sent only once the task is recorded and queued, so it can be polled at once. The request records no call and no use.
- **Running.** The agent runs as a [started](https://www.twisp.com/docs/reference/actions/tools.md#starting-a-tool) execution, under its own `execution` settings, with retries and recovery. Its authority is the principal's, then the server's policies, then the agent's own, checked again at each invocation: revoking the principal, or its credential expiring, stops the work that has not run.
- **Polling.** `tasks/get` answers with the task's `status`, and its `lastUpdatedAt`, when the execution last changed:

  | Execution                                          | `status`    | Also                                                                                                                                     |
  | -------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
  | `QUEUED`, `RUNNING`, `ACCEPTED`, `RETRY_SCHEDULED` | `working`   |                                                                                                                                          |
  | `SUCCEEDED`                                        | `completed` | `result`: the agent's output, rendered as a `tools/call` result is.                                                                      |
  | `FAILED`                                           | `completed` | `result`: a tool error with the execution's error code, the message `The task's agent failed`, and its hint.                             |
  | `EXHAUSTED`                                        | `failed`    | `statusMessage`, and `error`: `-32603`, with the execution's error code in `data.code` and its hint in `data.hint`. Its retries ran out. |
  | `CANCELLED`                                        | `cancelled` |                                                                                                                                          |

  The execution's full error, whose message and hint can name principals, policies, and actions, stays on it for you to investigate: a hint the task returns names a denying layer by name alone, and a hint that would name more is replaced by a generic one.

  A result is readable only once the execution that recorded it committed. The poll that returns an agent's output records it as a `TOOL` [use](https://www.twisp.com/docs/reference/actions/skills.md#output-uses) of the poll's execution, even after the server stops referencing the agent. A task stays readable after its `ttlMs`.

- **Updating.** Twisp never asks the client for input, so `tasks/update` has nothing to update, and is acknowledged.
- **Cancelling.** `tasks/cancel` [cancels](https://www.twisp.com/docs/reference/actions/executions.md#cancellation) the task, and is acknowledged, also for a task that has finished. A queued task is `cancelled` at once. A running agent stops at its next step, its writes roll back, and it becomes `cancelled`. The execution's `cancelledBy` names the principal and the `tasks/cancel` request. A cancellation is not a business write: on a server in preview mode it takes effect too, though the request is marked `preview`.
- **Which tasks.** A task is reached only by requests to the same server in the session that created it. A task ID that is unknown, or belongs to another session or server, gets `-32602` with `mcp_task_not_found`, the same in every case, so a request cannot learn that another session's task exists.
- **How many.** A session has at most 32 unfinished tasks, and so does a user, one principal and token subject, across all of a server's sessions. An agent [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke) runs counts against the same limits. A call beyond them is a tool error, `start_limit`, and starts nothing. A finished task frees its place.
- **Without the extension.** A call that answers with a task, from a client that does not declare the extension, starts it only on a server that lists the native tools `get_task` and `cancel_task`, and answers with a tool result whose text names it: `task_id`, `status: "working"`, `poll_interval_ms`, and a `hint`. On a server that does not list both, such a call is refused with JSON-RPC `-32021`, `mcp_task_required`, whose `data.requiredCapabilities` names the extension, and an `invoke` of an agent with a tool error of the same code, so a client is never started an agent it cannot read or cancel. Read the task with `get_task`:

  | Argument  | Meaning                                     |
  | --------- | ------------------------------------------- |
  | `task_id` | The `task_id` a call returned. Required.    |
  | `intent`  | The call's [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent). |

  `get_task` answers with `task_id`, `status` (`working`, `completed`, `failed`, or `cancelled`, as for `tasks/get`), `poll_interval_ms` while it is `working`, the `result` of a `completed` task, rendered as a `tools/call` result is but with the agent's output once, as `structuredContent` when it has one, and the `error` of a `failed` one. It reaches the same tasks `tasks/get` does, and gives the same answer for every task it cannot reach: a tool error, `mcp_task_not_found`. The read that returns an agent's output records it as a `TOOL` use. `get_task` is read-only.

  Cancel the task with the native tool `cancel_task`, with the same arguments, when the server lists it. It cancels the task as `tasks/cancel` does, in the same scope, and answers with its `task_id` and a `hint`, also for a task that has finished; a task it cannot reach is a tool error, `mcp_task_not_found`. The execution's `cancelledBy` names the principal and the `tools/call` request. It is never annotated read-only: a running agent's writes roll back.

  `tasks/get`, `tasks/update`, and `tasks/cancel` themselves need the extension: from a client that does not declare it they are refused with `400` and JSON-RPC `-32021`, whose `data.requiredCapabilities` names the extension and `data.method` the method, even for a task that a request declaring it created.

Each `tasks/get`, `tasks/update`, and `tasks/cancel` is its own execution of the server, in its session, and counts against its rate limit. A `tasks/get` records the status it returned in its `invocation.output`.

## Skills

A server serves each of its `skills` as one file, `SKILL.md`, at a URI that names the skill version it renders:

```
skill://v<version>/<name>/SKILL.md
```

`<name>` is the skill's registry name, and `<version>` the version the reference resolves to when the request runs: its pinned `version`, or the skill's latest. A host can cache a skill's content by its URI for an hour.

The file is YAML frontmatter with the skill's `name` and `description`, then its `instructions`, then an `## Examples` section with each example's situation, resolution, and reason. When the skill's `tools` list tools the server offers, a closing `## Tools` section names them as the external agent sees them; tools the server does not offer are not mentioned. The file never declares `allowed-tools`: a skill grants nothing.

| Method                           | Result                                                                                                                                                                                     |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `skills/list`                    | One entry per skill: its `uri`, its `frontmatter`, identical to the file's, and `resources`, the file's `uri`, `digest` (`sha256:` and its SHA-256 in lowercase hex), and `size` in bytes. |
| `skills/get`                     | The entry for one skill's `SKILL.md` URI.                                                                                                                                                  |
| `resources/list`                 | Each skill's `SKILL.md`, with `mimeType: text/markdown`, the skill's `name` and `description`, and `size`. Cached for 60 seconds.                                                          |
| `resources/read` of a `SKILL.md` | The file as `text/markdown`, whose bytes match the entry's `digest` and `size`.                                                                                                            |
| `prompts/list`                   | Each skill as a prompt without arguments, named after the skill. Cached for 60 seconds.                                                                                                    |
| `prompts/get`                    | One `user` message with the same file.                                                                                                                                                     |

- **Loads.** A `resources/read` of a `SKILL.md`, or a `prompts/get`, loads the skill: its one call is a load of that exact version, the same load an agent makes, recorded as a child execution. The request's `invocation.output` records the file's `uri`, `digest`, and `size`, and the load's output is a `SKILL` [use](https://www.twisp.com/docs/reference/actions/skills.md#output-uses). A load from an MCP server makes none of the skill's `tools` available: the external agent can call only the server's own tools. Listings load nothing, and record in `invocation.skills` the version of each skill they offered.
- **Versions.** When a skill gets a new version, a reference without a `version` serves it at a new URI. A `skills/get` or `resources/read` of the previous URI gets `-32602` with `mcp_skill_superseded`, so the host fetches the current one.
- **Availability.** A skill whose action or resolved version is disabled or deleted, or whose latest version's `description` is empty or longer than 1,024 characters, is left out of every listing, and a request for it gets `-32602`.

A host caches a skill's content by its URI, so later reads of the same version may not reach Twisp. The first read of each version by each host is recorded, and that is when the content left Twisp.

## Resources

Resources are context a host attaches to a conversation, often because the user named something, such as an account. Each `mcp.resources` entry is a template of resource URIs, read by one of the server's tools:

```graphql
mcp: {
  resources: [{ uriTemplate: "twisp://accounts/{account_id}", name: "account", description: "An account and its balances.", tool: "get_account" }]
}
```

- **Listing.** `resources/templates/list` returns the templates, each with its `uriTemplate`, `name`, and `description`, and a `mimeType` when the tool's output schema fixes one. It can be cached for 60 seconds. `resources/list` lists no resources besides skills' files.
- **Reading.** `resources/read` matches the URI against the templates in order, and the first that matches reads it. Each variable matches one or more characters, percent-decoded, and becomes a top-level string property of the tool's input; the tool reference's fixed input still applies, and replaces a variable of the same name. The read is the request's one call, recorded like any other, and its output is a `TOOL` use. The result is one item for the URI: `application/json` with the output's canonical JSON, or `text/plain` for a string output. It is never cached, because ledger data is live and every read is recorded. A read states no [intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent): the host makes it, not the model.
- **Read-only.** A host may read a resource without asking anyone, so a template's tool must be read-only by the [derived rule](https://www.twisp.com/docs/reference/actions/mcp.md#annotations). The definition write checks it, and every request checks it again for a reference without a `version`: when a later version of the tool can write, its templates are left out of `resources/templates/list` and their reads get `-32602` with `mcp_resource_unavailable`.
- **Errors.** A URI no template matches gets `-32602` with `mcp_resource_not_found` and the URI in `data.uri`. A read whose tool fails gets `-32602` with the call's `code` and `hint` and the URI; the failed call is recorded with the request.

In [preview mode](https://www.twisp.com/docs/reference/actions/mcp.md#preview-mode), a resource read and a skill load run as preview calls, and a [replay](https://www.twisp.com/docs/reference/actions/mcp.md#replay) reruns them against the draft's versions.

## Stated intent

Twisp never sees why an external model makes a call, so an MCP server asks. With `intent: REQUIRED`, the default, every tool gains a required `intent` argument, a sentence of 1 to 1,024 characters. The server removes it from the arguments before the call and records it on the request, so the tool never receives it. A call without it is a tool error that asks for one, and is recorded. `OPTIONAL` offers the argument without requiring it, and `NONE` adds nothing.

A tool that declares its own top-level `intent` property cannot be offered while intent is on: the definition write rejects it. If a later version of a tool adds one, that tool is left out of `tools/list`, and a call to it is a tool error whose hint says to pin a version or rename the property. The same applies to a later version whose name another of the server's tools already has.

The stated intent is the external model's own account. It is evidence for an investigator, and is never used for authorization.

## Annotations

The tool annotations hosts use to decide when to ask a person before a call are derived from policies, not declared:

- **`readOnlyHint: true`** when the server's policies, or the tool's own, allow only `SELECT`: every `ALLOW` policy names only `SELECT` actions. Any other action counts as a write, including `INVOKE`, `RESPOND`, and a wildcard such as `*` or `actions:*`. For a callable agent, its policies are the tool's. `cel` is always read-only, and `graphql` has only the server's policies.
- **`destructiveHint: false`** when the server's or the tool's policies allow only `SELECT` and `INSERT`. Otherwise it is omitted.
- **`openWorldHint: true`** for a [model](https://www.twisp.com/docs/reference/actions/models.md), whose call sends its input to the model's provider, and for a tool or callable agent that declares one, directly or through the tools it declares, at the versions its references resolve to. [`program`](https://www.twisp.com/docs/reference/actions/mcp.md#programs) is open world when the server lists an open-world tool. Every other tool is `openWorldHint: false`.

Annotations are derived for each listing from the versions it offers, so they change when a tool's policies do. Give a tool read-only policies, and hosts can call it without asking. [`program`](https://www.twisp.com/docs/reference/actions/mcp.md#programs) and [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke) allow everything, so they have only the server's policies.

## Programs

A server that lists the native tool `program` offers it to the external agent. It runs a program the external agent writes, in Starlark, with the conventions of a [Starlark tool](https://www.twisp.com/docs/reference/actions/starlark.md): it defines `run(event)`, reads its input as `event["data"]`, calls the server's tools as `tools.<name>(input)`, and returns the result's value. `graphql` and `cel` are available only when the server lists them. [CEL's functions](https://www.twisp.com/docs/reference/actions/tools.md#the-tool-handler), such as `decimal("10.50")` and `money("5", "USD")`, are builtins in every program and run inside it; the ones that read the clock or randomness, such as `uuid.New()`, are `cel` calls and need the server to list `cel`. A program can do nothing its tools could not do one call at a time.

| Argument | Meaning                                                               |
| -------- | --------------------------------------------------------------------- |
| `src`    | The program, at most 64 KiB. Required.                                |
| `input`  | An object, passed to the program as `event["data"]`.                  |
| `commit` | `true` commits the program's writes. `false` previews them. Required. |
| `intent` | The call's [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent), as for every tool.        |

- **Checked first.** Before a program runs, a GraphQL document it passes to `graphql()` as a literal or a constant, directly or through its own functions, is validated against the schema, with its `operation_name` and the variables it requires, and each `tools.<name>` against the server's tools. A program that fails is refused with `action_input_invalid`, whose message says how the document reaches the call, and makes no call. A document the program builds or chooses at run time, such as one alternative of a conditional or one element of a loop, or passes to a call with a `check` argument other than `True`, is validated when the call runs.
- **Atomic.** Every call a program makes commits together, or none does. An error the program does not handle, such as `fail(...)` or a tool error it did not catch with `check=False`, rolls back every write and returns a tool error with its `code`, `message`, and `hint`, the message led by the traceback lines of the program's own source. A policy denial's message names the operation, the resource, and the denying layer, as a call's does; the program's record keeps the error in full.
- **Listings.** A program's calls share one transaction, so an eventually consistent listing, such as `accounts` by `NAME`, does not include records the program wrote earlier. Read them by ID or unique key instead; see [Consistency of Reads](https://www.twisp.com/docs/reference/api/indexes.md#consistency-of-reads).
- **Recorded.** The program is the request's one call: a child execution of the native `program` tool, whose input is the program's arguments, source included, and whose output is its result. Each call it makes is a child of the program, including calls a failed program rolled back, which carry `rolledBackByExecutionId`.
- **Previews.** With `commit: false`, the program runs every call and commits none. Its result is `{value, writes, unreadable_writes, omitted_writes}`: `writes` lists each row it would write that the principal may read, through its own policies and the server's, with the row's `table`, `key`, and new `value`; rows it may not read, and deleted rows, are counted in `unreadable_writes`, not shown, so a preview cannot reveal a balance the principal cannot read. Rows beyond the result's size limit are counted in `omitted_writes`. The preview's execution is recorded with `preview` set and every row it would write in its `writeSet`, so an investigator sees the preview and the commit that followed it. Reading the execution shows the same limit: its `writeSet`, and each call's, lists only the rows the reader may read, and counts the rest in `writeSetUnreadable`. A committed program's result is `{value}`.
- **Preview mode.** In [preview mode](https://www.twisp.com/docs/reference/actions/mcp.md#preview-mode) a program never commits: with `commit: true` it runs as it would live, its writes are rolled back and recorded in its `writeSet`, and its result carries the preview marker.
- **Annotations.** `program` is `readOnlyHint: true` only when the server's own policies allow only `SELECT`, and `destructiveHint: false` only when they allow only `SELECT` and `INSERT`: a program can call every tool the server lists.
- **Limits.** At most 64 tool calls, 64 `graphql` and 64 `cel` calls, and 1,000,000 execution steps, within the server's `responseTimeout`. The program's call and each call it makes count against the session's [rate limit](https://www.twisp.com/docs/reference/actions/mcp.md#rate-limits). A program cannot call `program`, `invoke`, `get_task`, or a callable agent. A program that passes the deadline fails the request with `mcp_request_deadline`, and nothing it did is recorded.

## Invoking an action

A server that lists the native tool `invoke` lets the external agent run one of your [tools](https://www.twisp.com/docs/reference/actions/tools.md) or [callable agents](https://www.twisp.com/docs/reference/actions/agents.md#callable-agents) on an input it chooses. With `commit: false`, the default, it previews: a stored version, a disabled one, or an unpublished definition runs, and none of its writes commit. With `commit: true`, it runs a published, enabled version for real. So an agent that builds tools and agents through the server can run what it built before anything depends on it, and check it once it is live.

| Argument     | Meaning                                                                                                                                                                                                                                                                                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_id`  | The action to invoke. It must exist and not be deleted. A preview may run one that is disabled, or not yet published. Required.                                                                                                                                                                                                                                 |
| `version`    | A stored version of the action. With neither `version` nor `definition`, the current version runs.                                                                                                                                                                                                                                                              |
| `definition` | An unpublished definition of the action, in the shape `createAction`'s `input` takes, as JSON: `config` and schemas as objects, enums such as `ALLOW` and `SELECT` by name, and durations as strings such as `"30s"`. It is validated as `createAction` validates its input, and never stored. Only with `commit: false`; it cannot be combined with `version`. |
| `input`      | The input to call the action with, an object. Required.                                                                                                                                                                                                                                                                                                         |
| `commit`     | `false`, the default, previews. `true` runs a published, enabled version, and commits what it writes.                                                                                                                                                                                                                                                           |
| `intent`     | The call's [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent), as for every tool.                                                                                                                                                                                                                                                                                                  |

- **What runs.** An action of kind `tool`, `graphql`, or `cel` runs in the request. An agent with a `tool` block answers with a [task](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), in both modes. A skill, an MCP server, a native tool, an agent without a `tool` block, and a tool with a [session](https://www.twisp.com/docs/reference/actions/sessions.md#tool-sessions) cannot be invoked.
- **A tool the server lists.** `invoke` never goes around the server's reference to an action it lists: the reference's fixed input applies, in both modes and to a draft, and the version the reference names runs unless a draft does. Another `version` is refused with `action_input_invalid`.
- **The loop.** An action created with `enabled: false` is never triggered or called, but can be previewed: create a new tool disabled, preview it, preview drafts of it until it works, then `updateAction` with the draft that worked and `enabled: true`, and `invoke` it with `commit: true` where a real run is safe. Preview a change to a live action as a draft, before `updateAction` publishes it to every caller.
- **Authority.** What `invoke` runs runs under the principal, then the server's policies, then the action's own policies, which can only narrow them, then each tool it calls, as a call of a tool the server lists would. Before anything runs, the principal and the server must be allowed to read the action and `INVOKE` it, and, for a draft, to `INVOKE` every action the draft references and every tool its skills make available. So a server's policies need `SELECT` and `INVOKE` on the actions it invokes. An action the principal or the server may not read is refused as one that does not exist.
- **A draft keeps its kind.** A draft of another kind than the action's is refused with `action_input_invalid`. The principal and the server must be allowed to read and `INVOKE` the action as the draft would store it, as well as the stored action, so `INVOKE` assertions on `definition` apply to what runs.
- **A draft uses only what the server offers.** A draft is your code, so it reaches what the server lists only as the server does: a call it makes, or a draft agent's, to a tool the server lists goes through the server's reference, at its version and with its fixed input, whatever the draft's own reference says. It reaches the native `graphql` and `cel` tools only when the server lists them. A draft of a native tool's kind, or whose `tools`, or whose skills' tools, name a native tool the server does not list, is refused with `action_input_invalid`: "the definition uses the native tool graphql, which this server does not offer". A Starlark draft can call `graphql()` and `cel()` without declaring them; through a server that does not list the one it calls, that call fails with `tool_not_declared`, and the result reports it as the tool's error. A stored action runs as it would when called, and so do the stored tools a draft calls.
- **What it offers.** `invoke` can run any tool or agent the principal and the server may `INVOKE`, and a draft can call any of them, not only the tools the server lists. Their reads are real; a preview's writes are rolled back, and a commit's are committed. List `invoke` only on a server agents build with.
- **Nothing a preview commits.** A preview's writes are rolled back, on a `LIVE` server as on a `PREVIEW` one. Work that would follow a commit is dropped: a `createAction` inside a preview publishes nothing, and no dispatch, workflow, or search index change happens. An operation that acts outside the ledger, or writes to another tenant, fails. Reads are real, and so are an agent's model calls. On a `PREVIEW` server, a commit commits nothing either, as no call there does.
- **Annotations.** `invoke` allows everything, so its [annotations](https://www.twisp.com/docs/reference/actions/mcp.md#annotations) are the server's: it is read-only only when the server's policies allow only `SELECT`.

### Invoking a tool

A tool runs at once, as the `invoke` call's own call, with its own fixed input, input and output schemas, response timeout, and policies:

```json
{
  "name": "invoke",
  "arguments": {
    "action_id": "0b6a4c4e-2f7e-4c55-9d43-6a3f4e2b8d10",
    "input": { "account_id": "70eec1a6-eac1-47aa-a507-e18f2d486425" },
    "intent": "Check the new low-balance flag before enabling it."
  }
}
```

A preview's result `structuredContent`:

```json
{
  "execution_id": "4b1d2a38-0c7e-4d1a-9b55-2f8e6c0a9d31",
  "status": "SUCCEEDED",
  "output": { "flagged": true, "balance": "12.50" },
  "calls": [{ "name": "graphql", "status": "SUCCEEDED", "execution_id": "9c0e5f12-…" }],
  "writes": [
    {
      "table": "KVValue",
      "key": "…",
      "value": { "namespace": "alerts", "key": "70eec1a6-…", "value": { "low": true } }
    }
  ],
  "unreadable_writes": 0,
  "omitted_writes": 0
}
```

- **Writes.** `writes` lists each row the preview would have written that the principal may read, through its own policies and the server's, with the row's `table`, `key`, and new `value`, as a [program's preview](https://www.twisp.com/docs/reference/actions/mcp.md#programs) does. Rows it may not read, and deleted rows, are counted in `unreadable_writes`; rows beyond the result's size limit are counted in `omitted_writes`.
- **Calls.** `calls` lists the tool's direct calls in order, each with its `name`, `status`, and `execution_id`, its `error` when it failed, and `rolled_back: true` when the tool's failure discarded its writes. The whole call tree is readable by `execution_id`.
- **A commit.** With `commit: true`, the result has `execution_id`, `status`, and `output` or `error`, and no `calls` or `writes`: what it wrote is committed, and readable as any other record.
- **Failure.** A tool that runs and fails is not a tool error: the invocation succeeded, and the tool's failure is in the result. The result has `status: "FAILED"`, no `output`, `error` with its `code`, `message`, and `hint`, and for a preview its calls and empty `writes`. A Starlark error's message is the traceback of the tool's own source. A policy denial's message names the operation, the resource, and the denying layer, as a call's does:

  ```json
  {
    "execution_id": "4b1d2a38-0c7e-4d1a-9b55-2f8e6c0a9d31",
    "status": "FAILED",
    "error": {
      "code": "action_failure",
      "message": "Traceback (most recent call last):\n  program.star:9:22: in run\n  program.star:3:15: in ratio\nError: floored division by zero"
    },
    "calls": [
      {
        "name": "get_account",
        "status": "SUCCEEDED",
        "execution_id": "a7f2c0d4-…",
        "rolled_back": true
      }
    ],
    "writes": [],
    "unreadable_writes": 0,
    "omitted_writes": 0
  }
  ```

- **Limits.** A tool is bounded by the server's `responseTimeout`, and by its own; one that passes the request's deadline fails the request with `mcp_request_deadline`. The `invoke` call and the calls beneath it count against the session's [rate limit](https://www.twisp.com/docs/reference/actions/mcp.md#rate-limits).

### Invoking an agent

An agent runs for minutes, so `invoke` starts it as a [task](https://www.twisp.com/docs/reference/actions/mcp.md#tasks) and answers at once, in both modes. To a client without the Tasks extension:

```json
{
  "task_id": "7d2a9e04-5b1c-4f3e-8a6d-0c9b2e7f4a18",
  "status": "working",
  "poll_interval_ms": 5000,
  "hint": "It runs on its own, and nothing it does is committed. Read it with get_task, passing task_id, every poll_interval_ms, or with tasks/get when your client has the Tasks extension."
}
```

A client with the extension gets the task itself, and polls it with `tasks/get`. Either way, a read of a finished task returns the agent's `result`.

- **Running.** The agent runs as a [started](https://www.twisp.com/docs/reference/actions/tools.md#starting-a-tool) execution, under its own execution settings and limits, and under the principal and the server's policies, then its own. In a preview its calls' writes are discarded and recorded in their write sets, and a call that requires [approval](https://www.twisp.com/docs/reference/actions/sessions.md#approvals) runs as if approved, marked `grantedInPreview`, as in preview mode. A preview of a draft keeps nothing of the agent's, such as its memory. Read its transcript and write sets with `execution(id:)`.
- **Who can read it.** Only requests to the same server, in the session that started it, by the same principal and user. Any other task is `mcp_task_not_found`.
- **How many.** An agent `invoke` runs counts against the session's and the user's 32 unfinished [tasks](https://www.twisp.com/docs/reference/actions/mcp.md#tasks); one beyond them is `start_limit`, and starts nothing.

### Refused invocations

An invocation that cannot run is a tool error, with `isError`, which the external agent can act on. The `invoke` call is recorded `FAILED`, with the refusal.

| `code`                 | When                                                                                                                                                                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_not_callable`  | The action does not exist, is deleted, may not be read by the principal or the server, is a native tool, or the definition has no `tool` block or has a tool session; or, for a commit, the version is not published and enabled.                             |
| `action_input_invalid` | Arguments that break the input schema, `version` with `definition`, a `definition` with `commit: true`, a version the action does not have, an invalid draft, whose message names the rule, or a definition that uses a native tool the server does not list. |
| `action_access_denied` | The principal or the server may not `INVOKE` the action, or an action the draft references.                                                                                                                                                                   |
| `start_limit`          | An agent beyond the session's or the user's 32 unfinished tasks.                                                                                                                                                                                              |
| `action_failure`       | The deployment cannot decode a draft, or cannot roll a preview back.                                                                                                                                                                                          |

### Records

- **The request** records `invoke`'s arguments, a draft in full, and the stated intent.
- **The call.** Every `invoke` call is an execution of the native `invoke` tool, a call of the request, as a [program](https://www.twisp.com/docs/reference/actions/mcp.md#programs) is. Its input is the arguments, a draft in full, and its output is what it answered. A refused invocation is that execution, `FAILED` with the refusal.
- **The invoked execution** is the action's own execution. A preview's is `preview`, as is every call beneath it, each with its write set. For a draft, `draft` is the unpublished definition it ran, with its references resolved, and `actionVersion` the action's version when it ran. For a stored version, `draft` is null, and `actionVersion` is the version that ran.
- **Lineage.** A tool is a call of the `invoke` call, and its calls are its own, so [lineage](https://www.twisp.com/docs/reference/actions/executions.md#lineage) reads: the request, the `invoke` call, the tool, its calls. An agent is started by the request, as a task is: its `startedBy` names the request, and the `invoke` call's output names the task. Lineage reads the request, then the `invoke` call, and, through a `START` edge from the request, the agent and its calls.
- **Failed tools.** Beneath a failed tool, each call is kept, with its write set, and carries `rolledBackByExecutionId`, naming the failed execution. When an invocation of an agent fails, the calls it made are kept the same way.
- **Finding previews.** `actions { executions(where: { actionId: $id, preview: true }) }` lists what committed nothing, previews included, and `preview: false` leaves them out of an action's history. `action(id:) { executions(where: { preview: false }) }` takes the same filter.
- **Uses.** As for any call, a successful `invoke` call's output is a `TOOL` [use](https://www.twisp.com/docs/reference/actions/skills.md#output-uses) of the request. A read that returns an agent's output records a use of the agent's execution.
- **No rerun or replay of a draft.** A draft's preview is never rerun or replayed: `rerunExecution` and `replayExecution` of one are rejected. Preview the draft again.
- **Replay of the request.** A [replay](https://www.twisp.com/docs/reference/actions/mcp.md#replay) of a request that invoked a tool reruns it against its recorded arguments, rolled back as always, and records an agent's start as not started.
- **Nesting.** A program cannot call `invoke`.

## Sessions

Every request is recorded in a session of the server. `mcp.session.start.input` maps the request, `execution.input`, to the session's `input`, and the `session` block's `id` names the session. A request finds its session, or creates it.

| Grouping                       | `start.input`                                                                  |
| ------------------------------ | ------------------------------------------------------------------------------ |
| Per user                       | `principal: "execution.input.principal"`, `subject: "execution.input.subject"` |
| Per user of each host          | the above, and `host: "execution.input.oauth_client_id"`                       |
| Per principal                  | `principal: "execution.input.principal"`                                       |
| Per host-supplied conversation | `conversation: "execution.input.meta[\"com.example/conversation\"]"`           |

The principal of a request is Twisp sign-in, shared by every user of your organization, and its `subject` is the user; group by principal and subject to give each user a session.

Build `id` so that distinct inputs can never produce the same ID. Joining fields with a separator such as `'|'` can collide when a field contains the separator, which puts two users in one session. Prefix each field but the last with its length, as the example above does: `string(size(session.input.principal)) + ':' + session.input.principal + ':' + session.input.subject`.

- **A session's input is fixed when it is created.** If a later version of the server maps a request to a different `input` under the `id` of an existing session, such as after adding a field to `start.input` that `id` does not include, that session's requests are refused with `400` and `mcp_session_invalid` until you close it. Include every field of `start.input` in `id`.

- **History.** Each request adds one `REQUEST` input to its session when it is admitted, with the execution in `executionId`. The input is `CLAIMED` while the request runs, and `CONSUMED` once its outcome is recorded, with the number of tool calls it made, including those made beneath them, in `calls`. The session's `inputs` are the ordered request log, and `executions(where: {actionId, sessionId})` lists the requests' executions. `ActionExecution.sessionId` names a request's session.
- **Closing.** `closeSession` stops one user, principal, host, or conversation. Its new requests fail with `session_closed`: a tool error for `tools/call`, and a JSON-RPC error otherwise. They run nothing. A request already running when the session closes finishes and is recorded in it. Disabling the server likewise refuses new requests and lets running ones finish.
- **Mismatched requests.** A request whose record does not produce a valid session input, such as one without the `_meta` field its grouping reads, is refused with `400`.

### Rate limits

`mcp.limits.callsPerMinute` bounds each session: a request counts as one from the moment it is admitted, while it runs, or as the number of tool calls it made, including those beneath them, once its outcome is recorded, when that is more. A request made when the session's requests admitted in the last minute reach the limit gets `429` with a `Retry-After` header. Requests to one session made at the same moment are admitted one after another, so they cannot pass the limit together; when many arrive at once, some may instead be refused with `mcp_transaction_error` and `retryable: true`.

### Refusals

A request refused after its token was checked, for a missing `INVOKE` permission, a method the server does not serve, a task request from a client without the Tasks extension, a closed session, a mismatched request, the rate limit, or a request too large to record, is recorded as a `FAILED` execution of the server with its request and the reason, outside any session. It runs nothing. So that a flood of refused requests cannot become a flood of records, only the first refusal of a principal and subject at a server in each minute is recorded: the users who share a principal never hide one another's refusals. The refusals after it in that minute are counted, and the next recorded refusal's `invocation.error.details` carries `unrecorded_refusals`, the number counted since the refusal recorded before it, and `window`, the minute it opened. The count is best effort.

| Refusal `code`          | HTTP  | When                                                                                                                                                                                                                                                                   |
| ----------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_access_denied`  | `403` | The principal has no client in the account, or lacks `INVOKE` on the server.                                                                                                                                                                                           |
| `mcp_method_not_found`  | `404` | The server does not serve the request's method; the answer is `-32601`.                                                                                                                                                                                                |
| `mcp_task_required`     | `400` | A task request is made, and the client did not declare the Tasks extension; the answer is `-32021`. A call that would start a task, from such a client on a server that does not list both `get_task` and `cancel_task`, gets `-32021` from its execution, with `200`. |
| `mcp_request_too_large` | `400` | The request is larger than a request's record can hold.                                                                                                                                                                                                                |
| `mcp_session_invalid`   | `400` | The request does not produce a valid session input, or its session's input differs.                                                                                                                                                                                    |
| `session_closed`        | `200` | The request's session is closed.                                                                                                                                                                                                                                       |
| `mcp_rate_limited`      | `429` | The session is over its rate limit.                                                                                                                                                                                                                                    |
| `mcp_tenant_busy`       | `429` | Too many of the account's MCP requests are running at once, across all its servers and sessions; with `Retry-After`.                                                                                                                                                   |
| `mcp_user_busy`         | `429` | Too many of the user's MCP requests, those made with one principal and token subject, are running at once, across all the account's servers and sessions; with `Retry-After`.                                                                                          |
| `mcp_transaction_error` | `200` | The request could not be admitted to its session; `retryable: true`.                                                                                                                                                                                                   |

A name that is not an MCP server the principal can read, or a disabled server, gets `404` with `mcp_server_not_found` and is not recorded.

## Preview mode

Every MCP server starts in preview mode. In preview mode the server runs every call exactly as it would live, and commits none of its writes: each call is recorded with `preview` set and the writes it would have made in its `writeSet`. The request's execution, its session history, and its uses commit, and each result carries `"com.twisp/preview": true` in `_meta`, so an informed host can say so. The external agent gets real outputs from your data.

A task a preview request creates is preview too: the agent runs as it would live, and none of its writes commit; each of its calls records what it would have written.

Connect a new external agent in preview mode, review what it would write, then promote the server with `updateAction`. A supplied `mcp` block replaces the whole block, so send all of it, with `mode: LIVE`; a block with only `mode` is rejected, since `name` and `session` are required.

```graphql
mutation {
  actions {
    updateAction(
      id: "3c1e4d1a-7a1b-4bde-9b7e-6b8f3f0c5a21"
      input: {
        mcp: {
          name: "fraud-ops"
          instructions: "Tools for reviewing account activity."
          session: {
            start: {
              input: { principal: "execution.input.principal", subject: "execution.input.subject" }
            }
          }
          mode: LIVE
        }
      }
    ) {
      version
    }
  }
}
```

## Replay

`replayExecution` accepts a request's execution. It reruns the recorded request, such as the tool call with its recorded arguments, against a later version of the server or an unpublished `definition`, reading current data. Its writes are always rolled back and recorded as write sets, and it records no request in any session. A reference without a version calls the version the original request used, and a pinned reference calls its pin, so a draft that pins a new version of a tool tests exactly that change. Replay the recent requests to a server against a draft before publishing a change to it, and compare outputs and write sets.

A replay runs under both the principal replaying it and the principal of the original request, then the server's policies and each tool's: it can do nothing either principal could not. The original principal's policies are evaluated with the original request's claims, so authority that depends on claims, such as a signed-in user's own policies, is the original user's. The replayer needs `INVOKE` on the server. A request's replay involves no model, so `model: RECORDED` is rejected. A refused request ran nothing and cannot be replayed. A replay of a request that was answered with a JSON-RPC error fails with the same `code`. `rerunExecution` of a request is rejected: a request runs only when its endpoint serves it.

A replay starts nothing and cancels nothing. A replayed `tools/call` that created a task records the task it would have started as not started: `CANCELLED`, preview, with `startedBy.notStarted` set and the agent's input, and never queued. A replayed `invoke` reruns a tool, rolled back as always, and records an agent's start as not started in the same way. A replayed `tasks/cancel` cancels nothing. Such a task never ran, so it cannot be [rerun](https://www.twisp.com/docs/reference/actions/executions.md#manual-reruns). A rerun of a task the original request did start is a new start by you, in preview mode when that task was a preview.

## Errors

JSON-RPC errors that carry a Twisp error identifier put it in `error.data.code`, with a `hint` when there is one: `-32603` when Twisp failed to serve the request, and `-32602` otherwise. An error's `message` is a fixed sentence for its code, whatever the request; what the request named is in `error.data`: the server's `name` for `mcp_server_not_found`, the `method` for `-32601`, the tool's `name` for `tool_not_declared`, the `method` for a task request refused with `-32021`, the `uri` for `mcp_resource_not_found`, `mcp_skill_superseded`, `mcp_resource_unavailable`, and a failed read or load, the session's `limit` in calls per minute for `mcp_rate_limited`, and the server's response `timeout` in seconds for `mcp_request_deadline`. What went wrong in detail is recorded on the execution's `invocation.error`, for you to investigate.

| Situation                                                                                                                                                                                                                                                                                                                                                                               | HTTP  | Result                                                                                                                                 |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------- |
| The name is not an MCP server the principal can read, or the server is disabled                                                                                                                                                                                                                                                                                                         | `404` | `-32602`, `mcp_server_not_found`. Not recorded.                                                                                        |
| The principal lacks `INVOKE` on the server                                                                                                                                                                                                                                                                                                                                              | `403` | `-32602`, `action_access_denied`. A refusal.                                                                                           |
| The request's session is closed                                                                                                                                                                                                                                                                                                                                                         | `200` | A tool error `session_closed` for `tools/call`; otherwise `-32602`, `session_closed`. A refusal.                                       |
| The request does not produce a valid session input, or its session exists with a different input                                                                                                                                                                                                                                                                                        | `400` | `-32602`, `mcp_session_invalid`. A refusal.                                                                                            |
| The session is over its rate limit                                                                                                                                                                                                                                                                                                                                                      | `429` | `-32602`, `mcp_rate_limited`, with `Retry-After`. A refusal.                                                                           |
| Too many of the account's MCP requests are running at once                                                                                                                                                                                                                                                                                                                              | `429` | `-32602`, `mcp_tenant_busy`, with `Retry-After`. A refusal.                                                                            |
| Too many of the user's MCP requests are running at once                                                                                                                                                                                                                                                                                                                                 | `429` | `-32602`, `mcp_user_busy`, with `Retry-After`. A refusal.                                                                              |
| A method of a capability the server does not offer                                                                                                                                                                                                                                                                                                                                      | `404` | `-32601`. A refusal.                                                                                                                   |
| A `tools/call` naming no tool the server offers, including a native tool it does not list                                                                                                                                                                                                                                                                                               | `200` | `-32602`, `tool_not_declared`. `FAILED`.                                                                                               |
| A `resources/read` or `skills/get` of a URI the server does not serve                                                                                                                                                                                                                                                                                                                   | `200` | `-32602`, `mcp_resource_not_found`, with `data.uri`. `FAILED`.                                                                         |
| A `resources/read` or `skills/get` of a skill version the server no longer serves                                                                                                                                                                                                                                                                                                       | `200` | `-32602`, `mcp_skill_superseded`, with `data.uri`. `FAILED`.                                                                           |
| A `resources/read` whose template's tool is no longer read-only, or can no longer be called                                                                                                                                                                                                                                                                                             | `200` | `-32602`, `mcp_resource_unavailable`, with `data.uri`. `FAILED`.                                                                       |
| A `resources/read` whose tool fails, or a skill load that fails                                                                                                                                                                                                                                                                                                                         | `200` | `-32602` with the call's `code` and `hint`, and `data.uri`. `FAILED`, with the failed call.                                            |
| A `prompts/get` naming no prompt the server offers                                                                                                                                                                                                                                                                                                                                      | `200` | `-32602`, `mcp_prompt_not_found`. `FAILED`.                                                                                            |
| A task request from a client without the Tasks extension                                                                                                                                                                                                                                                                                                                                | `400` | `-32021` naming the Tasks extension. A refusal.                                                                                        |
| A task request naming a task the session did not create                                                                                                                                                                                                                                                                                                                                 | `200` | `-32602`, `mcp_task_not_found`. `FAILED`.                                                                                              |
| A `tools/call` to a callable agent, or an `invoke` of an agent, while the session has 32 unfinished tasks, or its user has 32 across the server's sessions                                                                                                                                                                                                                              | `200` | A tool error `start_limit`. `SUCCEEDED`.                                                                                               |
| A `tasks/cancel` whose task kept changing while it was being cancelled                                                                                                                                                                                                                                                                                                                  | `200` | `-32603`, `action_conflict`, `retryable: true`. `FAILED`.                                                                              |
| A `tools/call` without an `intent` under `REQUIRED`, or with an invalid one                                                                                                                                                                                                                                                                                                             | `200` | A tool error `mcp_intent_required` or `mcp_intent_invalid`. `SUCCEEDED`.                                                               |
| A call to a tool the server can no longer offer at its latest version                                                                                                                                                                                                                                                                                                                   | `200` | A tool error `mcp_tool_unavailable`, with a hint. `SUCCEEDED`.                                                                         |
| A call error, including arguments the tool's schema rejects and a policy denial                                                                                                                                                                                                                                                                                                         | `200` | A tool error with the call's code, message, and hint. `SUCCEEDED`.                                                                     |
| An `invoke` that cannot run                                                                                                                                                                                                                                                                                                                                                             | `200` | A tool error, as in [refused invocations](https://www.twisp.com/docs/reference/actions/mcp.md#refused-invocations). `SUCCEEDED`, with the `invoke` call `FAILED`.                         |
| The request passed the server's `responseTimeout`                                                                                                                                                                                                                                                                                                                                       | `200` | `-32603`, `mcp_request_deadline`, `retryable: true`, with a hint. Rolled back; `FAILED`.                                               |
| The principal's client was revoked, or its token expired, after the request was admitted                                                                                                                                                                                                                                                                                                | `200` | `-32602`, `action_access_denied`, with a hint. `FAILED`.                                                                               |
| The request's transaction did not commit, or did not finish committing within 10 seconds after its `responseTimeout`                                                                                                                                                                                                                                                                    | `200` | `-32603`, `mcp_transaction_error`, `retryable: true`. Rolled back; `FAILED`.                                                           |
| Twisp could not run the admitted request, or it did not answer in time                                                                                                                                                                                                                                                                                                                  | `200` | `-32603`, `mcp_execution_unavailable`, `retryable: true`. Its changes were not committed; `FAILED`.                                    |
| The request's outcome was recorded, but its answer was lost                                                                                                                                                                                                                                                                                                                             | `200` | `-32603`, `mcp_reply_lost`, when it succeeded: it took effect. When it failed, `-32603` with the recorded failure's `code` and `hint`. |
| The client closed the request before its result                                                                                                                                                                                                                                                                                                                                         | —     | Rolled back; `CANCELLED`, with `cancelledBy` naming the principal.                                                                     |
| A definition with triggers, a `tool` or `agent` block, a `config` other than `{}`, no `session`, `approval`, `preload`, colliding tool names, a tool declaring `intent` while intent is on, an invalid skill name or a skill description that is empty or too long, a resource whose tool is not read-only or whose variables do not fit its input, or more than 128 tools or 64 skills | —     | Error whose message names the rule; the write is rejected.                                                                             |

## The default server

The [local environment](https://www.twisp.com/docs/infrastructure/local-environment.md) installs an MCP server named `default`, at `http://localhost:8080/mcp/v1/000000000000/default`. It is made of ordinary actions, which you can change. It lists the native tools `graphql`, `cel`, [`program`](https://www.twisp.com/docs/reference/actions/mcp.md#programs), [`invoke`](https://www.twisp.com/docs/reference/actions/mcp.md#invoke), and [`get_task` and `cancel_task`](https://www.twisp.com/docs/reference/actions/mcp.md#tasks), the tool `audit`, and the skills `basics`, `design`, `verify`, `actions`, `agents`, and `mcp`. Its policies allow every action on every resource, so each request is bounded by the principal's own policies. It runs `LIVE`, so its writes commit, requires [stated intent](https://www.twisp.com/docs/reference/actions/mcp.md#stated-intent), and gives each principal and subject a session.

`audit` reads the session's own record back: a tool with `SELECT`-only policies, whose reference fixes `session_id` to `session.id` and `server` to the server's name. The external agent passes:

| Argument            | Meaning                                                                  |
| ------------------- | ------------------------------------------------------------------------ |
| `since`             | Only requests after this time.                                           |
| `limit`             | The most recent requests to return, 1 to 1,000. Defaults to 100.         |
| `include_discovery` | Also list discovery requests, such as `tools/list`. Defaults to `false`. |

It answers with `server`, `session_id`, `steps`, the requests oldest first, and `complete`, false when there were more requests than it returned. Each step has `at`, `did`, the tool called and what it ran, such as a GraphQL operation's head or whether a program previewed or committed, the `intent`, `status`, the execution's status or `TOOL_ERROR` for a tool error, `error` when the request or its call failed, and `execution_id`. The last few requests can take a moment to appear.

## The `mcp` handler

Serves the requests of agents running outside Twisp over the Model Context Protocol, each as one execution whose input is the request. Kind `mcp` requires an `mcp` block and a `session`, and its `config` is `{}`; it runs only when its endpoint serves a request. See [MCP Servers](https://www.twisp.com/docs/reference/actions/mcp.md).
