# MCP server

Quivvo has a hosted MCP server, so coding agents like Claude Code, Cursor and Codex can create and drive cloud machines directly. It speaks Streamable HTTP at `https://tryquivvo.com/api/mcp`. You sign in through your browser, or pass a Quivvo API key.

## How do I set it up?

Add the server and sign in when your client asks. No key to paste:

```bash
claude mcp add --transport http quivvo https://tryquivvo.com/api/mcp
```

In Cursor or VS Code: [add to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=quivvo&config=eyJ1cmwiOiJodHRwczovL3RyeXF1aXZ2by5jb20vYXBpL21jcCJ9), or [install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22quivvo%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Ftryquivvo.com%2Fapi%2Fmcp%22%7D). Each opens the client with this server filled in.

In Claude Code, run `/mcp` to sign in. Your browser shows which app is asking and where it will send you back. Read and write are ticked, and deleting is off unless you tick it. The app gets a key that works only on this server, listed under [API keys](https://tryquivvo.com/app/keys) until you revoke it. It doesn't expire, and there's no refresh token: if it's revoked, the client asks you to sign in again.

For CI, or a client that can't sign in, use an API key. The CLI writes that configuration for you:

```bash
quivvo mcp init claude
quivvo mcp init cursor
quivvo mcp init codex
```

The config refers to `$QUIVVO_API_KEY` rather than holding your key, so it's safe in a repo and the key stays out of transcripts. Set that variable wherever your client starts from, then start a new session. If you've signed in with the CLI, this reads the saved key without showing it:

```bash
export QUIVVO_API_KEY="$(node -p "require('$HOME/.config/quivvo/credentials.json').api_key")"
```

Add `--project` to write a project file instead of your user configuration. `--inline-key` writes the key itself instead of a reference, and that file must stay out of git. `quivvo init` does the same for every AI tool it finds, and installs the [skill](https://tryquivvo.com/skill.md) too.

To add it by hand, Claude Code takes:

```bash
claude mcp add --transport http --scope user quivvo https://tryquivvo.com/api/mcp --header 'Authorization: Bearer ${QUIVVO_API_KEY}'
```

Cursor, in `~/.cursor/mcp.json`:

```json
{ "mcpServers": { "quivvo": { "url": "https://tryquivvo.com/api/mcp", "headers": { "Authorization": "Bearer ${env:QUIVVO_API_KEY}" } } } }
```

Codex, in `~/.codex/config.toml`. Codex gives up on a tool call after 60 seconds by default, so raise that if you'll ask `exec` to wait longer than its default:

```toml
[mcp_servers.quivvo]
url = "https://tryquivvo.com/api/mcp"
bearer_token_env_var = "QUIVVO_API_KEY"
tool_timeout_sec = 300
```

For a client that only supports stdio, `quivvo mcp start` runs a local server that forwards to the hosted one.

These setups follow each client's own documentation, and clients change. We test the server with the MCP SDK's client. If one of the above doesn't work in yours, tell us with `report_problem` or `quivvo feedback`.

## How do I call it by hand?

A client does this for you. With `curl`, `tools/list` gives each tool's argument names, such as `cmd` for `exec` and `confirm` for `delete_machine`. A request with no protocol version needs no more than the key:

```bash
curl -s https://tryquivvo.com/api/mcp -H "Authorization: Bearer $QUIVVO_API_KEY" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"exec","arguments":{"machine":"work","cmd":"uname -a"}}}'
```

`2026-07-28` wants its version in the headers and in `params._meta`, along with the client's capabilities. `Mcp-Method` and, for `tools/call`, `Mcp-Name` have to repeat what the body says, or the answer is error `-32020`:

```bash
curl -s https://tryquivvo.com/api/mcp -H "Authorization: Bearer $QUIVVO_API_KEY" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' -H 'Mcp-Name: exec' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"exec","arguments":{"machine":"work","cmd":"uname -a"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
```

## What tools does it have?

| Tool | What it does |
| --- | --- |
| `create_machine` | Creates a machine and waits until it runs. Takes `env`, `disk_gib`, `auto_suspend_minutes` and `delete_after_minutes` |
| `list_machines`, `get_machine` | Lists machines, or shows one, including how its setup script went and what its last suspend turned into |
| `exec` | Runs a shell command as root and returns its exit code and output |
| `job_status`, `job_logs`, `job_kill`, `list_jobs` | Follows, reads, stops and finds commands that are still running |
| `read_file`, `write_file` | Reads a file or lists a directory, or writes a text file up to 4 MB |
| `start_machine`, `stop_machine`, `restart_machine` | Boots, shuts down or reboots a machine |
| `suspend_machine`, `resume_machine` | Suspends, falling back to a stop when it can't keep memory, or resumes. `suspend_outcome` and `resume_outcome` say which happened |
| `fork_machine` | Copies a machine's disk into new machines. `failed` lists any that couldn't be made |
| `publish_port`, `unpublish_port`, `list_ports` | Gives a port a public HTTPS URL, or takes it away. The URL serves one port, the last published, and `replaced_port` says which it stopped serving |
| `save_snapshot`, `list_snapshots`, `delete_snapshot` | Saves, lists or deletes disk snapshots. Deleting needs the exact name to confirm |
| `delete_machine` | Deletes a machine and its disk; needs its exact name to confirm |
| `get_usage`, `get_catalog`, `whoami` | Shows cost and credit; templates, sizes, regions and prices; what this key may do and its place in the queue |
| `report_problem` | Tells our team something went wrong |

Before anyone signs in, a client can see all of these tools, and the first call to one starts sign-in. After that, an agent only sees the tools its key's scopes allow. A key from [agent sign-up](https://tryquivvo.com/docs/agents) can't manage keys or billing.

## How do long commands work?

`exec` always starts a job, so the command outlives the call, and waits for its result:

- `wait_seconds` is 50 by default, at most 280, and 0 answers once it has started. Keep it under your client's tool timeout.
- If it finishes in time, you get its result. If not, you get `job_id`, the output so far, `next_offset`, and a `next` instruction. Don't run it again.
- `timeout_seconds` kills it after that long: 3600 by default, at most 86400.
- If your client hangs up during the wait, the command is stopped with TERM, then KILL, and history marks it `cancelled`. Otherwise it couldn't be found again, and a retry would run it twice.
- Output is one merged `output` field, stdout and stderr together. `tail_bytes` returns only the end. Pass secrets in `env`: it stays out of history, and the command text doesn't.

Follow it with `job_status` (`wait_seconds` up to 60, default 0), `job_logs` (`offset`, `tail_bytes`, and `next_offset` to read on) and `job_kill` (`TERM`, `INT` or `KILL`). `list_jobs` finds one whose id you lost.

Other long calls, such as `create_machine`, start, resume, restart, stop, suspend, `save_snapshot` and `fork_machine`, answer at about 50 seconds with where things got to and a `next` field, and the work carries on.

## Which tools wake a machine?

`exec`, `write_file`, `start_machine`, `resume_machine` and `restart_machine` do. Reading never does, because a tool marked read-only shouldn't start a bill. `read_file`, `job_status`, `job_logs`, `list_jobs` and `job_kill` answer `machine_not_running` on a machine that isn't running, naming the tool to call. This differs from the REST API, where reading files and jobs wakes a machine.

If a machine is still waking, `exec` and `write_file` answer `machine_not_running` with `retry_after_seconds` and say nothing has run. When one of them did wake it, the answer has `resume_outcome`, and `warnings` if it was a cold boot.

## How big are the answers?

Default answers stay under 32,000 characters. Long command output keeps its start and end, with `[... N bytes omitted ...]` where the middle was cut. `truncated` and `more` say how to fetch the rest, and lists page with `offset`. `max_bytes` and `tail_bytes` can ask for up to 1 MiB. `read_file` returns `encoding` as `utf-8` or `base64`, with `size`, `offset` and `next_offset`.

## What do errors look like?

JSON text with `isError` set, and the same fields as the [REST API](https://tryquivvo.com/docs/errors): `code`, `message`, `retryable`, `retry_after_seconds`, `docs_url`, `owner_action` and `request_id`. Unknown arguments are refused, naming the valid ones. A workspace waiting in the queue gets [`queued`](https://tryquivvo.com/docs/errors#queued) from `create_machine` and `fork_machine`: pass `owner_action` to your owner, and don't retry.

## How does sign-in work?

It's OAuth 2.1 with PKCE (S256 only), for public clients that register themselves. A call with no valid token answers HTTP 401 with `WWW-Authenticate: Bearer resource_metadata=...`, which leads to the [protected resource metadata](https://tryquivvo.com/.well-known/oauth-protected-resource/api/mcp) and then the [authorization server metadata](https://tryquivvo.com/.well-known/oauth-authorization-server). Redirect URIs must be `https`, or `http` on `localhost`, `127.0.0.1` or `[::1]`, where any port works. `state` is required, and a token is bound to `https://tryquivvo.com/api/mcp`, so it doesn't work on the REST API.

## Which protocol versions does it speak?

Both eras of the protocol share the endpoint: `2026-07-28`, where every request carries its own version and `server/discover` needs no key, and the older `initialize` handshake from `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`. It's stateless, with JSON responses and no sessions. Its [server card](https://tryquivvo.com/api/mcp/server-card) says how to connect.
