# MCP servers

isolance runs your MCP server in a sandbox and serves it at an authenticated endpoint, `https://mcp.caa.sh/<server-id>`. MCP clients connect with a client token. For each request, the gateway checks the token and the tools it may call, records a trace, and passes the request to your server. The server stops when it's idle and starts again on the next request.

A server serves MCP over Streamable HTTP on a port of its sandbox. It can run:

- **A command** on the Default image (Alpine Linux with bash, coreutils, Node and Docker).
- **Your own image** from `registry.caas.sh`, with its own entrypoint or a command you give.

The server's sandbox is an ordinary sandbox of your workspace: you manage its size, outbound hosts, labels, files and snapshots like any other (see [Sandboxes](/docs/sandboxes)), and it carries the label `caas.sh/mcp-server` with the server's ID, which a [connector](/docs/connectors)'s **Reaches** can name to give the server an API key without it ever holding the key. You can also add a server to a sandbox you already have.

Sandboxes run on a prepaid balance, so [add funds](/docs#add-funds) before you deploy.

## Deploy from the console

1. Open **MCP servers** and choose **Deploy server**.
2. Under **1. Choose an image**, choose **Default** or **Your image**.

   With **Default**, enter the server's **Command**, which serves MCP over HTTP on the port you give below.

   With **Your image**, enter the image's digest reference, `registry.caas.sh/<tenant-id>/my-server@sha256:…`, with the digest that `docker push` prints. **Command** is optional. It replaces the image's entrypoint and command.

   Enter the **Port** the server listens on for MCP over HTTP.

3. Under **2. Configure**, you can enter a **Name**: 1 to 63 lowercase letters, digits and hyphens, unique among the workspace's servers. Its sandbox gets the same name when it's free (see [Names](/docs/sandboxes#names)). Without one, the server is named `mcp-` and 12 hex digits. Choose a **Size**. The default is M (1/2 vCPU, 4 GiB of memory, 8 GB of disk), and a server on your own image needs M or larger.
4. In **Allowed outbound hosts**, list the hosts the server calls, one per line, or `*` for every host. Everything else is blocked, and an empty list reaches no host.
5. Leave **Keep state between restarts** checked to keep `/workspace` between restarts.
6. Choose **Deploy server**.

The server's page opens with its **Initial client** token. Copy it now, because it isn't shown again. This token can call every tool and never expires. To give each client its own token, see [Client tokens](#client-tokens).

## Deploy with the CLI

First set up the `caas` CLI with an API key, as [The CLI](/docs/cli) describes. `caas mcp create` creates a server in a new sandbox of its own.

### Your own image

Push the image to your workspace's namespace of `registry.caas.sh` first, as [Images and the registry](/docs/images) describes. `--image` takes a name with a tag or a digest, or `default` for the Default image. The CLI resolves a tag to its digest and deploys that digest.

Give the port the server listens on with `--http`. The gateway sends requests to the path `/mcp` on that port, or to `--path`.

```sh
caas mcp create --image server:1.2.3 --http 3000 --command "node http.js"
```

Without `--egress`, the server's sandbox reaches every host. To allow only the hosts it calls, pass `--egress` once per host.

The port must be from 1024 to 65535; it needn't be one of the sandbox's ports, which the gateway doesn't use. Listing it with `--port` as well also gives it its provider's own URL, where the provider's access control applies and client tokens don't guard it. `--command` replaces the image's entrypoint and command. Without it, the server runs the image's own entrypoint and command. A server on your own image needs size M or larger.

### In a sandbox you already have

`--sandbox` adds a server to an existing sandbox of the workspace, by its ID or name, which runs it itself:

```sh
caas mcp create --sandbox web --http 3000 --path /mcp --name web-tools
```

The sandbox keeps its own configuration, so the sandbox options below don't apply. A sandbox hosts at most one server. Deleting such a server leaves the sandbox.

### Options

`--name NAME` names the server: 1 to 63 lowercase letters, digits and hyphens, unique among the workspace's servers. A new sandbox takes the sandbox options of `caas sandboxes create` (see [Fields](/docs/sandboxes#fields)), such as:

| Option | What it does |
| --- | --- |
| `--size SIZE` | `xs`, `s`, `m` (the default), `l`, `xl`, `2xl`, or `custom` with `--vcpu`, `--memory` and `--disk`. |
| `--egress HOST` | A host the server may reach. Repeat it for each host. |
| `--env NAME=VALUE` | A variable for the server. |
| `--idle-stop MINUTES` | Stop after this many minutes without activity, from 5 to 240. The default is 15. |
| `--no-volumes` | Keep nothing in `/workspace` between restarts. |

The CLI prints the endpoint, the sandbox the server runs in, and the secret of each token it minted: **Initial client**, unless a server file declares its own. This is the only time a secret is shown.

### From a server file

One YAML or JSON file can declare a whole server, client tokens included:

```yaml
server: search-mcp
source:
  image: search-mcp:1
  port: 8080
size: m
state:
  persistent: true
idle: stop after 30m
tokens:
  - name: laptop-claude
    tools: [search.query, search.fetch]
  - name: ci-pipeline
    tools: [search.query]
    expires: 30d
egress: [api.search.example.com]
```

```sh
caas mcp create -f search-mcp.yaml
```

| Field | Value |
| --- | --- |
| `server` | Required. The server's name, unique in the workspace. |
| `source` | `image: IMAGE`, an optional `command`, and the `port` the server serves MCP over HTTP on. |
| `size` | A size name, `xs` to `2xl`, or `"<vCPU> vCPU · <GiB> GiB · <GB> GB"`. isolance rounds a written size up to one it can run, and the CLI prints a note when it does. |
| `state` | `persistent: true` (the default) or `false`. |
| `idle` | How long the server runs idle before it stops, such as `stop after 30m` or `stop after 2h`: 5 minutes to 4 hours. |
| `tokens` | Up to 20 client tokens. Each has a `name`, optional `tools` (every tool when left out) and an optional `expires` from `1d` to `365d` (never when left out). |
| `egress` | The hosts the server may reach: `["*"]` for every host, `[]` for none. Without it, the server reaches every host. |

A file that starts with `{` is read as JSON with the same fields. The YAML can't use tabs, anchors, block scalars or more than one document.

The CLI checks the whole file before it sends anything. Then it creates the server and its tokens in one request, and prints the endpoint and each token's secret, once. You can add sandbox options the file doesn't declare, such as `--label` and `--env`.

In the console, **Deploy from a server file** on the **Deploy a server** page takes the same file, pasted or chosen. There, an image must be a digest reference; deploy a file that names a tag with the CLI, which pins it.

## Connect a client

You can find the endpoint URL under **Endpoint** on the server's page, in the CLI's output (`caas mcp get <server>`), and as `url` in the API's view of the server. The ID in it is opaque: it doesn't reveal your workspace.

Clients connect over Streamable HTTP and send a client token as a bearer token:

```text
Authorization: Bearer cmcp_…
```

The server's page shows a configuration to add to an MCP client:

```json
{
  "mcpServers": {
    "search-mcp": {
      "url": "https://mcp.caa.sh/<server-id>",
      "headers": { "Authorization": "Bearer $CAAS_MCP_TOKEN" }
    }
  }
}
```

Put the client token in place of `$CAAS_MCP_TOKEN`, unless your client fills in environment variables there. In Claude Code, for example:

```sh
claude mcp add --transport http search-mcp https://mcp.caa.sh/<server-id> --header "Authorization: Bearer <client-token>"
```

To check the endpoint from a terminal, send an `initialize` request:

```sh
curl -i https://mcp.caa.sh/<server-id> \
  -H "Authorization: Bearer $CAAS_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
```

If the server is stopped, the first request starts it and can take a while, as [Idle stop and wake](#idle-stop-and-wake) explains.

The gateway applies these limits:

- A request body can be up to 4 MiB.
- Each endpoint takes about 1,200 requests a minute. Beyond that, the gateway answers `429`.
- The gateway waits up to 100 seconds for your server to start answering a request. After that, the client gets `504`.
- A request that sends an `Origin` header other than `https://mcp.caa.sh` gets `403`, so a web page on another site can't call the endpoint from a browser. MCP clients that aren't browsers send no `Origin`.

## Client tokens

A client token gives access to one server's endpoint and nothing else. Its secret starts with `cmcp_` and is shown only once, when the token is created. isolance stores only a hash of it.

- A deploy from the console's form, or with `--image` or `--sandbox`, creates one token, **Initial client**. It can call every tool and never expires.
- A deploy from a server file creates the tokens its `tokens` list declares instead, or **Initial client** if the list is empty or missing.

Give each client its own token. Each client's calls then show under its name, and you can revoke one client's token without affecting the others.

To create a token in the console:

1. On the server's page, under **Client tokens**, choose **New token**.
2. Enter a **Client name**. Under **Allowed tools**, list the tools it may call, one per line, or leave it empty to allow every tool.
3. Choose **Create token**, and copy the token. It expires in 90 days.

With the CLI:

```sh
caas mcp tokens create search-mcp --name "Claude Desktop"
caas mcp tokens create search-mcp --name ci-pipeline --expires-days 30 --allow-tool search.query
caas mcp tokens list search-mcp
caas mcp tokens revoke search-mcp <token-id>
```

Each command takes the server's name or ID. `--expires-days` takes 1 to 365, and the default is 90. `tokens list` shows each token's ID, name, state (active, expired or revoked), allowed tools, and counts of its calls and errors. On the server's page, the **Client tokens** list shows when each token was created and last used, and how many requests it made.

Revoking a token takes effect at once: its next request gets `401`. It doesn't stop the server or affect other tokens. To revoke a token in the console, choose **Revoke** beside it.

A server can have up to 20 tokens in force at once. Revoked tokens stay listed for 30 days.

With [the API](/docs/api), a token can expire up to a year ahead, or never:

```sh
curl https://api.caas.sh/v1/mcp-servers/search-mcp/tokens \
  -H "Authorization: Bearer <api-key>" \
  -H "Content-Type: application/json" \
  -d '{"label": "ci-pipeline", "allowedTools": ["search.query"]}'
```

The answer is the token with its `secret`. `expiresAt` takes a time in milliseconds since the Unix epoch, from a minute to a year ahead. Without it, the token never expires. A `GET` to the same path lists the tokens, and `DELETE …/tokens/<token-id>` revokes one.

## Tool grants

A token can be limited to the tools you name: up to 32 exact, case-sensitive tool names. A limited token:

- Can call only those tools with `tools/call`. A call to any other tool gets `403`, and its trace reads **Tool not granted**.
- Sees only those tools in `tools/list`.
- Can still use resources, prompts and the other methods that aren't tool calls.
- Can't use the `tasks/` methods.

A token without a list can call every tool. In a server file or the API, an empty list allows no tool calls. The gateway checks a grant before a request reaches your server or wakes it. This works the same for every kind of server, your own image included.

You can't change a token's grant. Instead, create a token with the grant you want, then revoke the old one.

## Traces and health

On a server's page, **Request traces** lists its newest requests. Each one shows:

- the time
- the tool called, or the MCP method
- the caller: the name of the client token
- the duration, highlighted at 1 second or more
- the result: **OK**, **Error** with the HTTP status, **Tool not granted**, **Timed out**, or **Blocked at gateway**

**Blocked at gateway** is a request refused for its token: it had no token, an invalid one, or an expired or revoked one. Its caller reads `no token` or `invalid token`, or names the expired or revoked token. During a burst of refused requests, only a sample of them is traced, but the blocked count includes them all. Traces record the names of methods and tools, never their arguments or results. `caas mcp traces <server>` prints the newest 100, and `caas mcp stats <server>` the last 24 hours.

**MCP servers** shows the last 24 hours across all your servers: requests, requests blocked at the gateway, and the cost of the gateway requests. For each server it shows requests, median latency, and the share of requests that failed with a `5xx` status.

A running server's status is **Healthy**, or **Slow** while its latency is up. A server is slow when both of these hold:

- Its median latency over the last hour is at least twice the median of the day before, and at least 500 ms.
- The last hour and the day before each had at least 20 requests.

The server's page then names the tool, or the method, with the most slow calls.

For what the server process itself prints, see **Runtime output** on its page, or run `caas sandboxes logs <sandbox>` with its sandbox's name.

## Idle stop and wake

By default, a server's sandbox stops after 15 minutes without activity. You can set from 5 minutes to 4 hours, with `--idle-stop`, the server file's `idle`, or `caas sandboxes update <sandbox> --idle-stop`. Each request that passes the gateway's checks and reaches your server resets the timer. A response that's still streaming keeps the server running until the response ends. While the server is stopped, it bills no vCPU, memory or disk, only the storage of its snapshots.

The next message a client sends starts the server again:

- The request waits up to a minute for the sandbox to start. If it's still starting after that, the client gets `503` with `Retry-After: 5`, and should retry.
- Requests refused for their token never start the server, and neither do calls to tools the token isn't granted.
- If the server's process exits, the next request starts it again. A server process runs for a day at most, and the next request after that starts it again too.
- After a failed start, requests get `503` with `Retry-After` for 1 minute, or for 4 minutes after a second failure in a row. After three failed starts in a row, they get `409` until you start the server yourself.

A stopped server doesn't keep processes or sessions. If you kept state, `/workspace` is restored, but clients start new MCP sessions.

If you stop a server yourself, requests don't start it again. They get `409` until you start it. You can stop, start or delete a server from its row's menu in **MCP servers**. With the CLI, stop and start its sandbox, and delete the server:

```sh
caas sandboxes stop search-mcp
caas sandboxes start search-mcp
caas mcp delete search-mcp
caas mcp delete search-mcp --keep-sandbox
```

Deleting a server ends its endpoint and tokens, and deletes the sandbox its create made, unless `--keep-sandbox`. A server added to an existing sandbox always leaves the sandbox. Deleting the sandbox deletes its server.

Servers also stop when the workspace's balance reaches $0, and stay stopped until funds arrive. See [Billing and usage](/docs/billing).

## Outbound access

A server reaches only the hosts its sandbox's outbound hosts allow. You set them when you deploy, and you can change them later with **Edit** on the server page's **Outbound policy** card, which opens its sandbox's network page. While a fresh workspace sets up, cloning a repository and installing `package.json` dependencies, the server can also reach the repository's host and `registry.npmjs.org`. A command that runs `npx` needs `registry.npmjs.org` among the hosts itself. To call an API that needs a key, don't put the key in the server. Instead, add the API as a [connector](/docs/connectors) whose **Reaches** picks the server's sandbox (`caas.sh/mcp-server=<server ID>`, or a label of your own), and isolance adds the key to the server's outbound requests.

## isolance's own servers in your sandboxes

The AI tools in your sandboxes can also call MCP servers you don't deploy: each connector's that has one, such as GitHub's or Linear's (see [What a sandbox sees](/docs/connectors#what-a-sandbox-sees)), and isolance's own, `browser` for the [Browser](/docs/browser) connector, whose tools drive a web browser signed in with the sandbox's user's logins. They take no client token: isolance adds the sandbox's access to each on its way out, so nothing in the sandbox holds a key, and they serve only your sandboxes, never clients elsewhere.

## Change a server

`caas mcp update` renames a server, or changes the port and path it serves on.

```sh
caas mcp update search-mcp --name search
caas mcp update web-tools --http 3001 --path /rpc
```

A server's port and path apply at the next request. A server's sandbox can't change. With the API, `PATCH /v1/mcp-servers/<server>` with `{ "name" }` or `{ "transport" }`.

## Use the API

Every path below is under `https://api.caas.sh/v1`, with an API key as `Authorization: Bearer <api-key>`. `<server>` is a server's ID or name.

| Request | Does |
| --- | --- |
| `POST /mcp-servers` | Creates a server: `{ "name", "sandbox" or "sandboxId", "transport", "tokens" }`. Send an `Idempotency-Key`. |
| `GET /mcp-servers` | Lists servers, newest first, with `nextCursor` |
| `GET /mcp-servers/<server>` | Reads one: its `url`, sandbox, transport, state and `stats` |
| `PATCH /mcp-servers/<server>` | Renames it or changes its transport |
| `DELETE /mcp-servers/<server>` | Deletes it, and its own sandbox unless `?keepSandbox=true` |
| `GET`, `POST /mcp-servers/<server>/tokens` | Lists or mints client tokens |
| `DELETE /mcp-servers/<server>/tokens/<token-id>` | Revokes a token |
| `GET /mcp-servers/<server>/traces` | The newest 100 request traces |
| `GET /mcp-servers/<server>/stats` | The gateway's last 24 hours |

`sandbox` is a new sandbox's create, exactly as `POST /v1/sandboxes` takes it. `transport` is `{ "http": { "port": 3000, "path": "/mcp" } }`, the port the sandbox's server listens on and the path the gateway sends requests to (`/mcp` by default).

```sh
curl https://api.caas.sh/v1/mcp-servers \
  -H "Authorization: Bearer $CAAS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "name": "search-mcp", "sandbox": { "image": "registry.caas.sh/<tenant-id>/search-mcp@sha256:…", "egress": ["api.search.example.com"] },
        "transport": { "http": { "port": 8080 } } }'
```

The answer, `201`, is the server with `tokens`, each minted token with its `secret`, once. A repeat with the same key answers the server without its tokens.

## Pricing

The MCP Gateway bills per request. That price includes the endpoint, authentication, routing, persistence and observability. A request with a valid client token counts whatever its result, including a tool call the grant denied. Requests the gateway refuses for their token, their size or the rate limit are free.

The server's sandbox bills like any sandbox. On Native:

| Meter | Price | When |
| --- | --- | --- |
| CPU | On the [pricing page](/pricing.html) | While the server runs, whether it's busy or waiting |
| Memory | On the [pricing page](/pricing.html) | While the server runs |
| Disk | On the [pricing page](/pricing.html) | While the server runs |
| Egress | On the [pricing page](/pricing.html) | Outbound data, including the responses your server sends to clients |
| Snapshot storage | On the [pricing page](/pricing.html) | For each snapshot you keep |

A server that stops when idle pays nothing for CPU, memory and disk while it's stopped. On your own Daytona, Freestyle, Maritime or E2B account, that provider bills the server's compute and egress instead, and the gateway's requests still bill here.

See [pricing](/pricing.html) for every rate, and [Billing and usage](/docs/billing) for how usage draws down the balance.
