Labsco
Neanderthal logo

MCP QEMU VM Control

14

from Neanderthal

Give your AI full computer access — safely. Let Claude (or any MCP-compatible LLM) see your screen, move the mouse, type on the keyboard, and run commands — all inside an isolated QEMU virtual machine. Perfect for AI-driven automation, testing, and computer-use experiments without risking your host system.

🔥🔥🔥🔥✓ VerifiedFreeAdvanced setup

MCP QEMU VM Control

Give your AI full computer access — safely.

Let Claude (or any MCP-compatible LLM) see your screen, move the mouse, type on the keyboard, and run commands — all inside an isolated QEMU virtual machine. Perfect for AI-driven automation, testing, and computer-use experiments without risking your host system.

A Model Context Protocol (MCP) server for controlling QEMU virtual machines via SSH. This server enables LLMs to interact with VMs through mouse/keyboard control, screenshots, and SSH command execution.

Table of Contents

Features

  • Mouse Control - Move cursor and click buttons
  • Keyboard Input - Type text and send key combinations
  • Action Batching - Execute sequences of UI actions in one call
  • Screenshots - Capture and retrieve VM screenshots
  • SSH Command Execution - Run shell commands on the VM
  • File Transfer - Upload and download files via SFTP
  • Project Management - Organize outputs into project folders with logs, results, and advice
  • Advice System - Save and retrieve tips for future LLM sessions

Tools Reference

Project Management

Projects organize all outputs (screenshots, logs, results, advice) into timestamped folders under data/projects/.

ToolDescription
project_init(name, description)Create a new project (required before screenshots)
project_load(project_path)Load an existing project
project_list()List all projects
project_info()Get current project statistics
project_log(message, level)Add a log entry
project_read_logs(lines, level_filter)Read project logs
project_save_result(filename, content)Save a result file
project_save_advice(title, content)Save tips for future sessions
project_read_advice()Read all saved advice

Mouse & Keyboard

ToolDescription
move_mouse(x, y, mode)Move cursor (mode: "absolute" or "relative")
click(button, count, x, y)Click a button; optional x, y move-and-click in one op
click_in_window(x, y, button, count)Click at coords relative to the active window's client area
get_active_window_info()Active window id, title, position & geometry
scroll(direction, amount)Mouse-wheel scroll (up/down/left/right) at cursor
drag(x1, y1, x2, y2, button)Press at start, drag to end, release (select/slider/DnD)
type_text(text, human)Type text (newlines → Return, UTF-8 safe); human=True types at a slower, lifelike cadence (varied per-word speed + random pauses)
press_keys(keys)Press key combo, e.g., ["Ctrl", "L"]
key_down(keys) / key_up(keys)Hold / release a key or modifier (e.g. Shift-click)
set_clipboard(text)Load the VM clipboard (fast insert for big ASCII)
paste(text)Set clipboard (if text) and Ctrl+V
activate_window(title, window_id)Focus & raise a window by title or id
wait(seconds)Pause execution
run_actions(actions)Execute a sequence of actions in one call

Batch Actions Example

[
  {"action": "press_keys", "keys": ["Ctrl", "Shift", "p"]},
  {"action": "wait", "seconds": 0.5},
  {"action": "type_text", "text": "Terminal: Focus Terminal"},
  {"action": "press_keys", "keys": ["Return"]}
]

Object Location (OCR)

Locate on-screen elements by their visible text — exact pixel coordinates, no coordinate guessing. The host OCRs the full-resolution screenshot (tesseract) and maps the match straight into the click path. Works on any visible text, including nested Citrix/web where accessibility APIs can't reach; does not find unlabeled icons.

ToolDescription
find_text(query, min_conf)OCR the screen; return every match's center & box
click_text(query, index, button, count)Find text and click its center (precise)
click_text("Submit")          # finds "Submit" and clicks its exact center
find_text("File")             # lists all matches with coordinates
click_text("OK", index=1)     # click the 2nd "OK" if several match

Zoom (magnify a region, then click it precisely)

