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:
105
README.md
105
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.2.0"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
21
src/proxmox_mcp/tools/storage.py
Normal file
21
src/proxmox_mcp/tools/storage.py
Normal 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)
|
||||
Reference in New Issue
Block a user