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:

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, Cursor, Codex, OpenCode, Pi, GitHub Copilot, and Gemini CLI is below.

Configure your client

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

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:

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

Claude Code

Add it from the command line. The single quotes keep ${VOLCANO_PROJECT_TOKEN} a reference, so Claude Code reads the variable when it connects instead of saving the token:

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:

{
  "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:

{
  "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:

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:

[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.

OpenCode

Put this in opencode.json in your project root, or ~/.config/opencode/opencode.json to use it everywhere:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "volcano": {
      "type": "remote",
      "url": "https://api.volcano.dev/mcp",
      "oauth": false,
      "headers": {
        "Authorization": "Bearer {env:VOLCANO_PROJECT_TOKEN}"
      }
    }
  }
}

OpenCode writes variables as {env:NAME}, without a $. Set oauth to false: otherwise a 401 from an unset variable makes OpenCode start an OAuth flow this endpoint does not offer. opencode mcp list shows the server and its status.

Pi

Pi has MCP support built in from version 0.99. Add the server from the command line. Like Codex, Pi takes the variable's name:

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

That writes ~/.pi/agent/mcp.json. Add --local to write the project's .pi/mcp.json instead, or edit either file directly:

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

pi mcp list connects to the server and lists its tools. Run /reload in a session that was already open.

GitHub Copilot

Copilot in VS Code and the Copilot CLI read the same user file, ~/.copilot/mcp-config.json:

{
  "mcpServers": {
    "volcano": {
      "type": "http",
      "url": "https://api.volcano.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${VOLCANO_PROJECT_TOKEN}"
      },
      "tools": ["*"]
    }
  }
}

Use the braced ${NAME} form: VS Code sends a bare $NAME through as text. As with Cursor, VS Code reads the variable from the environment it was launched from, so start it from the shell where you exported the token. To share the server with a team, put the same entry in a .mcp.json at the workspace root. VS Code's older .vscode/mcp.json format uses servers and ${env:NAME} instead.

Gemini CLI

Put this in .gemini/settings.json in your project, or ~/.gemini/settings.json to use it everywhere:

{
  "mcpServers": {
    "volcano": {
      "httpUrl": "https://api.volcano.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${VOLCANO_PROJECT_TOKEN}"
      }
    }
  }
}

