A unified MCP server for driving VMs. An AI agent uses it to create a VM, see its screen, click and type, move files, and clone repos. The agent does not need to know what runs inside the VM. It sees one interface: a VM it created, viewed remotely.
One server. Many OS adapters. The server presents 22 vm_* tools. Each tool works the same way no matter what runs inside the VM: Hyprland, X11, Windows, macOS, or Android.
The server hides the machinery. The agent never touches Proxmox, never writes SSH config, never learns the difference between a Wayland compositor and a Windows desktop. It asks for a VM, waits for it to be ready, looks at the screen, acts, and releases the lease.
| Term | Meaning |
|---|---|
| Adapter | A module that translates vm_* tool calls into OS-specific actions (clicking, typing, screenshots). One adapter per OS family: Hyprland, X11, Windows, macOS, Android. |
| Golden image | A pre-built VM template in Proxmox. vmhub clones golden images to create working VMs. The agent never builds VMs from scratch. |
| Stub | An adapter that declares zero capabilities. It exists so the tool surface stays complete: every tool is present, but a stub returns CAPABILITY_UNAVAILABLE for tools it cannot serve. |
| Lease | A time-limited claim on a VM. Created by the agent, destroyed by the reaper when it expires or the agent releases it. |
| Reaper | The independent process that destroys expired leases and their VMs. It reads the database directly and works even when the control plane is down. |
| Control plane (lite) | The REST server that manages leases, templates, and VM state. Binds localhost in v1. |
| MCP server | The stdio server that agents talk to. It exposes the 22 vm_* tools and delegates to adapters and the control plane. |
| Proxmox API token | A scoped credential for the Proxmox REST API. vmhub uses tokens, never root passwords. |
| CursorTouch | An in-VM MCP server for Windows. The Windows adapter talks to it over HTTP to control the Windows desktop. |
| Identity tag | A Proxmox tag in the format vmhub-<prefix>-<uuid>. Used for identity-verified teardown. |
Five steps from zero to a running vmhub.
# Bun (JavaScript runtime)
curl -fsSL https://bun.sh/install | bash
# Doppler (secrets manager) — required for deploy/install.sh
curl -fsSL https://get.doppler.com | sh
doppler logingit clone https://github.com/Keylessboi/vmhub.git
cd vmhub
bun install
bun run build # compiles dist/vmhub-mcp
bun run build:lite # compiles dist/vmhub-lite
bun run build:reaper # compiles dist/vmhub-reaperCopy the example env and fill in values:
cp .env.example .env
# Edit .env with your Proxmox host, token, etc.Critical: PVE_HOST must include the port. Proxmox listens on 8006, not 443.
PVE_HOST=192.168.1.220:8006
For single-node setups, set three variables: PVE_HOST, PVE_TOKEN_ID, and PVE_TOKEN. Leave the VMHUB_NODE_* variables blank.
# Start the control plane (REST API, SQLite)
dist/vmhub-lite &
# Start the MCP server (stdio, talks to lite)
dist/vmhub-mcpAdd the vmhub MCP entry to your opencode.json. The file lives in your project root or ~/.config/opencode/.
Environment variables:
| Variable | Required | Default | Notes |
|---|---|---|---|
VMHUB_LITE_URL |
Yes | http://127.0.0.1:8787 |
Where the MCP server finds lite |
VMHUB_SCREENSHOT_DIR |
No | ~/Pictures/vmhub |
Where screenshots are saved |
CURSORTOUCH_AUTH_KEY |
For Windows VMs | (empty) | Auth key for the in-VM CursorTouch server |
Verify it works:
Ask your agent to call vm_list_templates. If templates appear with capabilities, the connection is live.
| Tool | Purpose |
|---|---|
vm_list_templates |
What VMs can I create, and what can each one do? |
vm_capabilities |
What can this specific VM do? |
vm_health |
Is the server and its adapters alive? |
vm_lease_create |
Create a VM from a template. Returns immediately. |
vm_lease_status |
Poll readiness. Bounded wait. |
vm_lease_renew |
Push the expiry forward. |
vm_lease_release |
Tear down the VM and its secrets. |
vm_screenshot |
See the screen. Returns an image plus coordinates. |
vm_inspect |
Read the semantic element tree (for text-only models). |
vm_list_windows |
List open windows. |
vm_click / vm_type / vm_key / vm_paste / vm_drag |
Drive input. |
vm_launch / vm_focus / vm_close |
Manage apps and windows. |
vm_dispatch |
Validated escape hatch per adapter. |
vm_put_file / vm_get_file |
Move files in and out. |
vm_clone_repo |
Clone a repository into the VM. |
Each adapter declares what it can do. The server reads that declaration and answers two questions:
- Before creation:
vm_list_templatesshows each template's capabilities and availability. - At runtime:
vm_capabilitiesshows what the live VM can do.
A tool is never missing. If an adapter cannot serve a tool, the tool returns a typed CAPABILITY_UNAVAILABLE error. A stub adapter is visible as a stub, with a reason. Nothing is hidden.
src/mcp/— the unified MCP server. The only interface agents see.src/lite/— the control plane. Eight REST endpoints, SQLite state, Proxmox client.src/reaper/— the independent lease reaper. It runs on its own timer, reads its own database, and destroys expired VMs by identity tag. It does not depend on the control plane being alive. That independence is the point: VMs die even when the server that made them is dead.adapters/— one module per OS family. Each implements the sameDesktopAdaptercontract.src/shared/— the capability contract. All parts import from here.
- One runtime credential: a scoped Proxmox API token in
.env. Never the root password. Never in git. - Per-lease secrets: SSH keys and GitHub tokens are created for one VM, injected at clone time, and destroyed at teardown. They never live in golden images.
- Identity-verified teardown: the reaper matches the Proxmox tag
vmhub-<prefix>-<uuid>. It never trusts a VMID. - No secrets in goldens. No public ports. The control plane binds localhost in v1.
vmhub supports two credential layouts for backward compatibility:
Single-node (most users): set three variables.
| Variable | Purpose |
|---|---|
PVE_HOST |
Proxmox host with port, e.g. 192.168.1.220:8006 |
PVE_TOKEN_ID |
Token ID, e.g. vmhub@pve!automation |
PVE_TOKEN |
Token secret value |
Multi-node (multiple Proxmox hosts): set VMHUB_NODES plus per-node variables.
| Variable | Purpose |
|---|---|
VMHUB_NODES |
Comma-separated node IDs, e.g. DL360P,DL380G9 |
VMHUB_NODE_<ID>_BASE_URL |
Per-node host:port |
VMHUB_NODE_<ID>_TOKEN |
Per-node API token |
Precedence: per-node tokens win over PVE_TOKEN for their node. PVE_TOKEN serves as fallback for the default node. See docs/setup.md for details.
bun install
bun test # 76 tests
bun run typecheck
bun run build # compiles dist/vmhub-mcp
bun run build:lite
bun run build:reaperStart the control plane, then point an MCP client at the compiled server:
dist/vmhub-lite # http://127.0.0.1:8787
dist/vmhub-mcp # stdio MCP serverWire into opencode (see Quickstart above for full setup):
{
"mcp": {
"vmhub": {
"type": "local",
"command": ["/path/to/vmhub/dist/vmhub-mcp"],
"environment": {
"VMHUB_LITE_URL": "http://127.0.0.1:8787",
"VMHUB_SCREENSHOT_DIR": "/home/you/Pictures/vmhub",
"CURSORTOUCH_AUTH_KEY": "your-key"
},
"timeout": 30000
}
}
}CURSORTOUCH_AUTH_KEY: Required only for Windows VMs. This key authenticates the Windows adapter to the in-VM CursorTouch MCP server. Without it, Windows templates return a CAPABILITY_UNAVAILABLE error. For local setups, generate a random string and configure the same value inside the Windows golden image. For production, use a secrets manager (Doppler, Vault, etc.). See the glossary for details.
Phase 1–2 complete: shared contract, control plane (mock Proxmox), reaper with kill-test, and the unified MCP with a live Hyprland adapter. Phase 3 (real Proxmox, CursorTouch in-VM adapters, Android ADB, GitHub App tokens) is the server-integration milestone.
docs/architecture.mddocs/security.mddocs/adapters.mddocs/setup.md
MIT
{ "mcp": { "vmhub": { "type": "local", "command": ["/path/to/vmhub/dist/vmhub-mcp"], "environment": { "VMHUB_LITE_URL": "http://127.0.0.1:8787", "VMHUB_SCREENSHOT_DIR": "/home/you/Pictures/vmhub", "CURSORTOUCH_AUTH_KEY": "your-key-here" }, "timeout": 30000 } } }