Skip to content

Guides

Use Drums from Claude Code, Cursor and Codex (MCP)

One MCP server, two ways to reach it. Install it in Claude Code, Cursor or Codex, then drive desktops, snapshots, tasks, runs and keys from the agent.

Drums publishes one MCP server. It exposes the workspace (computers, snapshots, tasks, runs and evaluations, API keys, the audit log) and the desktop itself (GUI actions, screenshots, shell, files, verify, fork, reset) as tools, so an agent in Claude Code, Cursor, Codex or any other MCP client works on Drums desktops the way the SDK does. Every tool call is scoped to the workspace of the API key, is checked against the key's scopes, and is written to the audit log.

Two ways to reach it

TransportWhere it runsAddressWhen to use it
Remote (Streamable HTTP)Inside the gatewayhttps://beta.drums.run/gw/mcp (your workspace's gateway URL plus /mcp)Nothing to install. Clients that speak HTTP MCP: Claude Code, Cursor
Local (stdio)drums mcp, from the Python SDK (pip install drums-sdk, 0.3.0 or later)Your machine, talking to DRUMS_URL with DRUMS_TOKENClients that only start a command: Codex, or a Claude Code setup you want to keep local

Both servers list exactly the same tools with the same schemas and give the same answers; the local one calls the same gateway routes. Authentication is the workspace API key in either case (Authorization: Bearer on the remote endpoint, DRUMS_TOKEN for the local command). There is no OAuth flow: create a key under API keys and put it where the snippet says drums_sk_.... The key is never shown in the console again, so treat the client's configuration file like a secret.

With the SDK installed and DRUMS_URL exported as in the quickstart, drums mcp --print-config prints the configuration for a client, with your gateway URL filled in and drums_sk_... where the key goes. It never prints a key. The four forms are claude, claude-local, cursor and codex; the sections below show each command and its output as printed by SDK 0.3.0. The console's Connect sheet (MCP tab) shows the same snippets for your workspace.

Claude Code

Terminal
drums mcp --print-config claude

prints the registration command for the remote endpoint; Claude Code keeps it in its settings:

Output
claude mcp add --transport http drums https://beta.drums.run/gw/mcp --header "Authorization: Bearer drums_sk_..."

Put your key in place of drums_sk_... (or write $DRUMS_TOKEN there and let the shell expand it) and run it. To run the server locally instead:

Terminal
drums mcp --print-config claude-local
Output
claude mcp add drums -e DRUMS_URL=https://beta.drums.run/gw -e DRUMS_TOKEN=drums_sk_... -- drums mcp

Then ask Claude Code for the tools: list_templates, create_computer and the rest appear under the drums server. The SDK repository also ships a Claude Code plugin (see The Claude Code plugin).

Cursor

Terminal
drums mcp --print-config cursor

prints the object for ~/.cursor/mcp.json (every project) or .cursor/mcp.json (one project); put your key in place of drums_sk_...:

.cursor/mcp.json
{
  "mcpServers": {
    "drums": {
      "url": "https://beta.drums.run/gw/mcp",
      "headers": { "Authorization": "Bearer drums_sk_..." }
    }
  }
}

Codex or any stdio client

Codex starts MCP servers as commands, so it uses the local server:

Terminal
drums mcp --print-config codex

prints the table for ~/.codex/config.toml; put your key in place of drums_sk_...:

~/.codex/config.toml
[mcp_servers.drums]
command = "drums"
args = ["mcp"]
env = { DRUMS_URL = "https://beta.drums.run/gw", DRUMS_TOKEN = "drums_sk_..." }

drums mcp is the same as python -m drums.mcp. drums mcp --env followed by a computer id binds the desktop tools to that computer, so the agent can leave computer_id out. drums mcp --create makes one computer, binds it and closes it when the server exits (--task loads a task JSON file into it).

What the local server refuses

  • A key over plain HTTP. DRUMS_URL must be https://, or http:// to loopback (localhost, 127.0.0.0/8, ::1: a local drumsd). Any other http:// URL makes drums mcp exit 2 before any request, with a one-line message that names the host, never the key. For a trusted network, opt in with drums mcp --allow-insecure-http or DRUMS_ALLOW_INSECURE_HTTP=1.
  • A redirect to another origin. The client follows a redirect only to the same scheme, host and port, at most 5 hops; anything else is RedirectRefused, so the key is never re-sent elsewhere.

Tools

Every tool, in the order the servers list them. workspace tools act on the workspace through the gateway; desktop tools act inside one computer and take its computer_id (the id that create_computer and list_computers return). The scope is the API-key scope the call needs; "any key" means any active key of the workspace, with the rules in the tool's description. A destructive tool is refused unless the call passes confirm: true.

