# MCP server

C3 hosts a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server so coding agents can use C3 directly. An agent connected to it can list hardware and prices, deploy a job to a chosen GPU class or provider, wait for it, read its logs, and collect its results. Nothing needs to be installed on the machine running the agent.

The MCP server is a third thin client next to the CLI and the web dashboard. Every tool mirrors one `c3` command, calls the same API the CLI calls with the same credential, and returns `cli_equivalent`, the exact `c3` invocation that reproduces what it did. If you can do it with `c3`, an agent can do it through MCP; if `c3` refuses, so does the tool, with the same error.

## Endpoint[​](#endpoint "Direct link to Endpoint")

| Environment     | URL                                 |
| --------------- | ----------------------------------- |
| Production      | `https://api.cthree.cloud/mcp`      |
| Staging preview | `https://test.api.cthree.cloud/mcp` |

The setup examples below use the production URL; substitute the staging URL to try a change before it reaches production. The transport is Streamable HTTP. Every tool call is a self-contained request; there are no sessions to keep alive.

## Two ways to authenticate[​](#two-ways-to-authenticate "Direct link to Two ways to authenticate")

**URL only (OAuth).** Give your host just the URL. On first use it receives a login challenge, discovers C3's Auth0 tenant from the endpoint's metadata, registers itself, and opens a browser login. Tokens refresh automatically. Use this on your own machine.

**API key (header).** Create a key with `c3 apikey create <name>` or on the [dashboard settings page](https://cthree.cloud/dashboard/settings) and send it as `Authorization: Bearer c3_key_...`. No browser is involved. Use this for headless agents, CI, or hosts that cannot run a browser. A configured key never triggers a login; a revoked key returns a `403` so you see the problem instead of a login prompt.

Both paths give the agent exactly the access the same credential has in the CLI, including the verified-email requirement and any account restriction.

## Host setup[​](#host-setup "Direct link to Host setup")

| Host                         | URL-only (OAuth) | API-key header |
| ---------------------------- | ---------------- | -------------- |
| Claude Code                  | yes              | yes            |
| claude.ai and Claude Desktop | yes              | no             |
| Cursor                       | yes              | yes            |
| ChatGPT                      | yes              | no             |
| Codex                        | yes              | yes            |
| VS Code                      | yes              | yes            |

These are host authentication capabilities, not a verified C3 support matrix. Only Claude Code with an API-key header has been exercised against staging (2 September 2026); OAuth and the other hosts remain unverified.

### Claude Code[​](#claude-code "Direct link to Claude Code")

URL only:

```
claude mcp add --transport http c3 https://api.cthree.cloud/mcp
# then, inside a session, run /mcp and choose "Authenticate" for c3
```

API key:

```
claude mcp add --transport http c3 https://api.cthree.cloud/mcp \
  --header "Authorization: Bearer c3_key_..."
```

Headless use works with the API-key form, for example `claude -p "List the hardware C3 offers and deploy run.sh on an l40"`.

### claude.ai and Claude Desktop[​](#claudeai-and-claude-desktop "Direct link to claude.ai and Claude Desktop")

Open **Settings → Connectors → Add custom connector**, enter `https://api.cthree.cloud/mcp`, and complete the login when prompted. Connectors use OAuth only.

### Cursor[​](#cursor "Direct link to Cursor")

Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global). Omit `headers` to use OAuth instead of a key.

```
{
  "mcpServers": {
    "c3": {
      "url": "https://api.cthree.cloud/mcp",
      "headers": { "Authorization": "Bearer c3_key_..." }
    }
  }
}
```

### ChatGPT[​](#chatgpt "Direct link to ChatGPT")

Enable developer mode under **Settings → Connectors**, choose **Create**, and enter `https://api.cthree.cloud/mcp`. ChatGPT connectors use OAuth only.

### Codex[​](#codex "Direct link to Codex")

Add to `~/.codex/config.toml`. For OAuth omit `bearer_token_env_var` and run `codex mcp login c3`.

```
[mcp_servers.c3]
url = "https://api.cthree.cloud/mcp"
bearer_token_env_var = "C3_API_KEY"
```

Set `C3_API_KEY` in the environment Codex runs in.

### VS Code[​](#vs-code "Direct link to VS Code")

