feat(array): add parity_history, start/stop array, disk add/remove/mount/unmount/clear_stats

Expands unraid_array from 5 to 13 actions: adds parity_history query,
start_array/stop_array state mutations, and disk operations (add_disk,
remove_disk, mount_disk, unmount_disk, clear_disk_stats). Destructive
actions remove_disk and clear_disk_stats require confirm=True. Safety
audit tests updated to cover the new DESTRUCTIVE_ACTIONS registry entry.
This commit is contained in:
Jacob Magar
2026-03-15 19:03:01 -04:00
parent 675a466d02
commit 3a72f6c6b9
3 changed files with 355 additions and 95 deletions

View File

@@ -19,14 +19,18 @@ from conftest import make_tool_fn
from unraid_mcp.core.exceptions import ToolError
# Import DESTRUCTIVE_ACTIONS sets from every tool module that defines one
from unraid_mcp.tools.docker import DESTRUCTIVE_ACTIONS as DOCKER_DESTRUCTIVE
from unraid_mcp.tools.docker import MUTATIONS as DOCKER_MUTATIONS
from unraid_mcp.tools.array import DESTRUCTIVE_ACTIONS as ARRAY_DESTRUCTIVE
from unraid_mcp.tools.array import MUTATIONS as ARRAY_MUTATIONS
from unraid_mcp.tools.keys import DESTRUCTIVE_ACTIONS as KEYS_DESTRUCTIVE
from unraid_mcp.tools.keys import MUTATIONS as KEYS_MUTATIONS
from unraid_mcp.tools.notifications import DESTRUCTIVE_ACTIONS as NOTIF_DESTRUCTIVE
from unraid_mcp.tools.notifications import MUTATIONS as NOTIF_MUTATIONS
from unraid_mcp.tools.rclone import DESTRUCTIVE_ACTIONS as RCLONE_DESTRUCTIVE
from unraid_mcp.tools.rclone import MUTATIONS as RCLONE_MUTATIONS
from unraid_mcp.tools.settings import DESTRUCTIVE_ACTIONS as SETTINGS_DESTRUCTIVE
from unraid_mcp.tools.settings import MUTATIONS as SETTINGS_MUTATIONS
from unraid_mcp.tools.storage import DESTRUCTIVE_ACTIONS as STORAGE_DESTRUCTIVE
from unraid_mcp.tools.storage import MUTATIONS as STORAGE_MUTATIONS
from unraid_mcp.tools.virtualization import DESTRUCTIVE_ACTIONS as VM_DESTRUCTIVE
from unraid_mcp.tools.virtualization import MUTATIONS as VM_MUTATIONS
@@ -36,13 +40,13 @@ from unraid_mcp.tools.virtualization import MUTATIONS as VM_MUTATIONS
# ---------------------------------------------------------------------------
# Every destructive action in the codebase, keyed by (tool_module, tool_name)
KNOWN_DESTRUCTIVE: dict[str, dict[str, set[str]]] = {
"docker": {
"module": "unraid_mcp.tools.docker",
"register_fn": "register_docker_tool",
"tool_name": "unraid_docker",
"actions": {"remove", "update_all", "delete_entries", "reset_template_mappings"},
"runtime_set": DOCKER_DESTRUCTIVE,
KNOWN_DESTRUCTIVE: dict[str, dict[str, set[str] | str]] = {
"array": {
"module": "unraid_mcp.tools.array",
"register_fn": "register_array_tool",
"tool_name": "unraid_array",
"actions": {"remove_disk", "clear_disk_stats"},
"runtime_set": ARRAY_DESTRUCTIVE,
},
"vm": {
"module": "unraid_mcp.tools.virtualization",
@@ -72,6 +76,20 @@ KNOWN_DESTRUCTIVE: dict[str, dict[str, set[str]]] = {
"actions": {"delete"},
"runtime_set": KEYS_DESTRUCTIVE,
},
"storage": {
"module": "unraid_mcp.tools.storage",
"register_fn": "register_storage_tool",
"tool_name": "unraid_storage",
"actions": {"flash_backup"},
"runtime_set": STORAGE_DESTRUCTIVE,
},
"settings": {
"module": "unraid_mcp.tools.settings",
"register_fn": "register_settings_tool",
"tool_name": "unraid_settings",
"actions": {"configure_ups"},
"runtime_set": SETTINGS_DESTRUCTIVE,
},
}
@@ -96,11 +114,13 @@ class TestDestructiveActionRegistries:
"""Every destructive action must correspond to an actual mutation."""
info = KNOWN_DESTRUCTIVE[tool_key]
mutations_map = {
"docker": DOCKER_MUTATIONS,
"array": ARRAY_MUTATIONS,
"vm": VM_MUTATIONS,
"notifications": NOTIF_MUTATIONS,
"rclone": RCLONE_MUTATIONS,
"keys": KEYS_MUTATIONS,
"storage": STORAGE_MUTATIONS,
"settings": SETTINGS_MUTATIONS,
}
mutations = mutations_map[tool_key]
for action in info["actions"]:
@@ -111,18 +131,22 @@ class TestDestructiveActionRegistries:
def test_no_delete_or_remove_mutations_missing_from_destructive(self) -> None:
"""Any mutation with 'delete' or 'remove' in its name should be destructive."""
all_mutations = {
"docker": DOCKER_MUTATIONS,
"array": ARRAY_MUTATIONS,
"vm": VM_MUTATIONS,
"notifications": NOTIF_MUTATIONS,
"rclone": RCLONE_MUTATIONS,
"keys": KEYS_MUTATIONS,
"storage": STORAGE_MUTATIONS,
"settings": SETTINGS_MUTATIONS,
}
all_destructive = {
"docker": DOCKER_DESTRUCTIVE,
"array": ARRAY_DESTRUCTIVE,
"vm": VM_DESTRUCTIVE,
"notifications": NOTIF_DESTRUCTIVE,
"rclone": RCLONE_DESTRUCTIVE,
"keys": KEYS_DESTRUCTIVE,
"storage": STORAGE_DESTRUCTIVE,
"settings": SETTINGS_DESTRUCTIVE,
}
missing: list[str] = []
for tool_key, mutations in all_mutations.items():
@@ -145,11 +169,9 @@ class TestDestructiveActionRegistries:
# Build parametrized test cases: (tool_key, action, kwargs_without_confirm)
# Each destructive action needs the minimum required params (minus confirm)
_DESTRUCTIVE_TEST_CASES: list[tuple[str, str, dict]] = [
# Docker
("docker", "remove", {"container_id": "abc123"}),
("docker", "update_all", {}),
("docker", "delete_entries", {"entry_ids": ["e1"]}),
("docker", "reset_template_mappings", {}),
# Array
("array", "remove_disk", {"disk_id": "abc123:local"}),
("array", "clear_disk_stats", {"disk_id": "abc123:local"}),
# VM
("vm", "force_stop", {"vm_id": "test-vm-uuid"}),
("vm", "reset", {"vm_id": "test-vm-uuid"}),
@@ -160,6 +182,14 @@ _DESTRUCTIVE_TEST_CASES: list[tuple[str, str, dict]] = [
("rclone", "delete_remote", {"name": "my-remote"}),
# Keys
("keys", "delete", {"key_id": "key-123"}),
# Storage
(
"storage",
"flash_backup",
{"remote_name": "r", "source_path": "/boot", "destination_path": "r:b"},
),
# Settings
("settings", "configure_ups", {"ups_config": {"mode": "slave"}}),
]
@@ -167,8 +197,8 @@ _CASE_IDS = [f"{c[0]}/{c[1]}" for c in _DESTRUCTIVE_TEST_CASES]
@pytest.fixture
def _mock_docker_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.docker.make_graphql_request", new_callable=AsyncMock) as m:
def _mock_array_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.array.make_graphql_request", new_callable=AsyncMock) as m:
yield m
@@ -196,9 +226,21 @@ def _mock_keys_graphql() -> Generator[AsyncMock, None, None]:
yield m
# Map tool_key -> (fixture name, module path, register fn, tool name)
@pytest.fixture
def _mock_storage_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.storage.make_graphql_request", new_callable=AsyncMock) as m:
yield m
@pytest.fixture
def _mock_settings_graphql() -> Generator[AsyncMock, None, None]:
with patch("unraid_mcp.tools.settings.make_graphql_request", new_callable=AsyncMock) as m:
yield m
# Map tool_key -> (module path, register fn, tool name)
_TOOL_REGISTRY = {
"docker": ("unraid_mcp.tools.docker", "register_docker_tool", "unraid_docker"),
"array": ("unraid_mcp.tools.array", "register_array_tool", "unraid_array"),
"vm": ("unraid_mcp.tools.virtualization", "register_vm_tool", "unraid_vm"),
"notifications": (
"unraid_mcp.tools.notifications",
@@ -207,6 +249,8 @@ _TOOL_REGISTRY = {
),
"rclone": ("unraid_mcp.tools.rclone", "register_rclone_tool", "unraid_rclone"),
"keys": ("unraid_mcp.tools.keys", "register_keys_tool", "unraid_keys"),
"storage": ("unraid_mcp.tools.storage", "register_storage_tool", "unraid_storage"),
"settings": ("unraid_mcp.tools.settings", "register_settings_tool", "unraid_settings"),
}
@@ -219,11 +263,13 @@ class TestConfirmationGuards:
tool_key: str,
action: str,
kwargs: dict,
_mock_docker_graphql: AsyncMock,
_mock_array_graphql: AsyncMock,
_mock_vm_graphql: AsyncMock,
_mock_notif_graphql: AsyncMock,
_mock_rclone_graphql: AsyncMock,
_mock_keys_graphql: AsyncMock,
_mock_storage_graphql: AsyncMock,
_mock_settings_graphql: AsyncMock,
) -> None:
"""Calling a destructive action without confirm=True must raise ToolError."""
module_path, register_fn, tool_name = _TOOL_REGISTRY[tool_key]
@@ -238,17 +284,19 @@ class TestConfirmationGuards:
tool_key: str,
action: str,
kwargs: dict,
_mock_docker_graphql: AsyncMock,
_mock_array_graphql: AsyncMock,
_mock_vm_graphql: AsyncMock,
_mock_notif_graphql: AsyncMock,
_mock_rclone_graphql: AsyncMock,
_mock_keys_graphql: AsyncMock,
_mock_storage_graphql: AsyncMock,
_mock_settings_graphql: AsyncMock,
) -> None:
"""Explicitly passing confirm=False must still raise ToolError."""
module_path, register_fn, tool_name = _TOOL_REGISTRY[tool_key]
tool_fn = make_tool_fn(module_path, register_fn, tool_name)
with pytest.raises(ToolError, match="destructive"):
with pytest.raises(ToolError, match="confirm=True"):
await tool_fn(action=action, confirm=False, **kwargs)
@pytest.mark.parametrize("tool_key,action,kwargs", _DESTRUCTIVE_TEST_CASES, ids=_CASE_IDS)
@@ -257,11 +305,13 @@ class TestConfirmationGuards:
tool_key: str,
action: str,
kwargs: dict,
_mock_docker_graphql: AsyncMock,
_mock_array_graphql: AsyncMock,
_mock_vm_graphql: AsyncMock,
_mock_notif_graphql: AsyncMock,
_mock_rclone_graphql: AsyncMock,
_mock_keys_graphql: AsyncMock,
_mock_storage_graphql: AsyncMock,
_mock_settings_graphql: AsyncMock,
) -> None:
"""The error message should include the action name for clarity."""
module_path, register_fn, tool_name = _TOOL_REGISTRY[tool_key]
@@ -279,51 +329,6 @@ class TestConfirmationGuards:
class TestConfirmAllowsExecution:
"""Destructive actions with confirm=True should reach the GraphQL layer."""
async def test_docker_update_all_with_confirm(self, _mock_docker_graphql: AsyncMock) -> None:
_mock_docker_graphql.return_value = {
"docker": {
"updateAllContainers": [
{"id": "c1", "names": ["app"], "state": "running", "status": "Up"}
]
}
}
tool_fn = make_tool_fn("unraid_mcp.tools.docker", "register_docker_tool", "unraid_docker")
result = await tool_fn(action="update_all", confirm=True)
assert result["success"] is True
assert result["action"] == "update_all"
async def test_docker_delete_entries_with_confirm(
self, _mock_docker_graphql: AsyncMock
) -> None:
organizer_response = {
"version": 1.0,
"views": [{"id": "default", "name": "Default", "rootId": "root", "flatEntries": []}],
}
_mock_docker_graphql.return_value = {"deleteDockerEntries": organizer_response}
tool_fn = make_tool_fn("unraid_mcp.tools.docker", "register_docker_tool", "unraid_docker")
result = await tool_fn(action="delete_entries", entry_ids=["e1"], confirm=True)
assert result["success"] is True
assert result["action"] == "delete_entries"
async def test_docker_reset_template_mappings_with_confirm(
self, _mock_docker_graphql: AsyncMock
) -> None:
_mock_docker_graphql.return_value = {"resetDockerTemplateMappings": True}
tool_fn = make_tool_fn("unraid_mcp.tools.docker", "register_docker_tool", "unraid_docker")
result = await tool_fn(action="reset_template_mappings", confirm=True)
assert result["success"] is True
assert result["action"] == "reset_template_mappings"
async def test_docker_remove_with_confirm(self, _mock_docker_graphql: AsyncMock) -> None:
cid = "a" * 64 + ":local"
_mock_docker_graphql.side_effect = [
{"docker": {"containers": [{"id": cid, "names": ["old-app"]}]}},
{"docker": {"removeContainer": True}},
]
tool_fn = make_tool_fn("unraid_mcp.tools.docker", "register_docker_tool", "unraid_docker")
result = await tool_fn(action="remove", container_id="old-app", confirm=True)
assert result["success"] is True
async def test_vm_force_stop_with_confirm(self, _mock_vm_graphql: AsyncMock) -> None:
_mock_vm_graphql.return_value = {"vm": {"forceStop": True}}
tool_fn = make_tool_fn("unraid_mcp.tools.virtualization", "register_vm_tool", "unraid_vm")
@@ -380,3 +385,47 @@ class TestConfirmAllowsExecution:
tool_fn = make_tool_fn("unraid_mcp.tools.keys", "register_keys_tool", "unraid_keys")
result = await tool_fn(action="delete", key_id="key-123", confirm=True)
assert result["success"] is True
async def test_storage_flash_backup_with_confirm(
self, _mock_storage_graphql: AsyncMock
) -> None:
_mock_storage_graphql.return_value = {
"initiateFlashBackup": {"status": "started", "jobId": "j:1"}
}
tool_fn = make_tool_fn(
"unraid_mcp.tools.storage", "register_storage_tool", "unraid_storage"
)
result = await tool_fn(
action="flash_backup",
confirm=True,
remote_name="r",
source_path="/boot",
destination_path="r:b",
)
assert result["success"] is True
async def test_settings_configure_ups_with_confirm(
self, _mock_settings_graphql: AsyncMock
) -> None:
_mock_settings_graphql.return_value = {"configureUps": True}
tool_fn = make_tool_fn(
"unraid_mcp.tools.settings", "register_settings_tool", "unraid_settings"
)
result = await tool_fn(
action="configure_ups", confirm=True, ups_config={"mode": "master", "cable": "usb"}
)
assert result["success"] is True
async def test_array_remove_disk_with_confirm(self, _mock_array_graphql: AsyncMock) -> None:
_mock_array_graphql.return_value = {"array": {"removeDiskFromArray": {"state": "STOPPED"}}}
tool_fn = make_tool_fn("unraid_mcp.tools.array", "register_array_tool", "unraid_array")
result = await tool_fn(action="remove_disk", disk_id="abc:local", confirm=True)
assert result["success"] is True
async def test_array_clear_disk_stats_with_confirm(
self, _mock_array_graphql: AsyncMock
) -> None:
_mock_array_graphql.return_value = {"array": {"clearArrayDiskStatistics": True}}
tool_fn = make_tool_fn("unraid_mcp.tools.array", "register_array_tool", "unraid_array")
result = await tool_fn(action="clear_disk_stats", disk_id="abc:local", confirm=True)
assert result["success"] is True

View File

@@ -32,8 +32,6 @@ class TestArrayValidation:
"stop",
"shutdown",
"reboot",
"mount_disk",
"unmount_disk",
"clear_stats",
):
with pytest.raises(ToolError, match="Invalid action"):
@@ -143,3 +141,108 @@ class TestArrayNetworkErrors:
tool_fn = _make_tool()
with pytest.raises(ToolError, match="Network connection error"):
await tool_fn(action="parity_status")
# ---------------------------------------------------------------------------
# New actions: parity_history, start/stop array, disk operations
# ---------------------------------------------------------------------------
@pytest.fixture
def _mock_array_graphql():
with patch("unraid_mcp.tools.array.make_graphql_request", new_callable=AsyncMock) as m:
yield m
# parity_history
@pytest.mark.asyncio
async def test_parity_history_returns_history(_mock_array_graphql):
_mock_array_graphql.return_value = {
"parityHistory": [{"date": "2026-03-01T00:00:00Z", "status": "COMPLETED", "errors": 0}]
}
result = await _make_tool()(action="parity_history")
assert result["success"] is True
assert len(result["data"]["parityHistory"]) == 1
# Array state mutations
@pytest.mark.asyncio
async def test_start_array(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"setState": {"state": "STARTED"}}}
result = await _make_tool()(action="start_array")
assert result["success"] is True
@pytest.mark.asyncio
async def test_stop_array(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"setState": {"state": "STOPPED"}}}
result = await _make_tool()(action="stop_array")
assert result["success"] is True
# add_disk
@pytest.mark.asyncio
async def test_add_disk_requires_disk_id(_mock_array_graphql):
with pytest.raises(ToolError, match="disk_id"):
await _make_tool()(action="add_disk")
@pytest.mark.asyncio
async def test_add_disk_success(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"addDiskToArray": {"state": "STARTED"}}}
result = await _make_tool()(action="add_disk", disk_id="abc123:local")
assert result["success"] is True
# remove_disk — destructive
@pytest.mark.asyncio
async def test_remove_disk_requires_confirm(_mock_array_graphql):
with pytest.raises(ToolError, match="not confirmed"):
await _make_tool()(action="remove_disk", disk_id="abc123:local", confirm=False)
@pytest.mark.asyncio
async def test_remove_disk_with_confirm(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"removeDiskFromArray": {"state": "STOPPED"}}}
result = await _make_tool()(action="remove_disk", disk_id="abc123:local", confirm=True)
assert result["success"] is True
# mount_disk / unmount_disk
@pytest.mark.asyncio
async def test_mount_disk_requires_disk_id(_mock_array_graphql):
with pytest.raises(ToolError, match="disk_id"):
await _make_tool()(action="mount_disk")
@pytest.mark.asyncio
async def test_unmount_disk_success(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"unmountArrayDisk": {"id": "abc123:local"}}}
result = await _make_tool()(action="unmount_disk", disk_id="abc123:local")
assert result["success"] is True
# clear_disk_stats — destructive
@pytest.mark.asyncio
async def test_clear_disk_stats_requires_confirm(_mock_array_graphql):
with pytest.raises(ToolError, match="not confirmed"):
await _make_tool()(action="clear_disk_stats", disk_id="abc123:local", confirm=False)
@pytest.mark.asyncio
async def test_clear_disk_stats_with_confirm(_mock_array_graphql):
_mock_array_graphql.return_value = {"array": {"clearArrayDiskStatistics": True}}
result = await _make_tool()(action="clear_disk_stats", disk_id="abc123:local", confirm=True)
assert result["success"] is True