ToolLayerWhat it doesScopeDestructive
list_templates
no arguments
workspaceList the templates (base desktop images) this workspace can start a computer from. Use a template's checkpoint as the template argument of create_computer.computers:readNo
list_computers
include_destroyed?, limit?
workspaceList the live computers (desktops) of this workspace: id, name, state, validity, task, created_at. Pass include_destroyed to also see closed ones.computers:readNo
create_computer
template, task_id?, task?, ttl_s?, setup?
workspaceStart a new isolated desktop (computer) from a template or a snapshot, optionally with a task loaded. Returns the computer record; its id is the computer_id every desktop tool takes. The desktop is ready when state is ready and validity is valid.computers:writeNo
get_computer
computer_id
workspaceRead one computer's record: state, validity verdict and reasons, task, lease, epoch.computers:readNo
destroy_computer
computer_id, confirm
workspaceClose a computer for good (its desktop state is lost). Destructive: the call is refused unless confirm is true.computers:writeYes: needs confirm: true
list_snapshots
no arguments
workspaceList the saved desktop states (snapshots) of this workspace. A snapshot id can be the template of create_computer.computers:readNo
create_snapshot
computer_id, name?
workspaceSave a computer's current desktop state as a snapshot (the computer keeps running). Returns the snapshot record.computers:writeNo
delete_snapshot
snapshot_id, confirm
workspaceDelete a snapshot. Destructive: refused unless confirm is true.computers:writeYes: needs confirm: true
list_tasks
domain?, limit?
workspaceList the verified computer-use tasks available on this host (id, domain, instruction). Load one with create_computer(task_id) or load_task.computers:readNo
get_task
task_id
workspaceRead one task document (instruction, setup, checker).computers:readNo
load_task
computer_id, task_id?, task?
desktopLoad a task into a running computer (runs its setup and starts a new episode). After this, verify scores the desktop against this task's checker.computers:writeNo
list_runs
status?, limit?
workspaceList the runs (training, scripted tests) of this workspace, newest first: id, name, kind, status, counts.runs:readNo
create_run
spec, idempotency_key?
workspaceSubmit a run (RunSpec: name, kind, task_set_id, task_ids, template_version_id, agent, execution, budget). It is queued and the host's runner picks it up. Returns run_id and operation_id.runs:writeNo
get_run
run_id
workspaceRead a run's status and results: status, stop reason, attempt counts (done, succeeded, verified valid), metrics and the last events.runs:readNo
list_evaluations
status?, limit?
workspaceList evaluation runs of this workspace, newest first.runs:readNo
create_evaluation
spec, idempotency_key?
workspaceSubmit an evaluation (a run with kind evaluation): the same RunSpec as create_run; kind is set to evaluation.runs:writeNo
get_evaluation
run_id
workspaceRead an evaluation's status and results (per-task success and verified validity).runs:readNo
list_api_keys
no arguments
workspaceList this workspace's API keys (metadata only: id, name, prefix, scopes, created, last used, expiry; never a secret).any keyNo
create_api_key
name, scopes, expires_in_days?
workspaceCreate an API key in this workspace. The plaintext key is in this reply once and nowhere else. Scopes cannot exceed the calling key's own scopes.any keyNo
revoke_api_key
key_id, confirm
workspaceRevoke an API key; it is refused on its next call. Destructive: refused unless confirm is true.any keyYes: needs confirm: true
read_audit_log
action?, limit?, cursor?
workspaceRead this workspace's audit log, newest first: who did what, when, with what outcome. MCP calls appear with actor mcp and action mcp.<tool>.audit:readNo
gui
computer_id, code
desktopRun a GUI action in the desktop: Python using pyautogui (e.g. pyautogui.click(100, 200), pyautogui.write('text'), pyautogui.hotkey('ctrl', 's')). Screen is 1920x1080 pixels. Take a screenshot afterwards to see the result.computers:writeNo
screenshot
computer_id
desktopTake a screenshot of the desktop (PNG image).computers:readNo
shell
computer_id, command, timeout_s?
desktopRun a bash command in the desktop as the desktop user (no root, no internet). Returns returncode, stdout, stderr.computers:writeNo
read_file
computer_id, path
desktopRead a file in the desktop (~ is /home/user). Text comes back as text, binary as base64.computers:writeNo
write_file
computer_id, path, content
desktopWrite a text file in the desktop (creates folders; overwrites).computers:writeNo
list_dir
computer_id, path
desktopList a folder in the desktop: name, dir, size.computers:writeNo
office_convert
computer_id, path, format, outdir?
desktopConvert a document with LibreOffice (e.g. docx->pdf, xlsx->csv, csv->xlsx). Writes next to the source unless outdir is given.computers:writeNo
spreadsheet_read
computer_id, path
desktopRead the first sheet of a spreadsheet (xlsx, ods, csv) as rows of text, through LibreOffice.computers:writeNo
browser_tabs
computer_id
desktopList the open Chrome tabs (url, title). Needs Chrome started with --remote-debugging-port=1337.computers:writeNo
browser_open
computer_id, url
desktopOpen a URL in a new Chrome tab (Chrome must be running with --remote-debugging-port=1337).computers:writeNo
verify
computer_id
desktopScore the desktop's current state with the loaded task's checker: score (0..1), valid (every gate of the episode passed), trusted, audit flags.computers:writeNo
fork
computer_id, n?
desktopFork a running desktop into n children that each continue from this exact state (apps open, files as they are). Returns the children's computer ids. The parent keeps running.computers:writeNo
reset
computer_id
desktopReset a desktop to its clean template state and start a new episode (the loaded task's setup runs again).computers:writeNo

Tool results are JSON text, except screenshot, which returns a PNG image. A failed call returns a tool error with a one-line reason (for example refused: pass confirm: true to destroy_computer), never a protocol error, so the agent can read it and recover.

The workflow

  1. Create. list_templates (or list_snapshots) and list_tasks, then create_computer with a template and a task_id. Poll get_computer until state is ready and validity is valid; before that the desktop may still be restoring. Keep the computer id.
  2. Act. screenshot to see the desktop; gui for pyautogui actions (click, type, hotkeys) with a screenshot after each one; shell, read_file, write_file and list_dir for anything a terminal does better than a mouse; browser_open and browser_tabs for Chrome; office_convert and spreadsheet_read for documents. The desktop user has no root and no internet.
  3. Verify. verify scores the desktop with the loaded task's checker and reports whether the episode is still valid (every gate passed). load_task swaps the task on a running computer.
  4. Fork or reset. fork makes N children that continue from this exact state (the parent keeps running) to branch at a decision point; reset returns the desktop to the clean template and runs the task's setup again for another attempt. See Checkpoints, snapshots and forks.
  5. Results. get_run and get_evaluation for runs submitted with create_run or create_evaluation; read_audit_log for what happened, by whom. destroy_computer with confirm: true when the desktop is no longer needed (ttl_s closes it anyway when it expires).

Safety

  • Destructive tools need confirm: true from the caller. destroy_computer, delete_snapshot and revoke_api_key are refused before any network call unless the tool arguments carry confirm: true. Drums never prompts a human itself: the MCP client (Claude Code, Cursor, Codex) owns the user's approval of such a call, and a well-behaved agent tells you before it passes the flag.
  • Workspace scoping. A key sees only its own workspace. A computer, snapshot, run or key of another workspace does not exist for it: the tool returns a not-found error, never a hint that the id is real.
  • Scopes. Each tool needs the scope in the table; a key without it gets a forbidden error. create_api_key cannot grant more than the calling key has; only an owner or admin key may create or revoke keys; a key cannot revoke itself.
  • Audit log. The gateway records every tools/call as mcp.<tool> with actor mcp and origin mcp: tool name, duration, computer id and error code, never the arguments, screenshots or file contents. The underlying actions (computer.create, api_key.revoke, ...) keep their own rows. Filter the Audit log page or read_audit_log by action mcp.
  • Rate limits are the gateway's. Each MCP request takes one token from the key's bucket, and failed authentications count per source IP, exactly as for /v1; see Limits.
  • Secrets. The server never prints a key. create_api_key returns the new key once, in that reply only; the agent's transcript then holds it, so use it, store it and do not paste it elsewhere.

The Claude Code plugin

The SDK repository is also a Claude Code plugin: .claude-plugin/plugin.json, a .mcp.json that starts drums mcp with DRUMS_URL and DRUMS_TOKEN from your environment, and the drums-workflow skill. The skill teaches the workflow above (templates, create, wait until ready and valid, act with screenshots after GUI actions, verify, fork at decision points, reset for another attempt, destroy with confirm when done, read results) and the rules: never destroy without telling the user, prefer shell and read_file over the GUI for checks, keep computer ids. Install it from the repository the way you install any Claude Code plugin, with the two environment variables set.

Troubleshooting

SymptomMeaningWhat to do
The client reports 401 when connecting or listing toolsThe key is missing, mistyped, expired or revokedCheck the Authorization header or DRUMS_TOKEN; create a new key under API keys
404 or "not found" on a computer id that exists in the consoleThe id belongs to another workspace than the keyUse a key of that workspace, or the id of a computer in this one
429 with Retry-AfterThe key's request bucket is empty, or too many failed authentications from this addressWait the number of seconds in Retry-After; slow the agent down
refused: pass confirm: true to ...A destructive tool was called without confirmationConfirm with the user, then call again with confirm: true
not available on this server (needs the Drums gateway)drums mcp points at a plain runtime (DRUMS_URL is a drumsd, not the gateway)Runs, evaluations, keys and the audit log exist only behind the gateway; point DRUMS_URL at it
forbiddenThe key lacks the tool's scopeCreate a key with the scope the table lists (Manage API keys safely)
drums mcp exits 2: InsecureTransportDRUMS_URL is plain http:// to a host that is not loopbackUse the https:// gateway URL; on a trusted network only, --allow-insecure-http or DRUMS_ALLOW_INSECURE_HTTP=1
RedirectRefusedThe server redirected to another originPoint DRUMS_URL at the final URL (the gateway's own address)

Every gateway reply carries a request id (X-Request-Id on the remote endpoint); include it when you report a problem.