boxes-mcp
# boxes-mcp
[](.)
[](.)
[](LICENSE)
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](#installation)
- [Configuration](#configuration)
- [Available tools](#available-tools)
- [Usage examples](#usage-examples)
- [Development](#development)
- [Troubleshooting](#troubleshooting)
- [Security considerations](#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) and `qemu:///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 `libvirt` and `kvm` groups
- `virsh` available on `PATH` for 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:
```bash
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.
```bash
# 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.
```bash
# 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:
```bash
npx -y boxes-mcp@2.0.0 setup --dry-run
```
Configure one host explicitly when it is not discoverable on `PATH`:
```bash
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:
```bash
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`):
```bash
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
```bash
# 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`):
```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|libvirt` |
| `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
```bash
# 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
```bash
# 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% statements
- `libvirt.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:
```bash
npm run test:spice-helper
```
Run the disposable-VM suite only with all three safety variables set:
```bash
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
```bash
# 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:
```bash
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 `execFile` with 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_ROOT` after 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:
```json
{
"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
```bash
# 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
```bash
# 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:
```bash
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:
```bash
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:
```bash
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:
```bash
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-install` integration
- [ ] 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](CONTRIBUTING.md) for guidelines.
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Run tests (`npm test`)
4. Commit changes (`git commit -m 'Add amazing feature'`)
5. Push to branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Acknowledgments
- Built for [Claude Code](https://claude.com/claude-code)
- Uses [Model Context Protocol SDK](https://github.com/anthropics/mcp)
- Integrates with [libvirt](https://libvirt.org/) virtualization API
## Support
- **Issues**: [GitHub Issues](https://github.com/EF-Code/boxes-mcp/issues)
- **Discussions**: [GitHub Discussions](https://github.com/EF-Code/boxes-mcp/discussions)
- **Documentation**: [Project Wiki](https://github.com/EF-Code/boxes-mcp/wiki)
---
**Made with ā¤ļø for the Claude Code community**
TDQS
Scored across 19 tools
Most tools map cleanly to a distinct resource+action (e.g., snapshots.list, snapshots.revert). The only potential confusion is `boxes.capabilities`, which could overlap with `boxes.info` (state info) and `boxes.display` (screen state) for an agent. The input/interaction tools (keyboard, mouse, clipboard, screenshot) are clearly separated by modality.
Top-level lifecycle tools use consistent single verbs (start, shutdown, reboot, suspend, resume, undefine), but interaction tools shift to bare nouns (keyboard, mouse, clipboard, screenshot, capabilities). Snapshot nesting is consistent and clear. Mixing namespace prefixes (boxes.* and boxes.snapshots.*) with singular noun tools creates minor inconsistency in expected verb-phrase form.
19 tools is slightly above the typical sweet spot but justified given the breadth: 5 lifecycle, 4 snapshot, 2 info, and 8 interaction/display tools. The interaction sub-surface is granular (keyboard, mouse, clipboard, drag_drop, capabilities) and could be consolidated, but each maps to a real distinct operation. It never feels bloated enough to warrant a 3.
Lifecycle operations are well-covered for read/start/stop, snapshots have full CRUD with revert, and interaction features (screenshot, input, clipboard) are thorough. Missing pieces include VM creation/clone, device/storage management, and define/undefine pairing is lopsided (only undefine). Snapshot and interaction workflows feel complete for the stated GNOME Boxes/libvirt scope.