# MCP server

This page shows you how to connect an MCP client to Impello, how it signs in,
and which tools it gets. The Impello MCP server lets an agent create, inspect,
run commands in and kill your team's sandboxes, and read its templates, usage
and billing, without the SDK.

## Connect URLs

The server speaks the Streamable HTTP transport at two URLs.

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>URL</th><th>Tools</th></tr>
  </thead>
  <tbody>
    <tr><td>`https://dashboard.impello.ai/api/mcp`</td><td>Read and write tools</td></tr>
    <tr><td>`https://dashboard.impello.ai/api/mcp/readonly`</td><td>Read tools only</td></tr>
  </tbody>
</table>
</div>

Use the read-only URL for an agent that should watch sandboxes and spend but
never start, change or kill anything.

## Sign in

A client signs in one of two ways: OAuth, or an Impello API key.

### OAuth

Clients that support MCP OAuth (Claude Code, Claude Desktop, Cursor and Codex)
find it on their own. A request without a credential gets a 401 whose
`WWW-Authenticate` header points at
`/.well-known/oauth-protected-resource`, and that document points at
`/.well-known/oauth-authorization-server`. The client registers itself
dynamically, so there is no client ID to create.

1. The client opens a browser and you sign in to the dashboard.
2. You pick the team the client may act for. A grant is for one team.
3. A consent screen shows the scopes the client asked for. Allow or deny.
4. The client receives a token and connects.

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>Scope</th><th>Grants</th></tr>
  </thead>
  <tbody>
    <tr><td>`mcp:read`</td><td>The read tools</td></tr>
    <tr><td>`mcp:write`</td><td>The write tools, including `kill_sandbox`</td></tr>
    <tr><td>`offline_access`</td><td>A refresh token, so the client stays connected after its one-hour access token expires. Refresh tokens last 30 days and are replaced on each use</td></tr>
  </tbody>
</table>
</div>

