Descanto Docs
MCP

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 varRequiredDescription
CANTO_API_KEYyescanto_sk_... API key (or AuthKit JWT). The server exits immediately with a clear stderr message if this is unset or blank.
CANTO_BASE_URLnoOverride 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/mcp

Claude 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/mcp

or 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

ToolDescription
list_desktopsList desktops, optionally filtered by state. Returns {id, state, tier, billingMode, hostId} per desktop.
create_desktopProvision 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_desktopWake a hibernated desktop (desktop_id, wait default true). Waking typically takes ~1-10s.
hibernate_desktopHibernate an awake desktop to pause billing (desktop_id, wait default true).
destroy_desktopPermanently destroy a desktop and its disk (desktop_id). Always waits; irreversible.
set_idle_timeoutUpdate a desktop's idle_timeout_secs (desktop_id, idle_timeout_secs; 0 disables auto-hibernate).
fork_desktopFork 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.
execRun 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_processPoll 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_fileRead 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_fileWrite 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_urlGet a short-lived signed stream URL (desktop_id, claim: view/control, default view).
expose_portPublish 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_portsList a desktop's port exposures (desktop_id). Returns {ports: [...]} with status bound/unbound; never includes tokens.
stop_portStop an exposure by guest port number (desktop_id, port). The URL 404s afterwards.
usageAwake-time usage and projected cost, per desktop and org-wide (start_ms/end_ms, default last 30 days).
screenshotCapture 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.
clickClick (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_mouseMove the cursor to absolute coordinates (desktop_id, x, y).
dragDrag from from_x/from_y (default: current cursor) to to_x/to_y (desktop_id, optional button).
scrollScroll the wheel at the current cursor position (desktop_id, direction up/down/left/right, amount 1-50).
type_textType into the focused element (desktop_id, text up to 8KB) -- click the target first.
press_keyPress a key combo (desktop_id, combo: +-joined X keysyms, e.g. ctrl+shift+t, Return).
set_resolutionResize 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.

On this page