@descanto/mcp
MCP (Model Context Protocol) stdio server that gives AI clients — Claude Code, Claude Desktop, Cursor, and any other MCP-compatible tool — hands-on control of Canto cloud desktops: create, wake, run commands, read/write files, fork, and stream, all as MCP tools.
Ships as a single self-contained binary (@descanto/sdk is bundled in), so
there's nothing to install — point your client at npx -y @descanto/mcp.
Full docs: https://docs.descanto.com/docs/canto
Configuration
The server needs one environment variable:
| Env var | Required | Description |
|---|---|---|
CANTO_API_KEY |
yes | canto_sk_... API key (or AuthKit JWT). The server exits immediately with a clear message if unset. |
CANTO_BASE_URL |
no | Canto API base URL. Defaults to the hosted API (https://api.descanto.com); set it only to point at a different endpoint (e.g. http://127.0.0.1:8081 for a local controld). Older published versions defaulted to localhost, so the examples below set it explicitly — safe to keep either way. |
Claude Code
claude mcp add canto --env CANTO_API_KEY=canto_sk_... --env CANTO_BASE_URL=https://api.descanto.com -- npx -y @descanto/mcp
Claude Desktop / Cursor / Antigravity
These use the same mcpServers shape — add it to claude_desktop_config.json
(Claude Desktop), .cursor/mcp.json (Cursor), or ~/.gemini/config/mcp_config.json
(Antigravity, via … → MCP Servers → View raw config):
{
"mcpServers": {
"canto": {
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_...",
"CANTO_BASE_URL": "https://api.descanto.com"
}
}
}
}
Codex
codex mcp add canto --env CANTO_API_KEY=canto_sk_... --env CANTO_BASE_URL=https://api.descanto.com -- npx -y @descanto/mcp
or add it to ~/.codex/config.toml by hand:
[mcp_servers.canto]
command = "npx"
args = ["-y", "@descanto/mcp"]
env = { CANTO_API_KEY = "canto_sk_...", CANTO_BASE_URL = "https://api.descanto.com" }
GitHub Copilot (VS Code)
Add to .vscode/mcp.json — Copilot uses the top-level servers key and requires
"type": "stdio":
{
"servers": {
"canto": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_...",
"CANTO_BASE_URL": "https://api.descanto.com"
}
}
}
}
OpenCode
Add to opencode.json (project) or ~/.config/opencode/opencode.json (global) —
OpenCode nests servers under mcp, takes command as an array, and uses
environment rather than env:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"canto": {
"type": "local",
"command": ["npx", "-y", "@descanto/mcp"],
"enabled": true,
"environment": {
"CANTO_API_KEY": "canto_sk_...",
"CANTO_BASE_URL": "https://api.descanto.com"
}
}
}
}
Tools
| Tool | Description |
|---|---|
list_desktops |
List desktops, optionally filtered by state. Returns {id, state, tier, billingMode, hostId} per desktop. |
create_desktop |
Provision a desktop: tier (small/default/large), optional billing_mode (monthly/hourly), idle_timeout_secs, env (guest env vars → /root/.canto/env on first wake), setup_script (runs once on first wake). |
wake_desktop |
Wake a hibernated desktop (desktop_id, wait default true). Typically ~1–10s. |
hibernate_desktop |
Hibernate an awake desktop to pause billing (desktop_id, wait default true). |
destroy_desktop |
Permanently destroy a desktop and its disk (desktop_id). Always waits; irreversible. |
fork_desktop |
Clone a Hibernated source into count (1–20) new, also-Hibernated desktops (desktop_id, count, optional ephemeral, acknowledge_shared_state: must be true). Returns {desktopIds}. |
set_idle_timeout |
Update how long an awake desktop sits idle before auto-hibernating (desktop_id, idle_timeout_secs; 0 disables). |
exec |
Run a shell command on an awake desktop (desktop_id, command, optional timeout_secs). Pass detached: true to spawn a long-running command and get {processId} back immediately. |
get_process |
Poll a detached process (desktop_id, process_id, optional tail_bytes). Returns {processId, status, exitCode, stdoutTail, stderrTail}; status is running/exited/lost. |
read_file |
Read a file (desktop_id, path). Text if valid UTF-8 under 50KB, else base64 (capped) or a size error. |
write_file |
Write a file (desktop_id, path, content or content_base64). Base64 is strictly validated before decoding. |
get_stream_url |
Get a short-lived signed stream URL (desktop_id, claim: view/control, default view). |
expose_port |
Publish a guest TCP port at a stable URL (desktop_id, port, optional public). Desktop must be awake. Returns {url, token?, port, public} — the canto_pt_... token is shown once and grants access to whatever the guest serves there. |
list_ports |
List a desktop's port exposures (desktop_id). Returns {ports: [...]} with status bound/unbound; never includes tokens. |
stop_port |
Stop an exposure by guest port number (desktop_id, port). The URL 404s afterwards. |
usage |
Awake-time usage and projected cost, per desktop and org-wide (start_ms/end_ms, default last 30 days). |
screenshot |
Capture the desktop's display as a PNG image block (desktop_id, optional region {x,y,width,height}, scale_percent 1–100). Results over 2MB are rejected with a hint to pass scale_percent — always scale down on large displays. |
click |
Click (desktop_id, optional x/y to move first, button left/right/middle, double). Returns the final {cursor} — as does every input tool below; there is no separate cursor-read tool. |
move_mouse |
Move the cursor to absolute coordinates (desktop_id, x, y). |
drag |
Drag from from_x/from_y (default: current cursor) to to_x/to_y (desktop_id, optional button). |
scroll |
Scroll the wheel at the current cursor position (desktop_id, direction up/down/left/right, amount 1–50). |
type_text |
Type into the focused element (desktop_id, text ≤ 8KB) — click the target first. |
press_key |
Press a key combo (desktop_id, combo: +-joined X keysyms, e.g. ctrl+shift+t, Return). |
set_resolution |
Resize the display (desktop_id, width 640–2560, height 480–1600). Returns the size actually applied; a 501 error means this desktop's image can't resize at runtime — use screenshot's scale_percent instead. |
Computer-use tools require an awake desktop (wake_desktop first). Input is
never gated by noVNC control sessions — a human viewer and these tools can
interleave. Input calls are not transactional: an error mid-action-batch means
earlier effects already applied, so re-screenshot before retrying rather than
blindly re-sending clicks.
Context protection
exec stdout/stderr and get_process tails are each truncated at 50KB in the
tool result (never in the underlying API call), UTF-8-boundary-safe, with a
[truncated by canto-mcp] marker — this protects the calling model's context
window. For larger output, redirect to a file and fetch it with read_file, or
re-run with head/tail. read_file returns text only when it's valid UTF-8 and
small enough; otherwise base64, or a size error asking you to use exec.
Errors
A failed Canto API call never crashes the server. Every tool catches the error and
returns an MCP tool-error (isError: true) whose message includes the API's HTTP
status, title, and detail (and operation_id when relevant), so the model sees
exactly what happened and can react.
License
Elastic License 2.0. Free to use, copy, modify, and self-host — you just can't offer it to third parties as a hosted or managed service, or remove the license notices.