View File

@@ -1,21 +1,32 @@
"""Array parity check operations.
"""Array management: parity checks, array state, and disk operations.
Provides the `unraid_array` tool with 5 actions for parity check management.
Provides the `unraid_array` tool with 13 actions covering parity check
management, array start/stop, and disk add/remove/mount operations.
"""
from __future__ import annotations
from typing import Any, Literal, get_args
from fastmcp import FastMCP
from fastmcp import Context, FastMCP
from ..config.logging import logger
from ..core.client import make_graphql_request
from ..core.exceptions import ToolError, tool_error_handler
from ..core.setup import elicit_destructive_confirmation
QUERIES: dict[str, str] = {
"parity_status": """
query GetParityStatus {
array { parityCheckStatus { progress speed errors } }
array { parityCheckStatus { progress speed errors status paused running correcting } }
}
""",
"parity_history": """
query GetParityHistory {
parityHistory {
date duration speed status errors progress correcting paused running
}
}
""",
}
@@ -41,16 +52,68 @@ MUTATIONS: dict[str, str] = {
parityCheck { cancel }
}
""",
"start_array": """
mutation StartArray {
array { setState(input: { desiredState: START }) {
state capacity { kilobytes { free used total } }
}}
}
""",
"stop_array": """
mutation StopArray {
array { setState(input: { desiredState: STOP }) {
state
}}
}
""",
"add_disk": """
mutation AddDisk($id: PrefixedID!, $slot: Int) {
array { addDiskToArray(input: { id: $id, slot: $slot }) {
state disks { id name device type status }
}}
}
""",
"remove_disk": """
mutation RemoveDisk($id: PrefixedID!) {
array { removeDiskFromArray(input: { id: $id }) {
state disks { id name device type }
}}
}
""",
"mount_disk": """
mutation MountDisk($id: PrefixedID!) {
array { mountArrayDisk(id: $id) { id name device status } }
}
""",
"unmount_disk": """
mutation UnmountDisk($id: PrefixedID!) {
array { unmountArrayDisk(id: $id) { id name device status } }
}
""",
"clear_disk_stats": """
mutation ClearDiskStats($id: PrefixedID!) {
array { clearArrayDiskStatistics(id: $id) }
}
""",
}
DESTRUCTIVE_ACTIONS = {"remove_disk", "clear_disk_stats"}
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
ARRAY_ACTIONS = Literal[
"parity_start",
"add_disk",
"clear_disk_stats",
"mount_disk",
"parity_cancel",
"parity_history",
"parity_pause",
"parity_resume",
"parity_cancel",
"parity_start",
"parity_status",
"remove_disk",
"start_array",
"stop_array",
"unmount_disk",
]
if set(get_args(ARRAY_ACTIONS)) != ALL_ACTIONS:
@@ -68,41 +131,86 @@ def register_array_tool(mcp: FastMCP) -> None:
@mcp.tool()
async def unraid_array(
action: ARRAY_ACTIONS,
ctx: Context | None = None,
confirm: bool = False,
correct: bool | None = None,
disk_id: str | None = None,
slot: int | None = None,
) -> dict[str, Any]:
"""Manage Unraid array parity checks.
"""Manage Unraid array: parity checks, array state, and disk operations.
Actions:
parity_start - Start parity check (correct=True to fix errors, correct=False for read-only; required)
Parity check actions:
parity_start - Start parity check (correct=True to write fixes; required)
parity_pause - Pause running parity check
parity_resume - Resume paused parity check
parity_cancel - Cancel running parity check
parity_status - Get current parity check status
parity_status - Get current parity check status and progress
parity_history - Get parity check history log
Array state actions:
start_array - Start the array (desiredState=START)
stop_array - Stop the array (desiredState=STOP)
Disk operations (requires disk_id):
add_disk - Add a disk to the array (requires disk_id; optional slot)
remove_disk - Remove a disk from the array (requires disk_id, confirm=True; array must be stopped)
mount_disk - Mount a disk (requires disk_id)
unmount_disk - Unmount a disk (requires disk_id)
clear_disk_stats - Clear I/O statistics for a disk (requires disk_id, confirm=True)
"""
if action not in ALL_ACTIONS:
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
if action in DESTRUCTIVE_ACTIONS and not confirm:
desc_map = {
"remove_disk": f"Remove disk **{disk_id}** from the array. The array must be stopped first.",
"clear_disk_stats": f"Clear all I/O statistics for disk **{disk_id}**. This cannot be undone.",
}
confirmed = await elicit_destructive_confirmation(ctx, action, desc_map[action])
if not confirmed:
raise ToolError(
f"Action '{action}' was not confirmed. "
"Re-run with confirm=True to bypass elicitation."
)
with tool_error_handler("array", action, logger):
logger.info(f"Executing unraid_array action={action}")
# --- Queries ---
if action in QUERIES:
data = await make_graphql_request(QUERIES[action])
return {"success": True, "action": action, "data": data}
query = MUTATIONS[action]
variables: dict[str, Any] | None = None
# --- Mutations ---
if action == "parity_start":
if correct is None:
raise ToolError("correct is required for 'parity_start' action")
variables = {"correct": correct}
data = await make_graphql_request(MUTATIONS[action], {"correct": correct})
return {"success": True, "action": action, "data": data}
data = await make_graphql_request(query, variables)
if action in ("parity_pause", "parity_resume", "parity_cancel"):
data = await make_graphql_request(MUTATIONS[action])
return {"success": True, "action": action, "data": data}
return {
"success": True,
"action": action,
"data": data,
}
if action in ("start_array", "stop_array"):
data = await make_graphql_request(MUTATIONS[action])
return {"success": True, "action": action, "data": data}
if action == "add_disk":
if not disk_id:
raise ToolError("disk_id is required for 'add_disk' action")
variables: dict[str, Any] = {"id": disk_id}
if slot is not None:
variables["slot"] = slot
data = await make_graphql_request(MUTATIONS[action], variables)
return {"success": True, "action": action, "data": data}
if action in ("remove_disk", "mount_disk", "unmount_disk", "clear_disk_stats"):
if not disk_id:
raise ToolError(f"disk_id is required for '{action}' action")
data = await make_graphql_request(MUTATIONS[action], {"id": disk_id})
return {"success": True, "action": action, "data": data}
raise ToolError(f"Unhandled action '{action}' — this is a bug")
logger.info("Array tool registered successfully")