feat: v1.2.0 — deploy, lifecycle, discovery

- New: proxmox_create_lxc, proxmox_create_qemu (deploy)
- New: proxmox_start_vm, stop_vm, shutdown_vm, reboot_vm (lifecycle)
- New: proxmox_delete_vm, proxmox_clone_vm
- New: proxmox_list_storage, proxmox_list_templates (discovery)
- New: proxmox_get_next_vmid, proxmox_get_lxc_config
- Fix: LXC config parity with QEMU (get_lxc_config)
- Bump: 1.0.0 → 1.2.0 (29 tools, up from 17)
This commit is contained in:
root
2026-06-17 06:37:41 +00:00
parent 2e59fb5af2
commit 2e371b2709
7 changed files with 319 additions and 30 deletions

105
README.md
View File

@@ -1,6 +1,6 @@
# proxmox-mcp
# proxmox-mcp v1.2.0
MCP server for **Proxmox VE 9** — nodes, VMs, LXCs, status, exec. Sibling of `pfsense-mcp`.
MCP server for **Proxmox VE** — nodes, VMs, LXCs, status, exec, **deploy, lifecycle, and discovery**. Sibling of `pfsense-mcp`.
## Quick start
@@ -9,48 +9,103 @@ cd ~/Projects/proxmox-mcp
python3 -m venv .venv && .venv/bin/pip install -e .
```
### Cursor (`~/.cursor/mcp.json`)
### Hermes (`~/.hermes/config.yaml`)
```json
"proxmox": {
"command": "/Users/nearxos/Projects/proxmox-mcp/.venv/bin/proxmox-mcp",
"env": {
"PROXMOX_VERIFY_SSL": "false",
"PROXMOX_ALLOW_WRITES": "false",
"PROXMOX_TARGETS_PATH": "/Users/nearxos/.local/share/proxmox-mcp/targets.json"
}
}
```yaml
mcp_servers:
proxmox:
command: /usr/local/lib/hermes-agent/venv/bin/proxmox-mcp
timeout: 120
enabled: true
env:
PROXMOX_VERIFY_SSL: 'false'
PROXMOX_ALLOW_WRITES: 'true'
```
### Connect
```text
proxmox_connect(
url="https://10.20.30.254:8006",
token_id="root@pam!cursor",
token_secret="...",
name="golden-proxmox"
)
```
proxmox_connect(url="https://10.77.30.5:8006", token_id="root@pam!tokenname", token_secret="...", name="dell")
proxmox_test_connection()
proxmox_list_qemu(node="pve")
proxmox_list_qemu(node="dell")
```
## Tools
## Tools (30 total)
### Connection & Discovery
| Tool | Description |
|------|-------------|
| `proxmox_connect` / `proxmox_use_target` | Multi-target registry (persisted) |
| `proxmox_list_targets` / `proxmox_disconnect_target` | Target management |
| `proxmox_get_config` | Server config and active target |
| `proxmox_test_connection` | Version + nodes |
### Nodes & Cluster
| Tool | Description |
|------|-------------|
| `proxmox_list_nodes` | Cluster nodes |
| `proxmox_get_node_status` | Node resource status |
| `proxmox_get_next_vmid` | Next available VMID from cluster |
### Storage & Templates
| Tool | Description |
|------|-------------|
| `proxmox_list_storage` | Storage pools with usage stats |
| `proxmox_list_templates` | Templates and ISOs on a storage pool |
### Guests — Read
| Tool | Description |
|------|-------------|
| `proxmox_list_qemu` / `proxmox_list_lxc` | Guest inventory |
| `proxmox_get_qemu_status` / `proxmox_get_lxc_status` | Live status |
| `proxmox_get_qemu_config` | VM config |
| `proxmox_lxc_exec` / `proxmox_qemu_agent_exec` | Guest exec (writes) |
| `proxmox_get_task_log` | Async task output |
| `proxmox_get_qemu_config` / `proxmox_get_lxc_config` | Full config |
### Guests — Lifecycle (requires `PROXMOX_ALLOW_WRITES=true`)
| Tool | Description |
|------|-------------|
| `proxmox_start_vm` | Start a stopped VM/LXC |
| `proxmox_stop_vm` | Force stop a VM/LXC |
| `proxmox_shutdown_vm` | Graceful ACPI shutdown |
| `proxmox_reboot_vm` | Reboot a VM/LXC |
| `proxmox_lxc_exec` | Execute command inside an LXC |
| `proxmox_qemu_agent_exec` | Execute via QEMU guest agent |
### Guests — Deploy (requires `PROXMOX_ALLOW_WRITES=true`)
| Tool | Description |
|------|-------------|
| `proxmox_create_lxc` | Create a new LXC container |
| `proxmox_create_qemu` | Create a new QEMU VM |
| `proxmox_delete_vm` | Delete a VM/LXC (purge option) |
| `proxmox_clone_vm` | Clone a VM/LXC to new VMID |
### Tasks
| Tool | Description |
|------|-------------|
| `proxmox_get_task_status` | Async task status |
| `proxmox_get_task_log` | Async task log output |
## Golden target
Saved as `golden-proxmox` in `~/.local/share/proxmox-mcp/targets.json` (add API token locally).
Saved as `dell` in `~/.local/share/proxmox-mcp/targets.json` — Dell Proxmox at `10.77.30.5:8006`.
## Safety
- `PROXMOX_ALLOW_WRITES=false` by default
- All lifecycle/deploy tools require writes enabled
- Delete requires explicit `purge=true` to destroy data
- Token secrets stored with `chmod 600` in targets.json
## Changelog
### v1.2.0
- **New:** LXC create, QEMU create, delete, clone
- **New:** Start, stop, shutdown, reboot lifecycle tools
- **New:** Storage listing, template/ISO discovery
- **New:** `proxmox_get_next_vmid`, `proxmox_get_lxc_config`
- **Fix:** Config parity — LXC config now matches QEMU
### v1.0.0
- Initial release — read-only Proxmox API + exec
## License

View File

@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
[project]
name = "proxmox-mcp"
version = "1.0.0"
description = "Model Context Protocol server for Proxmox VE 9 management"
version = "1.2.0"
description = "Model Context Protocol server for Proxmox VE management — deploy, manage, and monitor VMs and LXCs"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"

View File

@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.2.0"

View File

@@ -1,10 +1,11 @@
from mcp.server.fastmcp import FastMCP
from proxmox_mcp.tools import guests, nodes, targets, tasks
from proxmox_mcp.tools import guests, nodes, storage, targets, tasks
def register_all(mcp: FastMCP) -> None:
targets.register(mcp)
nodes.register(mcp)
guests.register(mcp)
storage.register(mcp)
tasks.register(mcp)

View File

@@ -4,7 +4,7 @@ import json
from mcp.server.fastmcp import FastMCP
from proxmox_mcp.client import get_client
from proxmox_mcp.client import ProxmoxAPIError, get_client
from proxmox_mcp.guards import GuardError, require_writes
@@ -36,6 +36,60 @@ def register(mcp: FastMCP) -> None:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/qemu/{vmid}/config"), indent=2, default=str)
@mcp.tool()
def proxmox_get_lxc_config(node: str, vmid: int, target: str = "") -> str:
"""Get LXC container configuration."""
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/lxc/{vmid}/config"), indent=2, default=str)
# ── Lifecycle tools ──────────────────────────────────────────
@mcp.tool()
def proxmox_start_vm(node: str, vmid: int, target: str = "") -> str:
"""Start a stopped VM or LXC container."""
require_writes()
client = get_client(target=target)
try:
result = client.post(f"nodes/{node}/qemu/{vmid}/status/start")
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/start")
return json.dumps(result, indent=2, default=str)
@mcp.tool()
def proxmox_stop_vm(node: str, vmid: int, target: str = "") -> str:
"""Stop a VM or LXC container (immediate stop)."""
require_writes()
client = get_client(target=target)
try:
result = client.post(f"nodes/{node}/qemu/{vmid}/status/stop")
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/stop")
return json.dumps(result, indent=2, default=str)
@mcp.tool()
def proxmox_shutdown_vm(node: str, vmid: int, timeout: int = 60, target: str = "") -> str:
"""Gracefully shutdown a VM or LXC (ACPI shutdown). Timeout in seconds."""
require_writes()
client = get_client(target=target)
try:
result = client.post(f"nodes/{node}/qemu/{vmid}/status/shutdown", data={"timeout": timeout})
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/shutdown", data={"timeout": timeout})
return json.dumps(result, indent=2, default=str)
@mcp.tool()
def proxmox_reboot_vm(node: str, vmid: int, target: str = "") -> str:
"""Reboot a VM or LXC container."""
require_writes()
client = get_client(target=target)
try:
result = client.post(f"nodes/{node}/qemu/{vmid}/status/reboot")
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/reboot")
return json.dumps(result, indent=2, default=str)
# ── Guest exec tools ─────────────────────────────────────────
@mcp.tool()
def proxmox_lxc_exec(
node: str,
@@ -67,3 +121,155 @@ def register(mcp: FastMCP) -> None:
payload = {"command": [command] + (args or [])}
result = client.post(f"nodes/{node}/qemu/{vmid}/agent/exec", data={"command": json.dumps(payload["command"])})
return json.dumps(result, indent=2, default=str)
# ── Create tools ─────────────────────────────────────────────
@mcp.tool()
def proxmox_create_lxc(
node: str,
vmid: int,
ostemplate: str,
storage: str,
hostname: str,
password: str = "",
cores: int = 1,
memory: int = 512,
swap: int = 512,
disk: int = 8,
net0: str = "",
unprivileged: bool = True,
features: str = "",
start: bool = False,
target: str = "",
) -> str:
"""Create a new LXC container.
ostemplate: e.g. 'local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst'
net0: e.g. 'name=eth0,bridge=vmbr0,ip=10.77.30.100/24,gw=10.77.30.1'
features: e.g. 'nesting=1'
"""
require_writes()
client = get_client(target=target)
data: dict = {
"vmid": vmid,
"ostemplate": ostemplate,
"storage": storage,
"hostname": hostname,
"cores": cores,
"memory": memory,
"swap": swap,
"rootfs": f"{storage}:{disk}",
"unprivileged": int(unprivileged),
"start": int(start),
}
if password:
data["password"] = password
if net0:
data["net0"] = net0
if features:
data["features"] = features
result = client.post(f"nodes/{node}/lxc", data=data)
return json.dumps(result, indent=2, default=str)
@mcp.tool()
def proxmox_create_qemu(
node: str,
vmid: int,
name: str,
storage: str = "",
ostype: str = "l26",
cores: int = 1,
memory: int = 1024,
disk: int = 32,
net0: str = "",
scsihw: str = "virtio-scsi-single",
ide2: str = "",
start: bool = False,
target: str = "",
) -> str:
"""Create a new QEMU VM.
ide2: e.g. 'local:iso/debian-12-netinst.iso,media=cdrom'
net0: e.g. 'virtio,bridge=vmbr0'
"""
require_writes()
client = get_client(target=target)
data: dict = {
"vmid": vmid,
"name": name,
"ostype": ostype,
"cores": cores,
"memory": memory,
"scsihw": scsihw,
"start": int(start),
}
if storage and disk:
data["scsi0"] = f"{storage}:{disk}"
if net0:
data["net0"] = net0
if ide2:
data["ide2"] = ide2
result = client.post(f"nodes/{node}/qemu", data=data)
return json.dumps(result, indent=2, default=str)
# ── Delete / clone tools ─────────────────────────────────────
@mcp.tool()
def proxmox_delete_vm(
node: str,
vmid: int,
purge: bool = False,
destroy_unreferenced: bool = False,
target: str = "",
) -> str:
"""Delete a VM or LXC container.
Set purge=true to remove from disk (destroys all data).
Set destroy_unreferenced=true to also remove unreferenced disks.
"""
require_writes()
client = get_client(target=target)
# Determine type — try QEMU first
try:
client.get(f"nodes/{node}/qemu/{vmid}/status/current")
endpoint = f"nodes/{node}/qemu/{vmid}"
except ProxmoxAPIError:
endpoint = f"nodes/{node}/lxc/{vmid}"
params = {}
if purge:
params["purge"] = 1
if destroy_unreferenced:
params["destroy-unreferenced-disks"] = 1
# Append query params to endpoint
if params:
qs = "&".join(f"{k}={v}" for k, v in params.items())
endpoint = f"{endpoint}?{qs}"
result = client.delete(endpoint)
return json.dumps(result, indent=2, default=str)
@mcp.tool()
def proxmox_clone_vm(
node: str,
vmid: int,
newid: int,
name: str = "",
storage: str = "",
target: str = "",
) -> str:
"""Clone a VM or LXC to a new VMID."""
require_writes()
client = get_client(target=target)
try:
client.get(f"nodes/{node}/qemu/{vmid}/status/current")
endpoint = f"nodes/{node}/qemu/{vmid}/clone"
except ProxmoxAPIError:
endpoint = f"nodes/{node}/lxc/{vmid}/clone"
data: dict = {"newid": newid}
if name:
data["name"] = name
if storage:
data["storage"] = storage
result = client.post(endpoint, data=data)
return json.dumps(result, indent=2, default=str)

View File

@@ -33,3 +33,9 @@ def register(mcp: FastMCP) -> None:
def proxmox_get_node_status(node: str, target: str = "") -> str:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/status"), indent=2, default=str)
@mcp.tool()
def proxmox_get_next_vmid(target: str = "") -> str:
"""Get the next available VMID from the cluster."""
client = get_client(target=target)
return json.dumps(client.get("cluster/nextid"), indent=2, default=str)

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
import json
from mcp.server.fastmcp import FastMCP
from proxmox_mcp.client import get_client
def register(mcp: FastMCP) -> None:
@mcp.tool()
def proxmox_list_storage(node: str, target: str = "") -> str:
"""List storage pools on a node with usage stats."""
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/storage"), indent=2, default=str)
@mcp.tool()
def proxmox_list_templates(node: str, storage: str, target: str = "") -> str:
"""List available templates and ISOs on a storage pool."""
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/storage/{storage}/content"), indent=2, default=str)