Proxmox MCP fixes: P1 snapshot tools, P4 error wrapping

This commit is contained in:
root
2026-07-04 20:15:58 +00:00
parent 2e371b2709
commit 4c62d830a7
2 changed files with 248 additions and 128 deletions

View File

@@ -12,41 +12,60 @@ def register(mcp: FastMCP) -> None:
@mcp.tool()
def proxmox_list_qemu(node: str, target: str = "") -> str:
"""List QEMU VMs on a node."""
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/qemu"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_list_lxc(node: str, target: str = "") -> str:
"""List LXC containers on a node."""
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/lxc"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_qemu_status(node: str, vmid: int, target: str = "") -> str:
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/qemu/{vmid}/status/current"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_lxc_status(node: str, vmid: int, target: str = "") -> str:
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/lxc/{vmid}/status/current"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_qemu_config(node: str, vmid: int, target: str = "") -> str:
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/qemu/{vmid}/config"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_lxc_config(node: str, vmid: int, target: str = "") -> str:
"""Get LXC container configuration."""
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/lxc/{vmid}/config"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
# ── Lifecycle tools ──────────────────────────────────────────
@mcp.tool()
def proxmox_start_vm(node: str, vmid: int, target: str = "") -> str:
"""Start a stopped VM or LXC container."""
try:
require_writes()
client = get_client(target=target)
try:
@@ -54,10 +73,13 @@ def register(mcp: FastMCP) -> None:
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/start")
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_stop_vm(node: str, vmid: int, target: str = "") -> str:
"""Stop a VM or LXC container (immediate stop)."""
try:
require_writes()
client = get_client(target=target)
try:
@@ -65,10 +87,13 @@ def register(mcp: FastMCP) -> None:
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/stop")
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@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."""
try:
require_writes()
client = get_client(target=target)
try:
@@ -76,10 +101,13 @@ def register(mcp: FastMCP) -> None:
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/shutdown", data={"timeout": timeout})
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_reboot_vm(node: str, vmid: int, target: str = "") -> str:
"""Reboot a VM or LXC container."""
def proxmox_restart_vm(node: str, vmid: int, target: str = "") -> str:
"""Restart a VM or LXC container."""
try:
require_writes()
client = get_client(target=target)
try:
@@ -87,6 +115,8 @@ def register(mcp: FastMCP) -> None:
except ProxmoxAPIError:
result = client.post(f"nodes/{node}/lxc/{vmid}/status/reboot")
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
# ── Guest exec tools ─────────────────────────────────────────
@@ -99,6 +129,7 @@ def register(mcp: FastMCP) -> None:
target: str = "",
) -> str:
"""Execute a command inside an LXC (requires root@pam permissions)."""
try:
require_writes()
client = get_client(target=target)
data: dict[str, str] = {"command": command}
@@ -106,6 +137,8 @@ def register(mcp: FastMCP) -> None:
data["extra-args"] = args
result = client.post(f"nodes/{node}/lxc/{vmid}/exec", data=data)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_qemu_agent_exec(
@@ -116,11 +149,14 @@ def register(mcp: FastMCP) -> None:
target: str = "",
) -> str:
"""Execute via QEMU guest agent (VM must have agent enabled)."""
try:
require_writes()
client = get_client(target=target)
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)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
# ── Create tools ─────────────────────────────────────────────
@@ -148,6 +184,7 @@ def register(mcp: FastMCP) -> None:
net0: e.g. 'name=eth0,bridge=vmbr0,ip=10.77.30.100/24,gw=10.77.30.1'
features: e.g. 'nesting=1'
"""
try:
require_writes()
client = get_client(target=target)
data: dict = {
@@ -170,6 +207,8 @@ def register(mcp: FastMCP) -> None:
data["features"] = features
result = client.post(f"nodes/{node}/lxc", data=data)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_create_qemu(
@@ -192,6 +231,7 @@ def register(mcp: FastMCP) -> None:
ide2: e.g. 'local:iso/debian-12-netinst.iso,media=cdrom'
net0: e.g. 'virtio,bridge=vmbr0'
"""
try:
require_writes()
client = get_client(target=target)
data: dict = {
@@ -211,6 +251,8 @@ def register(mcp: FastMCP) -> None:
data["ide2"] = ide2
result = client.post(f"nodes/{node}/qemu", data=data)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
# ── Delete / clone tools ─────────────────────────────────────
@@ -227,6 +269,7 @@ def register(mcp: FastMCP) -> None:
Set purge=true to remove from disk (destroys all data).
Set destroy_unreferenced=true to also remove unreferenced disks.
"""
try:
require_writes()
client = get_client(target=target)
# Determine type — try QEMU first
@@ -247,6 +290,8 @@ def register(mcp: FastMCP) -> None:
endpoint = f"{endpoint}?{qs}"
result = client.delete(endpoint)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_clone_vm(
@@ -258,6 +303,7 @@ def register(mcp: FastMCP) -> None:
target: str = "",
) -> str:
"""Clone a VM or LXC to a new VMID."""
try:
require_writes()
client = get_client(target=target)
try:
@@ -273,3 +319,68 @@ def register(mcp: FastMCP) -> None:
data["storage"] = storage
result = client.post(endpoint, data=data)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
# ── Snapshot tools ───────────────────────────────────────────
@mcp.tool()
def proxmox_create_snapshot(
node: str,
vmid: int,
snapshot_name: str,
description: str = "",
target: str = "",
) -> str:
"""Create a VM snapshot. VM must be running or stopped."""
try:
require_writes()
client = get_client(target=target)
data: dict = {"snapname": snapshot_name}
if description:
data["description"] = description
result = client.post(f"nodes/{node}/qemu/{vmid}/snapshot", data=data)
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_list_snapshots(node: str, vmid: int, target: str = "") -> str:
"""List all snapshots for a VM."""
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/qemu/{vmid}/snapshot"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_rollback_vm(
node: str,
vmid: int,
snapshot_name: str,
target: str = "",
) -> str:
"""Rollback a VM to a snapshot. VM must be stopped."""
try:
require_writes()
client = get_client(target=target)
result = client.post(f"nodes/{node}/qemu/{vmid}/snapshot/{snapshot_name}/rollback")
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_delete_snapshot(
node: str,
vmid: int,
snapshot_name: str,
target: str = "",
) -> str:
"""Delete a VM snapshot."""
try:
require_writes()
client = get_client(target=target)
result = client.delete(f"nodes/{node}/qemu/{vmid}/snapshot/{snapshot_name}")
return json.dumps(result, indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)

View File

@@ -26,16 +26,25 @@ def register(mcp: FastMCP) -> None:
@mcp.tool()
def proxmox_list_nodes(target: str = "") -> str:
try:
client = get_client(target=target)
return json.dumps(client.get("nodes"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_node_status(node: str, target: str = "") -> str:
try:
client = get_client(target=target)
return json.dumps(client.get(f"nodes/{node}/status"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)
@mcp.tool()
def proxmox_get_next_vmid(target: str = "") -> str:
"""Get the next available VMID from the cluster."""
try:
client = get_client(target=target)
return json.dumps(client.get("cluster/nextid"), indent=2, default=str)
except (GuardError, ProxmoxAPIError) as exc:
return json.dumps({"status": "error", "message": str(exc)}, indent=2)