proxmox-mcp-server

proxmox-mcp-server

Enables Claude to manage a Proxmox VE host through natural language, including guest lifecycle, snapshots, host status monitoring, and shell execution via a companion bridge.

Category
访问服务器

README

proxmox-mcp-server

A Cloudflare Worker that lets Claude manage a Proxmox VE host over MCP. It handles everything structured through the Proxmox REST API, and hands shell work to a companion service, proxmox-mcp-bridge, which runs on the Proxmox host itself.

Nothing is exposed to the internet directly: the Worker reaches Proxmox through a Cloudflare Tunnel, authenticating with a Cloudflare Access service token.

Claude (claude.ai connector)
    |  MCP over HTTPS, bearer token
proxmox-mcp-server  (this repo, a Cloudflare Worker)
    |  + CF-Access service token headers
Cloudflare Access -> Cloudflare Tunnel -> Proxmox host
                                          |- pveproxy :8006    REST API
                                          '- exec bridge :5000 shell
                                             (proxmox-mcp-bridge)

Using it

Once the connector is added, you just talk to Claude normally. A few things worth knowing about how it behaves:

You rarely need to name the node. node and type are optional on every guest tool — the Worker looks the vmid up in /cluster/resources and fills them in. On a single-node install you never type them at all. On a cluster you still can, to disambiguate.

Write operations return a task, not a result. Proxmox runs starts, stops and snapshots asynchronously and hands back a UPID. Claude gets that UPID and can poll it with task_status, which also returns the task log — so "did that actually work?" is answerable rather than assumed.

Things you can ask for directly:

  • "What's running on Proxmox right now?"list_guests, one call, shows every VM and container with status and memory use.
  • "How's the host doing?"node_status for cpu, memory, swap, root filesystem, load average and uptime.
  • "Snapshot container 101 before I upgrade it."snapshot_guest, then rollback_snapshot if the upgrade goes badly.
  • "Gracefully shut down VM 100, and tell me when it's actually down."shutdown_guest followed by task_status on the returned UPID.
  • "Anything failed on the host lately?"list_tasks with errors: true.
  • "What storage do I have configured?" — no dedicated tool, so this falls to pve_api with GET /nodes/<node>/storage.

Responses can be trimmed server-side. pve_api, list_guests, guest_status, node_status and list_tasks all take fields and omit_fields; the list-shaped ones also take limit, which keeps the most recent entries. Trimming happens before the payload is serialised, so it is a real saving rather than a display filter.

Where it matters most:

  • A week of rrddata is ~330 points of ~20 metrics. Asking for fields: ["time","mem","maxmem"] cuts roughly 70%; adding limit takes it past 90%.
  • Guest configs from community install scripts carry large HTML blobs in description. omit_fields: ["description"] removes ~96% of a config read.
  • list_guests also takes status: "running" to skip stopped guests entirely.

Two things happen automatically, with no parameters:

  • task_status fetches the task log only when the task failed or is still running. The log of a successful start is noise, and this is the most-called tool since every write returns a UPID. Pass log: true to force it.
  • Responses over 2000 characters drop JSON indentation, worth about a third of a large payload. Smaller ones stay pretty-printed.
  • run_script output is capped at 40000 characters (~10k tokens), cut from the middle so both ends survive, with a marker saying how much was dropped. Raise it per-call with max_output — but filtering on the host with grep, tail -n or journalctl -n is almost always better.

With the bridge installed you additionally get run_script:

  • "Check disk usage inside container 101." — runs df -h via pct exec.
  • "Restart nginx in 101 and show me the last 20 log lines."
  • "What kernel is the host on?"

Tools

Tool What it does
list_guests Every VM and container with id, name, node, status, usage
guest_status Runtime detail for one guest
start_guest / shutdown_guest / stop_guest Lifecycle. shutdown is graceful, stop is a hard power cut
list_snapshots / snapshot_guest / rollback_snapshot Snapshot create, list and revert
list_nodes / node_status Host health: cpu, memory, load, uptime
list_tasks / task_status Task history, and polling the UPIDs the write tools return
pve_api Escape hatch to any other API endpoint, with guardrails
run_script Bash on the host, in an LXC, or in a VM. Only registered once EXEC_HOST is set

There is deliberately no tool for deleting a guest, a snapshot or a storage volume. pve_api refuses DELETE outright, so destructive cleanup stays a thing you do by hand in the Proxmox UI.


Prerequisites