When detail is too small/low-contrast to resolve in the full screenshot, magnify a region and click within it. The server keeps the crop mapping, so a point you pick in the zoomed image maps back to the exact full-screen pixel — no coordinate math.

ToolDescription
zoom(x, y, width, height, scale)Crop around (x, y) and magnify; returns a viewable image + mapping
click_zoomed(zx, zy, button, count)Click a point given in the last zoom's image coords
zoom(800, 600, width=400, height=300, scale=4)  # view a 4× magnified crop
click_zoomed(610, 250)                           # click that spot → exact full-screen pixel

Set-of-Mark (pick by number)

For dense or ambiguous screens, overlay numbered marks on every detected text element and pick one by its number — a discrete choice that's far more reliable than estimating coordinates.

ToolDescription
mark_screen(min_conf, max_marks)Annotate the screen with numbered boxes; returns the image + a legend
click_mark(n, button, count)Click the element labeled n
mark_screen()        # view the annotated screenshot + legend (0 -> "File", 1 -> "Edit", …)
click_mark(1)        # click element #1 at its exact center

Host requirements: tesseract (the binary) plus pillow and pytesseract in the server's Python env. These are optional — the rest of the server runs without them; only the OCR (find_text/click_text) and zoom (zoom) tools need them (zoom needs only pillow):

# Arch/Manjaro host
sudo pacman -S tesseract tesseract-data-eng
uv pip install pillow pytesseract

SSH Operations

ToolDescription
ssh_execute(command, as_desktop_user)Run a shell command on the VM
ssh_upload(local_path, remote_path)Upload file to VM
ssh_download(remote_path, local_path)Download file from VM
ssh_connection_info()Get connection status

Screenshots

ToolDescription
take_screenshot()Capture screenshot (requires active project)

Screenshots are saved to the project's screenshots/ folder and exposed as MCP resources at vm://screenshot/{id}.

Display Calibration

ToolDescription
display_calibration_info(recalibrate)Show the xdotool↔screenshot scale factors; recalibrate=True re-probes them

Scale factors are auto-detected at startup (HiDPI/scaling mismatches); coordinate tools apply them transparently.

Typical Workflow

1. project_init("my-task", "Description")
2. take_screenshot()
3. ... perform VM operations ...
4. project_read_logs()
5. project_save_result("output.txt", data)
6. project_save_advice("Title", "Lessons learned...")

For continuing work:

1. project_list()
2. project_load("data/projects/...")  # Shows any saved advice
3. ... continue work ...

Best Practices for LLM Automation

These lessons were learned from real-world usage and help avoid common pitfalls.

1. Always Screenshot Before Actions

Before ANY interaction:

  1. take_screenshot()
  2. Analyze the image
  3. Identify current focus (which window/field is active)
  4. Only then proceed with actions

Never skip screenshots to "save time" - blind actions lead to errors.

2. Don't Trust Mouse Clicks for Focus

Clicking on a window/terminal does NOT reliably switch focus, especially in:

  • Nested environments (Citrix, remote desktop)
  • High-latency connections
  • Applications with multiple panels (VS Code, IDEs)

Use keyboard shortcuts instead:

[
  {"action": "press_keys", "keys": ["Ctrl", "Shift", "p"]},
  {"action": "wait", "seconds": 0.5},
  {"action": "type_text", "text": "Terminal: Focus Terminal"},
  {"action": "wait", "seconds": 0.3},
  {"action": "press_keys", "keys": ["Return"]},
  {"action": "wait", "seconds": 0.5}
]

Then take_screenshot() to verify before typing.

3. Required Wait Times

After This ActionWait Time
Opening Command Palette0.5s
Typing search text0.3s
Pressing Enter/Return0.5-1.0s
Command execution1.0-2.0s
Window/focus switch0.5s

Never rapid-fire actions - they may arrive out of order.

4. Use Batch Actions

Use run_actions() instead of separate tool calls to reduce latency and ensure ordering:

