# MCP servers

caas.sh runs your MCP server in its own sandbox and serves it at an authenticated endpoint, `https://mcp.caa.sh/<endpoint-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 can run:

- **A stdio command** on the Default image (Alpine Linux with bash, coreutils, Node and Docker). This is the same command your MCP client runs locally, such as `npx search-mcp`. caas.sh's bridge serves it to the gateway over HTTP.
- **Code from a Git repository**, cloned into `/workspace` and started with a stdio command.
- **Your own image** from `registry.caas.sh`, speaking MCP over stdio, or over HTTP on a port.

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**, such as `npx search-mcp`. To run code from a repository, also enter its **Repository**, and its **Branch** if it isn't the default one. caas.sh clones the repository into `/workspace` and installs its `package.json` dependencies before the command runs. If the command runs your own files, such as `node server.js`, and you don't give a repository, attach a **Source archive**: a `.tar.gz` of those files.

   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. Keep **Speaks MCP over stdio** checked for a stdio server. For a server that serves MCP over HTTP, uncheck it and enter the **Port** it listens on.

3. Under **2. Configure**, you can enter a **Name**: lowercase letters, digits and hyphens, unique in the workspace. 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. Everything else is blocked. If the command starts with `npx`, add `registry.npmjs.org` so it can download the package.
5. Leave **Keep state between restarts** checked to keep `/workspace` between restarts, with nightly snapshots. A server that runs a source archive must keep state.
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.

### A stdio command

```sh
caas mcp deploy --stdio "npx -y @modelcontextprotocol/server-filesystem /workspace"
```

When the command starts with `npx`, the CLI allows `registry.npmjs.org` for you. Add every other host the server calls with `--egress-host`, once per host:

```sh
caas mcp deploy --stdio "npx search-mcp" --name search-mcp --egress-host api.search.example.com
```

To run your own code, run the deploy at the root of its Git repository and add `--from .`. The CLI uploads the files committed at `HEAD`, so it refuses if you have uncommitted changes or untracked files, and it never uploads ignored files. If the code has a `package.json`, the sandbox installs its dependencies before the command runs. [Upload your code](/docs/cli#upload-your-code) lists the other rules.

```sh
caas mcp deploy --stdio "node server.js" --from .
```

### A Git repository

```sh
caas mcp deploy --repo github.com/acme/server --ref main --command "node server.js"
```

On the first start, caas.sh clones the repository into `/workspace` and installs its `package.json` dependencies. Then it runs `--command` as a stdio server, or `npm start` if you leave `--command` out.

### 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. The CLI resolves a tag to its digest and deploys that digest.

A stdio server runs behind caas.sh's bridge, which runs on Node, so the image needs Node:

```sh
caas mcp deploy --image server:1.2.3
```

For a server that serves MCP over Streamable HTTP, give the port it listens on. The gateway sends requests to the path `/mcp` on that port.

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

The port must be from 1024 to 65535, other than 8788. `--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.

### Options

| Option | What it does |
| --- | --- |
| `--name NAME` | A name unique in the workspace: lowercase letters, digits and hyphens. |
| `--size SIZE` | `xs`, `s`, `m` (the default), `l`, `xl`, `2xl`, or `custom` with `--vcpu`, `--memory` and `--disk`. See [Sandboxes](/docs/sandboxes). |
| `--egress-host HOST` | A host the server may reach. Repeat it for each host. |
| `--idle-stop MINUTES` | Stop after this many minutes without activity, from 5 to 240. The default is 15. |
| `--wake-on-request` | Let a request start the server even after you stopped it yourself. |
| `--ephemeral` | Keep nothing in `/workspace` between restarts, and take no automatic or nightly snapshots. Can't be combined with `--from`. |

The CLI prints the new server, including:

- `id`: the server's sandbox ID, which the token commands take.
- `endpointUrl`: the endpoint.
- `clientToken`: the secret of the **Initial client** token. This is the only time it's shown.

### From a server file

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

```yaml
server: search-mcp
source:
  stdio: npx search-mcp
size: m
state:
  persistent: true
  snapshots: nightly at 02:00
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 deploy -f search-mcp.yaml
```

