Proxmox MCP Server

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.

Category
Visit Server

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

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured