# MCP server

Connect an AI agent to one Volcano project over the Model Context Protocol, using the same project access token as the REST API.

Volcano hosts a Model Context Protocol (MCP) server, so an agent in your editor
can inspect a project — its functions, frontends, deployments, logs, its
durable executions and their messages, and the approvals its durable workflows
are waiting on — and, with a full-scope token, change it.

It takes the same project access token as the REST API, over one endpoint:

```text
POST https://api.volcano.dev/mcp
```

The agent operates the project that token belongs to, and only that one. No tool
takes a project argument, so an agent cannot point a call at a project its
credential does not cover. Configuration for [Claude Code](#claude-code),
[Cursor](#cursor), and [Codex](#codex) is below.

## Configure your client

The endpoint is one URL, and authentication is a request header:

```text
POST https://api.volcano.dev/mcp
Authorization: Bearer pt-your-project-access-token
```

Every client below reads the token from an environment variable, so the secret
stays out of a file you might commit. Export it first:

```bash
export VOLCANO_PROJECT_TOKEN=pt-your-project-access-token
```

### Claude Code

Add it from the command line:

```bash
claude mcp add --transport http volcano https://api.volcano.dev/mcp \
  --header "Authorization: Bearer $VOLCANO_PROJECT_TOKEN"
```

Or commit the server (not the token) to `.mcp.json` in your project root:

```json
{
  "mcpServers": {
    "volcano": {
      "type": "http",
      "url": "https://api.volcano.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${VOLCANO_PROJECT_TOKEN}"
      }
    }
  }
}
```

`type` is required for Claude Code to treat the entry as remote. Restart the
session after editing the file.

If tools come back `401`, check that the variable reached the header rather than
being passed through literally — some Claude Code versions have shipped without
expanding `${VAR}` inside `headers`. `claude mcp list` warns when a variable is
unset. This endpoint does not offer OAuth, so a missing header fails as `401`
rather than silently starting a browser login.

### Cursor

Put this in `.cursor/mcp.json` for one project, or `~/.cursor/mcp.json` to use it
everywhere:

```json
{
  "mcpServers": {
    "volcano": {
      "url": "https://api.volcano.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${env:VOLCANO_PROJECT_TOKEN}"
      }
    }
  }
}
```

Cursor infers the transport from `url`, and uses `${env:NAME}` rather than
`${NAME}`. The variable has to be set in the environment Cursor itself was
launched from — if you start it from a desktop icon rather than a shell, a
variable exported in `.zshrc` will not reach it.

### Codex

Codex uses TOML, and takes the _name_ of the environment variable rather than
the token:

```bash
codex mcp add volcano \
  --url https://api.volcano.dev/mcp \
  --bearer-token-env-var VOLCANO_PROJECT_TOKEN
```

That writes `~/.codex/config.toml`, which you can also edit directly:

```toml
[mcp_servers.volcano]
url = "https://api.volcano.dev/mcp"
bearer_token_env_var = "VOLCANO_PROJECT_TOKEN"
```

Check what was saved with `codex mcp get volcano --json`. The output should show
the variable name under `bearer_token_env_var` — if a real token appears there or
in the URL, remove the entry and add it again.

## Create a token for the agent

The MCP endpoint takes the same credential as the REST API: a project access token (`pt-`). Create one with your platform token, and start with `read_only`:

```bash
curl -X POST "https://api.volcano.dev/projects/$PROJECT_ID/access-tokens" \
  -H "Authorization: Bearer $PLATFORM_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "coding-agent",
    "scope": "read_only",
    "expires_at": "2027-07-01T00:00:00Z"
  }'
```

```json
{
  "id": "0b6f2f3e-8f4e-4f1e-9a3a-1f9a7c2d5b10",
  "project_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "name": "coding-agent",
  "token_prefix": "pt-000000000",
  "scope": "read_only",
  "status": "active",
  "expires_at": "2027-07-01T00:00:00Z",
  "created_at": "2024-07-02T09:14:22Z",
  "token": "pt-0000000000000000000000000000000000000000000000000000000000000000"
}
```

`read_only` covers everything an agent needs to investigate — the project, its functions and frontends, its deployments, its logs — and nothing it can use to change the project. Give an agent `full` only when it genuinely has to write: that scope is the whole project API, not just the tools listed here, so the same credential can delete the project. Set `expires_at` so a token you hand to an agent stops working on its own.

A **platform token (`pk-`) is refused with `403`.** The endpoint takes its project from the credential, and an account-wide token names no project:

```json
{
  "error": "this endpoint needs a project access token; a platform token does not name a project"
}
```

## Tools

Each tool calls its endpoint for the token's project: as `{id}` in the path, or
as `project_id` on the top-level LavaFlow endpoints. A tool never takes the
project as an argument.

| Tool                          | Scope       | What it does                                                                                                         | Endpoint                                                                                   |
| ----------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `get_project`                 | `read_only` | Returns the project's name, status, plan, and region                                                                 | `GET /projects/{id}`                                                                       |
| `get_project_usage`           | `read_only` | Reads project totals and time series, including Sandbox runtime, suspended time, and uncertain usage                 | `GET /projects/{id}/usage`                                                                 |
| `list_functions`              | `read_only` | Lists the project's standard functions, with runtime, status, and visibility                                         | `GET /projects/{id}/functions`                                                             |
| `list_frontends`              | `read_only` | Lists the project's frontends, with status, live URLs, and function routes                                           | `GET /projects/{id}/frontends`                                                             |
| `list_frontend_domains`       | `read_only` | Lists a frontend's custom domains, with status, pending DNS records, and redirect                                    | `GET /projects/{id}/frontends/{frontendId}/domains`                                        |
| `list_deployments`            | `read_only` | Lists recent deployments, newest first                                                                               | `GET /projects/{id}/deployments`                                                           |
| `search_logs`                 | `read_only` | Searches the project's logs                                                                                          | `POST /projects/{id}/logs/search`                                                          |
| `list_durable_approvals`      | `read_only` | Lists the approvals durable workflows are waiting on or have had decided, newest first                               | `GET /projects/{id}/durable-approvals`                                                     |
| `get_durable_approval_stats`  | `read_only` | Counts durable approvals by outcome, workflow, and day, with the approval rate and time to decision                  | `GET /projects/{id}/durable-approvals/stats`                                               |
| `list_durable_functions`      | `read_only` | Lists the project's durable functions, which `list_functions` leaves out                                             | `GET /projects/{id}/durable-functions`                                                     |
| `list_durable_executions`     | `read_only` | Lists a durable function's executions, newest first                                                                  | `GET /projects/{id}/durable-functions/{functionId}/executions`                             |
| `get_durable_execution`       | `read_only` | Returns one execution's status, result or error, and timing                                                          | `GET /projects/{id}/durable-functions/{functionId}/executions/{executionId}`               |
| `list_durable_messages`       | `read_only` | Lists the messages sent to an execution and published by it, oldest first                                            | `GET /projects/{id}/durable-functions/{functionId}/executions/{executionId}/messages`      |
| `list_durable_message_waits`  | `read_only` | Lists the message names an execution is waiting on                                                                   | `GET /projects/{id}/durable-functions/{functionId}/executions/{executionId}/message-waits` |
| `list_issues`                 | `read_only` | Lists runtime errors grouped into issues, most recently seen first                                                   | `GET /projects/{id}/issues`                                                                |
| `get_issue`                   | `read_only` | Returns one issue with its resource, error, counts, and status                                                       | `GET /projects/{id}/issues/{issueId}`                                                      |
| `get_lavaflow_settings`       | `read_only` | Returns whether LavaFlow is on, its autonomy, its merge policy, `allow_ungated_workflows`, and its context           | `GET /lavaflow/settings`                                                                   |
| `get_lavaflow_stats`          | `read_only` | Counts the issues LavaFlow took on and the pull requests it opened per UTC day                                       | `GET /lavaflow/stats`                                                                      |
| `list_lavaflow_resources`     | `read_only` | Lists each resource's LavaFlow activity and anomalies                                                                | `GET /lavaflow/resources`                                                                  |
| `list_lavaflow_runs`          | `read_only` | Lists LavaFlow runs, newest first, with each one's merge and hold reasons                                            | `GET /lavaflow/runs`                                                                       |
| `get_lavaflow_run`            | `read_only` | Returns one LavaFlow run with its diagnosis, review, pull request, ungated workflows it opened past, and merge state | `GET /lavaflow/runs/{run_id}`                                                              |
| `get_git_owner_push`          | `read_only` | Checks whether the project owner can push to the connected repository, and why not                                   | `GET /projects/{id}/git-connection/owner-push`                                             |
| `list_schedulers`             | `read_only` | Lists the project's function schedulers with their schedule, next run, and health                                    | `GET /projects/{id}/schedulers`                                                            |
| `list_capabilities`           | `read_only` | Lists which capabilities, such as Sandboxes, accept new work in this environment                                     | `GET /capabilities`                                                                        |
| `list_project_capabilities`   | `read_only` | Lists which capabilities accept new work for this project, including ones offered to some projects only              | `GET /projects/{id}/capabilities`                                                          |
| `list_variable_environments`  | `read_only` | Lists the project's variable Environments                                                                            | `GET /projects/{id}/variable-environments`                                                 |
| `list_git_deployment_runs`    | `read_only` | Lists the runs Git pushes started, newest first, with each one's migration runs and error                            | `GET /projects/{id}/git-deployment-runs`                                                   |
| `get_git_deployment_run`      | `read_only` | Returns one Git deployment run                                                                                       | `GET /projects/{id}/git-deployment-runs/{runId}`                                           |
| `list_databases`              | `full`      | Lists the project's databases. Returns connection strings                                                            | `GET /projects/{id}/databases`                                                             |
| `set_variable`                | `full`      | Creates or updates an environment variable                                                                           | `POST /projects/{id}/variables`                                                            |
| `create_variable_environment` | `full`      | Creates a custom variable Environment                                                                                | `POST /projects/{id}/variable-environments`                                                |
| `send_durable_message`        | `full`      | Sends a named message to a running durable execution                                                                 | `POST /projects/{id}/durable-functions/{functionId}/executions/{executionId}/messages`     |

Arguments are not listed here, because they are not defined here. Each tool's
input schema is generated from the API's OpenAPI specification for the endpoint
it calls, so a tool accepts exactly the parameters and body fields that
endpoint accepts — with their types, allowed values, bounds, and documentation —
and a change to the API is carried into the tool schema without anyone editing a
list. Call `tools/list` to see the current schemas, which are always the ones
your token will be held to.

That includes the shapes an argument could not otherwise be guessed from. The
`resource` argument of `search_logs`, for instance, arrives as a tagged union
with each variant and its `type` value spelled out, so an agent can construct
`{"resource": {"type": "function"}, "limit": 20}` from the schema alone.

Scope is enforced by the same rules as the REST API. A `read_only` token is not
shown the `full` tools in `tools/list`, and is refused if it calls one anyway —
`set_variable`, `create_variable_environment`, and `send_durable_message`
because they write, and `list_databases` because it hands back a credential that
keeps working after the token is gone. A `full` token sees every tool above, and
also the Sandbox mutation tools below.

## Sandbox tools

Sandbox tools use the same API authorization and billing rules. A session ID
must belong to the token's project, including on routes without a project ID.

| Tool                                                                                | Scope       | Purpose                                            |
| ----------------------------------------------------------------------------------- | ----------- | -------------------------------------------------- |
| `list_sandbox_presets`                                                              | `read_only` | Discover verified presets and memory sizes         |
| `list_sandboxes`, `get_sandbox`                                                     | `read_only` | Inspect project templates                          |
| `list_sandbox_deployments`, `get_sandbox_deployment`, `get_sandbox_deployment_logs` | `read_only` | Inspect deployment history, status, and build logs |
| `list_sandbox_sessions`, `get_sandbox_session`                                      | `read_only` | Inspect session state                              |
| `read_sandbox_file`                                                                 | `read_only` | Read session files                                 |
| `create_sandbox`, `update_sandbox`                                                  | `full`      | Create or rename a template                        |
| `create_sandbox_session`                                                            | `full`      | Start a session from a preset or template          |
| `execute_sandbox`                                                                   | `full`      | Execute once with automatic cleanup                |
| `execute_sandbox_session`                                                           | `full`      | Execute in an existing session                     |
| `write_sandbox_file`                                                                | `full`      | Write a session file                               |
| `suspend_sandbox_session`, `resume_sandbox_session`                                 | `full`      | Pause and resume a session                         |
| `terminate_sandbox_session`                                                         | `full`      | Permanently stop a session and release compute     |

Check `list_capabilities` before starting work. Confirmed cloud running time
consumes account allowance and paid overage credits. Suspended and uncertain
time are uncharged, and snapshot costs are included in runtime pricing.
Terminate sessions when finished; termination discards their files.

For operations with an `Idempotency-Key` argument, generate a UUID once and
reuse it only for retries with the same arguments. The tool forwards this as an
HTTP header, preventing duplicate starts or executions after a lost response.
Resource IDs such as `sessionId` and `sandboxId` are tool arguments; the project
ID always comes from the credential.

`create_sandbox_session` and `execute_sandbox` take their request fields inside
`body`. Supply exactly one of `body.preset` or `body.sandbox_id`. The
`Idempotency-Key` argument stays outside `body`.

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "execute_sandbox",
    "arguments": {
      "Idempotency-Key": "00000000-0000-4000-8000-000000000001",
      "body": {
        "preset": "python3.12",
        "region": "us-east-1",
        "command": "python --version"
      }
    }
  }
}
```

MCP requests are limited to 1 MiB and results to 64 KiB. Use the SDK or HTTP API
for larger files. Custom image uploads and source archive downloads remain in
the CLI/API. Template deletion, end-user grants, and port-access credential
issuance are not MCP tools. Project access tokens cannot manage end-user grants
or issue port credentials, even through direct HTTP requests.

## Call a tool

```bash
curl -X POST "https://api.volcano.dev/mcp" \
  -H "Authorization: Bearer $VOLCANO_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {"name": "list_functions", "arguments": {}}
  }'
