Proxmox MCP fixes: P1 snapshot tools, P4 error wrapping
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user