Skip to main content
Glama

PVEHub-MCP

One MCP server for Proxmox VE and Proxmox Backup Server, plus shell access to PVE hosts, LXC containers, QEMU VMs (guest agent) and the PBS host.

Status: 0.1.0 — core server, policy engine, curated PVE/PBS/exec tools and packaging are implemented. docs/TOOLS.md is generated from the code and shows exactly what exists today; docs/STATUS.md separates live-verified, mocked and untested behaviour.

Features

  • PVE REST API — curated tools plus a generic pve_api_schema / pve_api_call bridge for every endpoint the token can reach.

  • PBS REST API — datastores, namespaces, snapshots, prune, GC, verify, jobs, tasks, plus a generic bridge.

  • Shell — QEMU guest agent, LXC via pct exec, host and PBS host via SSH (opt-in).

  • Rights-aware — token permissions are discovered at startup; tools without the required privilege are not registered, and each call is pre-checked.

  • Blockable — every tool and category can be denied via YAML/env policy; denied tools do not appear in tools/list.

  • One container, one endpoint — Streamable HTTP at /mcp, /health, optional stdio.

Related MCP server: ProxmoxMCP-Plus

Architecture

flowchart LR
  client["MCP client<br/>Claude, Hermes, Open WebUI"]
  subgraph hub["pvehub-mcp container (UID 10001)"]
    server["server.py<br/>/mcp · /health<br/>bearer auth"]
    policy["policy.py<br/>categories · allow/deny<br/>generic API gates"]
    caps["capabilities.py<br/>token privileges"]
    tools["tools/<br/>curated + generic"]
    audit[("audit.jsonl<br/>/data")]
    server --> policy --> caps --> tools
    tools --> audit
  end
  client -->|"Streamable HTTP + Bearer"| server
  client -->|"stdio (docker run -i)"| server
  tools -->|"HTTPS · API token"| pve["Proxmox VE API"]
  tools -->|"HTTPS · API token"| pbs["Proxmox Backup Server API"]
  tools -->|"SSH · dedicated key"| nodes["PVE nodes / PBS host"]

Quickstart

git clone https://git.mls4tech.net/admin/PVEHub-MCP.git
cd PVEHub-MCP
cp .env.example .env
# edit .env: PVE/PBS URLs, token IDs/secrets, PVEHUB_MCP_AUTH_TOKEN
mkdir -p config ssh
cp config/examples/operator.yaml config/policy.yaml
docker compose up -d --build
curl -s http://127.0.0.1:8080/health

docker compose reads .env, mounts ./config and ./ssh read-only, keeps /data (audit log, caches) in the named volume pvehub-data, runs the container read-only with all capabilities dropped, and publishes 8080 on all interfaces. Bind it to LAN/VPN only: put a TLS reverse proxy in front of it, or change the port mapping to 127.0.0.1:8080:8080 and proxy from the same host.

The API token needs the privileges of the tools you want to use. Start read-only:

PVE:  Datastore.Audit, Sys.Audit, VM.Audit
PVE (exec/files): VM.GuestAgent.Unrestricted, VM.GuestAgent.FileRead, VM.GuestAgent.FileWrite,
                  VM.GuestAgent.Audit
PBS:  Datastore.Audit, Sys.Audit

Then widen the token ACL and restart the server. pvehub_capabilities shows what is available, what policy blocks and what the token is missing.

Clients

The server speaks Streamable HTTP at http://<host>:8080/mcp with Authorization: Bearer <PVEHUB_MCP_AUTH_TOKEN>; the http transport refuses to start without PVEHUB_MCP_AUTH_TOKEN (any bind address). PVEHUB_MCP_AUTH_REQUIRED=false is an explicit opt-out for loopback development setups only (warning logged at startup). Or stdio when started with --transport stdio / PVEHUB_TRANSPORT=stdio (no listener, no bearer token).

Claude Code / generic HTTP client

claude mcp add --transport http pvehub http://127.0.0.1:8080/mcp \
  --header "Authorization: Bearer $PVEHUB_MCP_AUTH_TOKEN"

Generic client entry (Claude Code .mcp.json, Cursor and similar):