# Instead of 5 separate calls:
run_actions([
    {"action": "press_keys", "keys": ["Ctrl", "Shift", "p"]},
    {"action": "wait", "seconds": 0.5},
    {"action": "type_text", "text": "command"},
    {"action": "wait", "seconds": 0.3},
    {"action": "press_keys", "keys": ["Return"]}
])

5. SSH Scope Limitation

ssh_execute only reaches the first VM layer. For nested environments (VM → Citrix → Windows), use UI automation to type commands in the visible terminal.

6. Recovery Commands

ProblemSolution
Typed in wrong window (few chars)Escapeu (undo in Vim)
Multiple lines in wrong placeEscapeuuuuuuu
File corruptedEscape:e!Enter (reload)
VS Code revertCtrl+Shift+P → "Revert File"

7. Common Mistakes to Avoid

  1. Typing immediately after clicking terminal (focus may not have switched)
  2. Skipping screenshots to "save time"
  3. Using ssh_execute for nested environment commands
  4. Not waiting between actions
  5. Assuming focus switched without verification

Architecture

┌─────────────┐         SSH          ┌──────────────┐
│             │ ◄──────────────────► │              │
│  MCP Server │                      │   QEMU VM    │
│   (Host)    │                      │   (Linux)    │
│             │                      │              │
└──────┬──────┘                      └──────────────┘
       │                                    │
       │ MCP Protocol                       │
       │ (stdio)                            │
       │                                    │
       ▼                                    ▼
┌─────────────┐                      xdotool, scrot
│  LLM Client │                      X11 automation
│  (Claude)   │
└─────────────┘

Network topology:

┌────────────────────────────────────────────────────┐
│  Host (192.168.122.1)                              │
│  ┌──────────┐                                      │
│  │ virbr0   │◄── NAT bridge                        │
│  └────┬─────┘                                      │
│       │                                            │
│  ┌────┴─────┐                                      │
│  │ QEMU VM  │ 192.168.122.79                       │
│  │ (manjaro)│                                      │
│  └──────────┘                                      │
└────────────────────────────────────────────────────┘

UI Action Dispatch

All xdotool interactions are built from a small set of pure command builders (_type_cmd, _keys_cmd, _click_cmd, _move_cmd) so the shell command for an action is constructed in exactly one place. Each builder takes an already shlex.quote()d display string and returns the command to run on the VM; the builders also own input validation (key-name pattern, button map, click-count clamp) and the UTF-8 locale prefix for typing.

Two paths consume these builders:

  • Standalone tools (move_mouse, click, type_text, press_keys, wait) — individually exposed MCP tools with typed signatures and rich docstrings.
  • run_actions — the batch path. It dispatches through ACTION_HANDLERS, a {name: async handler} registry that is the single source of truth for which actions a batch supports. Each handler shares the signature async (app_ctx, display, action_dict) -> summary. Unknown action names raise and stop the batch (consistent with its "stops on first error" contract).
run_actions(actions)
      │  for each action
      ▼
ACTION_HANDLERS[name]  ──►  _act_*(app, display, action)
                                   │ uses
                                   ▼
                       _type_cmd / _keys_cmd / _click_cmd / _move_cmd
                                   │
                                   ▼
                             run_vm_cmd(ssh, …)  ──►  xdotool over SSH

Adding a new batch action: write a _act_<name>(app, display, action) handler (reusing or adding a _*_cmd builder) and add one entry to ACTION_HANDLERS. No changes to the dispatch loop are needed.

Project Structure

mcp-qemu-vm/
├── server.py           # Main MCP server (single file)
├── pyproject.toml      # Project metadata, ruff & pytest config
├── requirements.txt    # Python dependencies
├── .env.example        # Documented env var template
├── test_ssh_tools.py   # Unit tests (no-VM) + manual SSH smoke check
├── LICENSE             # MIT
├── data/
│   └── projects/       # Project folders
│       └── YYYYMMDD-HHMMSS_name/
│           ├── screenshots/
│           ├── logs/
│           ├── results/
│           └── advice/
└── README.md

License

Released under the MIT License — © 2026 Sergey Istomin.

Related