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.
curl against the API before touching application code.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
claudeCLI 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.
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.