MCP server
Drive Descanto desktops from Claude Code, Cursor, Codex, Copilot, Antigravity, OpenCode, and any MCP client.
@descanto/mcp is an MCP (Model Context Protocol) stdio server exposing Descanto
cloud desktops to MCP-compatible AI clients -- Claude Code, Claude Desktop,
Cursor, Codex, GitHub Copilot, Antigravity, OpenCode, and any other MCP client.
It's a thin wrapper over
@descanto/sdk: one process, one org's worth of
desktops, driven entirely through the tools below.
No install needed
The server ships as a self-contained binary on npm (@descanto/sdk is bundled
in), so point your client at npx -y @descanto/mcp -- npm fetches and runs the
latest version for you.
Configuration
| Env var | Required | Description |
|---|---|---|
CANTO_API_KEY | yes | canto_sk_... API key (or AuthKit JWT). The server exits immediately with a clear stderr message if this is unset or blank. |
CANTO_BASE_URL | no | Override the Descanto API base URL. Defaults to the SDK's own default (controld's local-dev address); set this once a hosted Descanto API launches. |
Claude Code
claude mcp add descanto --env CANTO_API_KEY=canto_sk_... -- npx -y @descanto/mcpClaude Desktop
Add to claude_desktop_config.json (Settings → Developer → Edit Config):
{
"mcpServers": {
"descanto": {
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_..."
}
}
}
}Cursor
Add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{
"mcpServers": {
"descanto": {
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_..."
}
}
}
}Codex
Use the CLI:
codex mcp add descanto --env CANTO_API_KEY=canto_sk_... -- npx -y @descanto/mcpor add the server to ~/.codex/config.toml by hand:
[mcp_servers.canto]
command = "npx"
args = ["-y", "@descanto/mcp"]
env = { CANTO_API_KEY = "canto_sk_..." }GitHub Copilot (VS Code)
Add to .vscode/mcp.json. Copilot uses the top-level servers key and requires
"type": "stdio":
{
"servers": {
"descanto": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_..."
}
}
}
}Antigravity
In the agent panel, open … → MCP Servers → Manage MCP Servers → View raw
config (writes ~/.gemini/config/mcp_config.json):
{
"mcpServers": {
"descanto": {
"command": "npx",
"args": ["-y", "@descanto/mcp"],
"env": {
"CANTO_API_KEY": "canto_sk_..."
}
}
}
}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": {
"descanto": {
"type": "local",
"command": ["npx", "-y", "@descanto/mcp"],
"enabled": true,
"environment": {
"CANTO_API_KEY": "canto_sk_..."
}
}
}
}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), optional idle_timeout_secs, optional env and setup_script (bootstrap the guest on first wake). |
wake_desktop | Wake a hibernated desktop (desktop_id, wait default true). Waking typically takes ~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. |
set_idle_timeout | Update a desktop's idle_timeout_secs (desktop_id, idle_timeout_secs; 0 disables auto-hibernate). |
fork_desktop | Fork a hibernated desktop into count (1-20) independent copies (desktop_id, count, acknowledge_shared_state required true, optional ephemeral, wait default true). Returns the new desktop ids; forks come back hibernated. |
exec | Run a shell command on an awake desktop (desktop_id, command, optional timeout_secs, optional detached). Blocking: stdout/stderr each truncated at 50KB in the result. Detached (detached: true): returns {process_id} immediately instead of waiting. |
get_process | Poll a detached process's status (desktop_id, process_id, optional tail_bytes). Returns {process_id, status, exit_code, stdout_tail, stderr_tail}; status is running/exited/lost. |
read_file | Read a file from an awake desktop (desktop_id, path). Text if valid UTF-8 under 50KB, else base64 (capped at 64KB) or an error asking you to use exec with head/tail. |
write_file | Write a file (desktop_id, path, content or content_base64). content_base64 is strictly validated (alphabet, padding, round-trip) before decoding -- malformed input returns a tool error instead of silently writing garbage/zero bytes. |
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 up to 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. |
fork_desktop requires acknowledge_shared_state: true explicitly --
omitting it is rejected the same as passing false, since a fork
duplicates whatever in-guest state (including credentials) was live in the
source desktop's memory when it last hibernated. See
Concepts: Forking.
Computer-use tools (screenshot, click, move_mouse, drag, scroll,
type_text, press_key, set_resolution) require an awake desktop
and are never gated by a noVNC control session -- a human viewer and these
tools can interleave. They're also not transactional: an error mid-batch
means earlier effects already applied, so re-screenshot before retrying
rather than blindly re-sending clicks. See
Concepts: Computer use.
expose_port/list_ports/stop_port mirror
POST/GET/DELETE .../ports directly -- see
Concepts: Port ingress for the token/hibernation
model.
Output truncation (context protection)
exec's stdout/stderr are each truncated at 50KB in the tool result
(never in the underlying API call) with a trailing [truncated by canto-mcp] marker, UTF-8-boundary-safe. This exists purely to protect the
calling model's context window -- for larger output, redirect the
command's output to a file on the desktop and fetch it with read_file,
or re-run with head/tail.
read_file follows the same idea: content is returned as text only when
it's valid UTF-8 and under 50KB; otherwise it's base64-encoded, but only if
that encoding itself stays under 64KB (base64 inflates size ~4/3x, so this
in practice means small binary files only) -- otherwise the tool returns
{ size, error: "too large for tool result — use exec with head/tail" }.
Errors
A failed Descanto API call never crashes the server. Every tool catches
CantoApiError (and any other thrown error) and returns an MCP tool-error
(isError: true) whose message includes the API's HTTP status, title, and
detail (and operation_id when the problem concerns a specific
operation), so the calling model sees exactly what the API returned and
can react to it.