Home / Documentation

Desktop MCP server setup and tools

agentpc mcp is one stdio MCP server for every VM. Agents use it to create desktops, drive them, take screenshots and reset them, with no human in the loop.

Connect an agent

The installer already ran this for you. Run it again after installing a new agent, or pass client names to pick which ones to register:

$ agentpc mcp-install
$ agentpc mcp-install claude claude-desktop codex cursor gemini vscode

Supported: Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI and VS Code. Restart Claude Desktop after registering. For Codex it also raises the MCP timeouts so slow builds and boots don't trip them. agentpc mcp-uninstall removes the registration again.

Claude plugin

In Claude Code, the plugin bundles the MCP server with a skill that teaches Claude when and how to use the VMs. It runs the installed agentpc binary, so install that first.

/plugin marketplace add pawanpaudel93/agentpc
/plugin install agentpc@agentpc

With the plugin installed, agentpc mcp-install skips Claude Code so the server isn't registered twice.

Client configs

For any other MCP client, add the server by hand. The standard shape:

{
  "mcpServers": {
    "agentpc": { "command": "agentpc", "args": ["mcp"] }
  }
}

Codex

A first create_vm can take minutes while the image downloads, so the server needs longer timeouts than Codex's defaults. agentpc mcp-install codex adds these for you:

[mcp_servers.agentpc]
command = "agentpc"
args = ["mcp"]
startup_timeout_sec = 60
tool_timeout_sec = 900

VS Code

{
  "servers": {
    "agentpc": { "type": "stdio", "command": "agentpc", "args": ["mcp"] }
  }
}

The agentpc repository ships these as project configs (.mcp.json, .codex/config.toml, .cursor/mcp.json, .gemini/settings.json, .vscode/mcp.json), so an agent opened in a clone picks the server up automatically.

Tools

VMs

ToolUse
list_vms VMs (owner, state, size, checkpoints, viewer and, for running x86apps VMs, x86_tso) and the images they come from, with OS versions. Start here.
create_vm New clone, returned once its desktop is ready: Ubuntu or Arch ~1 s, Windows ~4 s (the first create of an image can take minutes). Takes os (ubuntu, windows or arch), and optionally version (Ubuntu "22.04", a pinned "24.04-YYYYMMDD" download, or "x86apps" / "22.04-x86apps" to also run x86_64 and i386 Linux programs; Windows "11-25h2", "11-24h2", "11-23h2"; Arch is rolling, so the default, "rolling", "x86apps", or a pinned "rolling-YYYYMMDD" / "rolling-x86apps-YYYYMMDD" download), name (up to 64 letters, digits, . - _), memory_gb (2–64, default 8 Windows / 4 Ubuntu and Arch), cpus (1–16, default 4), offline and open_in_browser (default false). Set open_in_browser: true to open the ready VM’s viewer in the Mac’s default browser, not a browser inside the guest. This is a per-call action, not a saved setting; a browser-launch failure warns without failing creation. A non-default size boots cold (~25 s Windows, ~15 s Ubuntu and Arch). Retrying with the same name and image in the same session returns the VM it already made, booting it if it was stopped. from_checkpoint ("<vm>/<label>") makes a new, independent VM from a checkpoint's disk instead: it boots cold, os/version may be omitted (if given, they must be the source's image), and the checkpoint's memory, port forwards, owner, offline and job records are not carried over. Errors before any work: invalid_checkpoint_ref, no_such_vm, no_such_checkpoint, image_mismatch.
start_vm / stop_vm Boot a stopped VM, waiting until it's ready / shut one down cleanly.
reset_vm Discard all changes: back to a fresh copy of the image, booted.
delete_vm Stop a VM and delete it with its disk and checkpoints.

Checkpoints

ToolUse
checkpoint_vm Save a VM's disk and memory under a label (replacing an older one of that name) before a risky step. A running VM's guest first writes its pending data to disk (at most ~10 s; otherwise the reply says "not flushed"), then pauses for a few seconds. New VMs can start from a checkpoint with create_vm's from_checkpoint.
restore_vm Go back to a checkpoint exactly as it was and start it, in seconds. Port forwards (TCP and UDP) must be set up again.
delete_checkpoint Delete one checkpoint by label; the VM is untouched.

Shell, files and ports