httpUrl selects the streamable HTTP transport; url would select SSE, which this endpoint does not serve. If the variable is not set where Gemini CLI starts, requests fail with 401. gemini mcp list shows whether the server connected.

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:

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"
  }'
{
  "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:

{
  "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.

ToolScopeWhat it doesEndpoint
get_projectread_onlyReturns the project's name, status, plan, and regionGET /projects/{id}
get_project_usageread_onlyReads project totals and time series, including Sandbox runtime, suspended time, and uncertain usageGET /projects/{id}/usage
list_functionsread_onlyLists the project's standard functions, with runtime, status, and visibilityGET /projects/{id}/functions
list_frontendsread_onlyLists the project's frontends, with status, live URLs, and function routesGET /projects/{id}/frontends
list_frontend_domainsread_onlyLists a frontend's custom domains, with status, pending DNS records, and redirectGET /projects/{id}/frontends/{frontendId}/domains
list_deploymentsread_onlyLists recent deployments, newest firstGET /projects/{id}/deployments
search_logsread_onlySearches the project's logsPOST /projects/{id}/logs/search
list_durable_approvalsread_onlyLists the approvals durable workflows are waiting on or have had decided, newest firstGET /projects/{id}/durable-approvals
get_durable_approval_statsread_onlyCounts durable approvals by outcome, workflow, and day, with the approval rate and time to decisionGET /projects/{id}/durable-approvals/stats
list_durable_functionsread_onlyLists the project's durable functions, which list_functions leaves outGET /projects/{id}/durable-functions
list_durable_executionsread_onlyLists a durable function's executions, newest firstGET /projects/{id}/durable-functions/{functionId}/executions
get_durable_executionread_onlyReturns one execution's status, result or error, and timingGET /projects/{id}/durable-functions/{functionId}/executions/{executionId}
list_durable_messagesread_onlyLists the messages sent to an execution and published by it, oldest firstGET /projects/{id}/durable-functions/{functionId}/executions/{executionId}/messages
list_durable_message_waitsread_onlyLists the message names an execution is waiting onGET /projects/{id}/durable-functions/{functionId}/executions/{executionId}/message-waits
list_issuesread_onlyLists runtime errors, Code Quality findings and reported problems as issues, most recently seen firstGET /projects/{id}/issues
get_issueread_onlyReturns one issue with its source, resource, error, counts, and status, and a Code Quality findingGET /projects/{id}/issues/{issueId}
get_lavaflow_settingsread_onlyReturns whether LavaFlow and Code Quality are on, its autonomy, its merge policy, allow_ungated_workflows, and its contextGET /lavaflow/settings
get_lavaflow_slack_channelsread_onlyLists the Slack channels that report problems to LavaFlow for the projectGET /lavaflow/slack-channels
get_lavaflow_statsread_onlyCounts the issues LavaFlow took on and the pull requests it opened per UTC dayGET /lavaflow/stats
list_lavaflow_resourcesread_onlyLists each resource's LavaFlow activity and anomaliesGET /lavaflow/resources
list_lavaflow_runsread_onlyLists LavaFlow runs with their categories, issues, resources, merges, hold reasons, skip reasons, and countsGET /lavaflow/runs
get_lavaflow_runread_onlyReturns one LavaFlow run with its source commit, diagnosis, changed files, review, pull request, ungated workflows it opened past, and merge stateGET /lavaflow/runs/{run_id}
list_lavaflow_scansread_onlyLists Code Quality scans, the waiting one first, with their counts and billed findingsGET /lavaflow/scans
list_lavaflow_skillsread_onlyLists the project's LavaFlow skills, newest first, by statusGET /lavaflow/skills
get_lavaflow_skillread_onlyReturns one LavaFlow skill with its active versionGET /lavaflow/skills/{skill_id}
list_lavaflow_skill_versionsread_onlyLists a LavaFlow skill's versions, newest firstGET /lavaflow/skill-versions
list_lavaflow_global_skillsread_onlyLists the skills Volcano wrote for every projectGET /lavaflow/global-skills
get_git_owner_pushread_onlyChecks whether the project owner can push to the connected repository, and why notGET /projects/{id}/git-connection/owner-push
list_schedulersread_onlyLists the project's function schedulers with their schedule, next run, and healthGET /projects/{id}/schedulers
list_capabilitiesread_onlyLists which capabilities, such as Sandboxes, accept new work in this environmentGET /capabilities
list_project_capabilitiesread_onlyLists which capabilities accept new work for this project, including ones offered to some projects onlyGET /projects/{id}/capabilities
list_variable_environmentsread_onlyLists the project's variable EnvironmentsGET /projects/{id}/variable-environments
list_git_deployment_runsread_onlyLists the runs Git pushes started, newest first, with each one's migration runs and errorGET /projects/{id}/git-deployment-runs
get_git_deployment_runread_onlyReturns one Git deployment runGET /projects/{id}/git-deployment-runs/{runId}
list_databasesfullLists the project's databases. Returns connection stringsGET /projects/{id}/databases
set_variablefullCreates or updates an environment variablePOST /projects/{id}/variables
create_variable_environmentfullCreates a custom variable EnvironmentPOST /projects/{id}/variable-environments
send_durable_messagefullSends a named message to a running durable executionPOST /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.

ToolScopePurpose
list_sandbox_presetsread_onlyDiscover verified presets and memory sizes
list_sandboxes, get_sandboxread_onlyInspect project templates
list_sandbox_deployments, get_sandbox_deployment, get_sandbox_deployment_logsread_onlyInspect deployment history, status, and build logs
list_sandbox_sessions, get_sandbox_sessionread_onlyInspect session state
read_sandbox_file, list_sandbox_filesread_onlyRead and list session files
create_sandbox, update_sandboxfullCreate or rename a template
create_sandbox_sessionfullStart a session from a preset or template
execute_sandboxfullExecute once with automatic cleanup
execute_sandbox_sessionfullExecute in an existing session
write_sandbox_filefullWrite a session file
suspend_sandbox_session, resume_sandbox_sessionfullPause and resume a session
terminate_sandbox_sessionfullPermanently 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.

{
  "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

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:

{
  "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":

{
  "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 changed_files, then its resources, then its diagnosis, then its held_workflows: each one left out is null with diff_omitted, changed_files_omitted, resources_omitted, diagnosis_omitted, or held_workflows_omitted set, and the run's status, review, and pull request survive. When the run opened a pull request, it has the full change. A change to code every function shares involves every function, so a run's resources alone can be too large.

list_lavaflow_runs leaves out the resources of the runs that list the most, largest first, until the page fits, and sets resources_omitted: true on each. get_lavaflow_run reads one run's resources. A page still over the cap without them comes back with fewer runs: the tool asks again with a lower limit and sets limit_reduced: true, and the page's next_cursor continues after its last run. Only a page that cannot be shortened to fit, such as one whose first run is over the cap on its own, is truncated like any other result; lower limit.

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.

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.

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. 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.

Nor can it change LavaFlow's settings or start a LavaFlow run, so it can't turn LavaFlow on, turn merging on, or have LavaFlow work on an issue it wouldn't start on by itself. Those are the project owner's decisions, and both endpoints refuse a full-scope project access token with 403 and code person_required. Nor can it open a fix LavaFlow held for ungated workflows: the owner reads the fix and the workflow jobs, and the open endpoint refuses a project access token the same way.

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:

curl -X DELETE "https://api.volcano.dev/projects/$PROJECT_ID/access-tokens/$TOKEN_ID" \
  -H "Authorization: Bearer $PLATFORM_TOKEN"
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.

MethodPurpose
initializeHandshake; returns the protocol revision and server info
notifications/initializedClient acknowledgement. The one method sent without an id; answered with 202 and no body
pingLiveness check
tools/listThe tools this credential may call
tools/callRun one tool

Any other method returns -32601.

What's next

ReferenceDescription
Project access tokensScopes, expiry, revocation, and the security model
Using the APIThe same project API over plain HTTP
OverviewBase URL, status codes, pagination
ErrorsError codes and handling

On this page