Add to `.vscode/mcp.json`. Delete the `headers` block to use OAuth.

```
{
  "inputs": [
    { "id": "c3-key", "type": "promptString", "description": "C3 API key", "password": true }
  ],
  "servers": {
    "c3": {
      "type": "http",
      "url": "https://api.cthree.cloud/mcp",
      "headers": { "Authorization": "Bearer ${input:c3-key}" }
    }
  }
}
```

## Tools[​](#tools "Direct link to Tools")

Each tool description begins with the `c3` command it mirrors. Results always include `cli_equivalent`. Money-moving commands (`c3 topup`, `c3 upgrade`), API-key management, and operator commands are deliberately not exposed; when a job needs credits the error names `c3 topup` for the user to run.

| Tool              | Mirrors                                                                       | What it does                                                                                                                                                                                     | Inputs (`?` = optional)                                                                                                                                                                                                                                                            |
| ----------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `whoami`          | `c3 whoami --json`                                                            | Show the identity behind the configured credential: user id, email, organisation, permissions, admin flag and C3 access status.                                                                  | none                                                                                                                                                                                                                                                                               |
| `list_hardware`   | `c3 list --json [--class <class>] [--provider <id>] [--all]`                  | List the hardware classes C3 can run jobs on with C3-billed prices and a coarse live availability signal.                                                                                        | `class?`, `provider?`, `all?`                                                                                                                                                                                                                                                      |
| `preview_route`   | `c3 deploy --dry-run --json [-p <provider>]`                                  | Show how a deploy with these selectors would route (provider, region, hardware profile, hourly rate, availability) without creating a job or reserving credit.                                   | `hardware?`, `gpu?`, `provider?`, `regions?`, `walltime_seconds?`, `capacity?`, `project?`                                                                                                                                                                                         |
| `balance`         | `c3 balance`                                                                  | Show the credit balance, subscription plan and storage usage for the account.                                                                                                                    | none                                                                                                                                                                                                                                                                               |
| `list_jobs`       | `c3 squeue --json [-n <history>]`                                             | List jobs the way `c3 squeue` does: every active job (PENDING, SCHEDULING, RUNNING) followed by the most recent terminal jobs (`history` of them, default 10).                                   | `status?`, `history?`, `limit?`, `offset?`, `hardware_profile?`                                                                                                                                                                                                                    |
| `get_job`         | `c3 squeue <job-id> --json`                                                   | Show one job in full: status, selected route (provider, region, hardware profile, rate), capacity policy, failure diagnosis and the latest attempt.                                              | `job_id`                                                                                                                                                                                                                                                                           |
| `job_logs`        | `c3 logs <job-id> [-v]`                                                       | Return one page of log lines for the job's latest attempt with a cursor for the next page.                                                                                                       | `job_id`, `cursor?`, `limit?`, `all_streams?`                                                                                                                                                                                                                                      |
| `wait_for_job`    | `c3 logs <job-id> -f`                                                         | Poll a job every 2 seconds until it reaches a terminal state (SUCCEEDED, FAILED, CANCELED, TIMED\_OUT) or the timeout elapses (default 30s, maximum 50s).                                        | `job_id`, `timeout_seconds?`                                                                                                                                                                                                                                                       |
| `cancel_job`      | `c3 cancel <job-id>`                                                          | Request cancellation of a job.                                                                                                                                                                   | `job_id`                                                                                                                                                                                                                                                                           |
| `deploy`          | `c3 deploy [script] --json [-p <provider>] [--storage <mode>]`                | Submit a job.                                                                                                                                                                                    | `storage?`, `hardware?`, `gpu?`, `provider?`, `regions?`, `walltime_seconds?`, `capacity?`, `project`, `script?`, `files?`, `workspace?`, `workspace_publication_id?`, `job_name?`, `datasets?`, `output?`, `python_project_dir?`, `docker_image?`, `docker_requires_accelerator?` |
| `list_artifacts`  | `c3 pull <job-id> --json`                                                     | List the files a job produced with sizes and short-lived download URLs.                                                                                                                          | `job_id`                                                                                                                                                                                                                                                                           |
| `read_artifact`   | `c3 pull <job-id>`                                                            | Return one artifact's content inline (text, or base64 for binary) when it is at most 1 MiB; larger files return the size and a download URL only.                                                | `job_id`, `path`                                                                                                                                                                                                                                                                   |
| `data_ls`         | `c3 data ls <path>`                                                           | List datasets, a dataset's versions, the files in a version, uploaded workspace versions, or a job's artifacts.                                                                                  | `path`                                                                                                                                                                                                                                                                             |
| `data_du`         | `c3 data du <path>`                                                           | Report storage used by datasets: total bytes across versions, unique bytes after content-addressed dedup, file and version counts.                                                               | `path?`                                                                                                                                                                                                                                                                            |
| `data_rm`         | `c3 data rm [-r] [-n] -f <path>`                                              | Remove a dataset version or a whole dataset with `recursive`, repointing latest to a surviving version when needed.                                                                              | `path`, `recursive?`, `dry_run?`                                                                                                                                                                                                                                                   |
| `data_upload`     | `c3 data cp <local-dir> /datasets/<name>/`                                    | Create a new version of a dataset from inline files (at most 20 MiB decoded and 500 files; for anything larger, hosts with a shell use prepare\_upload and hosts with the CLI use `c3 data cp`). | `dataset`, `files`                                                                                                                                                                                                                                                                 |
| `prepare_upload`  | `c3 data cp <local-dir> <dest> (upload phase; c3 deploy for a workspace)`     | For shell-capable hosts without the CLI.                                                                                                                                                         | `storage?`, `target`, `transfer?`, `files?`, `script?`, `python_project_dir?`, `docker_image?`, `docker_requires_accelerator?`, `output?`                                                                                                                                          |
| `complete_upload` | `c3 data cp <local-dir> <dest> (completion phase; c3 deploy for a workspace)` | Finish or check a retained owned upload prepared by prepare\_upload.                                                                                                                             | `target`, `transfer`, `parts?`                                                                                                                                                                                                                                                     |
| `ar_deploy`       | `c3 ar deploy <script> --json [-p <provider>]`                                | Start an autoresearch run from an experiment folder: `script` (run.py, which calls `autoresearch.run(...)` on the C3 research controller) plus the program being improved and its evaluator.     | `script`, `workspace?`, `files?`, `project?`, `name?`, `hardware_profile?`, `provider?`, `limits?`, `datasets?`, `python_project_dir?`, `docker_image?`, `docker_requires_accelerator?`                                                                                            |
| `ar_list_runs`    | `c3 ar squeue --json [-n N]`                                                  | List the authenticated organisation's research runs, newest first, with cursor pagination.                                                                                                       | `limit?`, `cursor?`                                                                                                                                                                                                                                                                |
| `ar_get_run`      | `c3 ar squeue <run-id> --json`                                                | Show one autoresearch run in full (status, engine, script, project, limits, usage, result, error) together with its progress steps and a count of steps by status.                               | `run_id`                                                                                                                                                                                                                                                                           |
| `ar_cancel_run`   | `c3 ar cancel <run-id>`                                                       | Cancel an autoresearch run: queued evaluations are dropped and running evaluation jobs are stopped.                                                                                              | `run_id`                                                                                                                                                                                                                                                                           |

