boxes-mcp
Enables managing GNOME Boxes virtual machines through libvirt/virsh, including lifecycle operations, snapshots, screenshots, and capability-gated keyboard, mouse, clipboard, and drag-and-drop interactions.
README
boxes-mcp
A local Model Context Protocol (MCP) server that enables compatible agents and developer harnesses to manage GNOME Boxes and virt-manager virtual machines through libvirt/virsh. It provides safe, reversible VM operations, snapshots, screenshots, bounded keyboard and mouse input, and capability-gated SPICE features.
The project intentionally targets GNOME Boxes' Linux libvirt/QEMU stack. VMware and VirtualBox are not currently supported; their display, input, guest-agent, clipboard, and drag/drop APIs have different trust and capability contracts and should be added as separate, evidence-backed providers rather than inferred from the libvirt implementation.
Contents
- Installation
- Configuration
- Available tools
- Usage examples
- Development
- Troubleshooting
- Security considerations
Features
- š„ļø VM Lifecycle Management - Start, stop, reboot, suspend, and resume VMs
- šø Snapshot Operations - Create, list, revert, and delete VM snapshots
- š VM Discovery - List and inspect all VMs with detailed information
- š Dual Connection Support - Manages both
qemu:///system(virt-manager) andqemu:///session(GNOME Boxes) simultaneously; per-domain operations automatically route to the owning connection - š Safe Operations - Storage preservation by default, no destructive actions
- šÆ GNOME Boxes Compatible - Works seamlessly with GNOME Boxes VMs
- š±ļø Controlled Interaction - Screenshot, allowlisted keyboard, and typed mouse tools
- š Capability-Gated SPICE - Optional native helper protocol for SPICE input, clipboard, and transfer
- ā” Fast & Lightweight - Minimal overhead, direct virsh integration
Installation
Host prerequisites
- Ubuntu 22.04/24.04 (or compatible Linux distribution)
- libvirt-daemon-system, qemu-kvm installed
- Node.js 18+ and npm
- User in
libvirtandkvmgroups virshavailable onPATHfor lifecycle, screenshot, keyboard, and QMP fallback operations
SPICE-backed tools additionally require a SPICE display, a guest virtio-serial agent
channel, and a running spice-vdagent (or equivalent guest agent). Clipboard support
also depends on the guest desktop integration supplied by that agent. The standard
spice-vdagent session component is X11-oriented; a Wayland/Hyprland guest may have
the package and service running while still reporting capability-missing for
clipboard. Build the optional
native helper only when the host provides spice-client-glib, json-glib, and GLib
development files. For libvirt domains whose graphics XML uses listen type='none',
the helper uses libvirt's local graphics-FD API; no remote-viewer, virt-viewer, or
public SPICE URI is required:
npm run build:spice-helper
BOXES_SPICE_HELPER="$PWD/native/boxes-spice-helper" npm test
The helper is not installed or selected automatically. Set BOXES_SPICE_HELPER only
to the reviewed executable built from this repository or another process implementing
the versioned protocol below.
# Install dependencies
sudo apt install -y libvirt-daemon-system qemu-kvm virt-manager
# Add your user to required groups
sudo usermod -aG libvirt,kvm "$USER"
newgrp libvirt
Install from npm
The npm package includes a guided installer for local MCP hosts. It installs the Node
server only; libvirt, virsh, QEMU, and optional SPICE development libraries remain
host prerequisites.
# Detect installed MCP hosts and configure them
npx -y boxes-mcp@2.0.0 setup
# Or install the command globally
npm install --global boxes-mcp@2.0.0
boxes-mcp setup
Preview configuration without writing files:
npx -y boxes-mcp@2.0.0 setup --dry-run
Configure one host explicitly when it is not discoverable on PATH:
npx -y boxes-mcp@2.0.0 setup --client codex
npx -y boxes-mcp@2.0.0 setup --client claude
npx -y boxes-mcp@2.0.0 setup --client openclaw
The installer detects or can explicitly configure Codex, Claude Code, OpenClaw,
Antigravity, Gemini CLI, OpenCode, Cursor, Windsurf, VS Code, Pi, Cline, Zed, and
Goose. Use --client generic to print a portable JSON configuration for another
stdio-capable agent:
npx -y boxes-mcp@2.0.0 setup --client generic
The setup command writes only the selected MCP entry, creates a one-time
.boxes-mcp.bak backup before changing an existing config, uses atomic replacement,
and never installs operating-system packages or changes VM definitions. Restart the
configured agent or harness after setup. Run boxes-mcp doctor to inspect Node,
virsh, and detected hosts.
Optional host settings can be persisted during setup (--libvirt-uri accepts a
single URI or a comma-separated list, e.g. qemu:///system,qemu:///session):
npx -y boxes-mcp@2.0.0 setup \
--libvirt-uri qemu:///system,qemu:///session \
--input-backend auto \
--spice-helper /absolute/path/to/native/boxes-spice-helper \
--transfer-root /absolute/path/to/approved/files
The native SPICE helper is not bundled as a universal binary. Build it on a compatible
Linux host after installing the host's SPICE/libvirt development packages, then pass
its reviewed absolute path with --spice-helper or BOXES_SPICE_HELPER.
Install from source
# Clone the repository for unreleased changes or development
git clone https://github.com/EF-Code/boxes-mcp.git
cd boxes-mcp
# Install dependencies
npm install
# Build the project
npm run build
# Run tests
npm test
# Configure a local checkout with the same guided installer
npm run setup:guided -- --client codex
Configuration
For a manual setup, add the server to your Claude Code config (~/.claude.json):
{
"mcpServers": {
"boxes": {
"command": "node",
"args": ["/absolute/path/to/boxes-mcp/dist/src/index.js"],
"env": {
"LIBVIRT_URI": "qemu:///system,qemu:///session",
"BOXES_INPUT_BACKEND": "auto"
}
}
}
}
Available Tools
VM Management
| Tool | Description | Parameters |
|---|---|---|
boxes.list |
List all VMs | - |
boxes.info |
Get VM details | nameOrUuid: string |
boxes.start |
Start a VM | nameOrUuid: string |
boxes.shutdown |
Shutdown VM (graceful) | nameOrUuid: string, force?: boolean |
boxes.reboot |
Reboot a VM | nameOrUuid: string |
boxes.suspend |
Suspend a VM | nameOrUuid: string |
boxes.resume |
Resume suspended VM | nameOrUuid: string |
boxes.undefine |
Remove VM (keeps storage) | nameOrUuid: string, keepStorage?: boolean |
boxes.display |
Get SPICE/VNC address | nameOrUuid: string |
Snapshot Management
| Tool | Description | Parameters |
|---|---|---|
boxes.snapshots.list |
List VM snapshots | nameOrUuid: string |
boxes.snapshots.create |
Create snapshot | nameOrUuid: string, snapshot: string, description?: string |
boxes.snapshots.revert |
Revert to snapshot | nameOrUuid: string, snapshot: string |
boxes.snapshots.delete |
Delete snapshot | nameOrUuid: string, snapshot: string |
Display and interaction
| Tool | Description | Parameters |
|---|---|---|
boxes.screenshot |
Capture a running domain display as MCP image content | `nameOrUuid, screen?: number, backend?: auto |
boxes.keyboard |
Send a bounded allowlisted Linux key sequence through virsh | nameOrUuid, keys: string[], holdMs?: number |
boxes.mouse |
Send typed move/button/click/scroll input | nameOrUuid, action, x, y, coordinateSpace?, button?, width?, height?, deltaX?, deltaY?, backend? |
boxes.clipboard |
Explicit UTF-8 clipboard read/write through the SPICE helper | nameOrUuid, operation, selection?, text? |
boxes.drag_drop |
Experimental confined transfer plus pointer sequence and separate evidence | nameOrUuid, sourcePath, x, y, coordinateSpace?, width?, height?, timeoutMs? |
Interaction tools never accept shell fragments, raw QMP JSON, arbitrary virsh flags, guest commands, or arbitrary transfer destinations. New operations require a running domain and return a stable capability/error code when their backend is not available.
Optional environment variables
| Variable | Default | Purpose |
|---|---|---|
LIBVIRT_URI |
qemu:///system,qemu:///session |
Comma-separated list of libvirt connections. Every URI is scanned by boxes.list, and per-domain operations resolve the owning connection automatically. Set a single URI to restrict the server to one connection. |
BOXES_INPUT_BACKEND |
auto |
Default mouse backend preference: auto, spice, or qmp |
BOXES_SPICE_HELPER |
unset | Explicit executable implementing the versioned SPICE helper protocol |
BOXES_SPICE_OPERATION_TIMEOUT_MS |
30000 |
Maximum one helper request duration |
BOXES_ARTIFACT_DIR |
process temp directory | Controlled parent directory for temporary screenshots |
BOXES_MAX_SCREENSHOT_BYTES |
20971520 |
Screenshot payload limit |
BOXES_TRANSFER_ROOT |
unset | Required canonical host root for drag/drop source files |
BOXES_MAX_TRANSFER_BYTES |
104857600 |
Transfer source size limit |
BOXES_MAX_CLIPBOARD_BYTES |
1048576 |
UTF-8 clipboard payload limit |
BOXES_TRANSFER_ROOT is deliberately required rather than inferred. Paths are
canonicalized and symlink escapes, directories, and special files are rejected.
boxes.capabilities reports observed states. Configuration alone is not treated as
connected: use probeQmp: true and/or probeSpice: true when an external status
probe is required. SPICE clipboard and transfer require a connected guest agent;
boxes.drag_drop reports applicationAccepted: "unknown" unless an external viewer
harness supplies application-level evidence.
Keyboard input uses one fixed Linux virsh codeset. Public key names are
case-insensitive and canonicalized to uppercase, but each key may occur only once
per bounded chord. The allowlist is: ALT, BACKSPACE, CAPSLOCK, CTRL,
DELETE, DIGIT_0 through DIGIT_9, DOWN, END, ENTER, ESC, ESCAPE,
F1 through F12, HOME, INSERT, LEFT, META, NUMLOCK, PAGEDOWN,
PAGEUP, PAUSE, PRINT, RIGHT, SHIFT, SPACE, SUPER, TAB, UP,
and A through Z. Guest keyboard layout determines the resulting character;
the key allowlist does not guarantee text independent of that layout.
Usage Examples
With Claude Code
User: "List all my VMs"
Claude: [Uses boxes.list tool]
User: "Start ubuntu-24.04"
Claude: [Uses boxes.start with nameOrUuid="ubuntu-24.04"]
User: "Create a snapshot called 'before-update' for my fedora VM"
Claude: [Uses boxes.snapshots.create]
Direct Usage
# Run the MCP server
LIBVIRT_URI=qemu:///system node dist/src/index.js
Development
Project Structure
boxes-mcp/
āāā src/
ā āāā index.ts # MCP server entry point
ā āāā tools.ts # Side-effect-free tool registry and handler boundary
ā āāā libvirt.ts # virsh operations & parsers
ā āāā virsh.ts # Shared executable and libvirt URI arguments
ā āāā exec.ts # Safe command execution
ā āāā screenshot.ts # Controlled libvirt screenshot capture
ā āāā keyboard.ts # Allowlisted virsh send-key adapter
ā āāā mouse.ts/qmp.ts # Typed mouse actions and QMP fallback
ā āāā spice.ts # Versioned companion-helper protocol client
ā āāā clipboard.ts # Explicit SPICE clipboard orchestration
ā āāā transfer.ts # Confined host-file validation
ā āāā drag-drop.ts # Experimental transfer/input coordination
ā āāā *.test.ts # Unit tests
āāā systemd/
ā āāā boxes-mcp.service # Systemd user service
āāā dist/ # Compiled JavaScript
āāā coverage/ # Test coverage reports
āāā package.json
āāā tsconfig.json
āāā vitest.config.ts
Testing
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Generate coverage report
npm run test:coverage
Local test coverage: the current checkout runs 95 passing tests and 9 gated live tests skipped by default. The default suite is safe to run without libvirt access.
exec.ts: 100% statementslibvirt.ts: 81.3% statements, 92.85% branches- Interaction validation, command construction, QMP response mapping, artifact cleanup, helper framing, capability discovery, and path-confinement tests
Run the explicit local native-helper process checks with:
npm run test:spice-helper
Run the disposable-VM suite only with all three safety variables set:
BOXES_INTEGRATION=1 \
BOXES_TEST_VM=an-explicit-disposable-domain \
BOXES_TEST_VM_DISPOSABLE=1 \
npm run test:integration
The live suite never selects a listed VM, changes VM definitions, or stops a guest
service itself. Guest-agent disconnect coverage requires the operator to manually
disconnect spice-vdagent in the explicitly disposable guest and add
BOXES_TEST_AGENT_DISCONNECTED=1; never do this to a non-disposable guest.
The default suite is mocked/local: it does not prove that QMP, SPICE, clipboard, or drag-and-drop works against a real VM. Live tests must be opt-in and target a specifically named disposable VM with snapshots; no arbitrary first-listed domain is ever selected by the interaction tools.
Building
# Build TypeScript
npm run build
# Watch mode for development
npm run dev
Optional systemd user service
The checked-in unit is intended for a source checkout. It is not needed when the server is launched by an agent's MCP configuration or installed globally with npm. Install it as a user service for automatic startup after building the checkout:
BOXES_MCP_DIR="$(pwd)"
NODE_BIN="$(command -v node)"
mkdir -p ~/.config/systemd/user
cp systemd/boxes-mcp.service ~/.config/systemd/user/
sed -i \
-e "s|/usr/bin/node|$NODE_BIN|g" \
-e "s|%h/projects/boxes-mcp|$BOXES_MCP_DIR|g" \
~/.config/systemd/user/boxes-mcp.service
systemctl --user daemon-reload
systemctl --user enable --now boxes-mcp
journalctl --user -fu boxes-mcp
Security Considerations
- ā
Sandboxed Execution: Uses Node.js
execFilewith timeout and buffer limits - ā No Arbitrary Commands: Only predefined virsh operations allowed
- ā Typed Input Boundary: QMP commands and SPICE operations are internal enums with validated arguments
- ā Bounded Payloads: Key counts, hold durations, coordinates, scroll deltas, screenshots, clipboard, and transfers are capped
- ā
Path Confinement: Drag/drop sources must remain beneath
BOXES_TRANSFER_ROOTafter canonicalization - ā Storage Preservation: VM storage not deleted by default
- ā LIBVIRT_URI Isolation: Respects environment-specified libvirt connections; per-domain operations resolve the owning connection before acting, so a same-named domain on another connection is never targeted by mistake
- ā ļø Permissions Required: User must have libvirt group membership
- ā ļø Network Exposure: Not designed for remote access without additional security
- ā ļø Expanded Control Surface: Screenshots and guest clipboard data are untrusted; keep the MCP server on local stdio
- ā ļø SPICE Helper Trust: The helper executable is an explicit host dependency and must not log credentials, clipboard contents, or file contents
SPICE helper protocol
The TypeScript server starts one persistent helper child and sends newline-delimited version-1 JSON requests over stdin, correlating responses by request ID. The helper is called with an explicit executable path and no caller-controlled arguments. The request envelope is shaped like:
{
"version": 1,
"id": "request-123",
"operation": "clipboard.read",
"domain": "guest-name",
"display": { "uri": "spice://127.0.0.1:5900" },
"arguments": { "selection": "clipboard", "maxBytes": 1048576 },
"libvirtUri": "qemu:///session"
}
libvirtUri is optional and carries the resolved libvirt connection for the target
domain; when present, the helper prefers it over its own LIBVIRT_URI environment
value. This keeps libvirt-fd transport working when the server is configured with
multiple comma-separated connections.
Supported operation names are internal (status, mouse, clipboard.read,
clipboard.write, file.transfer, and drag-drop). A helper error is mapped to a
stable MCP error such as SPICE_AGENT_DISCONNECTED, SPICE_CAPABILITY_MISSING, or
SPICE_UNAVAILABLE. Payloads, lines, pending requests, transfer sizes, clipboard
bytes, and operation time are bounded. Progress events never complete a request.
The helper does not log clipboard contents, file contents, SPICE tickets, or
credentials.
Capability matrix
| Capability | Libvirt/virsh | QMP fallback | SPICE helper |
|---|---|---|---|
| Screenshot | Implemented via virsh screenshot |
Not used | Adapter reserved, unavailable without helper |
| Keyboard | Implemented via allowlisted virsh send-key |
Not used | Not used |
| Mouse | Not used | Typed input-send-event after QMP discovery |
Selected by auto only after helper status proves channels and geometry |
| Clipboard | Not available | Not available | Real agent protocol in native helper; Wayland/Hyprland guests may report SPICE_CAPABILITY_MISSING |
| File transfer | Not available | Not available | Real SPICE async file-copy path in native helper; live transport completion observed when the guest agent advertises it |
| Drag-and-drop | Not available | Not available | Experimental transfer + pointer evidence; application acceptance remains unknown |
Clipboard support depends on guest desktop integration. The current SPICE guest
agent is X11-oriented, so Wayland guests such as Hyprland/Omarchy may report
SPICE_CAPABILITY_MISSING even when spice-vdagent is installed and running. Mouse
and file transfer can still work independently.
Troubleshooting
No VMs Listed
# Check libvirt URI
virsh -c qemu:///system list --all
virsh -c qemu:///session list --all
# Verify permissions
groups # Should include 'libvirt' and 'kvm'
Permission Denied
# Re-add to groups and re-login
sudo usermod -aG libvirt,kvm "$USER"
# Then logout/login or:
newgrp libvirt
VMs Not Showing in Boxes
By default the server scans both the system connection (virt-manager) and the user
session connection (GNOME Boxes), and boxes.list reports which connection each
domain was found on. Verify what each connection sees:
virsh -c qemu:///system list --all
virsh -c qemu:///session list --all
To restrict the server to specific connections, set LIBVIRT_URI to one URI or a
comma-separated list:
LIBVIRT_URI=qemu:///session node dist/src/index.js # GNOME Boxes only
LIBVIRT_URI=qemu:///system,qemu:///session node dist/src/index.js # both (default)
Domain names may contain spaces (for example Kali Live); they are parsed and
resolved correctly.
SPICE capability errors
If virsh domdisplay reports No graphical display found and the domain XML has
<graphics type='spice'><listen type='none'/></graphics>, that is an intentional
libvirt configuration with no public listener. Do not invent a port or change the VM
definition just to obtain a viewer URI. With the native helper configured, boxes-mcp
uses the internal spice+libvirt-fd://local transport and asks libvirt for a graphics
FD for each SPICE channel. The helper must use the same libvirt connection as the MCP
process:
LIBVIRT_URI=qemu:///session npm run build:spice-helper
BOXES_SPICE_HELPER="$PWD/native/boxes-spice-helper" \
LIBVIRT_URI=qemu:///session node dist/src/index.js
The domain must be running, the helper must be linked against libvirt and
spice-client-glib, and the guest must expose the virtio SPICE agent channel. A
connected agent may still lack clipboard capability; inspect boxes.capabilities with
probeSpice: true instead of inferring support from the XML alone.
Use boxes.capabilities with probeSpice: true and inspect the returned state:
configured: a reviewed helper and SPICE endpoint are configured, but connection proof has not been requested;connecting: the helper observed an incomplete channel set;connected: the required channels are connected;agent-disconnected: the guest agent is not connected;capability-missing: the backend, channel, helper, or guest capability is absent.
For example, a connected guest agent that supports file transfer but does not announce
clipboard is capability-missing, not agent-disconnected. To enable clipboard, the
guest must have its distribution's spice-vdagent service installed, running in the
desktop session, and connected through the virtio SPICE agent channel. On a
Wayland/Hyprland desktop, verify that the distro's agent actually supports that
compositor; an active service alone is not proof. The live Omarchy guest had
spice-vdagent 0.23.0-1 and an active user service, but logged xrandr output ID NOT FOUND and no owner for org.gnome.Mutter.DisplayConfig, so boxes-mcp correctly
returned SPICE_CAPABILITY_MISSING. Use an X11 guest session for the current
upstream agent, or provide a separately validated Wayland clipboard bridge. The
server does not install guest packages or start guest services automatically.
The persistent SPICE client also accepts an abort signal. Cancellation terminates the
current helper process, fails all pending operations deterministically, and allows the
next request to create a clean session; this is reported as OPERATION_CANCELLED.
Check the host dependencies and helper directly without sending input to a VM:
pkg-config --modversion spice-client-glib-2.0 json-glib-1.0 gio-unix-2.0
npm run build:spice-helper
The helper's local protocol test intentionally connects to 127.0.0.1:1 and
expects a typed unavailable/disconnected result. That is not live SPICE proof.
Roadmap
- [ ] VM creation via
virt-installintegration - [ ] Network management (
virsh net-list, port forwarding) - [ ] Storage pool information (
virsh vol-list) - [ ] VM import from OVA/QCOW2
- [ ] Remote libvirt connection support
- [ ] Performance metrics and monitoring
Contributing
Contributions welcome! Please read CONTRIBUTING.md for guidelines.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Run tests (
npm test) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- Built for Claude Code
- Uses Model Context Protocol SDK
- Integrates with libvirt virtualization API
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Project Wiki
Made with ā¤ļø for the Claude Code community
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.