Proxmox MCP Server
by ry-ops
README.md
<p align="center">
<img src="docs/hero.svg" width="100%" alt="You ask your assistant to clone VM 9000 to web-02 and start it; clone_vm and start_vm flow over MCP, by stdio or HTTP, through proxmox-mcp-server to the Proxmox cluster, and the new VM lights up on pve2.">
</p>
<p align="center">
<a href="https://github.com/ry-ops/proxmox-mcp-server/releases"><img src="https://img.shields.io/github/v/release/ry-ops/proxmox-mcp-server?color=ff8a1f&label=release" alt="Release"></a>
<img src="https://img.shields.io/badge/tools-346-ff8a1f" alt="346 tools">
<a href="#pdm"><img src="https://img.shields.io/badge/Datacenter_Manager-89_tools-3ddc84" alt="Proxmox Datacenter Manager: 89 tools"></a>
<a href="#deploy"><img src="https://img.shields.io/badge/deploy_by_asking-Ubuntu_%C2%B7_Kali_%C2%B7_Win11_%C2%B7_PDM-b58cff" alt="Deploy Ubuntu, Kali, Windows 11 and PDM by asking"></a>
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-3ec7ff" alt="Python 3.10+"></a>
<a href="https://modelcontextprotocol.io/"><img src="https://img.shields.io/badge/MCP-2.x-b58cff" alt="MCP SDK 2.x"></a>
<a href="https://github.com/ry-ops/proxmox-mcp-server/pkgs/container/proxmox-mcp-server"><img src="https://img.shields.io/badge/docker-ghcr.io-3ddc84" alt="Docker image on ghcr.io"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-ffb02e" alt="MIT"></a>
</p>
<p align="center"><b>Talk to your Proxmox cluster.</b> A Model Context Protocol server that gives Claude, or any MCP client, the <b>whole Proxmox VE API</b>: 346 tools across VMs, containers, storage, clustering, Ceph, SDN and more. Add <b>Proxmox Datacenter Manager</b> for 89 more that reach every cluster and Backup Server it manages, and <b>ask for a machine by name</b>: Ubuntu, Kali, Windows 11 or a Datacenter Manager.</p>
<p align="center">
<a href="#talk">Talk to it</a> ·
<a href="#how">How it works</a> ·
<a href="#tools">346 tools</a> ·
<a href="#prompts">Prompts</a> ·
<a href="#quick-start">Quick start</a> ·
<a href="#clusters">Clusters</a> ·
<a href="#deploy">Deploy by asking</a> ·
<a href="#pdm">Datacenter Manager</a> ·
<a href="#toolsets">Toolsets</a> ·
<a href="#http">HTTP</a> ·
<a href="#read-only">Safety</a> ·
<a href="#wait">Waiting</a> ·
<a href="#a2a">A2A</a> ·
<a href="#troubleshooting">Troubleshooting</a>
</p>
---
## ✨ Highlights
- 🧑🚀 **Ask for a machine, get a running one.** "Spin up a Kali VM" runs `deploy_kali_vm`: official image, checksum checked, installed unattended, address reported. Ubuntu, Windows 11 (TPM, Secure Boot, VirtIO) and Datacenter Manager work the same way. [More](#deploy)
- 🛰️ **Every cluster through one connection.** Point it at Proxmox Datacenter Manager and 89 tools reach every Proxmox VE cluster and Backup Server it manages, including cross-cluster migration. [More](#pdm)
- 🧰 **The whole API, not a demo.** 346 tools across 14 areas: nodes, QEMU VMs, LXC containers, storage and backup, cluster and HA, access control, firewall, disks, Ceph, ACME, SDN, notifications, pools and appliances.
- 🧭 **Workflows built in.** Five MCP prompts, such as a cluster health check and a snapshot-change-verify routine, plus live cluster state as MCP resources.
- 🖥️ **Inside your guests too.** Run commands through the QEMU guest agent (`vm_agent_exec` → `vm_agent_exec_status`) and read back the exit code and output.
- 🔒 **Look-but-don't-touch with one switch.** `PROXMOX_READ_ONLY=true` refuses every write inside the server, and the client sees only the 151 read-only tools.
- 🧯 **Safe writes, protected guests.** `PROXMOX_NO_DESTRUCTIVE=true` hides the 73 tools that delete, stop, wipe or roll back. `PROXMOX_PROTECTED_VMIDS` names guests no tool may change.
- ⏳ **Waits for the slow stuff.** Clone, migrate, backup and restore take `wait: true`, and `wait_for_task` follows any task to the end, with the log when it fails.
- 🎯 **Load only what you use.** `PROXMOX_TOOLSETS=nodes,qemu,lxc,storage` cuts the tool list from 346 to 127, and the context it costs by more than half. Results come back as compact JSON.
- 🚦 **Labelled for safe approval.** Every tool carries MCP `readOnlyHint` and `destructiveHint` annotations, so clients can auto-approve reads and ask before a delete, stop or rollback.
- 🆕 **Current protocol, old clients welcome.** Built on MCP SDK 2: clients on the stateless 2026-07-28 protocol and on every handshake version back to 2024-11-05 connect. Failed calls are flagged as errors, so clients show them as failures.
- 🔑 **API tokens or passwords.** Tokens are recommended; pair them with a `PVEAuditor` role for monitoring.
- 🌐 **Local or on the network.** Clients spawn it over stdio, or `proxmox-mcp-server-http` serves it over streamable HTTP. Give each client its own token and a role: `read`, `operator` or `admin`.
- 📜 **Every change on the record.** `PROXMOX_AUDIT_LOG` writes one JSON line per write call: who, what, the outcome and the task, with secrets redacted.
- 🐳 **Runs anywhere.** Use `uv` from a clone, or the multi-arch image on `ghcr.io`.
- ♻️ **Long sessions just work.** Password logins renew their Proxmox ticket before it expires.
- 🤝 **A2A-ready.** `agent-card.json` lists every tool, Proxmox VE and Datacenter Manager, by skill category, for agent-to-agent discovery.
- 🛡️ **Appliances.** `deploy_fortigate_vm` builds a FortiGate-VM from Fortinet's KVM image, with WAN/LAN NICs on the bridges and VLANs you choose. `deploy_cloud_vms` builds cluster-ready VMs from a cloud image, with your SSH key and a static IP each, ready for k3s-mcp-server's `create_cluster`.
<a id="talk"></a>
## 💬 Talk to your cluster
<p align="center">
<img src="docs/chat.svg" width="100%" alt="A chat runs df -h inside VM 120 via the QEMU guest agent, with vm_agent_exec labelled destructive and vm_agent_exec_status labelled read-only, and shows the disk usage; a side panel cycles through more questions and the tools that answer them.">
</p>
Once it's connected, just ask:
> *"Can you list all VMs in my Proxmox cluster?"*
> *"What's the status of VM 100 on node pve1?"*
> *"Create a snapshot called 'backup-2025' for VM 100 on pve1."*
> *"Show me the storage usage on all nodes."*
> *"List all running tasks in the cluster."*
> *"Clone VM 9000 to a new VM called web-02 and start it."*
> *"Run `df -h` inside VM 120 using the guest agent and show me the output."*
> *"Make three Ubuntu VMs on pve1 at 192.168.1.181–183 with my SSH key, ready for a K3s cluster."*
There are more worked examples in [USAGE.md](USAGE.md).
<a id="prompts"></a>
### 🧭 Built-in prompts and resources
<p align="center">
<img src="docs/prompts.svg" width="100%" alt="Typing /mcp__proxmox__ in the client lists five prompts. Choosing cluster_health_check runs five read-only tools and the assistant reports one critical item, one warning and what is OK. Resources such as proxmox://cluster/resources attach live state without a tool call.">
</p>
The server ships **MCP prompts**, ready-made workflows your client lists by name. In Claude Code, type `/` to see them, for example `/mcp__proxmox__cluster_health_check`.
| Prompt | What it does |
|---|---|
| `cluster_health_check` | Read-only check of quorum, nodes, guests, failed tasks, HA and Ceph, reported by severity |
| `capacity_report` | Read-only storage, CPU and memory report with the biggest consumers; takes `threshold_percent` |
| `safe_change` | Snapshot a guest, make your change after your go-ahead, verify it, and offer a rollback; takes `vmid` and `change` |
| `investigate_failed_tasks` | Read-only: failed tasks, their logs, grouped by cause with fixes; takes `hours` |
| `decommission_guest` | Back up, stop and delete a guest, asking before every change; takes `vmid` |
Prompts only guide the model. Every step is still an ordinary tool call, so roles, read-only mode and protected guests apply as usual.
**MCP resources** let a client attach live cluster state as context without spending tool calls:
| Resource | Contents |
|---|---|
| `proxmox://cluster/status` | Quorum and node membership |
| `proxmox://cluster/resources` | Every node, VM, container and storage with live usage |
| `proxmox://cluster/tasks` | Recent tasks, with status |
| `proxmox://nodes` | Nodes with CPU, memory and uptime |
| `proxmox://storage` | Storage definitions |
| `proxmox://nodes/{node}/status` | One node in detail |
| `proxmox://vms/{node}/{vmid}/config`, `proxmox://cts/{node}/{vmid}/config` | One guest's configuration |
Each resource is a single read, so resources work in read-only mode and for every role.
<a id="how"></a>
## ⚙️ How it works
<p align="center">
<img src="docs/how-it-works.svg" width="100%" alt="MCP clients talk to proxmox-mcp-server over stdio or HTTP; the server calls the Proxmox VE API over HTTPS on port 8006. With read-only mode on, only the 150 read tools are listed, GET requests pass, and POST, PUT and DELETE are refused inside the server.">
</p>
- **Your MCP client** (Claude Desktop, Claude Code, or anything that speaks MCP) starts the server and talks to it over **stdio**. Or you run it as a service and clients connect over **[streamable HTTP](#http)**.
- **`PROXMOX_TOOLSETS`** decides which API areas the server loads, and so which tools the client sees. Each tool is labelled read-only or destructive.
- **The server** (Python, `httpx`, the MCP SDK) turns each tool call into a request to the **Proxmox VE REST API** over HTTPS on port **8006**, authenticated with an **API token** or a **password**.
- **Read-only mode** hides the write tools and is also checked in the server's HTTP client, so a refused write never leaves your machine.
<a id="tools"></a>
## 🧰 346 tools, 14 areas
<p align="center">
<img src="docs/tools.svg" width="100%" alt="A tile map of the 341 tools by toolset: QEMU VMs 46, nodes 38, cluster and HA 37, access control 33, Ceph 33, firewall 31, LXC 28, notifications 23, disks 17, ACME 17, SDN 16, storage and backup 14, pools 5, appliances 2, plus wait_for_task, which every toolset loads. Then all but nodes, qemu, lxc and storage dim, and the count drops to 127.">
</p>
| Area | Tools | Highlights |
|------|------:|------------|
| Nodes | 38 | Status, config, DNS/hosts/time, network interfaces, services, APT updates, syslog, power |
| QEMU VMs | 46 | Lifecycle, create/clone/delete, config, snapshots, migration, disk resize/move, RRD metrics, guest agent (`vm_agent_exec`, `vm_agent_exec_status`), VNC proxy |
| LXC containers | 28 | Lifecycle, create/clone/delete, config, snapshots, migration, resize |
| Storage & backup | 14 | Storage definitions, content and volumes, URL downloads, `vzdump` backup and restore |
| Cluster | 37 | Status, resources, options, log, tasks, HA resources/groups, replication, backup jobs |
| Access control | 33 | Users, API tokens, groups, roles, ACLs, realms |
| Firewall | 31 | Cluster and node rules, security groups, aliases, IP sets, macros, logs (per-VM and per-container rules are in the guest modules) |
| Disks | 17 | SMART, wipe, GPT init, LVM, LVM-thin, ZFS, directory storage |
| Ceph | 33 | Status, OSDs, monitors, managers, MDS, pools, flags, CRUSH |
| ACME & certificates | 17 | ACME accounts and plugins, node certificate ordering, custom certs |
| SDN | 16 | Zones, VNets, subnets, apply |
| Notifications | 23 | Gotify, sendmail, SMTP and webhook endpoints, matchers |
| Pools | 5 | Resource pool CRUD |
| Appliances | 2 | `deploy_fortigate_vm`: FortiGate-VM from Fortinet's KVM image, WAN/LAN NICs on chosen bridges and VLANs, sized for the free evaluation license. `deploy_cloud_vms`: VMs from a cloud image (e.g. Ubuntu's) with cloud-init user, SSH key and a static IP or DHCP each, disk grown and started |
One more tool, `wait_for_task`, is loaded with every toolset. It waits for any Proxmox task to finish; see [Waiting for long tasks](#wait).
Tool names follow the Proxmox API: `list_*`, `get_*`, `create_*`, `update_*`/`set_*`, `delete_*`, plus actions such as `start_vm`, `migrate_container` or `apply_sdn`. Each area is one module in [`proxmox_mcp/tools/`](proxmox_mcp/tools/). Your MCP client shows every tool with its full input schema, or you can list them all locally:
```bash
uv run python -c "from proxmox_mcp.server import ALL_TOOLS; print('\n'.join(sorted(t.name for t in ALL_TOOLS)))"
```
### Handy recipes
| Task | Tools |
|------|-------|
| Inventory | `list_nodes`, `get_cluster_resources`, `list_vms`, `list_containers`, `list_storage` |
| VM lifecycle | `start_vm`, `shutdown_vm`, `stop_vm`, `reboot_vm`, `create_vm`, `clone_vm`, `migrate_vm` |
| Disk images | `download_url_to_storage` with `content=import`, then `create_vm` with `scsi0=local-lvm:0,import-from=local:import/<image>`, or `import_vm_disk` |
| Snapshots | `create_vm_snapshot`, `list_vm_snapshots`, `rollback_vm_snapshot`, `delete_vm_snapshot` |
| Monitoring | `get_node_status`, `get_vm_status`, `get_vm_rrddata`, `get_node_storage_status` |
| Tasks | `list_cluster_tasks`, `list_node_tasks`, `get_task_status`, `get_task_log` |
| Guest agent | `vm_agent_exec` (returns a PID), then `vm_agent_exec_status` for the exit code and output |
| Cluster nodes | `download_url_to_storage` for a cloud image with `content=import`, then `deploy_cloud_vms`; hand the IPs to [k3s-mcp-server](https://github.com/ry-ops/k3s-mcp-server)'s `create_cluster` |
<a id="quick-start"></a>
## 🚀 Quick start
You need **Python 3.10+** with [`uv`](https://github.com/astral-sh/uv), or Docker, and **Proxmox VE 6.0 or later**.
**1. Make an API token.** In the Proxmox UI, go to **Datacenter → Permissions → API Tokens → Add**. For full access, uncheck *Privilege Separation* so the token inherits your user's permissions. Copy the secret; you won't see it again. The full token ID looks like `root@pam!automation`.
**2. Run the server** with one of these:
<details open>
<summary><b>uv, from a clone</b></summary>
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh # if you don't have uv
git clone https://github.com/ry-ops/proxmox-mcp-server && cd proxmox-mcp-server
./setup.sh # or: uv sync
export PROXMOX_HOST="192.168.1.100"
export PROXMOX_USER="root@pam"
export PROXMOX_TOKEN_NAME="automation"
export PROXMOX_TOKEN_VALUE="your-token-here"
./test-connection.sh # checks connection, auth and permissions
```
A `.env` file in the project directory is loaded automatically; see [`.env.example`](.env.example).
</details>
<details>
<summary><b>Docker</b></summary>
A multi-arch image is published to GitHub Container Registry on every push to `main` and for each release tag:
```bash
docker pull ghcr.io/ry-ops/proxmox-mcp-server:latest # or a version tag, e.g. :2.14.0
docker run -i --rm --env-file .env ghcr.io/ry-ops/proxmox-mcp-server:latest
```
MCP clients talk over stdio, so run the container interactively (`-i`). `docker-compose.yaml` does the same using your `.env`. To serve it over HTTP instead, see [Run it over HTTP](#http).
</details>
**3. Connect your MCP client.** For Claude Desktop, edit `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, or `%APPDATA%/Claude/claude_desktop_config.json` on Windows:
<details open>
<summary><b>uv + API token (recommended)</b></summary>
```json
{
"mcpServers": {
"proxmox": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/proxmox-mcp-server", "run", "proxmox-mcp-server"],
"env": {
"PROXMOX_HOST": "192.168.1.100",
"PROXMOX_USER": "root@pam",
"PROXMOX_TOKEN_NAME": "automation",
"PROXMOX_TOKEN_VALUE": "your-token-value-here"
}
}
}
}
```
Use the **absolute** path to your clone.
</details>
<details>
<summary><b>uv + password</b></summary>
```json
{
"mcpServers": {
"proxmox": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/proxmox-mcp-server", "run", "proxmox-mcp-server"],
"env": {
"PROXMOX_HOST": "192.168.1.100",
"PROXMOX_USER": "root@pam",
"PROXMOX_PASSWORD": "your-password-here"
}
}
}
}
```
</details>
<details>
<summary><b>Docker</b></summary>
```json
{
"mcpServers": {
"proxmox": {
"command": "docker",
"args": ["run", "-i", "--rm",
"-e", "PROXMOX_HOST", "-e", "PROXMOX_USER", "-e", "PROXMOX_TOKEN_NAME", "-e", "PROXMOX_TOKEN_VALUE",
"ghcr.io/ry-ops/proxmox-mcp-server:latest"],
"env": {
"PROXMOX_HOST": "192.168.1.100",
"PROXMOX_USER": "root@pam",
"PROXMOX_TOKEN_NAME": "automation",
"PROXMOX_TOKEN_VALUE": "your-token-value-here"
}
}
}
}
```
</details>
<details>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add proxmox \
-e PROXMOX_HOST=192.168.1.100 -e PROXMOX_USER=root@pam \
-e PROXMOX_TOKEN_NAME=automation -e PROXMOX_TOKEN_VALUE=your-token-value-here \
-- uv --directory /absolute/path/to/proxmox-mcp-server run proxmox-mcp-server
```
Add `-e PROXMOX_TOOLSETS=nodes,qemu,lxc,storage` to keep the tool list small. For a server you run over HTTP, see [Run it over HTTP](#http).
</details>
**4. Restart your client completely** (quit it, don't just close the window), and ask it to list your VMs. More detail in [QUICKSTART.md](QUICKSTART.md) and [SETUP.md](SETUP.md).
<a id="clusters"></a>
### 🏢 Several clusters
Run one server per cluster, each under its own name. Every cluster then gets its own credentials, toolsets, safety settings and audit log, and the assistant always knows which cluster a tool acts on, because the tool names carry the server name (`mcp__proxmox-lab__start_vm` and `mcp__proxmox-prod__start_vm`).
```json
{
"mcpServers": {
"proxmox-lab": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/proxmox-mcp-server", "run", "proxmox-mcp-server"],
"env": { "PROXMOX_HOST": "10.0.10.5", "PROXMOX_USER": "root@pam",
"PROXMOX_TOKEN_NAME": "automation", "PROXMOX_TOKEN_VALUE": "lab-token" }
},
"proxmox-prod": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/proxmox-mcp-server", "run", "proxmox-mcp-server"],
"env": { "PROXMOX_HOST": "10.0.20.5", "PROXMOX_USER": "mcp@pve",
"PROXMOX_TOKEN_NAME": "readonly", "PROXMOX_TOKEN_VALUE": "prod-token",
"PROXMOX_NO_DESTRUCTIVE": "true", "PROXMOX_PROTECTED_VMIDS": "100-110",
"PROXMOX_TOOLSETS": "nodes,qemu,lxc,storage" }
}
}
}
```
Give each extra cluster only the toolsets it needs, because every server's tools count against the client's context. Over HTTP, run one `proxmox-mcp-server-http` per cluster on its own port.
<a id="deploy"></a>
### 🧑🚀 Deploy an OS by asking
<p align="center">
<img src="docs/deploy.svg" width="100%" alt="You ask for a Kali VM and the server runs deploy_kali_vm: it fetches Kali's official cloud image and checks its SHA-256, repacks the sparse 25 GiB disk as a 1.06 GiB qcow2, builds a cloud-init seed ISO, creates and boots VM 120, waits for the guest agent to report 10.0.0.77, and deletes the seed ISO. Below, Ubuntu, Kali, Windows 11 and Datacenter Manager build side by side, each finishing with how to reach the machine.">
</p>
Ask for a VM by name and the server does the rest: fetches the official image (checksum verified, cached on storage and reused), builds a small seed ISO with the first-boot settings, creates and starts the VM, waits until its guest agent answers, and reports how to reach it.
| Tool | What you get |
|---|---|
| `deploy_ubuntu_vm` | Ubuntu 24.04 LTS from the official cloud image |
| `deploy_kali_vm` | Kali Linux from its official cloud image. It ships as `.tar.xz`, which Proxmox can't import, so the server repacks its sparse 25 GiB disk as qcow2 and uploads only the ≈1 GiB of data |
| `deploy_pdm_vm` | Proxmox Datacenter Manager on a Debian 13 VM, installed by cloud-init from the no-subscription repository; reports `https://<ip>:8443` and the `root@pam` login |
| `deploy_windows11_vm` | Windows 11 installed unattended: q35, UEFI, Secure Boot keys, TPM 2.0, VirtIO disk and network, VirtIO guest tools. Uses a Windows 11 ISO already on storage (Microsoft has no stable download link); installs without a product key, so activation is yours |
| `list_os_images` | What's cached on the node, and with `check_upstream`, the latest official versions |
Give an SSH public key, or the tool generates a password and returns it once (the audit log redacts passwords). The seed ISO holds those credentials, so it's deleted as soon as the guest is up. Each tool takes the usual sizing and network inputs (`cores`, `memory`, `disk_gb`, `bridge`, `vlan`, a static `ip` or DHCP) and `wait_until_ready` / `ready_timeout`. Windows Setup takes 20 to 40 minutes; with a shorter timeout the tool returns `installing` and says what to clean up after.
<a id="pdm"></a>
### 🛰️ Proxmox Datacenter Manager
<p align="center">
<img src="docs/pdm.svg" width="100%" alt="proxmox-mcp's pdm toolset connects to Proxmox Datacenter Manager with an API token. One pdm_list_resources call fans out to three remotes and returns their guests, nodes and datastores. pdm_pve_remote_migrate_guest moves web-01 from pve-lab to the pve-prod cluster. pdm_pve_stop_guest on the protected guest pve-lab:104 is refused, and pdm_collect_metrics, which PDM reserves for root@pam, explains the elevated permission it needs.">
</p>
If you run [Proxmox Datacenter Manager](https://pdm.proxmox.com/) (PDM), point the server at it and the `pdm` toolset loads: 89 tools that reach every Proxmox VE cluster and Proxmox Backup Server PDM manages, through one connection. It can run alongside a direct Proxmox VE connection, or on its own.
- **Across everything:** resources, top consumers, tasks, pending updates and subscriptions for all remotes at once.
- **Per remote:** nodes, guests, configs, snapshots, storage, firewall, tasks and metrics on Proxmox VE remotes; datastores, namespaces, snapshots and tasks on Backup Server remotes; SDN and Ceph.
- **Actions:** start, stop, shut down and resume guests; create, delete, roll back and describe snapshots; migrate within a cluster or **to another cluster** (`pdm_pve_remote_migrate_guest`); stop tasks; refresh updates, task caches and metrics. Actions take `wait`.
Tools that cover both VMs and containers take `type` (`qemu` or `lxc`). Every input is checked against PDM's own schema before a request is sent.
```json
"env": {
"PDM_HOST": "10.0.0.20",
"PDM_TOKEN_ID": "root@pam!proxmox-mcp",
"PDM_TOKEN_SECRET": "…",
"PDM_CA_BUNDLE": "/path/to/pdm-api.pem",
"PDM_TLS_NAME": "pdm",
"PDM_PROTECTED_GUESTS": "pve01:104,pve01:105"
}
```
Create the token in PDM under **Configuration → Access Control → API Tokens**, then give it roles under **Permissions**. PDM tokens start with no permissions, and never get more than their user:
| Path | Role | Covers |
|---|---|---|
| `/resource` | **Auditor** for read-only, **Administrator** for actions | Every remote: reads need `Resource.Audit`; actions need `Resource.Manage` (power, snapshots, stopping tasks), `Resource.Modify` (refreshing package lists) and `Resource.Migrate` |
| `/system` | **Auditor** | `pdm_node_status` (the PDM host's own usage) |
Seven tools reach endpoints PDM 1.1 allows only for `root@pam` logged in with a password, so with an API token they return 403 whatever its roles: `pdm_pve_list_remotes` and `pdm_pbs_list_remotes` (use `pdm_list_remotes` instead), `pdm_metric_collection_status`, `pdm_collect_metrics`, `pdm_get_remote_rrddata`, and the two EVPN lookups. Their descriptions say so, and when PDM refuses one, the error explains the elevated permissions it needs and what to use instead.
PDM can also protect a guest itself: a **NoAccess** entry for the token on `/resource/<remote>/guest/<vmid>` (for example `/resource/pve01/guest/104`) makes PDM refuse every call about that guest, whatever this server does. For `PDM_CA_BUNDLE`, copy `/etc/proxmox-datacenter-manager/auth/api.pem` from the PDM host. Its self-signed certificate names `pdm` and `localhost`, not the host's IP, so when `PDM_HOST` is an IP also set `PDM_TLS_NAME=pdm`: the certificate is checked against that name while the server connects to the IP.
**Protected guests.** VMIDs are only unique within a cluster, so PDM tools use `PDM_PROTECTED_GUESTS`, with the remote in each entry: `pve01:104`, or `*:9000-9010` for every remote. A migration is refused when its source or its target is protected. `PROXMOX_PROTECTED_VMIDS` doesn't apply to PDM tools. `PROXMOX_READ_ONLY`, `PROXMOX_NO_DESTRUCTIVE`, HTTP roles and the audit log apply as usual.
The tools are generated from PDM's published API schema; [ROADMAP.md](ROADMAP.md) maps all 327 PDM endpoints and what's planned next.
## 🔧 Configuration
| Variable | Required | Default | What it does |
|---|:---:|---|---|
| `PROXMOX_HOST` | ✅¹ | | Hostname or IP, **without** scheme or port (`192.168.1.100`, not `https://192.168.1.100:8006`) |
| `PROXMOX_USER` | ✅¹ | | e.g. `root@pam`, `admin@pve` |
| `PROXMOX_TOKEN_NAME` + `PROXMOX_TOKEN_VALUE` | one of | | API token auth (recommended) |
| `PROXMOX_PASSWORD` | one of | | Password auth |
| `PROXMOX_PORT` | | `8006` | API port |
| `PROXMOX_VERIFY_SSL` | | `false` | Verify the server's TLS certificate against the system's trusted CAs |
| `PROXMOX_CA_BUNDLE` | | | CA file to verify against instead, e.g. a copy of `/etc/pve/pve-root-ca.pem`; works with Proxmox's default self-signed certificates |
| `PROXMOX_READ_ONLY` | | `false` | Refuse every write and advertise only read tools; see below |
| `PROXMOX_TOOLSETS` | | all | Comma-separated API areas to load; see below |
| `PROXMOX_NO_DESTRUCTIVE` | | `false` | Hide the 73 tools that delete, stop, wipe or roll back; see [Safety](#read-only) |
| `PROXMOX_PROTECTED_VMIDS` | | | Guest IDs and ranges no tool may change, e.g. `100,101,9000-9010` |
| `PROXMOX_WAIT_TIMEOUT` | | `900` | Seconds a `wait` lasts before it returns with the task still running |
| `MCP_HTTP_HOST` | | `127.0.0.1` | HTTP entrypoint only: bind address |
| `MCP_HTTP_PORT` | | `8811` | HTTP entrypoint only: port |
| `MCP_AUTH_TOKEN` | | | HTTP entrypoint only: one admin bearer token; a token is required off loopback |
| `MCP_AUTH_TOKENS` | | | HTTP entrypoint only: several tokens with roles, `name:role:token,…`; see [Roles](#roles) |
| `MCP_AUTH_TOKENS_FILE` | | | HTTP entrypoint only: the same, one `name role token` per line |
| `PROXMOX_AUDIT_LOG` | | | File, or `stderr`, for a JSON-lines log of every write call; see [Audit log](#audit) |
| `PROXMOX_AUDIT_READS` | | `false` | Log read calls too |
| `MCP_HTTP_ALLOW_NO_AUTH` | | `false` | HTTP entrypoint only: allow a non-loopback address with no token |
| `PDM_HOST` | ✅¹ | | Proxmox Datacenter Manager hostname or IP; loads the `pdm` toolset. See [Datacenter Manager](#pdm) |
| `PDM_PORT` | | `8443` | PDM API port |
| `PDM_TOKEN_ID` + `PDM_TOKEN_SECRET` | with `PDM_HOST` | | PDM API token, `user@realm!name`, and its secret |
| `PDM_CA_BUNDLE` | | | PDM's certificate (or its CA) to verify against |
| `PDM_TLS_NAME` | | | Name to check PDM's certificate against when `PDM_HOST` is an IP it doesn't name, e.g. `pdm` |
| `PDM_VERIFY_SSL` | | `false` | Verify PDM's certificate against the system's trusted CAs instead |
| `PDM_PROTECTED_GUESTS` | | | `remote:ID` entries PDM tools may not change, e.g. `pve01:104,*:9000-9010` |
¹ Set `PROXMOX_HOST` (with `PROXMOX_USER` and a token or password) to talk to Proxmox VE directly, `PDM_HOST` to go through Datacenter Manager, or both.
<a id="toolsets"></a>
### 🎯 Toolsets: load only what you use
Every tool the server advertises sits in the client's context on every turn, and all 346 schemas come to roughly 44k tokens. Set `PROXMOX_TOOLSETS` to the areas you need:
```bash
PROXMOX_TOOLSETS=nodes,qemu,lxc,storage
```
| Setting | Tools | Approx. tokens |
|---|--:|--:|
| all (default) | 346 | 44k |
| `PROXMOX_NO_DESTRUCTIVE=true` | 273 | 35k |
| `nodes,qemu,lxc,storage,cluster` | 164 | 23k |
| `nodes,qemu,lxc,storage` | 127 | 19k |
| `PROXMOX_READ_ONLY=true` | 151 | 13k |
| `qemu` | 47 | 8k |
Every count includes `wait_for_task`, which loads with any selection.
The names are `nodes`, `qemu`, `lxc`, `storage`, `cluster`, `access`, `firewall`, `disks`, `ceph`, `acme`, `sdn`, `notifications`, `pools` and `appliances`. `vms` and `containers` work as aliases for `qemu` and `lxc`. A misspelt name stops the server at startup with the list of valid ones. Toolsets combine with read-only mode.
### 🚦 Tool annotations
Each tool tells the client what it does, through standard MCP annotations:
- **`readOnlyHint: true`** on the 151 tools that only read.
- **`destructiveHint: true`** on the 73 that delete, stop, wipe, roll back, restore over, or run commands in a guest.
- **`destructiveHint: false`** on the other writes, such as create, start or update.
A test sends every tool through a recording client and checks its read-only label against the HTTP methods it sends, so the labels can't drift from the code.
<a id="http"></a>
### 🌐 Run it over HTTP
`proxmox-mcp-server-http` serves the same tools over MCP streamable HTTP at `/mcp`, so the server can run as a network service instead of a process each client spawns. `PROXMOX_TOOLSETS` and `PROXMOX_READ_ONLY` apply as usual, and `/healthz` answers without auth for container health checks.
```bash
# Local only, no token needed:
uv run proxmox-mcp-server-http # http://127.0.0.1:8811/mcp
# On the network, a token is required:
MCP_HTTP_HOST=0.0.0.0 MCP_AUTH_TOKEN="$(openssl rand -hex 32)" uv run proxmox-mcp-server-http
```
The same image runs it in Docker:
```bash
docker run -p 8811:8811 --env-file .env -e MCP_HTTP_HOST=0.0.0.0 -e MCP_AUTH_TOKEN \
ghcr.io/ry-ops/proxmox-mcp-server:latest proxmox-mcp-server-http
```
Clients send `Authorization: Bearer <token>`. For Claude Code:
```bash
claude mcp add --transport http proxmox http://pve-mcp.lan:8811/mcp \
--header "Authorization: Bearer $MCP_AUTH_TOKEN"
```
**Anyone who can reach this endpoint can use every loaded tool with your Proxmox credentials.** So the server binds to `127.0.0.1` by default. It refuses any other address unless a token is set, or you opt out with `MCP_HTTP_ALLOW_NO_AUTH=true` because a proxy in front of it authenticates clients. The token check runs in constant time. On loopback, DNS-rebinding protection rejects requests whose Host header isn't a local address. Put TLS in front of it, for example with a reverse proxy, when the token crosses a network.
<a id="roles"></a>
#### 🎭 One token per client, each with a role
<p align="center">
<img src="docs/http-roles.svg" width="100%" alt="Four clients call the HTTP server: ops with the admin role sees 341 tools, ci with the operator role sees 268, grafana with the read role sees 150, and a client with no token gets 401. The audit log records ci's clone as ok and grafana's stop_vm as refused.">
</p>
Give every client its own token, so you can tell them apart in the [audit log](#audit) and revoke one without the others. Each token carries a role:
| Role | Can use |
|---|---|
| `read` | only tools that read, like a dashboard or a monitoring agent |
| `operator` | everything except destructive tools: start, create, clone, snapshot, migrate |
| `admin` | everything the server loads |
```bash
MCP_AUTH_TOKENS="ops:admin:$(openssl rand -hex 32),grafana:read:$(openssl rand -hex 32)"
# or a file with one "name role token" per line, comments with #:
MCP_AUTH_TOKENS_FILE=/run/secrets/proxmox-mcp-tokens
```
Each client sees only the tools its role allows, and a call outside its role is refused before anything reaches Proxmox. Roles only narrow what the server already allows: `PROXMOX_READ_ONLY`, `PROXMOX_NO_DESTRUCTIVE`, toolsets and protected guests still apply to everyone. Tokens must be at least 16 characters, and names and tokens must be unique. `MCP_AUTH_TOKEN` still works, as an admin token named `default`.
<a id="wait"></a>
### ⏳ Waiting for long tasks
<p align="center">
<img src="docs/wait.svg" width="100%" alt="clone_vm with wait: true returns the finished task's outcome in one call. A backup outlasts the timeout and returns status running; wait_for_task then follows it to OK.">
</p>
Clone, migrate, backup, restore, disk moves, snapshots and power actions start a Proxmox task and return its ID, a UPID. Pass `wait: true` to any of these 30 tools and the result also carries the task's outcome:
```json
{"data":"UPID:pve2:0003A1B2:…:qmclone:9000:root@pam:","task":{"status":"stopped","exitstatus":"OK","ok":true}}
```
`wait_for_task` does the same for any UPID, including ones from the cluster task list. A failed task includes the last 20 lines of its log. A wait never fails just because a task is slow. After `PROXMOX_WAIT_TIMEOUT` seconds, 900 by default, it returns `status: "running"`, and calling `wait_for_task` again keeps waiting.
<a id="read-only"></a>
## 🔒 Safety: read-only mode, destructive tools and protected guests
<p align="center">
<img src="docs/safety.svg" width="100%" alt="Bars show 341 tools by default, 268 with PROXMOX_NO_DESTRUCTIVE and 150 with PROXMOX_READ_ONLY. Guests 100 and 101 are protected: stop_vm on 120 succeeds, delete_vm on 100 and node_stopall without a list are refused, and each call lands in the audit log.">
</p>
**`PROXMOX_READ_ONLY=true`** makes the server look-but-don't-touch. Only GET requests are sent, and every create, update, delete or action tool is refused inside the server before it reaches the network. **The client sees only the 151 read-only tools**, so it never plans around a tool that would be refused. Three read-style tools send POST requests, so they're hidden too:
- `vm_agent_ping`;
- `get_vm_vnc_proxy` and `get_vm_spice_proxy`, which create console access tickets.
**`PROXMOX_NO_DESTRUCTIVE=true`** is the middle ground. It hides the 73 tools labelled destructive: deleting, stopping, wiping, rolling back, restoring over a guest, and running commands inside one. Safe writes such as start, create, clone, snapshot and config changes stay available, so 268 tools remain.
**`PROXMOX_PROTECTED_VMIDS=100,101,9000-9010`** names guests no tool may change, such as your router, DNS or backup server. The server checks every write before sending it: power actions, config changes, snapshots, migration, disk moves, deletion, guest-agent commands, HA changes, and deleting the guest's volumes or backups. Reads always work. Cloning from a protected template, backing it up and opening its console stay allowed, because they leave it as it is. Node-wide and bulk start, stop and migrate actions need an explicit `vms` list while any guest is protected, since they would otherwise include it. Whole-node shutdown and reboot are not covered.
<a id="audit"></a>
**`PROXMOX_AUDIT_LOG=/var/log/proxmox-mcp/audit.jsonl`** records every write call as one JSON line, whether it succeeded, failed or was refused. Set it to `stderr` to send the lines to your container logs instead. `PROXMOX_AUDIT_READS=true` logs reads too.
```json
{"ts":"2026-10-30T21:04:12.418+00:00","who":"ci","via":"http","tool":"clone_vm","args":{"node":"pve2","vmid":9000,"newid":123},"outcome":"ok","upid":"UPID:pve2:…","task_ok":true,"ms":41873}
{"ts":"2026-10-30T21:05:02.007+00:00","who":"grafana","via":"http","tool":"stop_vm","args":{"node":"pve1","vmid":120},"outcome":"refused","error":"stop_vm is not allowed for grafana (role: read)","ms":0}
```
Arguments whose names look secret, such as passwords, tokens, private keys and cloud-init passwords, are written as `***`. A failure to write the log is reported on stderr and never fails the tool call.
The rest of the security picture:
- **For monitoring-only use, combine both locks:** `PROXMOX_READ_ONLY=true` *and* a token with the **`PVEAuditor`** role. The flag stops the server sending writes; the role makes Proxmox refuse them anyway.
- **The token's permissions are the real limit.** The server exposes destructive operations: deleting VMs, wiping disks, and running commands inside guests. Whatever the token can do, a connected AI client can ask for. Grant the minimum.
- **Prefer API tokens** over passwords. They can be revoked on their own.
- **Verify the TLS certificate.** Without it, anyone on the network path can read the token. With Proxmox's default self-signed certificates, copy `/etc/pve/pve-root-ca.pem` from any node and set `PROXMOX_CA_BUNDLE` to it; with a publicly trusted certificate, set `PROXMOX_VERIFY_SSL=true`. The server prints a warning at startup when neither is set.
- **Keep credentials out of version control**, and restrict network access to the API with firewall rules.
<a id="a2a"></a>
## 🤝 Agent-to-agent (A2A)
[`agent-card.json`](agent-card.json) is this server's identity and capability manifest for other agents. It lists **all 346 Proxmox VE tools in 14 skill categories, plus the 89 Datacenter Manager tools**, each with its name, description, inputs, and `read_only` and `destructive` flags. A test fails if it drifts from the tool registry. It also covers authentication requirements and MCP protocol details.
| Category | Tools | Category | Tools |
|---|--:|---|--:|
| `node_management` | 38 | `disk_management` | 17 |
| `virtual_machine_management` | 46 | `ceph_management` | 33 |
| `container_management` | 28 | `certificate_management` | 17 |
| `storage_management` | 14 | `sdn_management` | 16 |
| `cluster_management` | 38 | `notification_management` | 23 |
| `access_control` | 33 | `pool_management` | 5 |
| `firewall_management` | 31 | `appliances` | 2 |
<details>
<summary><b>Discover the agent's skills</b></summary>
```python
import json
with open('agent-card.json') as f:
agent_card = json.load(f)
print(f"Agent: {agent_card['name']} Version: {agent_card['version']}")
for skill in agent_card['skills']:
print(f"\n{skill['category']}:")
for capability in skill['capabilities']:
print(f" - {capability['name']}: {capability['description']}")
```
</details>
<details>
<summary><b>Call its tools over MCP</b></summary>
```python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="uv",
args=["--directory", "/path/to/proxmox-mcp-server", "run", "proxmox-mcp-server"],
env={
"PROXMOX_HOST": "192.168.1.100",
"PROXMOX_USER": "root@pam",
"PROXMOX_TOKEN_NAME": "automation",
"PROXMOX_TOKEN_VALUE": "your-token-value",
},
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool("list_vms", arguments={})
print(result.content)
```
</details>
<details>
<summary><b>Orchestrate it with other agents</b></summary>
```python
# Snapshot every VM, then hand off to other agents
async def backup_workflow():
vms = await proxmox_agent.call_tool("list_vms", {})
for vm in vms['data']:
await proxmox_agent.call_tool("create_vm_snapshot", {
"node": vm['node'],
"vmid": vm['vmid'],
"snapname": f"backup-{datetime.now():%Y%m%d-%H%M%S}",
})
await storage_agent.call_tool("check_capacity", {})
await notification_agent.call_tool("send_alert", {"message": f"Backup completed: {len(vms['data'])} VMs"})
```
</details>
<a id="troubleshooting"></a>
## 🩺 Troubleshooting
<details>
<summary><b>Authentication errors</b></summary>
- Double-check the credentials and that the user has the permissions you need.
- For API tokens, make sure the token hasn't expired or been revoked.
- If the token was created with *Privilege Separation* on, give the token its own permissions, or recreate it with that option off.
</details>
<details>
<summary><b>Connection errors</b></summary>
- Check `PROXMOX_HOST` (no scheme, no port) and `PROXMOX_PORT`.
- Check you can reach the host: `curl -k https://YOUR_HOST:8006/api2/json/version`
- If `PROXMOX_VERIFY_SSL=true` or `PROXMOX_CA_BUNDLE` is set, the certificate must be valid for `PROXMOX_HOST`. Proxmox's node certificates name the node's hostname and IP addresses, so use one of those.
</details>
<details>
<summary><b>Permission errors</b></summary>
The user or token needs privileges for each operation. Common ones: `VM.Monitor`, `VM.Audit`, `Datastore.Audit`, `Sys.Audit`, `VM.PowerMgmt`, `VM.Snapshot`.
</details>
<details>
<summary><b>Tools don't show up in Claude</b></summary>
- The path in the config must be **absolute**.
- The config file must be valid JSON.
- Quit Claude Desktop completely and start it again.
- Check Claude Desktop's logs. The server logs its authentication method and connection status to stderr, which shows up there.
</details>
<details>
<summary><b>A tool is missing, or "Unknown or disabled tool"</b></summary>
- `PROXMOX_TOOLSETS` loads only the areas it names. Add the area, or remove the variable to load all of them.
- `PROXMOX_READ_ONLY=true` hides every write tool. Set it to `false` to create, change or delete.
- `PROXMOX_NO_DESTRUCTIVE=true` hides the delete, stop, wipe and rollback tools.
- "guest … is protected" means `PROXMOX_PROTECTED_VMIDS` covers that guest.
- The server logs how many tools it loaded, and from which toolsets, to stderr at startup.
</details>
<details>
<summary><b>HTTP: 401, 421, or the server won't start</b></summary>
- **401 Unauthorized**: the client must send `Authorization: Bearer <token>` with one of the configured tokens.
- **"… is not allowed for NAME (role: read)"**: that client's token has a role that excludes the tool. Use a token with a wider role.
- **421 Misdirected Request**: on `127.0.0.1` the server only accepts local Host headers. Connect to `127.0.0.1` or `localhost`, or bind to another address with a token.
- **"Refusing to serve on 0.0.0.0 without authentication"**: set `MCP_AUTH_TOKEN` or `MCP_AUTH_TOKENS`, or `MCP_HTTP_ALLOW_NO_AUTH=true` if a proxy in front authenticates clients.
</details>
<details>
<summary><b>Check the connection before involving Claude</b></summary>
```bash
./test-connection.sh
```
It checks the connection, the API, authentication, permission to list nodes, and access to cluster resources.
</details>
## 🛠️ Development
```bash
uv sync # dependencies, including dev tools
uv run proxmox-mcp-server # run the server directly (stdio)
uv run proxmox-mcp-server-http # or over streamable HTTP
uv run pytest # tests
uv run ruff check . && uv run black .
```
```
proxmox_mcp/
├── server.py stdio entrypoint: tool calls, roles, guards, waits, audit
├── server_http.py streamable-HTTP entrypoint: bearer tokens, loopback default
├── registry.py toolsets, read-only and no-destructive filtering, annotations
├── auth.py HTTP tokens and roles
├── audit.py JSON-lines audit log
├── guards.py protected guests
├── tasks.py waiting for Proxmox tasks
├── prompts.py MCP prompts: built-in workflows
├── resources.py MCP resources: read-only cluster views
├── client.py Proxmox API client: token/password auth, ticket renewal, read-only guard
└── tools/ one module per API area:
nodes, qemu, lxc, storage, cluster, access, firewall,
disks, ceph, acme, sdn, notifications, pools, appliances
tests/ pytest suite, run in CI on Python 3.10–3.13
docs/ the animations on this page; docs/gen/build.py regenerates four of them
agent-card.json A2A agent card
Dockerfile, docker-compose.yaml, setup.sh, test-connection.sh
QUICKSTART.md · SETUP.md · USAGE.md · CHANGELOG.md
```
**Dependencies:**
- **`mcp`** (>=2.3, <3): the MCP SDK. It serves both the handshake protocol (2024-11-05 to 2025-11-25) and the stateless 2026-07-28 protocol, so older and newer clients both connect.
- **`httpx`**: the async HTTP client.
- **`python-dotenv`**: loads `.env`.
- **`starlette`** and **`uvicorn`**: serve the HTTP entrypoint. `mcp` already depends on both.
## 🗺️ Roadmap
- One server for several clusters without Datacenter Manager, chosen per call (today: [one server per cluster](#clusters), or [through PDM](#pdm))
- Progress notifications while waiting for long tasks
What has shipped is in [CHANGELOG.md](CHANGELOG.md).
## 🙌 Contributing
Issues and pull requests are welcome: new tools, better error handling, performance, docs, tests and bug fixes.
## 📚 Learn more
- [Proxmox VE API](https://pve.proxmox.com/wiki/Proxmox_VE_API) · [API viewer](https://pve.proxmox.com/pve-docs/api-viewer/) · [Admin guide](https://pve.proxmox.com/pve-docs/pve-admin-guide.html)
- [Model Context Protocol](https://modelcontextprotocol.io/) · [MCP servers](https://github.com/modelcontextprotocol/servers) · [uv](https://github.com/astral-sh/uv)
## License
MIT. See [LICENSE](LICENSE).
<!-- org-footer -->
---
<p align="center"><sub>Part of <a href="https://github.com/ry-ops">ry-ops</a> · building the pipes between infrastructure, automation, and observability · built by <a href="https://github.com/ry-ops">ry-ops</a></sub></p>
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessLimited data