Infrastructure AI Agents

Giving Claude Scoped, Read-Only Access to a Proxmox Homelab via MCP

How an MCP server got wired into a homelab so Claude could query a Proxmox cluster directly — and why the debugging process mattered more than the tutorial would.

July 27 2026 9 min read Homelab Build Log

Most homelab write-ups about connecting an AI agent to your infrastructure gloss over the part where things break. This one does the opposite, on purpose. The failure modes here are exactly the kind of thing a client environment runs into, and exactly the kind of thing an MSP should be able to diagnose fast instead of guessing.

The goal was simple: let Claude, running inside an AI Suite LXC alongside Claude Code and Paperclip, query a Proxmox cluster directly — list nodes, check VM and container status, and eventually (carefully) manage lifecycle — through the Model Context Protocol, instead of manually SSHing in and running pct list every time a status check was needed.

Why MCP, and Why Scoped Access Matters

MCP servers expose a defined set of tools to an AI agent over stdio or HTTP. The Proxmox ecosystem has a handful of implementations at varying levels of maturity. This build used gilby125/mcp-proxmox, a Node.js server with a design choice worth calling out: read-only by default, with destructive operations gated behind an explicit PROXMOX_ALLOW_ELEVATED flag.

That default matters more than it looks like at first glance. An agent with unscoped root@pam credentials and full write access is one bad prompt away from stopping a production VM. In a homelab, that is an annoyance. In a client's regulated environment, that is the kind of finding that ends up in a security review. So step one was not installing software. It was creating a Proxmox API token scoped to audit-only privileges, kept separate from any human user account.

pveum user add mcp@pve
pveum role add MCPReadOnly -privs "VM.Audit,Datastore.Audit,Sys.Audit"
pveum acl modify / -user mcp@pve -role MCPReadOnly
pveum user token add mcp@pve automation --privsep 1

The Gotcha Nobody's README Mentions

This is where twenty minutes went missing that did not need to. --privsep 1 is good practice — it separates the token's permissions from the parent user's. But it also means the ACL grant just run against the user does not automatically apply to the token. Two different security principals, two different ACL entries needed.

A privsep token is its own principal — grant the ACL to it explicitly.
Verify every claim with curl against the API before touching application code.
This detail is obvious once you know it and invisible until it burns you once.
pveum acl modify / -token 'mcp@pve!automation' -role PVEAuditor

Trust the Curl Test Before Trusting Your Code

Once the MCP server started throwing 401 Unauthorized, the instinct was to start reading the server's auth-header logic. That was the wrong instinct. The right move is to isolate whether the problem is Proxmox-side or application-side before reading a single line of the app.

curl -k -H 'Authorization: PVEAPIToken=mcp@pve!automation=<secret>' \
  https://192.168.0.250:8006/api2/json/nodes

That returned clean JSON, which meant the token itself was fine. The bug had to be somewhere in how the Node server was reading its own config — not in Proxmox, not in the ACL, and not worth any more time spent staring at permissions.

The Actual Bug: a Truncated UUID

It turned out to be almost embarrassingly simple. When the token secret got written into the server's .env file, the leading character of the UUID dropped somewhere in the copy-paste — 8df47af-... instead of b8df47af-.... Running cat -A .env made it visible immediately. That same command would also have caught CRLF line endings or stray whitespace, which are the more common versions of this exact bug.

The lesson generalizes past this one afternoon: when an API call fails with 401 but a manual curl with "the same" credentials succeeds, stop reading code and start diffing bytes.

Naming Mismatches Across Forks

One more friction point worth flagging for anyone doing this themselves: MCP server forks for the same underlying tool often do not agree on environment variable names. The docs referenced going in expected PROXMOX_TOKEN_SECRET and PROXMOX_TOKEN_ID. This particular fork wanted PROXMOX_TOKEN_VALUE and PROXMOX_TOKEN_NAME. The fix that actually works reliably is to stop trusting the README and grep the source directly:

grep -o "PROXMOX_[A-Z_]*" index.js | sort -u

Grep the source. Do not trust the README, and definitely do not trust a general answer about "how Proxmox MCP servers work" — verify against the specific fork actually running.

Where It Landed

After sorting the ACL grant, validating with curl, and fixing the truncated token, the setup came together cleanly. A plain-language query — list all Proxmox nodes — returned real cluster data. Node-level detailed stats (CPU, memory, uptime) needed one more permission bump, from the narrow custom role up to Proxmox's built-in PVEAuditor, and then full VM and LXC status across the cluster, memory and disk usage per guest, and even early signs of resource pressure on a couple of containers all surfaced through a plain-language query instead of a manual SSH session and a string of pct commands.

The whole thing still runs strictly read-only. PROXMOX_ALLOW_ELEVATED stays off until the token's privileges get expanded one capability at a time — start/stop first, tested and trusted, before anything that can delete or resize.

Standard Operating Procedure: Proxmox MCP Server Setup

The write-up above is the story. Below is the exact procedure, kept as a reference for repeating the build cleanly on another node or handing it to someone else to run.

1. Prerequisites

  • Root or sudo access on the Proxmox host and the target LXC.
  • Node.js 20 or newer on the LXC (node -v).
  • The claude CLI installed and reachable on $PATH.

Verify claude is on $PATH before starting:

which claude

If it is missing but installed via npm, add the global bin directory:

export PATH="$(npm config get prefix)/bin:$PATH"
echo 'export PATH="$(npm config get prefix)/bin:$PATH"' >> ~/.bashrc

2. Create a Scoped Proxmox API Token

Run this on the Proxmox host, not the LXC. Never use root@pam for agent tooling.

pveum user add mcp@pve
pveum role add MCPReadOnly -privs "VM.Audit,Datastore.Audit,Sys.Audit"
pveum acl modify / -user mcp@pve -role MCPReadOnly
pveum user token add mcp@pve automation --privsep 1

Copy the value field from the output immediately — it is shown only once.

Gotcha: --privsep 1 creates a token with its own ACL, separate from the user's. The user-level acl modify above does not cover the token. The token itself also needs the grant:

pveum acl modify / -token 'mcp@pve!automation' -role MCPReadOnly

For node-level detailed stats (CPU, memory, uptime beyond a basic listing), the token needs PVEAuditor rather than the narrower custom role:

pveum acl modify / -token 'mcp@pve!automation' -role PVEAuditor

Verify:

pveum acl list

3. Validate the Token Before Touching Any MCP Code

curl -k -H 'Authorization: PVEAPIToken=mcp@pve!automation=<token-secret>' \
  https://192.168.0.250:8006/api2/json/nodes

Expect JSON with node data back. Do not proceed to MCP server setup until this succeeds. If it 401s, it is a Proxmox-side permissions problem — fix it here first, not in the app code.

Note: ! inside double-quoted strings triggers bash history expansion (event not found). Use single quotes for any command containing an API token.

4. Clone the MCP Server

git clone https://github.com/gilby125/mcp-proxmox.git
cd mcp-proxmox
npm install

Check package.json before assuming a build step exists — this repo is plain JS with index.js as the entry point, and there is no npm run build script.

5. Configure the Environment

Find the actual variable names by grepping the source rather than trusting a README, since forks diverge:

grep -o "PROXMOX_[A-Z_]*" index.js | sort -u

For gilby125/mcp-proxmox, the required variables are:

PROXMOX_HOST=192.168.0.250
PROXMOX_PORT=8006
PROXMOX_USER=mcp@pve
PROXMOX_TOKEN_NAME=automation
PROXMOX_TOKEN_VALUE=<token-secret>
PROXMOX_VERIFY_TLS=false
PROXMOX_ALLOW_ELEVATED=false

Optional scoping, to restrict which nodes or VMIDs the agent can see:

PROXMOX_NODE_ALLOWLIST=
PROXMOX_VMID_ALLOWLIST=

The code loads this from <repo>/../.env (i.e. /root/.env if the repo lives at /root/mcp-proxmox), resolved via __dirname, so it is independent of the shell's current working directory. Create it:

cat > /root/.env << 'EOF'
PROXMOX_HOST=192.168.0.250
PROXMOX_PORT=8006
PROXMOX_USER=mcp@pve
PROXMOX_TOKEN_NAME=automation
PROXMOX_TOKEN_VALUE=<token-secret>
PROXMOX_VERIFY_TLS=false
PROXMOX_ALLOW_ELEVATED=false
EOF
chmod 600 /root/.env

