npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

boxes-mcp

v2.0.0

Published

A local MCP server for safe, observable GNOME Boxes and libvirt VM operation

Readme

boxes-mcp

Tests TypeScript License: MIT

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

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:

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 [email protected] setup

# Or install the command globally
npm install --global [email protected]
boxes-mcp setup

Preview configuration without writing files:

npx -y [email protected] setup --dry-run

Configure one host explicitly when it is not discoverable on PATH:

npx -y [email protected] setup --client codex
npx -y [email protected] setup --client claude
npx -y [email protected] 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 [email protected] 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 [email protected] 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|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

# 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% 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:

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 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:

{
  "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-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 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 file for details.

Acknowledgments

Support


Made with ❤️ for the Claude Code community