# 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")

Verification status is recorded after each host is exercised against staging.

| Host                         | URL-only (OAuth) | API-key header                               | Verified                                   |
| ---------------------------- | ---------------- | -------------------------------------------- | ------------------------------------------ |
| Claude Code                  | yes              | yes                                          | API-key header, 2 Sep 2026 (OAuth not yet) |
| claude.ai and Claude Desktop | yes              | no (connector settings have no header field) | not yet                                    |
| Cursor                       | yes              | yes                                          | not yet                                    |
| ChatGPT                      | yes              | no                                           | not yet                                    |
| Codex                        | yes              | yes                                          | not yet                                    |
| VS Code                      | yes              | yes                                          | not yet                                    |

### 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>]`                               | Submit a job.                                                                                                                                                                                    | `hardware?`, `gpu?`, `provider?`, `regions?`, `walltime_seconds?`, `capacity?`, `project`, `script?`, `files?`, `workspace?`, `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 (about one hour).                                                                                                         | `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`, cleaning up empty datasets and repointing latest to the newest survivor.                                                           | `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 hosts that can run a shell but not the CLI.                                                                                                                                                  | `target`, `files`, `script?`, `python_project_dir?`, `docker_image?`, `docker_requires_accelerator?`, `output?`                                                                                                                           |

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

Workspace file selection happens on the client host. The hosted MCP server cannot read your local `.c3ignore`, `.gitignore`, or `.c3 ignore_file` setting. Apply those rules **before** sending file contents or digests to a tool. If the host has the CLI, `c3 deploy --list-files --json` returns the selected paths locally without uploading or authenticating. Otherwise, explicitly select the files needed by the job. The built-in workspace exclusions still apply to submitted lists. See [Workspace upload exclusions](https://docs.cthree.cloud/submission.md#workspace-upload-exclusions) for comment syntax, imports, and `.env` examples.

The CLI automatically removes `api_key` from uploaded `.c3` copies. Hosted MCP uploads do not re-encode config files. Inline workspace `.c3` files must be valid YAML without a top-level `api_key` (including keys supplied by YAML merges); otherwise the tool rejects the request before storage calls. Digest-only `prepare_upload` cannot inspect or redact file contents. Omit `.c3` files from MCP workspaces when they are unnecessary, or prepare credential-free copies before hashing or sending them. Hash and upload the same prepared bytes; `--list-files` only lists paths and does not produce redacted copies.

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#./}"' _ {} \;
```

The agent calls `prepare_upload` with `target: /projects/<project>/workspace/`, the digests and `script: run.sh`. The result has presigned PUT URLs for only the files storage does not already hold, the manifest, and a `shell` field that is a complete `bash` script of `curl` PUTs (no credential in it). The agent runs that script, then calls `deploy` with `workspace: /projects/<project>/workspace/@<hash>`. Lock files, large sources and anything up to 5 GiB per file go this way. Datasets use the same flow with `target: /datasets/<name>/`.

### 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                                        |
| Files per `prepare_upload` call | 2000, 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 runs the `find`/`sha256sum` one-liner above, calls `prepare_upload`, runs the returned script with `bash`, and calls `deploy` with `workspace: /projects/matmul/workspace/@<hash>`. Only files not already in storage are uploaded, and the lock file travels as bytes over HTTPS, never through the model.

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