Set these up before deploying:

  • A Cloudflare Tunnel with a public hostname pointing at https://localhost:8006, with noTLSVerify: true — pveproxy uses a self-signed cert, so the tunnel must not try to validate it.
  • A Cloudflare Access application on that hostname with a service token only policy, and a service token issued for it.
  • A Proxmox API token (Datacenter → Permissions → API Tokens). PVEVMAdmin on /vms plus PVEAuditor on / covers everything these tools do without letting the token touch users or realms. Copy the secret when it is shown — Proxmox displays it exactly once.

Phase 2 needs a second tunnel hostname pointing at http://localhost:5000, with its own Access application and service token.

Deploy

This repo is wired for Cloudflare's git integration — pushing to main builds and deploys automatically.

  1. Create the KV namespace and put its id in wrangler.toml:
    npx wrangler kv namespace create PROXMOX_KV
    
    Do this before connecting the repo; a placeholder id fails the build. The namespace does one job — stopping an OAuth authorization code from being redeemed twice. It holds nothing else, and entries expire after 5 minutes.
  2. In the Cloudflare dashboard: Workers & Pages → Create → Workers → Import a repository. Defaults are correct: build npm install, deploy npx wrangler deploy.
  3. Set the secrets (dashboard Settings → Variables and Secrets, or CLI):
    npx wrangler secret put MCP_API_KEY              # password you'll type when connecting
    npx wrangler secret put PVE_HOST                 # https://your-tunnel-hostname (no trailing slash)
    npx wrangler secret put PVE_TOKEN                # user@realm!tokenid=uuid
    npx wrangler secret put CF_ACCESS_CLIENT_ID
    npx wrangler secret put CF_ACCESS_CLIENT_SECRET
    
    Secrets survive a git-triggered redeploy, which is why nothing lives in [vars] and no real hostname is committed anywhere in this repo.
  4. Check it came up:
    curl https://proxmox-mcp-server.<subdomain>.workers.dev/health
    
    You want "pve_configured": true. If a /health call returns HTML rather than JSON, Access rejected the service token.

Connect it to Claude

In claude.ai: Settings → Connectors → Add custom connector, URL https://proxmox-mcp-server.<subdomain>.workers.dev/mcp. You will be sent to an authorize page — enter the MCP_API_KEY value. Only claude.ai and claude.com redirect URIs are accepted.

Phase 2 secrets

Add these once proxmox-mcp-bridge is running. run_script does not appear in the tool list until EXEC_HOST exists, so Phase 1 deploys cleanly on its own.

npx wrangler secret put EXEC_HOST                    # https://your-exec-hostname
npx wrangler secret put EXEC_CF_ACCESS_CLIENT_ID     # service token for the exec hostname
npx wrangler secret put EXEC_CF_ACCESS_CLIENT_SECRET
npx wrangler secret put EXEC_SHARED_SECRET           # must match the bridge
npx wrangler secret put EXEC_SUDO_PASSWORD           # OS password of the bridge user
npx wrangler secret put EXEC_SUDO_ENABLED            # "true" or "false"

EXEC_SUDO_ENABLED is the kill switch. Set it to anything other than "true" and the Worker stops sending the sudo password at all, so scripts targeting host run as the unprivileged bridge user. Claude is never told the password and it never appears in a tool result. Flipping it takes effect on the next request — no redeploy, no push.

Configuration reference

Everything is a Worker secret — there are no [vars]. Secrets are the only binding type that survives wrangler deploy, so a value set in the dashboard is never silently reverted by an unrelated code push. EXEC_SUDO_ENABLED is a secret for that reason alone, not because "true"/"false" is sensitive.

Set any of them with npx wrangler secret put <NAME>, or in the dashboard under Settings → Variables and Secrets.

Phase 1 — required

Secret What it is Where it comes from
MCP_API_KEY Password you type on the authorize page when adding the connector. Also the HMAC key for the bearer tokens the Worker issues. You invent it. openssl rand -hex 32
PVE_HOST Origin of the tunnel hostname in front of pveproxy :8006. No trailing slash, no /api2/json. Your Cloudflare Tunnel
PVE_TOKEN Full Proxmox API token, user@realm!tokenid=uuid — all three parts, not just the secret. Datacenter → Permissions → API Tokens
CF_ACCESS_CLIENT_ID Access service token id, ends in .access. Zero Trust → Access → Service Auth
CF_ACCESS_CLIENT_SECRET The secret half. Shown once. ditto

Phase 2 — optional

run_script is not registered at all unless EXEC_HOST is set. Setting it without the rest means Claude sees the tool and every call fails, so set them together.