| Field | Value |
| --- | --- |
| `server` | Required. The server's name, unique in the workspace. |
| `source` | Exactly one of three: `stdio: COMMAND`; `repo: URL`, with optional `ref` and `command`; or `image: IMAGE`, with optional `command`, and `transport: http` with `port` for an HTTP server. |
| `size` | A size name, `xs` to `2xl`, or `"<vCPU> vCPU · <GiB> GiB · <GB> GB"`. caas.sh 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`, and `snapshots: nightly at HH:MM` or `off`. |
| `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. A `stdio` command that starts with `npx` also gets `registry.npmjs.org`. |

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 uploads 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 `--from .` for a `stdio` source with `persistent: true`, and `--wake-on-request`. The other deploy options can't be combined with `-f`.

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, and a command that runs your own files can't be uploaded. Deploy such a file with the CLI.

## Connect a client

You can find the endpoint URL in three places:

- under **Endpoint** on the server's page
- in the CLI's `endpointUrl`
- in `endpointUrl` in the API's view of the server

The endpoint ID is opaque: it doesn't reveal your workspace or the server's ID.

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/<endpoint-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/<endpoint-id> --header "Authorization: Bearer <client-token>"
```

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

```sh
curl -i https://mcp.caa.sh/<endpoint-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`.

### What the stdio bridge supports

The bridge starts one process of your command for each MCP session and ties the session to the client token that opened it. A session ends when the client closes it, or after 30 minutes without requests. Requests that use the 2026-07-28 protocol without a session go to one process per client token. Clients that share a token share that process. The bridge runs up to 32 of these processes at once.

Your command must write only JSON-RPC messages to stdout, one per line. The bridge stops a process that writes anything else, or a line over 4 MiB. Write logs to stderr instead. When the server stops running, the end of its stderr shows under **Runtime output** on its page.

The bridge answers each request with one JSON response and carries no server-sent stream. Messages your server sends on its own, outside a response, don't reach the client. The bridge doesn't support 2026 discovery, `subscriptions/listen` or multi-round-trip input requests. If your server needs these, serve MCP over HTTP from your own image.

## 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. caas.sh stores only a hash of it.

- A deploy from the console's form, or with `--stdio`, `--repo` or `--image`, 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 <sandbox-id> --name "Claude Desktop"
caas mcp tokens create <sandbox-id> --name ci-pipeline --expires-days 30 --allow-tool search.query
caas mcp tokens list <sandbox-id>
caas mcp tokens revoke <sandbox-id> <token-id>
```

`<sandbox-id>` is the server's ID. It's `id` in the deploy output, the first column of `caas sandboxes list`, and **ID** on the server's page. `--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 and expired tokens are deleted 30 days later.

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

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

The response holds the `secret` and the `token`. `expiresAt` takes a time in milliseconds since the Unix epoch. Without it, the token never expires. A `GET` to the same path lists the tokens, and a `POST` to `.../client-tokens/<token-id>/revoke` 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. They're kept for 7 days.

**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** and **Log archives** on its page, or run `caas sandboxes logs <sandbox-id>`.

## Idle stop and wake

By default, a server stops after 15 minutes without activity. You can set from 5 minutes to 4 hours, with `--idle-stop` or the server file's `idle`. 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. On a stdio server, a request for a session the bridge doesn't know gets `404`, which tells an MCP client to start a new session.

If you stop a server yourself, requests don't start it again. They get `409` until you start it, unless you deployed it with `--wake-on-request`. You can stop, start or delete a server from its row's menu in **MCP servers**, or with the CLI:

```sh
caas sandboxes stop <sandbox-id>
caas sandboxes start <sandbox-id>
caas sandboxes delete <sandbox-id>
```

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 outbound policy allows. You set these hosts when you deploy, and you can change them later under **Outbound policy** on the server's 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` in the policy itself. To call an API that needs a key, don't put the key in the server. Instead, have caas.sh add the key to the server's outbound requests. [Credentials and outbound access](/docs/credentials) shows how.

## Pricing

The MCP Gateway bills $0.005 per 1,000 requests, which is $5 per million. 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:

| Meter | Price | When |
| --- | --- | --- |
| Active vCPU | $0.15 / vCPU-hour | Only while computing |
| Memory | $0.018 / GiB-hour | While the server runs |
| Disk | $0.001 / GB-hour | While the server runs |
| Egress | $0.06 / GB | Outbound data, including the responses your server sends to clients |
| Snapshot storage | $0.025 / GB-month | For each snapshot you keep |

For example, an M server (1/2 vCPU, 4 GiB of memory, 8 GB of disk) costs $0.08 for each hour it runs: 4 × $0.018 for memory plus 8 × $0.001 for disk. On top of that it pays for the vCPU it uses: up to $0.075 for an hour of full use. 100,000 requests through the gateway add $0.50. A server that stops when idle pays nothing for memory and disk while it's stopped.

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