homelab-mcp-server
v0.2.0
Published
Tiered MCP server for Proxmox — least-privilege by default, every write backed up and revertible, fully audited
Downloads
145
Maintainers
Readme
homelab-mcp-server
Tiered MCP server for Proxmox — least-privilege by default, every write backed up and revertible, fully audited.
Designed by a human through a documented architecture-first process. Implemented by Claude against those specifications. See Design process.

Why this exists
Managing a homelab has always meant SSH sessions full of the same ceremony: ls your way to the right directory, cat the config to understand what's there, open nano to make a two-line change, echo something into a file you could've just edited directly. Every task requires you to be the relay — gathering context, executing instructions, pasting output back, iterating.
From the operator's perspective, the immediate improvement is that the relay role mostly disappears. You describe what you want done. The tools handle the context-gathering, the execution, and the verification. The filesystem crawling and nano sessions that used to fill an SSH session are replaced by a single conversation turn.
From the AI's perspective, the improvement is precision. Without tools, suggesting a config change means giving instructions that might be applied to the wrong file, typed incorrectly, or made without knowing what was already there. With tools, the read-before-write cycle is automatic: check the current state, make the targeted change, read it back to confirm. Tasks that used to require ten back-and-forth exchanges — "run this, paste the output, now try this" — collapse into one. The feedback loop that makes useful assistance possible closes entirely on the AI side rather than depending on the operator to close it manually.
The tier model addresses the trust side of this. Giving an AI assistant access to a production-ish server is a reasonable thing to be cautious about. The answer isn't to refuse the capability — it's to calibrate it. Start at observe, where Proxmox itself enforces the limits and no software bug or misbehaving prompt can exceed the token's privileges. Escalate to companion only when you need it. The audit log records every write regardless of tier, and the backup pipeline makes every change reversible. The design invites you to trust incrementally rather than all at once.
Tier model
The server runs at one of four tiers. Choose the lowest tier that covers what you need.
| Tier | Credentials | Enforced by | What it adds | |------|-------------|-------------|--------------| | observe (default) | API token (PVEAuditor role) | Proxmox RBAC | Read-only tools | | operate | API token (MCPOperate role) | Proxmox RBAC | Guest start / stop / restart | | companion | + root SSH key | MCP server | In-guest exec, file I/O, snapshots, log tailing, config history | | root | + acknowledgment flag | MCP server | Host shell, host file read/write |
observe and operate are enforced by Proxmox itself — a bug or injected prompt cannot exceed the token's privileges. companion and above are enforced by the MCP server's guardrails (registration filtering + denylist + confirm gate), which are tripwires rather than a sandbox.
Tools
observe
| Tool | Description |
|------|-------------|
| describe_homelab | Secret-redacted census of the node: guests, storage, network, services, Tailscale |
| health_check | Fixed-probe node health → ok / warn / crit per section (node, storage, guests, units, updates) |
| pct_list | List LXC containers and status |
| qm_list | List QEMU/KVM VMs and status |
| query_audit | Filter and summarize the local audit log |
operate (adds to observe)
| Tool | Description |
|------|-------------|
| guest_start | Start a VM or container |
| guest_stop | Stop a VM or container (confirm-gated) |
| guest_restart | Restart a VM or container |
companion (adds to operate)
| Tool | Description |
|------|-------------|
| pct_exec | Run a command inside an LXC container |
| pct_read_file / pct_write_file | Container file I/O via pct pull / pct push |
| qm_agent_ping | Check a VM's QEMU guest agent responsiveness |
| qm_exec | Run a command in a VM via the guest agent |
| qm_read_file / qm_write_file | VM file I/O via the guest agent |
| tail_log | Bounded, always-redacted journal or file tail (host or container) |
| diff_config | Preview a revert: current → backup diff |
| revert_file / list_backups | Restore a file from a local backup; list versions |
| snapshot_create / snapshot_list / snapshot_rollback / snapshot_delete | Server-managed (mcp-) guest snapshots |
| config_sweep | Hash-compare sweep of watched paths into a local git mirror; captures out-of-band edits |
root (adds to companion)
| Tool | Description |
|------|-------------|
| execute | Run a shell command on the Proxmox host |
| read_file | Read a file from the host filesystem (stat-gated, windowed) |
| write_file | Write a file on the host — full backup pipeline + audit on every write |
| list_directory | List a host directory |
Sample output
{
"schemaVersion": 1,
"ts": "2026-06-12T03:16:08.616Z",
"host": "10.0.0.10",
"depth": "summary",
"sections": {
"node": {
"version": "9.2.3",
"uptime": "up 1 week, 6 days, 23 hours, 42 minutes",
"cpu": 8,
"memBytes": 16413732864,
"memUsedBytes": 3904413696,
"load": [0.1, 0.07, 0.08],
"zpool": { "healthy": true, "detail": "no pools" }
},
"storage": [
{ "name": "local", "type": "dir", "active": true, "totalBytes": 100861726720, "usedBytes": 9079173120, "availBytes": 86611816448 },
{ "name": "local-lvm", "type": "lvmthin", "active": true, "totalBytes": 374538764288, "usedBytes": 143373437952, "availBytes": 231165325312 },
{ "name": "media-backup", "type": "dir", "active": true, "totalBytes": 3936820731904, "usedBytes": 261289168896, "availBytes": 3475475435520 }
],
"containers": [
{ "vmid": 100, "name": "adguard-dns", "status": "running" },
{ "vmid": 101, "name": "dockerBoss", "status": "running" }
],
"vms": [],
"services": [
{ "vmid": 101, "failedUnits": [], "docker": [
{ "name": "jellyfin", "image": "jellyfin/jellyfin:latest", "status": "Up 12 days (healthy)" },
{ "name": "sonarr", "image": "lscr.io/linuxserver/sonarr:latest", "status": "Up 12 days" },
{ "name": "radarr", "image": "lscr.io/linuxserver/radarr:latest", "status": "Up 12 days" },
{ "name": "prowlarr", "image": "lscr.io/linuxserver/prowlarr:latest", "status": "Up 12 days" },
{ "name": "qbittorrent", "image": "lscr.io/linuxserver/qbittorrent:latest", "status": "Up 12 days" },
{ "name": "gluetun", "image": "qmcgaw/gluetun:latest", "status": "Up 12 days (healthy)" },
{ "name": "jellyseerr", "image": "fallenbagel/jellyseerr:latest", "status": "Up 12 days" },
{ "name": "tailscale", "image": "tailscale/tailscale:latest", "status": "Up 12 days" },
{ "name": "portainer", "image": "portainer/portainer-ce:latest", "status": "Up 12 days" },
{ "name": "homepage", "image": "ghcr.io/gethomepage/homepage:latest", "status": "Up 22 hours (healthy)"},
{ "name": "watchtower", "image": "containrrr/watchtower:latest", "status": "Up 25 hours (healthy)"},
{ "name": "flaresolverr", "image": "ghcr.io/flaresolverr/flaresolverr:latest", "status": "Up 12 days" }
]}
]
},
"errors": [],
"redactions": 0
}Setup
Prerequisites: Node.js 20+, Claude Code, a Proxmox VE node on the same LAN.
git clone https://github.com/ethanblauw21/homelab-mcp-server
cd homelab-mcp-server
npm install && npm run buildThen run the setup ceremony for your platform:
Linux / macOS
./scripts/setup.shWindows
.\scripts\setup.ps1Both wrappers call the same Node.js setup script (scripts/setup.mjs). The ceremony walks you through the setup interactively — choose a tier, enter your node's address, pick a bootstrap mode:
- auto — one SSH root password prompt, then fully automated
- paste — prints a script to run in the Proxmox web shell (no root password touches your machine)
The ceremony then:
- Provisions
mcp@pvewith a tier-appropriate token on the node (idempotent — re-running changes the tier) - Captures both trust anchors: the API TLS certificate fingerprint and (at companion) the SSH host key fingerprint
- Runs a 403 negative test to confirm privilege separation is actually enforcing, not just configured
- Calls
claude mcp addto register thehomelabserver with Claude Code
Restart Claude Code when it completes.
All parameters can also be passed as flags for automated or repeated runs:
# Linux / macOS
./scripts/setup.sh --tier=observe --node-host=192.168.1.100
./scripts/setup.sh --tier=companion --node-host=192.168.1.100 --bootstrap-mode=paste
./scripts/setup.sh --tier=observe --node-host=192.168.1.100 --dry-run# Windows
.\scripts\setup.ps1 -Tier observe -NodeHost 192.168.1.100
.\scripts\setup.ps1 -Tier companion -NodeHost 192.168.1.100 -BootstrapMode paste
.\scripts\setup.ps1 -Tier observe -NodeHost 192.168.1.100 -DryRunClaude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"homelab": {
"command": "node",
"args": ["/absolute/path/to/homelab-mcp-server/dist/index.js"],
"env": {
"MCP_TIER": "observe",
"PVE_API_BASE_URL": "https://192.168.1.100:8006/api2/json",
"PVE_API_TOKEN_ID": "mcp@pve!mcp-observe",
"PVE_API_TOKEN_SECRET": "<secret from setup>",
"PVE_API_NODE": "proxmox",
"PVE_API_TLS_FINGERPRINT": "SHA256:<fingerprint from setup>"
}
}
}
}Upgrading tiers
Re-run the setup script at the new tier — it is idempotent. Downgrading from companion removes the SSH key from authorized_keys and deletes the local private key.
Root tier
Root is not selectable from the setup script by design. After a companion install, opt in by setting this exact env var in the registered MCP server configuration and restarting Claude Code:
MCP_HOST_ROOT_ENABLED=I-understand-Claude-gets-root-and-can-break-this-nodeAny other value (including true) is treated as disabled. There is no runtime escalation path.
Troubleshooting
If the server isn't connecting, run the pre-flight checker first:
npm run doctorIt checks Node version, the claude CLI, the built artifact, all required env vars, API reachability, and SSH connectivity. Most first-time issues surface here in under two seconds.
Architecture
The server uses a pure-core, injected-I/O design: guardrails, backup policy, audit record construction, and tier registry are all pure functions. Tool handlers depend on injected SshTransport and NodeOps interfaces and never touch the concrete SSH or HTTP clients directly. The API backend runs at every tier; the SSH backend handles companion+ operations. Both implement the same interface — the transport follows the tool, not the tier.
See ARCHITECTURE.md for the full design: hybrid transport, trust model, backup pipeline, audit log, config history, guardrails, and source layout.
Development
npm run build # tsc compile
npm run dev # tsx watch
npm test # all tests
npm run test:unit # unit tests only (fast, no Docker required)
npm run test:int # integration tests (requires Docker — Linux/CI only)
npm run lint
npm run typecheckIntegration tests spin up a Dockerized SSH container automatically and are skipped gracefully if Docker is absent. On Windows, unit tests are the local feedback loop.
See CONTRIBUTING.md for the ADR-first process, testing requirements, and what contributions are welcome.
Storage
All persistent data lives on the machine running the MCP server, not the Proxmox node:
| Data | Default location |
|------|-----------------|
| Backups | %LOCALAPPDATA%\claude-mcp\backups\ (Windows) / ~/.local/share/claude-mcp/backups/ |
| Audit log | %LOCALAPPDATA%\claude-mcp\audit.jsonl |
| Config history | %LOCALAPPDATA%\claude-mcp\config-history\ |
Point MCP_BACKUP_DIR at a synced folder or NAS for extra durability.
Security
The tier model is the primary defense: observe and operate are enforced by Proxmox RBAC, so no bug or injected prompt can exceed the token's privileges. At companion and above, the MCP server's guardrails take over — denylist, confirm gate, protected set, pinned trust, and an immutable audit trail.
See SECURITY.md for the full threat model, each layer of defense, and root tier guidance.
Design process
This project was built architecture-first. The design was specified through seven Architecture Decision Records before implementation began. Each ADR documents the decisions, constraints, and rationale for a slice of the system — the code was written to satisfy the spec, not the other way around.
| ADR | Scope |
|-----|-------|
| ADR-001 | SSH transport, initial tool surface, guardrails, testing strategy |
| ADR-002 | Census tool: redaction, drift detection, tier-aware sections |
| ADR-003 | Container file I/O, backup pipeline, snapshot guard |
| ADR-004 | Transport hardening: pinned trust, timeout enforcement, denylist, audit |
| ADR-005 | VM parity: qm_* tools, health check, log tailing, audit forensics |
| ADR-006 | Git-backed config history: mutation commits, config sweep, push modes |
| ADR-007 | Permission tiers, hybrid transport, root flag, protected set |
The ADRs are the authoritative specification — CLAUDE.md and the testing strategy reference them, not the reverse. The tier model, trust model, backup pipeline design, and guardrail architecture were all reasoned through in writing before a line of implementation existed.
Ethan designed and architected the system. Claude implemented the code against those specifications. The co-author attribution on commits reflects who wrote the implementation; the ADR corpus reflects who made the design decisions. These are different contributions, and the distinction is intentional.
See ROADMAP.md for planned work and known deferred items from the ADR series.
