# MCP server

`TaskCloud` provides a [Model Context Protocol](https://modelcontextprotocol.io) server. With it, an agent such as Claude Code, Claude Desktop, Cursor or Codex can create sandboxes, run commands, edit files, expose ports and drive a desktop, with no glue code.

| Mode | How | Auth |
| --- | --- | --- |
| **Local** (stdio) | `npx -y @taskcloud/mcp` or `taskcloud mcp` | env `TASKCLOUD_API_KEY` (optional `TASKCLOUD_API_URL`) |
| **Self-hosted** (Streamable HTTP) | `npx -y @taskcloud/mcp --http --port 8787`, or the Docker image | `Authorization: Bearer <api key>` on every request |
| **Hosted** (Streamable HTTP), *beta, coming soon* | `https://mcp.task.cloud/mcp` | `Authorization: Bearer <api key>` on every request |

Every mode calls the public [REST API](api-reference.md), so each tool call is subject to the scopes of the API key you use. The hosted endpoint runs the same server as the self-hosted HTTP mode; it is not live yet. During the private beta `@taskcloud/mcp` is shared with beta participants directly; publication on npm is coming soon.

## Use a restricted key

Don't give an agent your admin key. Create a key limited to sandbox work and, optionally, to sandboxes with a given label:

```bash
curl -X POST https://api.task.cloud/v1/api-keys \
  -H "Authorization: Bearer $TASKCLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "mcp-agent", "preset": "full-sandbox", "restrict": {"labels": {"owner": "mcp"}}, "expires_in_days": 30}'
```

Sandboxes created with this key automatically receive the label `owner=mcp`, and the key can only see and act on sandboxes with that label.

## Setup

### Claude Code

Local (stdio):

```bash
claude mcp add taskcloud --env TASKCLOUD_API_KEY=$TASKCLOUD_API_KEY -- npx -y @taskcloud/mcp
```

Remote (Streamable HTTP), against your own server or, once it is live, the hosted endpoint:

```bash
claude mcp add --transport http taskcloud http://127.0.0.1:8787/mcp \
  --header "Authorization: Bearer $TASKCLOUD_API_KEY"

# hosted (beta, coming soon)
claude mcp add --transport http taskcloud https://mcp.task.cloud/mcp \
  --header "Authorization: Bearer $TASKCLOUD_API_KEY"
```

### Claude Desktop / Cursor

Add the local server to the client's MCP configuration (`claude_desktop_config.json`, or `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "taskcloud": {
      "command": "npx",
      "args": ["-y", "@taskcloud/mcp"],
      "env": { "TASKCLOUD_API_KEY": "taskcloud_sk_..." }
    }
  }
}
```

Cursor can also connect to an HTTP server with a static header:

```json
{
  "mcpServers": {
    "taskcloud": {
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "Authorization": "Bearer taskcloud_sk_..." }
    }
  }
}
```

Claude Desktop's remote connectors sign in with OAuth, which the HTTP mode does not support yet (see [Authentication](#authentication)). Use the local server there for now.

### VS Code

`.vscode/mcp.json`:

```json
{
  "servers": {
    "taskcloud": {
      "type": "http",
      "url": "http://127.0.0.1:8787/mcp",
      "headers": { "Authorization": "Bearer ${input:taskcloud-key}" }
    }
  },
  "inputs": [{ "id": "taskcloud-key", "type": "promptString", "description": "TaskCloud API key", "password": true }]
}
```

### Codex and other clients

Codex (`~/.codex/config.toml`) reads the key from an environment variable:

```toml
[mcp_servers.taskcloud]
url = "http://127.0.0.1:8787/mcp"
bearer_token_env_var = "TASKCLOUD_API_KEY"
```

Any other MCP client works if it supports either stdio (run `npx -y @taskcloud/mcp` with `TASKCLOUD_API_KEY` in its environment) or Streamable HTTP with a custom `Authorization` header.

## Self-hosting the HTTP server

```bash
npx -y @taskcloud/mcp --http --port 8787                       # listens on 127.0.0.1:8787, endpoint /mcp
npx -y @taskcloud/mcp --http --host 0.0.0.0 --read-only        # all interfaces, read-only tools
```

The server has no API key of its own. Each MCP request must send `Authorization: Bearer <api key>`, and the tools of that request call the API with that key only. `TASKCLOUD_API_KEY` in the server's environment is ignored in this mode, so two users of one server never share a key. The server only checks that the header is present. The API decides whether the key is valid, so a revoked key gets tool results with `isError: true` and `401 unauthorized`.

### Docker

The image is built from the SDK sources (build context `sdks/`):

```bash
docker build -f sdks/mcp/Dockerfile -t taskcloud-mcp sdks
docker run --rm -p 8787:8787 taskcloud-mcp                     # add flags after the image name, e.g. --read-only
```

The container listens on `0.0.0.0:8787`, runs as a non-root user and has a `HEALTHCHECK` on `/healthz`. Because it binds to all interfaces, the `Host` check is off unless you pass `--allowed-hosts` (or `TASKCLOUD_MCP_ALLOWED_HOSTS`) with the host name clients use. Put it behind a reverse proxy that terminates TLS, because API keys travel in a header.

### Configuration

| Flag | Environment | Default | Meaning |
| --- | --- | --- | --- |
| `--http` | | off (stdio) | Serve MCP over Streamable HTTP |
| `--port N` | `PORT` | `8787` | Listen port (`0` picks a free port) |
| `--host H` | `HOST` | `127.0.0.1` (`0.0.0.0` in Docker) | Bind address |
| `--read-only` | | off | Only the 5 read-only tools |
| `--tools a,b` | | all 15 | Tool allowlist |
| `--max-body-bytes N` | | `1048576` | Larger request bodies get `413` |
| `--request-timeout S` | | `660` | Deadline for one MCP request, in seconds; a request over it gets `504` (`exec` waits up to 600 s) |
| `--allowed-origins a,b` | `TASKCLOUD_MCP_ALLOWED_ORIGINS` | none | Browser origins that may call `/mcp`; a request with any other `Origin` header gets `403` |
| `--allowed-hosts a,b` | `TASKCLOUD_MCP_ALLOWED_HOSTS` | `localhost`, `127.0.0.1`, `[::1]` when bound to loopback; no check otherwise | `Host` header values the server answers (DNS rebinding protection); any other `Host` gets `403`. Set it to your public host name when you bind to `0.0.0.0` (Docker) |
| | `TASKCLOUD_API_URL` | `https://api.task.cloud/v1` | API base URL |