```

A tool returns the API's own JSON response as text content:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"data\":[{\"id\":\"550e8400-e29b-41d4-a716-446655440000\",\"name\":\"checkout\",\"status\":\"active\",\"runtime\":\"nodejs24.x\"}],\"page\":1,\"limit\":10,\"total\":1,\"has_more\":false}"
      }
    ]
  }
}
```

A call that ran and failed comes back as a tool result with `isError` set and the HTTP status included, so the agent can tell "you may not" from "that does not exist":

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "set_variable failed with HTTP 403: {\"error\":\"project access token is read-only\"}"
      }
    ],
    "isError": true
  }
}
```

When the route answers with `Retry-After`, the text carries the wait, such as `get_git_owner_push failed with HTTP 503, retry after 5s: ...`. Wait that long before calling the tool again.

A JSON-RPC error means the request itself was wrong — an unknown tool or bad params (`-32602`), an unknown method (`-32601`) — and nothing ran.

Results are capped at 64 KiB. Past that the text ends with a note saying it was truncated and to narrow the request, so an agent knows it is reading a prefix. Use `limit` and the `search_logs` filters rather than paging through a truncated result.

`list_durable_messages` stays valid JSON instead: it leaves out the data of the largest messages until the page fits, sets `data_omitted: true` on each and its `data` to `null`, and says how many in the note. Every `seq` and `has_more` survive, so a message too large to show never blocks the next page. A page still over the cap without its data is truncated like any other result; lower `limit`.

`get_lavaflow_run` does the same with a run's `diff`, then its `diagnosis`: each one left out is `null` with `diff_omitted` or `diagnosis_omitted` set, and the run's status, review, and pull request survive. When the run opened a pull request, it has the full change.

`send_durable_message` echoes the message it sent. When that echo is over the cap, it leaves out the message's data the same way, so its `id`, `seq` and `status` still say how the send went.

## What it cannot do

A project access token is a control-plane credential: it deploys and configures, including running commands in project-owned Sandbox sessions. There is no tool to invoke a function or query a database — those need a service key or an anon key, which are data-plane credentials the agent should not be holding. See [Service keys](https://docs.volcano.dev/platform/authentication/security/service-keys.md).

It still reads what the project records about its work, and that can include your users' data: log lines with `search_logs`, an execution's result with `get_durable_execution`, and with `list_durable_messages` the data clients sent to an execution and the data it published. With a `full` token, `send_durable_message` resumes a workflow waiting for that message, which then runs your code. Give an agent a project's token only if it may see that data, and a `read_only` one if it should not resume workflows.

The rest of the boundary comes from the credential itself, and is the same over MCP as over HTTP: no other project, no account-level action, and no managing project access tokens — an agent cannot mint its replacement or erase the record of what it did. See [Project access tokens](https://docs.volcano.dev/platform/authentication/security/project-access-tokens.md).

Nor can it approve or deny a durable approval. An agent can list what workflows are waiting on, but a decision takes a person, in the dashboard or with a platform token, and the approve and deny endpoints refuse a project access token. See [Durable approvals](https://docs.volcano.dev/platform/functions/durable-approvals.md#who-can-decide). `send_durable_message` does not get around this: a message only resumes a workflow waiting for a message, never one waiting for an approval. See [Durable messages](https://docs.volcano.dev/platform/functions/durable-messages.md#messages-approvals-or-callbacks).

Nor can it change [LavaFlow's settings](https://docs.volcano.dev/platform/guides/lavaflow.md#settings), so it can't turn LavaFlow on or [turn merging on](https://docs.volcano.dev/platform/guides/lavaflow.md#turn-merging-on). Those are the project owner's decisions, and the settings endpoint refuses a full-scope project access token with `403` and code `person_required`.

## Revoke access

Revoking the token stops the agent. It takes effect immediately in the region handling the call and within seconds everywhere else, and the MCP endpoint then answers `401` like every other route:

```bash
curl -X DELETE "https://api.volcano.dev/projects/$PROJECT_ID/access-tokens/$TOKEN_ID" \
  -H "Authorization: Bearer $PLATFORM_TOKEN"