{
  "mcpServers": {
    "pvehub": {
      "type": "http",
      "url": "http://127.0.0.1:8080/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

Claude Desktop (stdio via Docker)

Claude Desktop launches stdio servers itself. Run the same image with -i and no port:

{
  "mcpServers": {
    "pvehub": {
      "command": "/usr/local/bin/docker",
      "args": [
        "run", "-i", "--rm",
        "--env-file", "/Users/you/pvehub/.env",
        "-e", "PVEHUB_TRANSPORT=stdio",
        "-v", "/Users/you/pvehub/config:/config:ro",
        "-v", "/Users/you/pvehub/ssh:/ssh:ro",
        "-v", "pvehub-data:/data",
        "pvehub-mcp:0.1.0"
      ]
    }
  }
}

Use the absolute path to docker (GUI apps often have a minimal PATH) and absolute host paths for the mounts. Docker Desktop must be running. -v pvehub-data:/data creates a volume of that name, separate from the compose volume (<project>_pvehub-data); point both at the same host directory if they should share audit/cache state. The image's HTTP healthcheck does not apply here; ignore the container health status in stdio mode.

Hermes

~/.hermes/config.yaml:

mcp_servers:
  pvehub:
    url: "http://127.0.0.1:8080/mcp"
    headers:
      Authorization: "Bearer ${PVEHUB_MCP_AUTH_TOKEN}"

Hermes expands ${VAR} in url/headers; keep the token in the environment, not in the file. For stdio, use command: docker with args: ["run", "-i", "--rm", ...] and env: { PVEHUB_TRANSPORT: "stdio" } instead of url.

Open WebUI

Admin Settings → Integrations → External Tool Servers → + Add Connection:

  • Type: MCP (Streamable HTTP)

  • URL: http://<docker-host>:8080/mcp

  • Auth: bearer token (PVEHUB_MCP_AUTH_TOKEN)

Open WebUI calls the tools through its backend, so the URL must be reachable from the Open WebUI container, not just from your browser (use the LAN address or a shared Docker network, not 127.0.0.1).

SSH access (optional)

Shell tools are the highest-risk part of the server. Host and PBS shells are disabled by default (exec.host: false, exec.pbs_host: false); LXC and VM exec are enabled but still require confirm=true and a VMID inside exec.allowed_vmids.

Minimum setup:

# 1. Dedicated key, no passphrase (it lives in the container)
ssh-keygen -t ed25519 -C pvehub-mcp -f ssh/id_ed25519 -N ''

# 2. Host keys, verified out of band before trusting them
ssh-keyscan -H 10.0.0.11 10.0.0.12 10.0.0.20 >> ssh/known_hosts
ssh-keygen -lf ssh/known_hosts        # compare with the node consoles

# 3. UID 10001 must be able to read the key
chown -R 10001:10001 ssh && chmod 700 ssh && chmod 600 ssh/id_ed25519 ssh/known_hosts

On every node, create a dedicated user, install the public key and map the PVE node name to its address in config/config.yaml:

ssh:
  nodes:
    pve1:
      host: 10.0.0.11
    pve2:
      host: 10.0.0.12
      sudo: true          # pct exec via passwordless sudo
  pbs:
    host: 10.0.0.20

Notes:

  • The nodes key must be the PVE node name, not the hostname.

  • pct exec needs root or passwordless sudo for /usr/sbin/pct; sudo: true runs sudo -n pct exec ..., so a missing NOPASSWD rule fails immediately instead of hanging.

  • Keep strict_host_key: true. known_hosts must exist or the shell tools stay hidden.

  • authorized_keys cannot use command="..." restrictions: the MCP server sends arbitrary commands (host shell, pct exec with a variable VMID). A forced command would break every call. Restrict by key (from="10.0.0.20"), a dedicated user, firewall rules and the policy instead. restrict (no forwarding, no agent, no pty) is fine.

  • Revoke by removing the key from authorized_keys; the MCP server reconnects with the key on the next call.

Security (short version)

  • Bind the MCP port to LAN/VPN only; put TLS in front of it (reverse proxy).

  • The http transport refuses to start without PVEHUB_MCP_AUTH_TOKEN (any bind address). PVEHUB_MCP_AUTH_REQUIRED=false is an explicit, logged opt-out for loopback development setups only; a non-loopback bind without a token is refused even with the opt-out.

  • Use a dedicated PVE/PBS API token with the minimum ACLs and no expiry surprises.

  • Start with config/examples/readonly.yaml; widen to operator.yaml when needed. admin.yaml (destructive + host shells) is for trusted single-operator setups only.

  • Shell access: dedicated SSH key, known_hosts verification, exec.host and exec.pbs_host stay false unless you really need them.

  • Destructive and exec tools require confirm=true; destructive tools support dry_run where the API does.

  • The generic API bridge always denies ticket/console endpoints (/access/ticket, token endpoints, vncshell, spiceshell, termproxy, QEMU monitor) and the guest-agent exec/file-write endpoints; use exec_vm / file_write_guest for those. Non-GET generic calls require confirm=true.

  • Audit JSONL is written to /data/audit.jsonl with secret redaction; result bodies are opt-in (PVEHUB_AUDIT_INCLUDE_RESULTS=true).

  • The container runs as UID 10001, read-only root filesystem, cap_drop: ALL, no-new-privileges.

Troubleshooting

Symptom

Cause / fix

refusing to bind a non-loopback address without mcp_auth_token

Set PVEHUB_MCP_AUTH_TOKEN (non-empty) or bind to 127.0.0.1

mcp_auth_token is required for the http transport

Set PVEHUB_MCP_AUTH_TOKEN (see docs/CONFIGURATION.md), or PVEHUB_MCP_AUTH_REQUIRED=false for loopback development setups

configuration error: ... extra_forbidden

Unknown YAML key; compare with docs/CONFIGURATION.md. extra="forbid" is intentional

policy error: ... invalid policy

Unknown category or key in policy.yaml; valid categories are read, lifecycle, write, destructive, exec, generic

401 unauthorized on /mcp

Missing/wrong Authorization: Bearer ... header

421 Invalid Host header or DNS-rebinding error

Set PVEHUB_ALLOWED_HOSTS to the hostnames clients use (including port if non-default)

docker compose complains about .env

cp .env.example .env first; compose requires the file

Tool missing from tools/list

Policy deny, category deny, missing token privilege, missing SSH target/key, or exec.host/exec.pbs_host disabled. Ask pvehub_capabilities for tools.blocked reasons

/health shows degraded

Permission discovery failed for at least one instance; the server still serves tools per capabilities_fail_open. Check logs and token ACLs

SSH key not found / known_hosts not found

Wrong mount path, wrong UID/permissions, or missing ssh.nodes entry for the node

sudo: a password is required

sudo: true is set but no NOPASSWD rule for pct exists on the node

Container unhealthy while using stdio

Expected: there is no HTTP listener, so the /health check fails. Harmless for docker run -i

Changes to policy/token ACLs have no effect

Settings and policy are read at startup; restart the container

Documentation

Document

Content

docs/ARCHITECTURE.md

components, request pipeline, why SSH is needed

docs/CONFIGURATION.md

every environment variable and YAML key, precedence, TLS, policy, audit

docs/TOOLS.md

generated tool catalog with categories and privileges

docs/DECISIONS.md

design decisions and trade-offs

docs/PLAN.md

phase plan

docs/REUSE_AUDIT.md

reference projects, licenses, what was reused

Development

make install        # uv sync --extra dev
make check          # ruff + mypy + pytest
make docs-tools     # regenerate docs/TOOLS.md from @hub.tool declarations
make docker-build   # docker build -t pvehub-mcp:0.1.0 .

The Docker image is built by CI (.gitea/workflows/ci.yml); the development sandbox has no Docker daemon, so the image build is not executed there (see docs/DECISIONS.md D010).

License

MIT — see LICENSE. Third-party inspiration: THIRD_PARTY.md.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables comprehensive management of Proxmox virtualization environments, including VM and container lifecycle, snapshots, backups, monitoring, and OpenAPI integration.
    12
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables management of Proxmox VE infrastructure via its REST API and execution of commands inside LXC containers via SSH, with built-in trust layers for planning, audit, undo, and diagnosis.
    6
    740 PyPI
    85
    AGPL 3.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    Exposes 338 tools for the Proxmox Virtual Environment API, enabling users to inspect and control nodes, QEMU VMs, LXC containers, storage and backups, clustering and HA, users and permissions, firewall rules, disks, Ceph, ACME certificates, SDN, notifications, and resource pools. Includes lifecycle actions, snapshots, migration, metrics, task tracking, and guest-agent command execution, with an optional read-only mode that blocks all writes.
    MIT