{
  "markdown": "<!-- mcp-name: io.github.akmalovaa/proxmox-mcp -->\n\n# Proxmox MCP server\n\n<p align=\"center\">\n  <img src=\"docs/banner.png\" alt=\"proxmox-mcp — MCP server for Proxmox VE\" width=\"720\"/>\n</p>\n\n[![CI](https://github.com/akmalovaa/proxmox-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/akmalovaa/proxmox-mcp/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/akmalovaa/proxmox-mcp)](https://github.com/akmalovaa/proxmox-mcp/releases)\n[![License: MIT](https://img.shields.io/github/license/akmalovaa/proxmox-mcp)](LICENSE)\n[![Python 3.14](https://img.shields.io/badge/python-3.14-3776AB?logo=python&logoColor=white)](https://www.python.org/)\n[![GHCR](https://img.shields.io/badge/ghcr-proxmox--mcp-2496ED?logo=docker&logoColor=white)](https://github.com/akmalovaa/proxmox-mcp/pkgs/container/proxmox-mcp)\n[![MCP](https://img.shields.io/badge/MCP-compatible-7C3AED)](https://modelcontextprotocol.io)\n\n## Simple Proxmox MCP\n\n<p align=\"center\">\n  <img src=\"docs/logo.png\" alt=\"proxmox-mcp logo\" width=\"140\"/>\n</p>\n\nMCP server for managing Proxmox VE\n\n**50 tools** — nodes, QEMU VMs, LXC containers, storage, cluster, snapshots.\n\n### Why this one?\n\n- **One image**, multi-arch — `docker run ghcr.io/akmalovaa/proxmox-mcp:latest` and you're done\n- **Just env vars** — no config files, no database, no state\n- **Read-only by default** — destructive ops are gated behind an explicit `PROXMOX_RISK_LEVEL`\n- **stdio or Streamable HTTP** — one env var apart; stdio by default, HTTP binds loopback\n- **Tiny codebase** — a thin layer over Proxmoxer, no config files, no database, no state\n- **Raw JSON out** — no formatting, no emoji; LLM gets clean data\n- **Readable failures** — a 403, a dead host or a blocked tier come back as a sentence, not a stack trace\n\n[![proxmox-mcp MCP server](https://glama.ai/mcp/servers/akmalovaa/proxmox-mcp/badges/card.svg)](https://glama.ai/mcp/servers/akmalovaa/proxmox-mcp)\n\n\n## Quick start\n\n**Image:** `ghcr.io/akmalovaa/proxmox-mcp:latest` (multi-arch: `amd64` + `arm64`).\n\n**1. Export credentials in your shell profile** (`~/.zprofile`, `~/.zshrc` or `~/.bashrc`):\n\n```bash\n# token auth (recommended — see \"Least privilege\" for the user and role to give it):\nexport PROXMOX_HOST=192.168.1.100\nexport PROXMOX_USER=mcp@pve\nexport PROXMOX_TOKEN_NAME=mcp\nexport PROXMOX_TOKEN_VALUE=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n\n# or password auth:\n# export PROXMOX_USER=root@pam\n# export PROXMOX_PASSWORD=your-password\n\n# optional:\nexport PROXMOX_RISK_LEVEL=read\n```\n\nReload: `source ~/.zprofile` (or restart the shell).\n\n**2. Add to `~/.claude/settings.json` (Claude Code) or `claude_desktop_config.json` (Claude Desktop)**:\n\n```json\n{\n  \"mcpServers\": {\n    \"proxmox\": {\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\",\n        \"-e\", \"PROXMOX_HOST\",\n        \"-e\", \"PROXMOX_USER\",\n        \"-e\", \"PROXMOX_PASSWORD\",\n        \"ghcr.io/akmalovaa/proxmox-mcp:latest\"]\n    }\n  }\n}\n```\n\nor token auth:\n\n```json\n{\n  \"mcpServers\": {\n    \"proxmox\": {\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\",\n        \"-e\", \"PROXMOX_HOST\",\n        \"-e\", \"PROXMOX_USER\",\n        \"-e\", \"PROXMOX_TOKEN_NAME\",\n        \"-e\", \"PROXMOX_TOKEN_VALUE\",\n        \"ghcr.io/akmalovaa/proxmox-mcp:latest\"]\n    }\n  }\n}\n```\n\n`docker run -e VAR` without a value passes the host variable through — no secrets in the config file. Restart the client — 32 read-only Proxmox tools become available (more if you raise `PROXMOX_RISK_LEVEL`).\n\nFor password auth, swap the token vars for `PROXMOX_PASSWORD`.\n\n> **Note:** Claude Desktop on macOS is launched via launchd and does **not** inherit `~/.zprofile`/`~/.zshrc`. Either put the exports in `~/.zshenv`, or fall back to an inline `\"env\": { ... }` block in the config.\n\n## Configuration\n\nAll settings are environment variables — set them in your shell profile, pass them inline to `docker run -e`, or declare them in your MCP client's `env` block.\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `PROXMOX_HOST` | — | Proxmox host (IP or hostname) |\n| `PROXMOX_USER` | `root@pam` | API user |\n| **Auth** | — | **token *or* password — see below** |\n| `PROXMOX_PORT` | `8006` | API port |\n| `PROXMOX_VERIFY_SSL` | `false` | Verify TLS certificate |\n| `PROXMOX_TIMEOUT` | `15` | Seconds to wait for each API request |\n| `PROXMOX_RISK_LEVEL` | `read` | `read` / `lifecycle` / `all` — see [Risk levels](#risk-levels) |\n| `PROXMOX_TOOLS_ALLOW` | — | Comma-separated tool names to register, on top of the risk level |\n| `PROXMOX_REDACT_SECRETS` | `true` | Mask `cipassword` / `sshkeys` in responses |\n\nTransport settings live under a `PROXMOX_MCP_` prefix — see [Streamable HTTP](#streamable-http).\n\n### Authentication: token *or* password\n\nPick **one**. If both are set, the token wins.\n\n**Token (recommended)**:\n\n```bash\nexport PROXMOX_USER=mcp@pve\nexport PROXMOX_TOKEN_NAME=mcp\nexport PROXMOX_TOKEN_VALUE=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n```\n\nSetting only one half of the pair is a startup error rather than a silent fallback to\nthe password — a typo in `PROXMOX_TOKEN_NAME` used to mean quietly running as whoever\n`PROXMOX_USER` is.\n\n**Password (fallback)**:\n\n```bash\nexport PROXMOX_PASSWORD=your-password\n```\n\n### Least privilege\n\nAn MCP server is reachable by a model acting on text it did not write, so give it its\nown user and its own token rather than `root@pam`. On the Proxmox host:\n\n```bash\n# a user that is not root, with a role that matches the risk level you plan to run\npveum user add mcp@pve\npveum acl modify / --users mcp@pve --roles PVEAuditor          # read\n# pveum acl modify / --users mcp@pve --roles PVEVMAdmin        # lifecycle / all\n\n# a token for that user. Privilege Separation ON (the default) means the token starts\n# with no rights at all, so grant it the same role explicitly:\npveum user token add mcp@pve mcp --privsep 1\npveum acl modify / --tokens 'mcp@pve!mcp' --roles PVEAuditor\n```\n\nCheck what the token actually ended up with:\n\n```bash\npveum user permissions mcp@pve --token mcp\n```\n\n`PVEAuditor` covers all 32 read tools. `PVEVMAdmin` on `/vms` adds guest lifecycle and\nsnapshots; `migrate_vm` additionally needs `VM.Migrate` on the target node, and\n`clone_vm` needs `Datastore.AllocateSpace` on the target storage. Narrow the ACL path\n(`/vms/101`, `/pool/homelab`) if the server should only see part of the cluster.\n\n`root@pam` still works and is the quickest thing for a local look around — it is just\nnot what should be left running.\n\n### Risk levels\n\n`PROXMOX_RISK_LEVEL` controls which tools exist. Tools above the active level are **not registered**, so they never appear in the MCP client's tool list:\n\n| Level | Tools | Adds |\n|-------|-------|------|\n| `read` *(default)* | 32 | read-only tools |\n| `lifecycle` | 46 | + start / stop / reboot / suspend / clone / migrate / create-snapshot |\n| `all` | 50 | + delete-snapshot / rollback-snapshot |\n\nEach elevated call is also re-checked at call time and logged to stderr (`ALLOW` / `DENY` + tool + tier).\n\nThe active level is not otherwise observable from the client side — a tool that is\nmissing looks the same as a tool that was never written — so `get_server_info` reports\nit, along with the versions and the tool count.\n\n`PROXMOX_TOOLS_ALLOW` narrows further **within** the tier, for a deployment that serves\none specific agent:\n\n```bash\nexport PROXMOX_TOOLS_ALLOW=list_nodes,list_containers,get_container_status,get_cluster_resources\n```\n\nNames that do not exist are a startup error, so a typo cannot silently amputate the\ntool list. Keep `get_server_info` on the list unless you have a reason not to — it is\nhow a client learns what the rest of the list means.\n\n### Response shape\n\nInventory listings (`list_nodes`, `list_vms`, `list_containers`, `get_cluster_resources`)\nreturn a compact subset of each row: identity, status, uptime, CPU and memory. What is\ndropped is per-second IO counters and PSI pressure gauges — real data, but nothing a\ntriage decision turns on, and several times the volume of what is kept. Pass\n`verbose=true` for the untouched rows.\n\nWrite tools answer with the task Proxmox accepted, not with a finished result:\n\n```json\n{\"state\":\"accepted\",\"operation\":\"start_vm\",\"node\":\"pve\",\"vmid\":101,\n \"upid\":\"UPID:pve:...\",\"poll_with\":\"get_task_status\"}\n```\n\n`cipassword` and `sshkeys` are replaced with `***redacted***` everywhere, keeping the key\nso the model can still tell that cloud-init is configured. Set\n`PROXMOX_REDACT_SECRETS=false` to get the raw values.\n\n### Streamable HTTP\n\nstdio stays the default and is what an MCP client on your own machine should use. For a\nshared deployment — a container in a cluster, several clients on one URL — set the\ntransport and nothing else changes:\n\n```bash\ndocker run --rm -p 8000:8000 \\\n  -e PROXMOX_HOST -e PROXMOX_TOKEN_NAME -e PROXMOX_TOKEN_VALUE \\\n  -e PROXMOX_MCP_TRANSPORT=streamable-http \\\n  -e PROXMOX_MCP_HOST=0.0.0.0 \\\n  -e PROXMOX_MCP_ALLOWED_HOSTS=proxmox-mcp.example.com \\\n  ghcr.io/akmalovaa/proxmox-mcp:latest\n```\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `PROXMOX_MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |\n| `PROXMOX_MCP_HOST` | `127.0.0.1` | Bind address — `0.0.0.0` in a container |\n| `PROXMOX_MCP_PORT` | `8000` | Port |\n| `PROXMOX_MCP_PATH` | `/mcp` | Endpoint path |\n| `PROXMOX_MCP_JSON_RESPONSE` | `true` | JSON responses instead of an SSE stream |\n| `PROXMOX_MCP_ALLOWED_HOSTS` | — | `Host` headers to accept; `*` disables the check |\n| `PROXMOX_MCP_ALLOWED_ORIGINS` | — | Browser origins allowed; empty = same-origin only |\n\nThe prefix is `PROXMOX_MCP_`, not `PROXMOX_`, because Kubernetes injects `<SERVICE>_PORT`\nfor every linked Service — a Service named `proxmox` would otherwise redefine\n`PROXMOX_PORT`.\n\nSessions are not used (`stateless_http`): they were removed from the protocol in\nrevision 2026-07-28, and without them several clients can share one URL and a rolling\nupdate does not cut anyone off.\n\n**Host and Origin validation.** Binding anything other than loopback requires\n`PROXMOX_MCP_ALLOWED_HOSTS`; the server refuses to start otherwise. A foreign `Origin`\ngets 403 and an unexpected `Host` gets 421 — this is what stops a page in a browser on\nthe same LAN from driving the server through DNS rebinding. A request with no `Origin`\nheader (curl, MCP clients) always passes. Set `PROXMOX_MCP_ALLOWED_HOSTS=*` only when\nsomething in front already validates it.\n\n**There is no authentication.** Anyone who can reach the endpoint gets whatever\n`PROXMOX_RISK_LEVEL` allows. Put it behind a gateway, an authenticating proxy or a\nnetwork boundary you trust, and keep `PROXMOX_RISK_LEVEL=read` unless the path to it is\nauthenticated.\n\n**Health endpoints** are served alongside `/mcp` and need no auth:\n\n| Path | Meaning |\n|------|---------|\n| `GET /healthz` | The process is up. Never touches Proxmox — a liveness probe that fails when Proxmox is down would restart the server in a loop and fix nothing. |\n| `GET /readyz` | Proxmox answered `version.get()`. This is the one that should gate traffic; 503 with a readable reason otherwise. |\n\n```yaml\nlivenessProbe:\n  httpGet: { path: /healthz, port: 8000 }\nreadinessProbe:\n  httpGet: { path: /readyz, port: 8000 }\n```\n\nFlags mirror the variables for interactive use: `uvx proxmox-ve-mcp --transport\nstreamable-http --port 8080`.\n\n### Sentry (optional)\n\nTool calls and failures can be shipped to [Sentry](https://sentry.io) — every `tools/call`\nbecomes a span, every failing tool an issue. Nothing is sent, and the SDK is never even\nimported, while `SENTRY_DSN` is unset.\n\nThe `ghcr.io` image already contains the SDK. From PyPI, install the extra:\n\n```bash\nuvx --from 'proxmox-ve-mcp[sentry]' proxmox-ve-mcp\n```\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `SENTRY_DSN` | — | Set it to enable reporting |\n| `SENTRY_ENVIRONMENT` | `production` | Free-form environment label |\n| `SENTRY_TRACES_SAMPLE_RATE` | `1.0` | Share of tool calls traced |\n| `SENTRY_SEND_DEFAULT_PII` | `false` | Send tool arguments and results as span data |\n\nLeave `SENTRY_SEND_DEFAULT_PII` off unless you mean it: with it on, tool arguments and\nresults are attached to spans, and `get_vm_config` returns ssh keys and `cipassword`\nhashes. A DSN set without the extra installed logs a warning and the server runs on.\n\n## Tools\n\n### Nodes (10)\n\n| Tool | Description |\n|------|-------------|\n| `list_nodes` | List all cluster nodes with status, CPU, memory, uptime |\n| `get_node_status` | Detailed node metrics (CPU, memory, disk, load, kernel) |\n| `get_node_networks` | Network interfaces on a node |\n| `get_node_disks` | Physical disks on a node |\n| `get_node_services` | Proxmox system services and their state |\n| `get_node_updates` | Pending APT package updates |\n| `get_node_rrd_data` | Historical CPU/memory/disk/network metrics (RRD) |\n| `get_node_tasks` | Recent tasks on a node, optionally errors only |\n| `get_task_status` | Status of a specific task by UPID |\n| `get_task_log` | Log output from a task |\n\n### QEMU VMs (17)\n\n| Tool | Tier | Description |\n|------|------|-------------|\n| `list_vms` | read | List all VMs, optionally filter by node |\n| `get_vm_status` | read | Current VM status (running/stopped, CPU, memory) |\n| `get_vm_config` | read | VM configuration (hardware, disks, network) |\n| `get_vm_network_interfaces` | read | **IP addresses** of a running VM (via QEMU guest agent) |\n| `get_vm_rrd_data` | read | Historical CPU/memory/disk/network metrics (RRD) |\n| `list_vm_snapshots` | read | List all snapshots of a VM |\n| `start_vm` | lifecycle | Start a VM |\n| `stop_vm` | lifecycle | Force-stop a VM — annotated destructive, unsaved guest state is lost |\n| `shutdown_vm` | lifecycle | Graceful ACPI shutdown with timeout |\n| `reboot_vm` | lifecycle | Reboot via ACPI |\n| `suspend_vm` | lifecycle | Suspend a VM |\n| `resume_vm` | lifecycle | Resume a suspended VM |\n| `clone_vm` | lifecycle | Full or linked clone |\n| `migrate_vm` | lifecycle | Move a VM to another node, online or offline |\n| `create_vm_snapshot` | lifecycle | Create a snapshot |\n| `delete_vm_snapshot` | all | Delete a snapshot |\n| `rollback_vm_snapshot` | all | Rollback to a snapshot |\n\n### LXC Containers (13)\n\n| Tool | Tier | Description |\n|------|------|-------------|\n| `list_containers` | read | List all LXC containers, optionally filter by node |\n| `get_container_status` | read | Current container status |\n| `get_container_config` | read | Container configuration |\n| `get_container_interfaces` | read | **IP addresses** of a running container |\n| `get_container_rrd_data` | read | Historical CPU/memory/disk/network metrics (RRD) |\n| `list_container_snapshots` | read | List all snapshots |\n| `start_container` | lifecycle | Start a container |\n| `stop_container` | lifecycle | Force-stop a container — annotated destructive |\n| `shutdown_container` | lifecycle | Graceful shutdown with timeout |\n| `reboot_container` | lifecycle | Reboot a container |\n| `create_container_snapshot` | lifecycle | Create a snapshot |\n| `delete_container_snapshot` | all | Delete a snapshot |\n| `rollback_container_snapshot` | all | Rollback to a snapshot |\n\n### Storage (2)\n\n| Tool | Description |\n|------|-------------|\n| `list_storage` | Storage pools with usage, optionally filter by node |\n| `get_storage_content` | Contents of a storage pool (ISOs, backups, images, templates) |\n\n### Cluster (7)\n\n| Tool | Description |\n|------|-------------|\n| `get_cluster_status` | Cluster health, quorum, node membership |\n| `get_cluster_resources` | All resources (VMs, containers, storage, nodes) |\n| `get_cluster_backups` | Configured backup jobs |\n| `get_ha_status` | High-availability resources and their state |\n| `list_pools` | Resource pools |\n| `get_cluster_log` | Cluster-wide event log, newest first |\n| `get_next_vmid` | Next available VM/container ID |\n\n### Server (1)\n\n| Tool | Description |\n|------|-------------|\n| `get_server_info` | This server's own risk level, tool count, versions and Proxmox reachability |\n\n## Architecture\n\n```\nsrc/proxmox_mcp/\n├── server.py    # MCPServer instance, health routes, transport selection, entry point\n├── config.py    # Pydantic Settings (PROXMOX_) + transport config (PROXMOX_MCP_)\n├── client.py    # Proxmoxer connection, built once per process\n└── tools/       # nodes, vms, containers, storage, cluster, server_info\n```\n\n- **Read-only by default** — elevated tools gated by `PROXMOX_RISK_LEVEL`\n- **Gated at registration** — a tool above the tier is absent from `tools/list`, not\n  refused at call time: what the model cannot see, it cannot retry\n- **Admission and annotations are separate** — `tier=` is the operator's policy,\n  `annotations=` is what the client is told a call does, so a force-stop can be\n  `destructiveHint: true` and still live at the `lifecycle` tier\n- **Lazy connection** — the Proxmoxer client is built on first use, once, and shared;\n  the server therefore starts cleanly even when Proxmox is unreachable\n- **Raw JSON output** — compact, no formatting; LLM consumes data directly\n- **Normalized errors** — Proxmox and network failures are translated into one\n  actionable sentence instead of a `requests` traceback\n\n## Development\n\n### Run standalone (testing)\n\n```bash\nexport PROXMOX_HOST=192.168.1.100\nexport PROXMOX_USER=root@pam\nexport PROXMOX_TOKEN_NAME=mcp\nexport PROXMOX_TOKEN_VALUE=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n\ndocker run -i --rm \\\n  -e PROXMOX_HOST -e PROXMOX_USER \\\n  -e PROXMOX_TOKEN_NAME -e PROXMOX_TOKEN_VALUE \\\n  ghcr.io/akmalovaa/proxmox-mcp:latest\n```\n\n### Without Docker (UV)\n\n```bash\ngit clone https://github.com/akmalovaa/proxmox-mcp.git && cd proxmox-mcp && uv sync\n```\n\nMCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"proxmox\": {\n      \"command\": \"uv\",\n      \"args\": [\"run\", \"--directory\", \"/path/to/proxmox-mcp\", \"python\", \"-m\", \"proxmox_mcp\"],\n      \"env\": {\n        \"PROXMOX_HOST\": \"192.168.1.100\",\n        \"PROXMOX_TOKEN_NAME\": \"mcp\",\n        \"PROXMOX_TOKEN_VALUE\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\"\n      }\n    }\n  }\n}\n```\n\n### Build from source\n\n```bash\ngit clone https://github.com/akmalovaa/proxmox-mcp.git\ncd proxmox-mcp\ndocker build -t proxmox-mcp .\n```\n\nThe image is multi-stage: `uv` builds the virtualenv in a throwaway layer, and the\nruntime stage carries only Python plus the venv and runs as the unprivileged `mcp`\nuser (uid 10001).\n\n### Lint, type-check, test\n\n```bash\nuv sync --locked --group dev\nuv run ruff check .\nuv run mypy src/\nuv run pytest -v\n```\n\n## License\n\nMIT\n",
  "bytes": 18507,
  "sha": "f732ed9cd606541e7526af32541f7970c921dda3cea6454fa09d1dfa6396490d",
  "repo_slug": "akmalovaa/proxmox-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_akmalovaa_proxmox_mcp_42312aea/readme"
}