Skip to main content
Glama
mr-mister007

Proxmox MCP Server

by mr-mister007

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.

Related MCP server: Proxmox MCP Server

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/CTs

  • list_nodes, node_stats (cpu/mem/disk/net per node)

  • list_vms, list_containers, vm_status, vm_config

  • list_storage, list_snapshots, list_pools

Control (safe)

  • vm_start, vm_shutdown, vm_reboot, vm_suspend, vm_resume

  • vm_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: true disables all 12 mutating tools server-side — the public URL can only read. Flip to false only if you truly want remote control.

  • mcp_http_token guards /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.

  • /health stays behind the static mcp_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 hardcodes token_endpoint_auth_methods_supported without "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)

  • mcp SDK v2.x removed mcp.server.fastmcp — pin mcp<2 (venv has 1.29.0).

  • proxmoxer 2.x uses service="PVE" (not "proxmox"), needs requests installed.

  • 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/ngrok in background shells (PATH differs), and free-tier may need ngrok-skip-browser-warning: true + http2=False on the client.

  • /auth/password-login-style auth is NOT used here; this is bearer-token auth.

  • OAuth expires_at must be an int (int(time.time())) — pydantic rejects floats.

  • authorize() must never pass scopes=None into AuthorizationCode — Gemini sends no scope param; fall back to the client's registered scope. Missing this caused a 500 at account-link time (fixed in oauth_provider.py).

  • Google/OpenAuth sends client_id ONLY in the Authorization: Basic header at /token (RFC 6749 §2.3.1). The mcp SDK's ClientAuthenticator AND its TokenHandler both require client_id in the form body → every Gemini token exchange failed with unauthorized_client: Missing client_id (later invalid_request: authorization_code.client_id: Field required). Fixed in http_entry.py: patched ClientAuthenticator.authenticate_request with a Basic-header fallback + custom /token endpoint (our Router handles it; injects the resolved client_id into the form before model validation). Re-verify both against the SDK on upgrade.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    C
    quality
    D
    maintenance
    Enables management of Proxmox VE infrastructure through natural language, providing 120+ tools to control virtual machines, containers, storage, cluster resources, users, and network configurations via the Proxmox API.
    100
    37
    MIT
  • A
    license
    -
    quality
    D
    maintenance
    Enables comprehensive management of Proxmox virtualization environments, including VM and container lifecycle, snapshots, backups, monitoring, and OpenAPI integration.
    14
    MIT

View all related MCP servers

Related MCP Connectors

  • Official Sevalla MCP — full PaaS API access through just 2 tools.

  • Tailscale device, route, DNS, key, user, and ACL management over MCP and CLI.

  • Production-grade cryptography toolkit with 31 MCP tools for classical, PQC, and KMS workflows.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mr-mister007/proxmox-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server