A secure Model Context Protocol (MCP) server for managing Proxmox VE infrastructure via Docker with multi-host support.
- 🔐 JWT-based authentication with short-lived tokens
- 🛡️ RBAC with operation whitelist/blacklist
- 🔒 TLS encrypted communication with Proxmox API
- 📦 Docker deployment with security hardening
- 📝 Audit logging for all operations
- 🌐 Multi-Host Support - Manage multiple Proxmox nodes from a single server
- Docker & Docker Compose
- Proxmox VE 8.x or 9.x
- Python 3.12+ (for local development)
On each Proxmox node:
# Create dedicated user
pveum user add mcp-service@pve
# Create custom role (principle of least privilege)
pveum role add MCP-Operator -privs \
VM.Audit \
VM.PowerVM \
VM.Read \
Datastore.Audit \
Sys.Audit
# Create token for each node
pveum token add mcp-service@pve mcp-token --privsep 0
# Grant role to user
pveum aclmod / -user mcp-service@pve -role MCP-OperatorSave the token values:
token_id:mcp-service@pve!mcp-tokentoken_secret:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Copy environment template
cp .env.example .env
# Edit with your values
nano .envFor Proxmox clusters, the server supports automatic node discovery and cluster-wide operations:
# GET /mcp/v1/cluster/members — auto-discover all cluster nodes
curl http://localhost:8000/mcp/v1/cluster/members \
-H "Authorization: Bearer $TOKEN"
# Returns all cluster members with status (no need to configure each node manually)Cluster-wide operations (no --node flag needed):
cluster.resources— all VMs/storage across clustercluster.members— discover cluster nodescluster.status— HA cluster statuscluster.config— cluster configurationstorage.list— all storage backendsbackup.list— all backup jobs
# Format: node_name:host:port,node_name:host:port
PROXMOX_NODES=pve11:IP_11:8006,pve12:IP_12:8006,pve13:IP_13:8006
# Per-node token overrides (node_name=token_id:token_secret,...)
# If not specified, falls back to PROXMOX_TOKEN_ID/SECRET
PROXMOX_NODE_TOKENS=pve11:root@pam!api-token-pve11:xxxxxxxx,pve12:root@pam!api-token-pve12:xxxxxxxx,pve13:root@pam!api-token-pve13:xxxxxxxxRequired environment variables:
| Variable | Description |
|---|---|
PROXMOX_NODES |
Multi-host config (node:host:port,...) |
PROXMOX_TOKEN_ID |
API token ID (fallback if per-node not set) |
PROXMOX_TOKEN_SECRET |
API token secret (fallback if per-node not set) |
PROXMOX_NODE_TOKENS |
Per-node token overrides |
JWT_SECRET |
JWT signing secret (min 32 chars) |
ADMIN_PASSWORD |
Password for admin user |
VERIFY_TLS |
Verify TLS certs (default: false for self-signed) |
# Build and start
docker-compose up -d --build
# Check logs
docker-compose logs -f
# Verify health
curl http://localhost:8000/healthExample request:
# Get JWT token
TOKEN=$(curl -s -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"P@ssw0rd"}' | \
python3 -c 'import sys,json; print(json.load(sys.stdin)["access_token"])')
# List VMs on specific node
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"method":"vm.list","params":{"node":"pve11"},"resource":"*/qemu/*"}'- Container runs in isolated network (
mcp-network) - No ports exposed to host by default
- Use nginx reverse proxy for TLS if external access needed
- Non-root user: Runs as user ID 1000
- Read-only filesystem: No writes to container layers
- No new privileges:
security_opt: no-new-privileges - Capability drop: All Linux capabilities removed
- Resource limits: Max 256MB RAM, 0.5 CPU
Client MCP Server Proxmox
│ │ │
│──── JWT Token ───────────▶│ │
│ │ │
│ │──── API Request ──────▶│
│ │ │
│ │◀─── Response ─────────│
│ │ │
│◀─── Result ──────────────│ │
- Client authenticates via
/auth/tokenendpoint - Client receives short-lived JWT (15 min)
- Client calls MCP endpoint with JWT
- MCP validates token, checks RBAC
- MCP proxies request to Proxmox API
# Get access token
curl -X POST http://localhost:8000/auth/token \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "P@ssw0rd"}'
# Refresh token
curl -X POST http://localhost:8000/auth/refresh \
-H "Authorization: Bearer <expired_token>"All operations require Authorization: Bearer <jwt> header.
# List VMs on specific node (params.node is required for multi-host)
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "vm.list",
"params": {"node": "pve11"},
"resource": "*/qemu/*"
}'
# Get VM status
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "vm.status",
"params": {"node": "pve11", "vmid": "501"},
"resource": "*/qemu/*"
}'
# Start VM
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "vm.start",
"params": {"node": "pve11", "vmid": "501"},
"resource": "*/qemu/*"
}'
# Get VM snapshots
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "vm.snapshot",
"params": {"node": "pve11", "vmid": "501"},
"resource": "*/qemu/*"
}'
# Node status
curl -X POST http://localhost:8000/mcp/v1/call \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"method": "node.status",
"params": {"node": "pve11"},
"resource": "*/node/*"
}'| Operation | Description | Risk Level |
|---|---|---|
vm.list |
List all VMs on a node | Low |
vm.status |
Get VM status | Low |
vm.snapshot |
List VM snapshots | Low |
vm.start |
Start a VM | Medium |
vm.stop |
Stop a VM (hard) | Medium |
vm.shutdown |
Graceful shutdown | Medium |
vm.create |
Create VM (admin only) | High |
vm.clone |
Clone VM (admin only) | High |
node.list |
List cluster nodes | Low |
node.status |
Get node status | Low |
storage.list |
List storage | Low |
backup.list |
List backups | Low |
backup.status |
Get backup status | Low |
These operations are always blocked regardless of role:
vm.delete/vm.destroynode.stop/node.reboot/node.shutdowncluster.*(all cluster operations)user.*(all user operations)storage.*(modify/delete)pool.*(all pool operations)vzdump.restore/vzdump.backup
When PROXMOX_NODES is configured, specify the target node in params.node:
# pve11
curl ... -d '{"method":"vm.list","params":{"node":"pve11"},...}'
# pve12
curl ... -d '{"method":"vm.list","params":{"node":"pve12"},...}'
# pve13
curl ... -d '{"method":"vm.list","params":{"node":"pve13"},...}'If node is omitted, defaults to the first node in PROXMOX_NODES.
# Create virtual environment
python -m venv venv
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Run with env file
export $(cat .env | xargs) && uvicorn src.server:app --reload --port 8000pytest tests/ -v --cov=src
# With coverage report
pytest tests/ -v --cov=src --cov-report=html
open htmlcov/index.html# Build image
docker build -t proxmox-mcp:latest .
# Run with environment file
docker run --env-file .env proxmox-mcp:latest
# Interactive test
docker run -it --env-file .env proxmox-mcp:latest python -m pytest# Allow only from internal network
ufw allow from 192.168.1.0/24 to any port 8000
# Or disable external access entirely
ufw deny 8000For external access, use nginx with self-signed cert:
# Generate certificate
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout certs/key.pem \
-out certs/cert.pem \
-subj "/CN=proxmox-mcp"events {
worker_connections 1024;
}
http {
server {
listen 8443 ssl;
server_name _;
ssl_certificate /certs/cert.pem;
ssl_certificate_key /certs/key.pem;
location / {
proxy_pass http://proxmox-mcp:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}# Add to crontab for health check
*/5 * * * * curl -f http://localhost:8000/health || \
docker-compose restart proxmox-mcpapiVersion: apps/v1
kind: Deployment
metadata:
name: proxmox-mcp
spec:
replicas: 1
selector:
matchLabels:
app: proxmox-mcp
template:
metadata:
labels:
app: proxmox-mcp
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: proxmox-mcp
image: proxmox-mcp:latest
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
resources:
limits:
memory: 256Mi
cpu: "0.5"
envFrom:
- secretRef:
name: proxmox-secrets401 Unauthorized
- Check JWT token hasn't expired (15 min default)
- Verify token signature matches JWT_SECRET
- Ensure
params.nodeis specified for multi-host setup
403 Forbidden
- Operation may be blacklisted
- User may not have permission for requested resource
- Some operations (vm.create, vm.clone) require admin role
Connection to Proxmox failed
- Verify
PROXMOX_NODESis correctly formatted - Check each node's API port (8006) is accessible
- Ensure API tokens are valid for each node
Certificate verify failed
- Set
VERIFY_TLS=falsefor self-signed certs (default) - For production, add CA bundle or use valid certs
Unknown node errors
- Verify node name matches exactly (case-sensitive)
- Check
PROXMOX_NODESformat:name:host:port,name:host:port - Ensure node is reachable from container
# Enable debug logging
sed -i 's/LOG_LEVEL=.*/LOG_LEVEL=DEBUG/' .env
docker-compose restart
# View verbose logs
docker-compose logs -f --tail=100# Debug inside container
docker exec -it proxmox-mcp /bin/sh
# Check Python environment
docker exec proxmox-mcp python -c "import sys; print(sys.version)"
# Test Proxmox connectivity directly
docker exec proxmox-mcp python -c "
from src.proxmox_client import ProxmoxClient
import asyncio
async def test():
client = ProxmoxClient(host='192.168.1.11', port=8006,
token_id='root@pam!token', token_secret='xxx',
verify_tls=False)
result = await client.ping()
print('Ping:', result)
asyncio.run(test())
"MIT
Sutian - Proxmox MCP Server v1.1.0