Proxmox MCP Server
Manages Proxmox VE clusters through the official PVE API with 22 tools for cluster status, VMs/containers, storage, snapshots, and provisioning. Supports stdio or HTTP/StreamableHTTP transports with optional OAuth for Gemini and multi-server configurations.
README
Proxmox MCP Server
MCP server that manages any Proxmox VE cluster through the official PVE API — 22 tools for cluster status, VMs/CTs, storage, snapshots, and provisioning. Works over stdio (Hermes / Claude / any MCP client) or HTTP (StreamableHTTP) with optional OAuth for Gemini.
Configuration
Point it at your server with either a YAML config file or environment
variables. Env vars always win. See proxmox-mcp.example.yaml for a fully
commented template (including multi-server).
# 1) config file — copy the example and fill it in
cp proxmox-mcp.example.yaml proxmox-mcp.yaml
# pve_url: "https://pve.yourhost:8006"
# pve_token: "root@pam!mcp=<secret>" # or pve_user + pve_password
# 2) …or env vars (equivalent)
export PVE_URL="https://pve.yourhost:8006"
export PVE_TOKEN="root@pam!mcp=<secret>" # preferred: API token
# export PVE_USER="root@pam" # fallback: password auth
# export PVE_PASSWORD="..."
Config file discovery order: $PROXMOX_MCP_CONFIG → ./proxmox-mcp.yaml →
~/.config/proxmox-mcp/config.yaml.
| Key | Env | Notes |
|---|---|---|
pve_url |
PVE_URL / PROXMOX_URL |
https://host:8006 (required) |
pve_token |
PVE_TOKEN / PROXMOX_TOKEN |
user@realm!tokenid=secret |
pve_user / pve_password |
PVE_USER / PVE_PASSWORD |
fallback auth |
pve_verify_ssl |
PVE_VERIFY_SSL |
0 (default, self-signed) / 1 |
pve_readonly |
PVE_READONLY |
1 disables all mutating tools |
mcp_allowed_hosts |
MCP_ALLOWED_HOSTS |
DNS-rebinding allowlist (ngrok) |
mcp_oauth |
MCP_OAUTH |
1 enables OAuth (Gemini) |
mcp_public_url |
MCP_PUBLIC_URL |
external base URL (OAuth issuer) |
mcp_http_token |
MCP_HTTP_TOKEN |
static token for /health |
mcp_http_host / mcp_http_port |
MCP_HTTP_HOST / MCP_HTTP_PORT |
bind addr, default 127.0.0.1:8766 |
Multi-server: define servers: {name: {…}, …} + default_server, pick at
runtime with PROXMOX_SERVER=name. MCP-side keys (mcp_*) stay global.
Run (stdio)
.venv/bin/python server.py # needs PVE_URL + creds from config or env
Tools (22)
Read-only
cluster_status— version + nodes + all VMs/CTslist_nodes,node_stats(cpu/mem/disk/net per node)list_vms,list_containers,vm_status,vm_configlist_storage,list_snapshots,list_pools
Control (safe)
vm_start,vm_shutdown,vm_reboot,vm_suspend,vm_resumevm_snapshot,vm_snapshot_delete
Control (destructive — require explicit require_confirm='YES')
vm_stop(hard kill),vm_delete(permanent + disks)node_restart(reboots a physical node)
Provisioning
vm_create_qemu(vmid, name, node, cores, memory, disk_size, storage, iso, bridge, start)vm_clone(full clone to new VMID)
Hermes / Claude registration
Point any stdio MCP client at server.py. Config goes in the client's env,
or in proxmox-mcp.yaml next to the repo. Hermes example
(~/.hermes/config.yaml):
mcp_servers:
proxmox:
command: "/path/to/proxmox-mcp/.venv/bin/python"
args: ["/path/to/proxmox-mcp/server.py"]
env:
PVE_URL: "https://pve.yourhost:8006"
PVE_TOKEN: "user@realm!tokenid=secret"
timeout: 60
connect_timeout: 30
Requires a Hermes gateway restart to pick up new MCP servers (no hot-reload).
Claude Desktop: claude_desktop_config.json, same command/args shape.
Install
python -m venv .venv && .venv/bin/pip install -r requirements.txt
Test
.venv/bin/python test_client.py # stdio: lists tools, calls cluster_status/list_vms/list_storage/list_nodes
HTTP mode (StreamableHTTP — for ngrok / LAN / remote clients)
Configure via proxmox-mcp.yaml (recommended) or env vars:
# proxmox-mcp.yaml
pve_url: "https://pve.yourhost:8006"
pve_token: "root@pam!mcp=<secret>"
pve_readonly: true # public endpoint = inspection only
mcp_http_host: "127.0.0.1"
mcp_http_port: 8766
mcp_http_token: "change-me" # static bearer token for /health
# start the local HTTP endpoint
.venv/bin/python http_entry.py
# → http://127.0.0.1:8766/mcp (health: /health, bearer-token protected)
# tunnel it publicly
/snap/bin/ngrok http 8766 --log stdout > ngrok.log 2>&1
# public URL: https://<random>.ngrok-free.app (MCP endpoint: /mcp)
# IMPORTANT: restart http_entry.py AFTER the tunnel is up so the OAuth issuer
# and DNS-rebinding allowlist use the public host (env example):
MCP_ALLOWED_HOSTS=<ngrok-host> MCP_OAUTH=1 MCP_PUBLIC_URL=https://<ngrok-host>.ngrok-free.app \
.venv/bin/python http_entry.py
Security model:
pve_readonly: truedisables all 12 mutating tools server-side — the public URL can only read. Flip tofalseonly if you truly want remote control.mcp_http_tokenguards/health(ops-only, not the MCP endpoint).- The Proxmox token itself never crosses the tunnel (lives server-side only).
Test:
MCP_HTTP_TOKEN=<token> .venv/bin/python http_test.py https://<host>.ngrok-free.app/mcp
OAuth mode (required for Gemini)
Gemini only connects to MCP servers that support standard OAuth. Enable it
in the config file (mcp_oauth: true, mcp_public_url, mcp_allowed_hosts)
or with env vars:
# tunnel must already be up
MCP_OAUTH=1 \
MCP_PUBLIC_URL=https://<ngrok-host>.ngrok-free.app \
MCP_ALLOWED_HOSTS=<ngrok-host> \
PVE_READONLY=1 MCP_HTTP_TOKEN=<token> \
.venv/bin/python http_entry.py
The OAuth Authorization Server then serves:
/.well-known/oauth-authorization-server(RFC 8414 metadata)/authorize— interactive HTML consent page for browsers (Google's account-linking UI requires a rendered grant page)/token(authorization-code + PKCE, refresh tokens 30d, rotated)/register(dynamic client registration — Gemini registers itself, no client id/secret to provision by hand)/revoke
Test the whole flow (register → PKCE authorize → token → MCP call → refresh):
.venv/bin/python oauth_test.py https://<host>.ngrok-free.app # SDK-style client
.venv/bin/python google_flow_test.py https://<host>.ngrok-free.app # Google/OpenAuth-exact (Basic-only auth)
.venv/bin/python browser_flow_test.py https://<host>.ngrok-free.app # browser consent-page flow
Notes:
- Everything is in-memory — server restart invalidates clients/tokens; clients re-register automatically.
/healthstays behind the staticmcp_http_token(ops-only, not MCP).- Non-browser authorize requests still get a plain 302 (no consent HTML).
- The OAuth metadata is served by
http_entry.py, not the SDK, because the SDK hardcodestoken_endpoint_auth_methods_supportedwithout"none"(public client / PKCE) — Gemini validates that list before registering. - Consent is effectively auto-approve (no login); anyone who can reach /authorize with a registered client_id gets a code, but codes require the PKCE verifier. Fine for a personal tunnel; reconsider if shared.
Pitfalls (for future edits)
mcpSDK v2.x removedmcp.server.fastmcp— pinmcp<2(venv has 1.29.0).proxmoxer2.x usesservice="PVE"(not"proxmox"), needsrequestsinstalled.- Token format for proxmoxer:
user='moritz@pve',token_name='moritz',token_value=secret. - mcp 1.29 HTTP client yields a 3-tuple
(read, write, get_session_id)— unpack 3, not 2. streamable_http_app()must be the top-level ASGI app — Mounting it inside another Starlette app skips its lifespan and every request fails with "Task group is not initialized".- Behind ngrok you get 421 "Invalid Host header" unless you pass
MCP_ALLOWED_HOSTS=<host>(DNS-rebinding protection). - ngrok on this box is a snap: use the absolute path
/snap/bin/ngrokin background shells (PATH differs), and free-tier may needngrok-skip-browser-warning: true+http2=Falseon the client. /auth/password-login-style auth is NOT used here; this is bearer-token auth.- OAuth
expires_atmust be an int (int(time.time())) — pydantic rejects floats. authorize()must never passscopes=NoneintoAuthorizationCode— Gemini sends noscopeparam; fall back to the client's registered scope. Missing this caused a 500 at account-link time (fixed inoauth_provider.py).- Google/OpenAuth sends
client_idONLY in theAuthorization: Basicheader at/token(RFC 6749 §2.3.1). The mcp SDK'sClientAuthenticatorAND itsTokenHandlerboth requireclient_idin the form body → every Gemini token exchange failed withunauthorized_client: Missing client_id(laterinvalid_request: authorization_code.client_id: Field required). Fixed inhttp_entry.py: patchedClientAuthenticator.authenticate_requestwith a Basic-header fallback + custom/tokenendpoint (our Router handles it; injects the resolved client_id into the form before model validation). Re-verify both against the SDK on upgrade.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。