### Endpoints and behavior

- `POST /mcp`: MCP messages (Streamable HTTP, protocol versions up to the current spec). The server is **stateless**: it issues no `Mcp-Session-Id`, every request is handled independently, and replicas behind a load balancer need no sticky sessions. Responses are plain JSON, not SSE streams.
- `GET /mcp` and `DELETE /mcp` return `405`, because a stateless server has no server-initiated stream and no session to end. Clients handle this per the spec.
- `GET /healthz`: `{"status":"ok", ...}` for liveness and readiness probes.
- Per-request narrowing: `/mcp?read_only=1` and `/mcp?tools=exec,read_file` shrink the tool list for that connection. They never widen what the server-wide `--read-only` or `--tools` allows.
- Timeouts: 15 s to receive headers, 30 s to receive the whole request, then the per-request deadline above.
- Logs: one JSON object per line on stderr, with `request_id` (taken from `X-Request-Id` or generated, and echoed in the response), status, duration, JSON-RPC methods, tool names and `key_fp`, the first 12 hex characters of the key's SHA-256. API keys, tool arguments and tool output are never logged.

### Authentication

Today the only credential is a TaskCloud API key in the `Authorization` header. MCP's OAuth 2.1 flow (protected resource metadata, browser sign-in, audience-bound tokens) is planned for the hosted endpoint. It needs an authorization server on the TaskCloud side, which does not exist yet. Until then, clients that only support OAuth for remote servers should use the local stdio server.

## Tools

| Tool | Key parameters | API endpoint |
| --- | --- | --- |
| `create_sandbox` | `template`, `size`, `name`, `labels`, `auto_pause`, `always_on`, `priority` | `POST /sandboxes` |
| `list_sandboxes` | `state`, `label`, `limit`, `cursor` | `GET /sandboxes` |
| `get_sandbox` | `sandbox_id` | `GET /sandboxes/{sandbox_id}` |
| `pause_sandbox` | `sandbox_id` | `POST /sandboxes/{sandbox_id}/pause` |
| `resume_sandbox` | `sandbox_id` | `POST /sandboxes/{sandbox_id}/resume` |
| `fork_sandbox` | `sandbox_id`, `name`, `labels`, `count` (1–50, all-or-nothing; >1 returns `{"data": [...]}`), `priority` | `POST /sandboxes/{sandbox_id}/fork` |
| `delete_sandbox` | `sandbox_id` | `DELETE /sandboxes/{sandbox_id}` |
| `exec` | `sandbox_id`, `command`, `cwd`, `timeout_seconds`, `background` | `POST /sandboxes/{sandbox_id}/exec` |
| `read_file` | `sandbox_id`, `path` | `GET /sandboxes/{sandbox_id}/files/content` |
| `write_file` | `sandbox_id`, `path`, `content` | `PUT /sandboxes/{sandbox_id}/files/content` |
| `list_files` | `sandbox_id`, `path`, `recursive` | `GET /sandboxes/{sandbox_id}/files` |
| `expose_port` | `sandbox_id`, `port`, `visibility` | `POST /sandboxes/{sandbox_id}/ports` |
| `desktop_stream_url` | `sandbox_id`, `ttl_seconds`, `read_only` | `POST /sandboxes/{sandbox_id}/desktop` |
| `screenshot` | `sandbox_id`, `scale`, `region` | `GET /sandboxes/{sandbox_id}/screenshot` (returned as an MCP image) |
| `computer_input` | `sandbox_id`, `actions` (same shape as [input actions](guides/desktop-and-computer-use.md#input)) | `POST /sandboxes/{sandbox_id}/input` |

Behavior notes:

- **Truncated output.** `exec` in sync mode returns `exit_code`, `stdout` and `stderr`. Output larger than what fits in a tool result is truncated, and the tool says where the full log can be read (`GET .../commands/{command_id}/stream`).
- **Text files only.** `read_file` and `write_file` handle text. For binary files, use the REST API or signed URLs.
- **Wake on request.** Tools that need a running VM wake a paused sandbox automatically.

## Limiting what the agent can do

The server accepts flags that remove tools from the list the agent sees:

```bash
npx -y @taskcloud/mcp --read-only                      # only list_sandboxes, get_sandbox, read_file, list_files, screenshot
npx -y @taskcloud/mcp --tools create_sandbox,exec,read_file,write_file,list_files,expose_port
```

For example, `--tools` without `delete_sandbox` stops the agent from deleting sandboxes. `--read-only` and `--tools` can be combined. API key scopes still apply.

The same flags work with `--http`. Over HTTP a client can also narrow its own tool list with `?read_only=1` or `?tools=a,b` in the endpoint URL, for example `https://mcp.task.cloud/mcp?tools=exec,read_file` once the hosted endpoint is live.

## Docs for agents

- A concise index of this documentation for LLMs: [`https://task.cloud/docs/llms.txt`](llms.txt)
- The OpenAPI spec for generating clients: see [API reference](api-reference.md)