Secret What it is Where it comes from
EXEC_HOST Origin of the tunnel hostname in front of the bridge :5000. Your Cloudflare Tunnel
EXEC_CF_ACCESS_CLIENT_ID Service token for that hostname. Service tokens are account-level, so the Phase 1 pair may be reused — the token just has to be in that application's policy. Zero Trust → Access → Service Auth
EXEC_CF_ACCESS_CLIENT_SECRET ditto ditto
EXEC_SHARED_SECRET Second check behind Access. Must match the bridge. cat /etc/claude-exec.env on the host
EXEC_SUDO_ENABLED "true" lets target: "host" escalate with sudo. Any other value, or absent, disables it. You choose
EXEC_SUDO_PASSWORD OS password of the bridge user. Only read when EXEC_SUDO_ENABLED is exactly "true"; never returned in a tool result. The password set by install.sh --with-host-sudo

Bindings

Binding Purpose
PROXMOX_KV Marks OAuth authorization codes as spent so one cannot be redeemed twice. Entries expire after 5 minutes. Optional — without it the connector still works, you just lose replay protection.

Checking what is set

GET /health reports configuration state without exposing any value:

{"status":"ok","version":"1.0.0","pve_configured":true,"exec_configured":true,"sudo_enabled":false}

pve_configured means PVE_HOST and PVE_TOKEN are both present, not that they are correct. exec_configured reflects EXEC_HOST alone — it is what decides whether run_script appears.

Local development

npm install
cp .dev.vars.example .dev.vars   # fill in real values; it is gitignored
npm run dev
npm run typecheck

The tunnel hostname is publicly reachable, so wrangler dev can talk to the real Proxmox API. To call /mcp by hand you need a bearer token; mint one with the same HMAC the Worker uses:

node -e 'const{createHmac,randomBytes}=require("crypto");const b=x=>x.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"");const p=`${Date.now()}.${b(randomBytes(12))}`;console.log(`${p}.${b(createHmac("sha256",process.argv[1]).update(`token:${p}`).digest())}`)' "$MCP_API_KEY"
curl -s localhost:8787/mcp -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Privilege model

Worth reading once rather than inferring, because the parts interact in ways that are easy to get wrong.

What the tool layer blocks

Blocked Where
DELETE, every path pve_api — the only hard write-wall in the Worker
Non-GET to /access/* and /cluster/config/* pve_api
Writes to /etc/pve, rm -rf on system paths, mkfs, passwd, reboots run_script deny-list

What it deliberately does not block

  • POST and PUT to everything else. These reach Proxmox and are gated by the API token's role, not by this Worker. If you widen that role, the Worker will not be what stops a destructive write.
  • Reads of /etc/pve. The run_script guard is write-scoped. ls and cat pass — which is harmless, because the files inside are root:www-data 0640 and the unprivileged bridge user cannot read them anyway.
  • Arbitrary host commands. The deny-list is a small blocklist of obviously destructive patterns. It does not reason about whether a command is harmful. rm -f somefile on the host runs and is stopped only by filesystem permissions.

How privilege actually works

This is the part most often stated incorrectly. The bridge account is not free of sudo rights:

claude ALL=(root) NOPASSWD: /usr/sbin/pct exec *
claude ALL=(root) NOPASSWD: /usr/sbin/qm guest exec *

Those are NOPASSWD, so they apply regardless of EXEC_SUDO_ENABLED — which is exactly why run_script gets root inside any container while /health reports sudo_enabled: false. And pct exec into a privileged container is a well-known route back to root on the host.

EXEC_SUDO_ENABLED governs one narrow thing: whether the Worker attaches a password, which only affects target: "host". It is not a global privilege switch, and it does not gate container access.

If the host was installed with --with-host-sudo, the account is additionally in the sudo group with a password — full root, password-gated.

Check what is actually granted rather than assuming:

sudo -l -U claude

The real boundaries

In rough order of how much they carry:

  1. Cloudflare Access on both tunnel hostnames — nothing reaches Proxmox without the service token.
  2. The PVE API token's role — the ceiling on every API tool, and the only thing gating non-DELETE writes.
  3. The bridge user's sudoers file — the ceiling on run_script. Broad by design, since pct exec is the whole point.
  4. The bearer token on /mcp, HMAC-derived from MCP_API_KEY.
  5. EXEC_SUDO_ENABLED — host-sudo only, per the caveat above.

Everything in "what the tool layer blocks" is a speed bump, not a boundary. Those are pattern matches; a determined encoding walks through them. Set the API token's role and the sudoers file as though the guardrails did not exist.

Nothing sensitive is committed here. Credentials live in Worker secrets, and local values in .dev.vars, which is gitignored — .dev.vars.example shows the shape with placeholders.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选