Verify the token secret was copied correctly, character for character, against the curl test in step 3:

cat -A /root/.env

cat -A reveals hidden characters — CRLF, trailing whitespace — and makes truncation errors, like a dropped leading character in a UUID, visible immediately.

6. Smoke-Test the Server Standalone

node /root/mcp-proxmox/index.js

Success looks like:

Proxmox MCP server running on stdio

No errors, and the process hangs — that is expected, it is waiting on stdio. Ctrl+C to exit. Any thrown error here (missing env var, etc.) needs to be resolved before wiring into Claude Code.

7. Register With Claude Code

claude mcp add proxmox -- node /root/mcp-proxmox/index.js
claude mcp list

Confirm proxmox shows connected. A failed connection with "Connection closed" means the subprocess crashed — almost always a missing or incorrect env var. Re-run step 6 manually to see the real error before assuming it is a Claude Code config problem.

8. Functional Test (Read-Only)

Start a session and ask, in order of increasing detail:

list all Proxmox nodes
show detailed status for node <name>
detailed vm/lxc status

PROXMOX_ALLOW_ELEVATED=false gates every write-capable tool call — start, stop, delete, resize, network, and disk operations all check the same flag — but it does not grant any Proxmox-side permission by itself. The actual authorization boundary is the token's ACL role, not this env var. That means flipping PROXMOX_ALLOW_ELEVATED=true with a read-only-scoped token is low-risk: the code will attempt the call, but Proxmox will still reject it with a 403 unless the token role includes the corresponding privilege.

9. Expanding to Write Access (Do This Last, Incrementally)

Flip the flag:

sed -i 's/PROXMOX_ALLOW_ELEVATED=false/PROXMOX_ALLOW_ELEVATED=true/' /root/.env

Restart the Claude Code session — the process must respawn to pick up the new env.

Grant one additional privilege at a time on the Proxmox side (for example, VM.PowerMgmt for start/stop) and test that specific capability before adding the next. Never grant VM.Allocate or delete privileges to an agent token until start, stop, and reboot have been tested and trusted individually.

Common Failure Signatures

Symptom Cause Fix
claude: command not found npm global bin not on $PATH Export $(npm config get prefix)/bin
Missing script: "build" Repo has no build step Check package.json, run the entry file directly
event not found on a token command ! in a double-quoted string Use single quotes
"Connection closed" in claude mcp list MCP subprocess crashed on start Run node index.js manually to see the real error
PROXMOX_HOST environment variable is required .env missing or empty Confirm the path via __dirname resolution, not a CWD assumption
401 Unauthorized (curl and MCP both) Token secret wrong or truncated cat -A .env, diff char-by-char against the token creation output
403 Permission check failed (Sys.Audit) Token ACL not granted, or granted to the user instead of the token (--privsep 1) pveum acl modify / -token 'user!tokenname' -role PVEAuditor

Why This Is the Actual Deliverable

The interesting part of this build is not "an AI agent got connected to a hypervisor." It is that every failure mode along the way — privsep ACL scoping, validating at the API boundary before debugging application code, environment variable naming drift across forks, byte-level diffing on auth secrets — is a pattern that shows up constantly in real infrastructure work, agent-related or not. Least-privilege service accounts. Verifying at the boundary before touching the app. Not trusting documentation over source. None of that is AI-specific. The agent just made it visible in one afternoon instead of scattered across a dozen unrelated incidents.

That is the pitch, essentially: an MSP that can wire up agent tooling safely is demonstrating the same discipline it would apply to any other privileged integration. The token model just happens to be the thing on the table this time.

Want This Level of Discipline Applied to Your Infrastructure?

Sylvect IT Services builds least-privilege service accounts, scoped API access, and documented recovery paths for servers, backups, and now agent tooling — so nothing in your environment runs on a shared root credential nobody can account for.

Book Infrastructure Consultation Read the Proxmox Skills Guide

Where This Fits in the Bigger IT Picture

Scoped access is a piece of a larger infrastructure discipline. Pair this walkthrough with How a Proxmox Home Lab Teaches Real Business IT Skills and the Small Business IT Audit Checklist to see how least-privilege thinking applies across servers, backups, and now AI tooling.