ToolUse
run_command PowerShell on Windows, bash on Ubuntu and Arch, over SSH. Returns typed structuredContent plus the readable exit code, stdout and stderr, each stream trimmed to its first and last 10,000 characters. A foreground run that reaches timeout (default 120 s) gets an attempt to stop it, and its partial output is kept; the reply says whether the stop was confirmed. A foreground run whose request is cancelled, or whose client disconnects, is stopped the same way. background: true returns a detached job id that cancellation does not stop. Results and recovery.
get_job_status A job's state (queued, running, exited with its code, stopped, ended without an exit code, or missing) and its log, typed. By default the tail (tail_lines, default 50, at most 16 KiB); wait_seconds (0 to 30, default 0) waits without ever stopping the job; cursor reads the log incrementally. Also takes the job_id of a foreground run whose outcome was uncertain.
list_jobs Find a job again when its id was lost (a cancelled or disconnected run_command): the VM's job records, newest id first, as metadata only (id, kind, state, exit code, stop reason, which records exist, file times), never a command, environment, output or log. limit 1 to 100 (default 20); next_cursor continues below the last job shown. Read-only. Results and recovery.
wait_for_service Wait until a TCP port accepts a connection, or an HTTP path answers its expected status, on the VM's own 127.0.0.1. timeout_seconds 1 to 120 (default 30). Read-only: it never stops or restarts the service or a job. Results and recovery.
stop_job Stop a background job (or a foreground run's job_id) and everything it started: on Linux its systemd scope, which processes that detach stay in, when the guest has a user manager, else the processes marked with the job and their descendants; on Windows its process tree. For a job that already exited, it stops only what the job left running.
upload_file / download_file Copy files or folders between the Mac and a VM.
forward_port Reach a server in the VM from the Mac at 127.0.0.1:<host_port>. An SSH tunnel, so it reaches servers bound to the guest's own 127.0.0.1. Lasts until the VM stops. protocol: "udp" forwards UDP instead, to a guest server listening on 0.0.0.0 (not on offline VMs).
list_forwards / delete_forward List a VM's forwards, their protocol and whether each is alive / stop one by its host port (and optional protocol).

Screen and desktop

ToolUse
take_screenshot PNG straight from the hypervisor, even while booting or hung. save_to also writes it to a path on the Mac.
list_desktop_tools The desktop-control tools inside a VM, with each one's required arguments and whether it's read-only, or one tool's full schema with its annotations (a mistyped name returns close matches).
use_desktop_tool Call one of them (tool plus arguments): click, type, launch apps, read the UI tree, and more. Wrong arguments or a wrong name return the tool's arguments or close matches. Marked destructive. Gives up after 120 s.
inspect_desktop The read-only twin of use_desktop_tool, with the same arguments. It runs a tool only if the VM's desktop driver explicitly marks it read-only, and refuses anything else before dispatching it. Marked read-only, so clients can auto-approve looking while still confirming acting.
read_vm_log Tail a VM's qemu or serial log (tail_lines, default 100) when it won't boot or the desktop is unreachable.

Desktop control

All three guests (Windows, Ubuntu, Arch) run cua-driver and are 1280×800. Work in a loop: look (take_screenshot or a snapshot tool, through inspect_desktop when you only need to look), act (use_desktop_tool), then look again to verify. Prefer run_command for anything a shell can do.

GuestHow to drive it
Windows launch_app takes a name such as notepad and returns the pid and window_ids. get_window_state(pid, window_id) returns numbered elements and a snapshot_id; click and type_text take that snapshot_id with an element_index. Input goes in the background first; typing into the focused field, scroll, drag and right-click often need "delivery_mode": "foreground", which the driver asks for when background input can't reach the target.
Ubuntu XFCE on X11. get_desktop_state returns a screenshot plus window pid/window_id values. Keyboard and mouse tools need "delivery_mode": "foreground". launch_app takes a command such as xfce4-terminal. Google Chrome is installed: launch it with launch_app (google-chrome, URL in additional_arguments) and read the page with get_window_state, or drive it with the browser_* tools after browser_prepare. Opening a .deb (a double-click, or xdg-open) installs it with apt in a terminal window.
Arch Arch Linux ARM, with the same XFCE desktop and tools as Ubuntu. The browser is Chromium: use chromium wherever Ubuntu uses google-chrome. Install packages with sudo pacman -Syu --noconfirm <pkg> (Arch doesn't support partial upgrades). The first -Syu may upgrade everything, so give run_command a longer timeout or background: true, and reboot (start_vm) after a kernel upgrade before loading new modules. /tmp is a tmpfs, cleared at every boot.

Open Notepad on Windows and type into it, as three use_desktop_tool calls. The pid and window_id come from launch_app; the snapshot_id and element_index from get_window_state:

{ "name": "windows-1", "tool": "launch_app", "arguments": { "name": "notepad" } }
{ "name": "windows-1", "tool": "get_window_state", "arguments": { "pid": 4120, "window_id": 65812 } }
{ "name": "windows-1", "tool": "type_text", "arguments": {
    "pid": 4120, "window_id": 65812, "snapshot_id": "s1a2b3c4d",
    "element_index": 7, "text": "Hello from the agent" } }

If a reply starts with a reconnect note (the VM or its driver restarted), earlier snapshot ids and browser sessions are gone: take a new snapshot and run browser_prepare again.

Guest tips

All guests are ARM64. On Windows, x64/x86 apps run through Prism (~2–4× slower; no x64 drivers). For x86_64 and i386 Linux programs, create ubuntu or arch with version: "x86apps": they run through FEX, about 2× slower (Node 6–7×). Go programs work; amd64 containers run with docker run --platform linux/amd64 (install Docker first: sudo apt install docker.io on Ubuntu, sudo pacman -Syu --noconfirm docker && sudo systemctl start docker on Arch); x86 Electron/Chromium apps need --no-sandbox. On Ubuntu, a missing x86 library installs with sudo apt install libfoo:amd64 and an x86 app's .deb with sudo apt install ./app_amd64.deb; on Arch (no multiarch) x86 programs use the image's x86 Arch Linux tree, and sudo fex-pacman -Sy --noconfirm --needed <pkg> installs more x86 packages into it. x86 systemd services run too, hardened ones included (a unit that runs its x86 program through a script needs sudo fex-unit <unit>). list_vms shows x86_tso: hardware (needs macOS 15+) or emulated.

Typical session

Test an install script on a clean machine, keeping a checkpoint to retry from:

create_vm        { "os": "ubuntu", "name": "install-test" }
upload_file      { "name": "install-test", "host_path": "./install.sh", "guest_path": "/tmp/" }
checkpoint_vm    { "name": "install-test", "label": "before-install" }
run_command      { "name": "install-test", "command": "bash /tmp/install.sh" }
take_screenshot  { "name": "install-test" }
restore_vm       { "name": "install-test", "label": "before-install" }  # retry from the same state
delete_vm        { "name": "install-test" }

Some prompts to try:

  • "Create an Ubuntu VM, open a terminal and run uname -a."
  • "Reset ubuntu-1 and check that my install script works on a clean machine."
  • "Open Notepad on windows-1, type a short note and show me a screenshot."

Long jobs and servers

A foreground run_command ends when the command returns, and is given an attempted stop at its timeout. Start builds, servers and GUI apps with background: true, then poll:

run_command     { "name": "ubuntu-1", "command": "npm run dev", "background": true }  # → job id 1
get_job_status  { "name": "ubuntu-1", "id": 1, "tail_lines": 40 }
stop_job        { "name": "ubuntu-1", "id": 1 }  # when done: the server and its children
forward_port    { "name": "ubuntu-1", "guest_port": 3000 }  # → 127.0.0.1:<port> on the Mac

get_job_status without wait_seconds or cursor answers at once, as it always has. To follow a job without polling in a loop, wait and read from a cursor:

get_job_status  { "name": "ubuntu-1", "id": 1, "cursor": "start", "wait_seconds": 10 }  # → output, next_cursor
get_job_status  { "name": "ubuntu-1", "id": 1, "cursor": "<next_cursor>", "wait_seconds": 30 }  # new output only

Reboots (some installers) drop SSH. Call start_vm on the same VM, which waits until the desktop is ready again even if it's already running, then rerun the command. If its reply was connection_lost with effect_unknown: true, check what it did first with get_job_status and its job_id.

Results and recovery

run_command, get_job_status, list_jobs and wait_for_service declare an outputSchema and return matching structuredContent (schema_version 1) next to the readable text scripts have always parsed (exit code: N, STATE: …). The text is a single block, not a JSON copy, so a client that joins text blocks sees no extra output. Read the typed fields and isError.

run_command

FieldMeaning
state exited (it ran to its end), timed_out, not_started (it never ran), connection_lost (SSH failed while it may have been running), started (a background job) or cancelled (its request was cancelled and agentpc tried to stop it).
exit_code, stdout, stderr The command's code and its separate streams, with stdout_bytes / stderr_bytes and truncation flags (output_truncated) saying whether part of a stream was left out. Foreground codes are the low 8 bits SSH reports: on Windows exit -1 reads 255.
timed_out, request_cancelled, termination_confirmed, effect_unknown Whether the call reached its timeout; whether its request was cancelled, its client disconnected or its session ended before the command's end was known; whether agentpc verified that none of the processes it identified is left (null when nothing needed stopping); and whether it is unknown if the command ran, finished or is still running.
execution_id, job_id, recovery_action A run is issued an execution_id (null only when the call failed before one was, such as an unknown VM). job_id is the handle for get_job_status and stop_job: set for a background job and for a foreground run whose fate is uncertain. recovery_action says what to do next: get_job_status, stop_job, start_vm, inspect_vm or retry (only when the command provably never ran).

isError is true for every nonzero exit, every timeout (even when the command's own exit code is known to be 0), every cancelled request, and every run that didn't start or lost its connection. It is false only for exit 0 and a background start. Commands are never retried automatically.

Timeouts

  • At timeout, agentpc attempts to stop the command and what it started, and keeps the partial stdout and stderr. The reply usually follows within a few seconds; the cleanup is bounded, about 50 seconds beyond the timeout at most.
  • termination_confirmed: true means agentpc verified that none of the processes it identified is left. On Linux those are the command's transient systemd user scope when the guest's user manager is available, the processes marked with its execution id and their descendants (all that is checked without a user manager, where a process that clears its environment and detaches can be missed), and its verified root. On Windows it is the verified process tree, best effort: a process whose launcher had already exited can be missed. It is not a sandbox guarantee. System services, other systemd units, and Windows services, scheduled tasks and WMI-created processes are not covered.
  • With termination_confirmed: false, or connection_lost with effect_unknown: true, the command may still be running. (A connection_lost with effect_unknown: false carries the VM's recorded exit_code and no job_id: it finished, and only its output may be incomplete.) Use the job_id with get_job_status and stop_job, and don't rerun it before you know. A process is only signalled after its pid and start time are proven to be the command's.
  • A command whose exit code the VM recorded but whose run had not ended by the timeout (a child it left may have held its output, or it ended right at the deadline) returns that exit code with timed_out: true, after cleanup verified that none of the processes agentpc identified remains. Start servers with background: true.
  • A Linux foreground command is limited to 92 KiB (94,208 UTF-8 bytes) of text. agentpc checks that before sending anything: a bigger one comes back not_started with no recovery_action. For a bigger script, upload_file it and run the file.

Cancellation

  • A foreground run_command is stopped in the VM, as at a timeout (bounded, identity-checked, never retried), when its request is cancelled (notifications/cancelled), the client disconnects or the MCP session ends; request_cancelled is then true and the call is an error.
  • cancelled with termination_confirmed: true: stopped and verified. exited with its code: it had already finished. not_started with no recovery_action: it never began. cancelled with termination_confirmed: false, effect_unknown: true and recovery_action: stop_job: it may still be running; use the job_id and don't rerun it before you know.
  • A client that cancelled usually never receives that reply. Find the run with list_jobs; get_job_status then shows stopped with stop_reason: request_cancelled, or not_started.
  • A background: true start is detached on purpose: cancelling its request, disconnecting or ending the session does not stop the job. End it with stop_job. AGENTPC_KEEP_RUNNING=1 keeps VMs, not foreground commands, and a hard kill of the MCP server cleans up nothing: the job records stay in the VM.

get_job_status

  • state: queued, running, exited, stopped, ended, not_started, missing or unknown (the VM couldn't be read: an error, never reported as ended). An exited job reports its own exit code even if it left a child running.
  • wait_seconds (0 to 30, default 0) waits for the job to leave running, or with a cursor for new output. If nothing happens it returns the current state with wait_timed_out: true. A wait only waits and never stops the job. It can run about 2 seconds past the wait on a slow VM; to wait longer, call again with the last next_cursor.
  • cursor (a next_cursor, or "start") returns at most 16 KiB from that byte position and advances by exactly the bytes it returned, so output is neither repeated nor skipped (has_more says call again). UTF-8 characters are never split across reads, invalid bytes become U+FFFD (invalid_utf8), and a byte-order mark is consumed unseen.
  • A cursor that no longer matches (VM reset or restored, guest reboot, log replaced, shortened, rewritten or removed) comes back with cursor_reset: true and a cursor_reset_reason, restarted at the log's beginning: discard what you assumed. Detection is best effort; it can miss a guest clock step, a Windows log recreated within the file system's tunneling window, and a log rewritten with the same first 64 bytes.
  • missing means nothing has recorded itself under that id yet. That doesn't prove the command never started, so don't blindly retry it.
  • A foreground job_id has status only (exited with its code, stopped at its timeout, or not started); its output went to the run_command reply, so there is no log to replay.

list_jobs

  • Read-only, metadata only: per job job_id, kind, state, exit_code, stop_reason (timeout, stop_job, request_cancelled), log_available, records, recorded_at and finished_at. Never a command, environment, output, log text or path. Newest id first; limit is 1 to 100 (default 20).
  • When has_more is true, pass next_cursor as cursor: the next page continues below the last job shown within the VM as the first page saw it, so jobs created meanwhile don't shift it.
  • A cursor from before a VM reset, restore or recreation is cursor_stale: the VM's generation changed, so its records may differ from what the earlier pages listed (a restored checkpoint can keep some) and the listing cannot safely continue. Call again without a cursor. A malformed cursor, or one of another VM, is invalid_cursor.
  • Best effort, not an immutable history: records age out, and a huge job directory keeps only its first entries (scan_truncated), so jobs may be missing. A queued, missing or unknown entry is unresolved, not proof the command never started. Cancelling the call abandons only the listing.

wait_for_service

  • Looks only at the VM's own IPv4 127.0.0.1 (never another host, a URL or a proxy). protocol: "tcp" (default) is ready when a connection is accepted; "http" is ready when one GET path (/ by default; a path, not a URL) answers expected_status (default 200). path and expected_status are HTTP-only and refused with tcp. Redirects are not followed, and no body, header or guest text is returned. Don't put credentials in path: it is echoed back and visible in the VM's process list.
  • timeout_seconds is 1 to 120 (default 30) and bounds the waiting, not the wall clock: a call can run about 5 s past it while a last probe finishes.
  • state is ready, timed_out, job_failed, cancelled or error, with the last tcp or http_outcome (status_matched, redirect, status_mismatched, no_response, malformed) and http_status. Anything but ready is an error and timed_out keeps the last observation. probe_unsupported means the guest's bash has no /dev/tcp or it has no timeout command.
  • With job_id the wait ends early (job_failed) only for a proven failure: a nonzero exit, a stopped job, or one that never started. A job that exited 0 (a server may outlive its wrapper), is queued, running, ended without a code, missing or unreadable is not failure.
  • Ready describes the endpoint, not a process: nothing checks who owns the port, so it doesn't prove the job started it. Waiting never stops or restarts the service or the job; cancelling it only stops probing. Each probe makes short-lived connections (and one GET), so it can consume a one-shot listener.

inspect_desktop

Before each dispatch, inspect_desktop asks the VM's driver for the tool's annotations and runs only a tool it explicitly marks readOnlyHint: true (list_desktop_tools shows [read-only]). A tool marked otherwise, unmarked or unknown is refused before it is dispatched, with nothing run (looking up the annotations does ask the VM's driver). After a dropped call the driver is asked again on the new connection; the call is re-sent only if the old connection had listed the tool read-only and the new one still does. The marking is the driver's own claim: a guard against mistakes, not a guarantee against a mislabelling driver or guest. For compact observation, get_window_state with include_screenshot: false returns the element tree without a screenshot, and its query argument narrows what comes back. use_desktop_tool is unchanged.

Session lifecycle

  • VMs a session created, started, reset or restored are stopped, never deleted, when the session ends. Set AGENTPC_KEEP_RUNNING=1 to keep them running.
  • list_vms shows each VM's owner. Agents should create their own uniquely named VM per task, and never reset, restore or delete one they didn't create. A VM whose session ended without stopping it (its server was killed) is stopped by the next MCP server and shows owner_running: false; create_vm with its name and image hands it back to the agent that made it.
  • Each running VM takes 4 GB (Ubuntu, Arch) or 8 GB (Windows) of RAM, so delete VMs when done.
  • The guest login is agent / agent (desktop and sudo; SSH takes only agentpc's key).

Approval prompts

Every desktop action goes through use_desktop_tool, which is marked destructive, so clients that confirm tool calls ask before each click or keystroke. inspect_desktop is marked read-only and only runs tools the driver marks read-only. To approve agentpc's tools up front:

ClientSetting
Claude CodeAllow mcp__agentpc (all of its tools) with /permissions, or in permissions.allow in settings.
Codexdefault_tools_approval_mode = "approve" under [mcp_servers.agentpc] in ~/.codex/config.toml.
Gemini CLI"trust": true on the agentpc server in ~/.gemini/settings.json.
OthersThe client's own tool-approval settings.

That approves every agentpc tool, including upload_file and download_file, which read and write files on your Mac. The VMs themselves are throwaway: reset_vm undoes anything done inside one.

Tools also carry MCP annotations, which some clients use to decide what to confirm: list_vms, take_screenshot, list_desktop_tools, inspect_desktop, get_job_status, list_jobs, wait_for_service, list_forwards and read_vm_log are read-only. Tools that discard or overwrite state are marked destructive, including download_file, which writes to your Mac. start_vm, stop_vm and stop_job are idempotent.