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
|
## Quick start
|
||||||
|
|
||||||
@@ -9,48 +9,103 @@ cd ~/Projects/proxmox-mcp
|
|||||||
python3 -m venv .venv && .venv/bin/pip install -e .
|
python3 -m venv .venv && .venv/bin/pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cursor (`~/.cursor/mcp.json`)
|
### Hermes (`~/.hermes/config.yaml`)
|
||||||
|
|
||||||
```json
|
```yaml
|
||||||
"proxmox": {
|
mcp_servers:
|
||||||
"command": "/Users/nearxos/Projects/proxmox-mcp/.venv/bin/proxmox-mcp",
|
proxmox:
|
||||||
"env": {
|
command: /usr/local/lib/hermes-agent/venv/bin/proxmox-mcp
|
||||||
"PROXMOX_VERIFY_SSL": "false",
|
timeout: 120
|
||||||
"PROXMOX_ALLOW_WRITES": "false",
|
enabled: true
|
||||||
"PROXMOX_TARGETS_PATH": "/Users/nearxos/.local/share/proxmox-mcp/targets.json"
|
env:
|
||||||
}
|
PROXMOX_VERIFY_SSL: 'false'
|
||||||
}
|
PROXMOX_ALLOW_WRITES: 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Connect
|
### Connect
|
||||||
|
|
||||||
```text
|
```
|
||||||
proxmox_connect(
|
proxmox_connect(url="https://10.77.30.5:8006", token_id="root@pam!tokenname", token_secret="...", name="dell")
|
||||||
url="https://10.20.30.254:8006",
|
|
||||||
token_id="root@pam!cursor",
|
|
||||||
token_secret="...",
|
|
||||||
name="golden-proxmox"
|
|
||||||
)
|
|
||||||
proxmox_test_connection()
|
proxmox_test_connection()
|
||||||
proxmox_list_qemu(node="pve")
|
proxmox_list_qemu(node="dell")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tools
|
## Tools (30 total)
|
||||||
|
|
||||||
|
### Connection & Discovery
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `proxmox_connect` / `proxmox_use_target` | Multi-target registry (persisted) |
|
| `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 |
|
| `proxmox_test_connection` | Version + nodes |
|
||||||
|
|
||||||
|
### Nodes & Cluster
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
| `proxmox_list_nodes` | Cluster nodes |
|
| `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_list_qemu` / `proxmox_list_lxc` | Guest inventory |
|
||||||
| `proxmox_get_qemu_status` / `proxmox_get_lxc_status` | Live status |
|
| `proxmox_get_qemu_status` / `proxmox_get_lxc_status` | Live status |
|
||||||
| `proxmox_get_qemu_config` | VM config |
|
| `proxmox_get_qemu_config` / `proxmox_get_lxc_config` | Full config |
|
||||||
| `proxmox_lxc_exec` / `proxmox_qemu_agent_exec` | Guest exec (writes) |
|
|
||||||
| `proxmox_get_task_log` | Async task output |
|
### 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
|
## 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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "proxmox-mcp"
|
name = "proxmox-mcp"
|
||||||
version = "1.0.0"
|
version = "1.2.0"
|
||||||
description = "Model Context Protocol server for Proxmox VE 9 management"
|
description = "Model Context Protocol server for Proxmox VE management — deploy, manage, and monitor VMs and LXCs"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.0.0"
|
__version__ = "1.2.0"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from mcp.server.fastmcp import FastMCP
|
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:
|
def register_all(mcp: FastMCP) -> None:
|
||||||
targets.register(mcp)
|
targets.register(mcp)
|
||||||
nodes.register(mcp)
|
nodes.register(mcp)
|
||||||
guests.register(mcp)
|
guests.register(mcp)
|
||||||
|
storage.register(mcp)
|
||||||
tasks.register(mcp)
|
tasks.register(mcp)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import json
|
|||||||
|
|
||||||
from mcp.server.fastmcp import FastMCP
|
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
|
from proxmox_mcp.guards import GuardError, require_writes
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +36,60 @@ def register(mcp: FastMCP) -> None:
|
|||||||
client = get_client(target=target)
|
client = get_client(target=target)
|
||||||
return json.dumps(client.get(f"nodes/{node}/qemu/{vmid}/config"), indent=2, default=str)
|
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()
|
@mcp.tool()
|
||||||
def proxmox_lxc_exec(
|
def proxmox_lxc_exec(
|
||||||
node: str,
|
node: str,
|
||||||
@@ -67,3 +121,155 @@ def register(mcp: FastMCP) -> None:
|
|||||||
payload = {"command": [command] + (args or [])}
|
payload = {"command": [command] + (args or [])}
|
||||||
result = client.post(f"nodes/{node}/qemu/{vmid}/agent/exec", data={"command": json.dumps(payload["command"])})
|
result = client.post(f"nodes/{node}/qemu/{vmid}/agent/exec", data={"command": json.dumps(payload["command"])})
|
||||||
return json.dumps(result, indent=2, default=str)
|
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:
|
def proxmox_get_node_status(node: str, target: str = "") -> str:
|
||||||
client = get_client(target=target)
|
client = get_client(target=target)
|
||||||
return json.dumps(client.get(f"nodes/{node}/status"), indent=2, default=str)
|
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