mirror of
https://github.com/jmagar/unraid-mcp
synced 2026-04-21 13:37:53 +00:00
92 lines
3.8 KiB
Python
92 lines
3.8 KiB
Python
"""VM domain handler for the Unraid MCP tool.
|
|
|
|
Covers: list, details, start, stop, pause, resume, force_stop*, reboot, reset* (9 subactions).
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from fastmcp import Context
|
|
|
|
from ..config.logging import logger
|
|
from ..core import client as _client
|
|
from ..core.exceptions import ToolError, tool_error_handler
|
|
from ..core.guards import gate_destructive_action
|
|
from ..core.utils import validate_subaction
|
|
|
|
|
|
# ===========================================================================
|
|
# VM
|
|
# ===========================================================================
|
|
|
|
# VmDomain only exposes id/name/state/uuid — no richer detail query exists in the
|
|
# Unraid GraphQL schema, so "details" reuses the same query and filters client-side.
|
|
_VM_LIST_QUERY = "query ListVMs { vms { id domains { id name state uuid } } }"
|
|
|
|
_VM_QUERIES: dict[str, str] = {
|
|
"list": _VM_LIST_QUERY,
|
|
}
|
|
|
|
_VM_MUTATIONS: dict[str, str] = {
|
|
"start": "mutation StartVM($id: PrefixedID!) { vm { start(id: $id) } }",
|
|
"stop": "mutation StopVM($id: PrefixedID!) { vm { stop(id: $id) } }",
|
|
"pause": "mutation PauseVM($id: PrefixedID!) { vm { pause(id: $id) } }",
|
|
"resume": "mutation ResumeVM($id: PrefixedID!) { vm { resume(id: $id) } }",
|
|
"force_stop": "mutation ForceStopVM($id: PrefixedID!) { vm { forceStop(id: $id) } }",
|
|
"reboot": "mutation RebootVM($id: PrefixedID!) { vm { reboot(id: $id) } }",
|
|
"reset": "mutation ResetVM($id: PrefixedID!) { vm { reset(id: $id) } }",
|
|
}
|
|
|
|
_VM_SUBACTIONS: set[str] = set(_VM_QUERIES) | set(_VM_MUTATIONS) | {"details"}
|
|
_VM_DESTRUCTIVE: set[str] = {"force_stop", "reset"}
|
|
_VM_MUTATION_FIELDS: dict[str, str] = {"force_stop": "forceStop"}
|
|
|
|
|
|
async def _handle_vm(
|
|
subaction: str, vm_id: str | None, ctx: Context | None, confirm: bool
|
|
) -> dict[str, Any]:
|
|
validate_subaction(subaction, _VM_SUBACTIONS, "vm")
|
|
if subaction != "list" and not vm_id:
|
|
raise ToolError(f"vm_id is required for vm/{subaction}")
|
|
|
|
await gate_destructive_action(
|
|
ctx,
|
|
subaction,
|
|
_VM_DESTRUCTIVE,
|
|
confirm,
|
|
{
|
|
"force_stop": f"Force stop VM **{vm_id}**. Unsaved data may be lost.",
|
|
"reset": f"Reset VM **{vm_id}**. This is a hard reset — unsaved data may be lost.",
|
|
},
|
|
)
|
|
|
|
with tool_error_handler("vm", subaction, logger):
|
|
logger.info(f"Executing unraid action=vm subaction={subaction}")
|
|
|
|
if subaction == "list":
|
|
data = await _client.make_graphql_request(_VM_QUERIES["list"])
|
|
if data.get("vms"):
|
|
vms = data["vms"].get("domains") or data["vms"].get("domain") or []
|
|
if isinstance(vms, dict):
|
|
vms = [vms]
|
|
return {"vms": vms}
|
|
return {"vms": []}
|
|
|
|
if subaction == "details":
|
|
# VmDomain has no richer fields than list — reuse the same query, filter client-side.
|
|
data = await _client.make_graphql_request(_VM_LIST_QUERY)
|
|
if not data.get("vms"):
|
|
raise ToolError("No VM data returned from server")
|
|
vms = data["vms"].get("domains") or data["vms"].get("domain") or []
|
|
if isinstance(vms, dict):
|
|
vms = [vms]
|
|
for vm in vms:
|
|
if vm.get("uuid") == vm_id or vm.get("id") == vm_id or vm.get("name") == vm_id:
|
|
return dict(vm)
|
|
available = [f"{v.get('name')} (UUID: {v.get('uuid')})" for v in vms]
|
|
raise ToolError(f"VM '{vm_id}' not found. Available: {', '.join(available)}")
|
|
|
|
data = await _client.make_graphql_request(_VM_MUTATIONS[subaction], {"id": vm_id})
|
|
field = _VM_MUTATION_FIELDS.get(subaction, subaction)
|
|
if data.get("vm") and field in data["vm"]:
|
|
return {"success": data["vm"][field], "subaction": subaction, "vm_id": vm_id}
|
|
raise ToolError(f"Failed to {subaction} VM or unexpected response")
|