```

```text
HTTP/1.1 204 No Content
```

Revoking does not undo what the agent already did. The token's usage counts show how much it called; rotate anything a `full` token could have read.

## Protocol details

Streamable HTTP: one JSON-RPC 2.0 object per request, one response per request. The server never initiates a message, so there is no server-to-client stream — a `GET` returns `405` with an `Allow: POST` header, and a batched array is rejected with a message saying so. `initialize` advertises protocol revision `2025-06-18` and a tools capability only.

Every request needs an `id`. Only the `notifications/*` methods may omit one, and only those are answered with `202` and no body; a `tools/call` without an `id` is refused with `-32600` and the tool does not run. A frame larger than 1 MiB is refused with `413`.

| Method                      | Purpose                                                                                      |
| --------------------------- | -------------------------------------------------------------------------------------------- |
| `initialize`                | Handshake; returns the protocol revision and server info                                     |
| `notifications/initialized` | Client acknowledgement. The one method sent without an `id`; answered with `202` and no body |
| `ping`                      | Liveness check                                                                               |
| `tools/list`                | The tools this credential may call                                                           |
| `tools/call`                | Run one tool                                                                                 |

Any other method returns `-32601`.

## What's next

| Reference                                                                    | Description                                        |
| ---------------------------------------------------------------------------- | -------------------------------------------------- |
| [Project access tokens](https://docs.volcano.dev/platform/authentication/security/project-access-tokens.md) | Scopes, expiry, revocation, and the security model |
| [Using the API](https://docs.volcano.dev/platform/api-reference/using-the-api.md)                           | The same project API over plain HTTP               |
| [Overview](https://docs.volcano.dev/platform/api-reference/overview.md)                                     | Base URL, status codes, pagination                 |
| [Errors](https://docs.volcano.dev/platform/api-reference/errors.md)                                         | Error codes and handling                           |