Each grant gets its own team API key, kept on the server and never shown to
the client. To revoke a grant, open
[dashboard.impello.ai/keys](https://dashboard.impello.ai/keys) and disconnect
the client under **Connected MCP clients**. Its tokens stop working at once,
and its key is deleted.

### API key

Send an Impello API key from
[dashboard.impello.ai/keys](https://dashboard.impello.ai/keys) as a bearer
token:

```http
Authorization: Bearer imp_...
```

An API key acts for its team with both scopes. See [API keys](/docs/api-keys).

<Callout kind="warning">
A key in a client config can do everything the key can do on the API. Keep the
config file out of repositories, and prefer OAuth where the client supports it.
</Callout>

## Configure your client

<CodeTabs>
<Tab label="Claude Code">

```bash
# OAuth: the first tool call opens the browser to sign in.
claude mcp add --transport http impello https://dashboard.impello.ai/api/mcp

# API key
claude mcp add --transport http impello https://dashboard.impello.ai/api/mcp \
  --header "Authorization: Bearer imp_..."
```

</Tab>
<Tab label="Claude Desktop">

Open **Settings → Connectors → Add custom connector** and enter
`https://dashboard.impello.ai/api/mcp`. Claude Desktop signs in with OAuth.

</Tab>
<Tab label="Cursor">

```json
{
  "mcpServers": {
    "impello": {
      "url": "https://dashboard.impello.ai/api/mcp"
    }
  }
}
```

Put this in `~/.cursor/mcp.json`. Cursor signs in with OAuth. To use an API key
instead, add `"headers": { "Authorization": "Bearer imp_..." }` next to `url`.

</Tab>
<Tab label="Codex">

```toml
[mcp_servers.impello]
url = "https://dashboard.impello.ai/api/mcp"
# For an API key, read from the environment:
# bearer_token_env_var = "IMPELLO_API_KEY"
```

Put this in `~/.codex/config.toml`. For OAuth, run
`codex mcp login impello`. For an API key, uncomment `bearer_token_env_var`
and export `IMPELLO_API_KEY`.

</Tab>
</CodeTabs>

For the read-only server, use `https://dashboard.impello.ai/api/mcp/readonly`
in any of these.

## Tools

Every tool declares its annotations (`readOnlyHint`, `destructiveHint`,
`idempotentHint`), so a client can ask before a write and warn before a
destructive call.

### Read tools

All read tools need `mcp:read`, and all are read-only and idempotent.

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>Tool</th><th>Scope</th><th>Annotations</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`list_sandboxes`</td><td>`mcp:read`</td><td>read-only</td><td>Lists sandboxes, filtered by `state`, `template`, `metadata` and `startedAfter`, sorted by `order`. Each item carries its latest CPU and memory reading, and the result includes `totalRunning`</td></tr>
    <tr><td>`get_sandbox`</td><td>`mcp:read`</td><td>read-only</td><td>One sandbox, with its latest metrics</td></tr>
    <tr><td>`get_sandbox_metrics`</td><td>`mcp:read`</td><td>read-only</td><td>A sandbox's metric series, plus the min, average and max of CPU, memory and disk</td></tr>
    <tr><td>`get_sandbox_logs`</td><td>`mcp:read`</td><td>read-only</td><td>A sandbox's logs, by `direction`, `level` and `search`. `limit` goes up to 1000</td></tr>
    <tr><td>`get_team_usage`</td><td>`mcp:read`</td><td>read-only</td><td>Concurrent sandboxes and start rate over time, with the peaks</td></tr>
    <tr><td>`list_templates`</td><td>`mcp:read`</td><td>read-only</td><td>The templates your team can launch</td></tr>
    <tr><td>`get_template`</td><td>`mcp:read`</td><td>read-only</td><td>One template</td></tr>
    <tr><td>`get_build_logs`</td><td>`mcp:read`</td><td>read-only</td><td>The logs of a template build</td></tr>
    <tr><td>`get_build_status`</td><td>`mcp:read`</td><td>read-only</td><td>A template build's `status` (`waiting`, `building`, `ready` or `error`), the `reason` when it failed, and `startedAt` and `finishedAt`</td></tr>
    <tr><td>`list_snapshots`</td><td>`mcp:read`</td><td>read-only</td><td>Your team's snapshots</td></tr>
    <tr><td>`get_usage`</td><td>`mcp:read`</td><td>read-only</td><td>Usage and cost series</td></tr>
    <tr><td>`list_top_sandboxes`</td><td>`mcp:read`</td><td>read-only</td><td>The sandboxes that used the most</td></tr>
    <tr><td>`get_limits`</td><td>`mcp:read`</td><td>read-only</td><td>Your team's limits</td></tr>
    <tr><td>`get_billing`</td><td>`mcp:read`</td><td>read-only</td><td>Plan, credits and plan state</td></tr>
    <tr><td>`list_invoices`</td><td>`mcp:read`</td><td>read-only</td><td>Your team's invoices</td></tr>
  </tbody>
</table>
</div>

### Write tools

Write tools need `mcp:write`, and are only on the full URL.

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>Tool</th><th>Scope</th><th>Annotations</th><th>What it does</th></tr>
  </thead>
  <tbody>
    <tr><td>`create_sandbox`</td><td>`mcp:write`</td><td>write</td><td>Starts a sandbox from `template`, with optional `timeout`, `metadata` and `env`</td></tr>
    <tr><td>`set_sandbox_timeout`</td><td>`mcp:write`</td><td>write, idempotent</td><td>Sets how long a sandbox has left. See [Timeouts](/docs/sandbox/timeouts)</td></tr>
    <tr><td>`pause_sandbox`</td><td>`mcp:write`</td><td>write, idempotent</td><td>Pauses a sandbox</td></tr>
    <tr><td>`resume_sandbox`</td><td>`mcp:write`</td><td>write</td><td>Resumes a paused sandbox</td></tr>
    <tr><td>`fork_sandbox`</td><td>`mcp:write`</td><td>write</td><td>Forks a sandbox into a new one</td></tr>
    <tr><td>`snapshot_sandbox`</td><td>`mcp:write`</td><td>write</td><td>Takes a snapshot of a sandbox. See [Pause, resume and snapshots](/docs/sandbox/pause-and-snapshots)</td></tr>
    <tr><td>`build_template`</td><td>`mcp:write`</td><td>write</td><td>Starts a build of template `name` from exactly one of `fromImage` or `fromTemplate`, with `RUN`, `ENV`, `WORKDIR` and `USER` steps and optional `startCmd`, `readyCmd`, `cpuCount`, `memoryMB` and `force`. A name your team already has gets a new build. Returns `templateId` and `buildId` at once, without waiting. See [Templates](/docs/templates)</td></tr>
    <tr><td>`run_command`</td><td>`mcp:write`</td><td>write</td><td>Runs `cmd`, with optional `cwd`, `env` and `timeoutMs`, and returns `stdout`, `stderr` and `exitCode`. Output is capped; `truncated` says when it was cut</td></tr>
    <tr><td>`read_file`</td><td>`mcp:write`</td><td>write</td><td>Reads a text file of up to 1 MiB</td></tr>
    <tr><td>`write_file`</td><td>`mcp:write`</td><td>write</td><td>Writes a text file of up to 1 MiB</td></tr>
    <tr><td>`list_files`</td><td>`mcp:write`</td><td>write</td><td>Lists a directory at `path` to a given `depth`, paged</td></tr>
    <tr><td>`kill_sandbox`</td><td>`mcp:write`</td><td>destructive</td><td>Kills a sandbox. It cannot be undone</td></tr>
  </tbody>
</table>
</div>

`read_file` and `write_file` handle text only, up to 1 MiB. A binary file
comes back as a `VALIDATION` error whose suggestion says what to do instead.

### Build a template

`build_template` returns as soon as the build starts. Poll `get_build_status`
until it is `ready`, read `get_build_logs` if it ends in `error`, then start
a sandbox from the name:

```json
{ "name": "build_template", "arguments": {
  "name": "jq-box",
  "fromTemplate": "base",
  "steps": [
    { "type": "USER", "user": "root" },
    { "type": "RUN", "command": "apt-get update && apt-get install -y jq" }
  ]
} }
```

```json
{ "name": "get_build_status", "arguments": { "templateId": "...", "buildId": "..." } }
```

```json
{ "name": "create_sandbox", "arguments": { "template": "jq-box" } }
```

A step is `{ "type": "RUN", "command", "user"? }`, `{ "type": "ENV", "key",
"value" }`, `{ "type": "WORKDIR", "path" }` or `{ "type": "USER", "user" }`.
`COPY` is not supported here, because it needs a file upload: write the file
in a `RUN` step with a heredoc, or `write_file` into a sandbox and
`snapshot_sandbox` it.

## Results

Every tool declares an `outputSchema`. A result carries `structuredContent`
that matches it, plus a one-line text summary for clients that only show text.

## Pagination

Every list tool takes `cursor` and `limit`, both optional, and returns
`items` and `nextCursor`. `limit` is 1 to 100 and defaults to 20;
`get_sandbox_logs` allows up to 1000. `nextCursor` is `null` on the last page.

```json
{ "items": [], "nextCursor": "..." }
```

A cursor is opaque: pass it back as it came. It is only valid with the same
filters it was issued for. Send it with different filters and the call fails
with `INVALID_CURSOR`; call again without a cursor to start from the first
page.

## Errors

A tool error comes back with `isError: true` and one text block holding a JSON
object with `code`, `message` and `suggestion`. It carries no
`structuredContent`, so it never fails a client's `outputSchema` check. The suggestion is written for the agent to
act on.

```json
{
  "code": "NOT_FOUND",
  "message": "...",
  "suggestion": "Check the id; list the resources first to see which exist."
}
```

<div className="docs-table-wrap">
<table>
  <thead>
    <tr><th>Code</th><th>Meaning</th></tr>
  </thead>
  <tbody>
    <tr><td>`INVALID_CURSOR`</td><td>The cursor is not valid for this call, usually because the filters changed. Start again without one</td></tr>
    <tr><td>`NOT_FOUND`</td><td>The sandbox, template or other resource does not exist for this team</td></tr>
    <tr><td>`FORBIDDEN`</td><td>The credential lacks the team or scope for this call</td></tr>
    <tr><td>`RATE_LIMITED`</td><td>The team went over its tool call limit, or the platform rate-limited the call. Wait and retry</td></tr>
    <tr><td>`UPSTREAM_ERROR`</td><td>The platform failed behind the server. Retry shortly</td></tr>
    <tr><td>`VALIDATION`</td><td>The arguments were rejected. Fix them and call again</td></tr>
  </tbody>
</table>
</div>

## Limits

Each team gets 120 tool calls per minute. A call beyond that fails with
`RATE_LIMITED`. Plan limits and credit apply as for any other call; see
[Limits and pricing](/docs/limits-and-pricing).

Every call is logged with the tool, the team, its duration and its result code.

Next: [Templates](/docs/templates).