Two read-only resources are also served in-band: `c3://docs/mcp` (this page) and `c3://docs/cli-reference` (the reference printed by `c3 docs`).

## Workspaces are data[​](#workspaces-are-data "Direct link to Workspaces are data")

File selection happens on the host. The server cannot read your `.c3ignore` or `.gitignore`, so apply them before sending contents or digests. With the CLI installed, `c3 deploy --list-files --json` returns the selected paths locally. The built-in exclusions still apply. See [What gets uploaded](https://docs.cthree.cloud/configuration.md#what-gets-uploaded).

The owned shell helper prepares private `.c3` copies using the CLI’s typed configuration rules, removes `api_key`, and hashes/uploads only the prepared bytes. Your originals remain unchanged. This step requires local PyYAML 6.0.3; install it explicitly in your Python environment before preparation. The helper never installs dependencies or sends configuration values through MCP. Missing YAML support or ambiguous configuration stops before uploading. Inline uploads reject `.c3` files containing `api_key`; legacy digest uploads still require you to prepare credential-free copies or omit them.

A workspace is the code and configuration a job needs: the submission script, a `.c3`, a `pyproject.toml`, your source files. C3 stores it the way it stores a dataset, as content-addressed blobs plus a manifest, and `c3 deploy` is that upload followed by a job that points at the manifest hash. Data belongs in datasets (mounted with `datasets:`), environments are rebuilt from `pyproject.toml` or a Docker image, and caches are excluded. Uploaded workspace versions are visible with `data_ls /projects/<project>/workspace/`.

There are two ways to get a workspace to C3 from an agent, chosen by what the host can do:

### Hosts with a shell (Claude Code, Cursor, Codex, VS Code)[​](#hosts-with-a-shell-claude-code-cursor-codex-vs-code "Direct link to Hosts with a shell (Claude Code, Cursor, Codex, VS Code)")

Never paste file contents into a tool call. Hash the files, send the digests, run the returned script, deploy the returned version:

```
# 1. digests only (paths, sha256, size); the model never sees the bytes
find . -type f -not -path './.git/*' -exec sh -c 'printf "%s %s %s\n" "$(sha256sum "$1" | cut -d" " -f1)" "$(stat -c %s "$1")" "${1#./}"' _ {} \;
```

Call `prepare_upload` with `target: /projects/<project>/workspace/`, `script: run.sh` and file digests. Datasets use `target: /datasets/<name>/`. The server selects the configured storage protocol.

When owned storage is enabled, the result contains `helper_script`, a Python 3 helper, and a `preparation` object. Workspace `.c3` preparation requires PyYAML 6.0.3 in that Python environment. Save the result privately as `preparation.json`, save the helper as `c3-owned-upload.py`, then run:

```
python3 c3-owned-upload.py build --plan preparation.json --state .c3-owned-upload.json
# For a large manifest, keep every {path, sha256, size_bytes, mode?} locally,
# one JSON object per line, and add: --digests all-digests.jsonl
```

The helper prints the next MCP call. Execute it and save each returned part window as `window.json`, then run `python3 c3-owned-upload.py upload --state .c3-owned-upload.json --plan window.json`. Repeat the emitted calls. The helper streams file ranges to presigned URLs, records each acknowledgement privately, and submits the original manifest after its files. API authentication stays in MCP; the helper takes no API, publisher or machine credential.

`complete_upload` submits the exact acknowledgements and checks platform verification. Keep the same transfer identity while the state is `publication_pending` or `manifest_pending`; neither state means the workspace is ready. Save a `published` file result or `committed` manifest result as `result.json`, then run `python3 c3-owned-upload.py record --state .c3-owned-upload.json --plan result.json`. Deploy only the final returned `workspace` together with `workspace_publication_id`. Dataset mounts can retain the exact `publication_id` alongside their reference.

After an interrupted PUT, repeat `prepare_upload` with the retained transfer to reconcile native part acknowledgements. The helper refuses blind replay of an uncertain part. For an expired or explicitly retried window, `python3 c3-owned-upload.py renew --state .c3-owned-upload.json` emits a new bounded window request for the same upload. Preserve the journal and original file bytes until publication completes. Configured server object, manifest and lifetime policies still apply; multipart transport does not raise release limits.

If the initial server capability selects legacy storage, the result instead contains the existing `shell` curl script. Run it before deploying the returned workspace reference. Legacy preparation supports at most 2000 files and 5 GiB per file. An owned transfer never falls back to this flow.

### Hosts without a shell (claude.ai, ChatGPT)[​](#hosts-without-a-shell-claudeai-chatgpt "Direct link to Hosts without a shell (claude.ai, ChatGPT)")

These hosts only have the small scripts the agent wrote, so `deploy` and `data_upload` accept the files inline: a list of `{ path, content }` entries, `encoding: "base64"` for binary files, `executable: true` for scripts. The standard exclusions apply. The limit is what a Worker request can hold:

| Limit                                        | Value                                                                                            |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Decoded inline payload                       | 20 MiB                                                                                           |
| Inline files                                 | 500                                                                                              |
| Owned `prepare_upload` digest transport page | 2000 entries / 1 MiB; larger manifests use local JSONL, with total policy enforced by the server |
| Owned part window                            | At most 25 signed parts per call                                                                 |
| Legacy `prepare_upload`                      | 2000 files, up to 5 GiB each                                                                     |
| Inline artifact read                         | 1 MiB (larger files return a download URL)                                                       |

Whichever path is used, the workspace manifest hash is identical to what `c3 deploy` produces for the same prepared file bytes and metadata, including credential-free `.c3` copies.

## Worked examples[​](#worked-examples "Direct link to Worked examples")

### Run a script on an H100 on a named provider[​](#run-a-script-on-an-h100-on-a-named-provider "Direct link to Run a script on an H100 on a named provider")

Ask the agent to preview first, then deploy:

> Preview an h100 route on nebius, then deploy `train.py` from this directory with `run.sh` as the script, project `matmul`, and wait for it.

The agent calls `preview_route` with `hardware: "h100", provider: "nebius"`, then `deploy` with the files, `script: "run.sh"`, `project: "matmul"`, the same selectors, and finally `wait_for_job`. The deploy result carries the CLI form, for example:

```
cli_equivalent: c3 deploy run.sh --json -p nebius
c3_config:
  project: matmul
  script: run.sh
  hardware: h100
  provider: nebius
```

### Deploy a real repository from Claude Code without the CLI[​](#deploy-a-real-repository-from-claude-code-without-the-cli "Direct link to Deploy a real repository from Claude Code without the CLI")

> Hash this directory, prepare the upload for project `matmul`, run the upload script, then deploy the returned workspace on an h100 and wait for it.

The agent hashes the selected files, calls `prepare_upload`, and follows the returned protocol through publication. For owned storage, it retains the helper journal and uses `complete_upload`, then passes both `workspace` and `workspace_publication_id` to `deploy`. The lock file travels as bytes over HTTPS, never through the model; an existing hash alone never proves ownership.

### Follow a job and read its results[​](#follow-a-job-and-read-its-results "Direct link to Follow a job and read its results")

`wait_for_job` returns after at most 50 seconds (under the tool timeout hosts enforce) with `terminal: false` if the job is still running; the agent calls it again. `job_logs` returns a page of lines and a cursor; passing the cursor back returns only new lines. When the job succeeds, `list_artifacts` lists the output files with download URLs and `read_artifact` returns a small result file (a metrics JSON, say) inline.

### Upload a dataset and mount it[​](#upload-a-dataset-and-mount-it "Direct link to Upload a dataset and mount it")

`data_upload` with `dataset: "my-data"` and a few files creates a new version. A later `deploy` mounts it with `datasets: [{ ref: "/datasets/my-data", mount: "/data" }]`, exactly as `datasets:` in `.c3` would.

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

| Symptom                                       | Meaning                                                             | Fix                                                                                               |
| --------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Host opens a browser login you did not expect | No credential was sent. This is the OAuth path working as designed. | Complete the login, or configure an API key header if you want no browser.                        |
| `403 Invalid or revoked API key`              | The `c3_key_` header is wrong or the key was revoked.               | Create a new key with `c3 apikey create <name>` and update the host.                              |
| `EMAIL_VERIFICATION_REQUIRED`                 | The account's email is not verified.                                | Run `c3 verify-email`, click the link, retry.                                                     |
| `INSUFFICIENT_CREDITS`                        | The balance cannot cover the job's reservation.                     | Run `c3 topup <amount>` or top up on the dashboard. The agent cannot do this for you.             |
| `GPU_OUT_OF_STOCK` or `PROVIDER_UNAVAILABLE`  | The requested class or pinned provider has no capacity.             | Call `list_hardware`, choose another class or provider, or set `capacity.on_unavailable: "wait"`. |
| `CONCURRENCY_LIMIT`                           | Your plan's concurrent job limit is reached.                        | Wait, `cancel_job`, or `c3 upgrade`.                                                              |
| `429 RATE_LIMITED`                            | More than 120 tool calls in a minute from one identity.             | Slow the loop; retry after the `Retry-After` interval.                                            |
| `WORKSPACE_TOO_LARGE`                         | Inline payload over 20 MiB or 500 files.                            | With a shell, use `prepare_upload`; with the CLI, run `c3 deploy` from the project directory.     |

Tokens issued by the OAuth login expire; hosts holding a refresh token renew silently. If a host shows `401 invalid_token` and does not recover, remove and re-add the server to trigger a fresh login.
