diff --git a/CLAUDE.md b/CLAUDE.md index 9a28b18..1f08ffe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,17 +84,16 @@ docker compose down - **Health Monitoring**: Comprehensive health check tool for system monitoring - **Real-time Subscriptions**: WebSocket-based live data streaming -### Tool Categories (10 Tools, 76 Actions) +### Tool Categories (9 Tools, 70 Actions) 1. **`unraid_info`** (19 actions): overview, array, network, registration, connect, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config -2. **`unraid_array`** (5 actions): parity_start, parity_pause, parity_resume, parity_cancel, parity_status -3. **`unraid_storage`** (6 actions): shares, disks, disk_details, unassigned, log_files, logs -4. **`unraid_docker`** (15 actions): list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates -5. **`unraid_vm`** (9 actions): list, details, start, stop, pause, resume, force_stop, reboot, reset -6. **`unraid_notifications`** (9 actions): overview, list, warnings, create, archive, unread, delete, delete_archived, archive_all -7. **`unraid_rclone`** (4 actions): list_remotes, config_form, create_remote, delete_remote -8. **`unraid_users`** (1 action): me -9. **`unraid_keys`** (5 actions): list, get, create, update, delete -10. **`unraid_health`** (3 actions): check, test_connection, diagnose +2. **`unraid_storage`** (6 actions): shares, disks, disk_details, log_files, logs +3. **`unraid_docker`** (15 actions): list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates +4. **`unraid_vm`** (9 actions): list, details, start, stop, pause, resume, force_stop, reboot, reset +5. **`unraid_notifications`** (9 actions): overview, list, warnings, create, archive, unread, delete, delete_archived, archive_all +6. **`unraid_rclone`** (4 actions): list_remotes, config_form, create_remote, delete_remote +7. **`unraid_users`** (1 action): me +8. **`unraid_keys`** (5 actions): list, get, create, update, delete +9. **`unraid_health`** (3 actions): check, test_connection, diagnose ### Environment Variable Hierarchy The server loads environment variables from multiple locations in order: diff --git a/HISTORY.md b/HISTORY.md index 3d55bb4..1218ab3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -191,7 +191,6 @@ Fix substring matching for sensitive data redaction - subscriptions/: Extracted SSL context creation to shared helper in utils.py, replaced deprecated ssl._create_unverified_context API - - tools/array.py: Renamed parity_history to parity_status, hoisted ALL_ACTIONS - tools/storage.py: Fixed dict(None) risks, temperature 0 falsiness bug - tools/notifications.py, keys.py, rclone.py: Fixed dict(None) TypeError risks - tests/: Fixed generator type annotations, added coverage for compound keys diff --git a/README.md b/README.md index cb5895d..88b1c16 100644 --- a/README.md +++ b/README.md @@ -218,13 +218,12 @@ UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle Each tool uses a consolidated `action` parameter to expose multiple operations, reducing context window usage. Destructive actions require `confirm=True`. -### Tool Categories (10 Tools, 76 Actions) +### Tool Categories (9 Tools, 70 Actions) | Tool | Actions | Description | |------|---------|-------------| | **`unraid_info`** | 19 | overview, array, network, registration, connect, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config | -| **`unraid_array`** | 5 | parity_start, parity_pause, parity_resume, parity_cancel, parity_status | -| **`unraid_storage`** | 6 | shares, disks, disk_details, unassigned, log_files, logs | +| **`unraid_storage`** | 6 | shares, disks, disk_details, log_files, logs | | **`unraid_docker`** | 15 | list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates | | **`unraid_vm`** | 9 | list, details, start, stop, pause, resume, force_stop, reboot, reset | | **`unraid_notifications`** | 9 | overview, list, warnings, create, archive, unread, delete, delete_archived, archive_all | @@ -242,14 +241,13 @@ Each tool uses a consolidated `action` parameter to expose multiple operations, ## 💬 Custom Slash Commands -The project includes **10 custom slash commands** in `commands/` for quick access to Unraid operations: +The project includes **9 custom slash commands** in `commands/` for quick access to Unraid operations: ### Available Commands | Command | Actions | Quick Access | |---------|---------|--------------| | `/info` | 19 | System information, metrics, configuration | -| `/array` | 5 | Parity check management | | `/storage` | 6 | Shares, disks, logs | | `/docker` | 15 | Container management and monitoring | | `/vm` | 9 | Virtual machine lifecycle | diff --git a/commands/array.md b/commands/array.md deleted file mode 100644 index 1b294e9..0000000 --- a/commands/array.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: Manage Unraid array parity checks -argument-hint: [action] [correct=true/false] ---- - -Execute the `unraid_array` MCP tool with action: `$1` - -## Available Actions (5) - -**Parity Check Operations:** -- `parity_start` - Start parity check/sync (optional: correct=true to fix errors) -- `parity_pause` - Pause running parity operation -- `parity_resume` - Resume paused parity operation -- `parity_cancel` - Cancel running parity operation -- `parity_status` - Get current parity check status - -## Example Usage - -``` -/array parity_start -/array parity_start correct=true -/array parity_pause -/array parity_resume -/array parity_cancel -/array parity_status -``` - -**Note:** Use `correct=true` with `parity_start` to automatically fix any parity errors found during the check. - -Use the tool to execute the requested parity operation and report the results. diff --git a/commands/info.md b/commands/info.md index 6fd79f3..237d34b 100644 --- a/commands/info.md +++ b/commands/info.md @@ -18,7 +18,6 @@ Execute the `unraid_info` MCP tool with action: `$1` **Network & Registration:** - `network` - Network configuration and interfaces - `registration` - Registration status and license info -- `connect` - Connect service configuration - `online` - Online status check **Configuration:** diff --git a/commands/storage.md b/commands/storage.md index 37acb37..608a69d 100644 --- a/commands/storage.md +++ b/commands/storage.md @@ -11,7 +11,6 @@ Execute the `unraid_storage` MCP tool with action: `$1` - `shares` - List all user shares with sizes and allocation - `disks` - List all disks in the array - `disk_details` - Get detailed info for a specific disk (requires disk identifier) -- `unassigned` - List unassigned devices **Logs:** - `log_files` - List available system log files @@ -23,7 +22,6 @@ Execute the `unraid_storage` MCP tool with action: `$1` /unraid-storage shares /unraid-storage disks /unraid-storage disk_details disk1 -/unraid-storage unassigned /unraid-storage log_files /unraid-storage logs /var/log/syslog ``` diff --git a/docs/research/competitive-analysis.md b/docs/research/competitive-analysis.md index e0af098..7f6547c 100644 --- a/docs/research/competitive-analysis.md +++ b/docs/research/competitive-analysis.md @@ -355,7 +355,6 @@ The project's documentation explicitly compares SSH vs API capabilities: | Network config | Y | Y | Y | Y | N | N | N | | Network bandwidth | N | Y | N | Y | N | N | N | | Registration/license info | Y | Y | Y | N | N | N | N | -| Connect settings | Y | Y | Y | N | N | N | N | | Unraid variables | Y | Y | Y | N | N | N | N | | System services status | N | Y | Y | N | N | N | N | | Flash drive info | N | Y | Y | N | N | Y | N | diff --git a/docs/research/unraid-api-crawl.md b/docs/research/unraid-api-crawl.md index a7b179a..67dc42e 100644 --- a/docs/research/unraid-api-crawl.md +++ b/docs/research/unraid-api-crawl.md @@ -665,7 +665,6 @@ type Query { servers: [Server!]! services: [Service!]! shares: [Share] - unassignedDevices: [UnassignedDevice] me: Me user(id: ID!): User users(input: usersInput): [User!]! @@ -743,7 +742,6 @@ type Subscription { service(name: String!): [Service!] share(id: ID!): Share! shares: [Share!] - unassignedDevices: [UnassignedDevice!] me: Me user(id: ID!): User! users: [User]! diff --git a/docs/unraid-schema.graphql b/docs/unraid-schema.graphql index 6459594..f1cf129 100644 --- a/docs/unraid-schema.graphql +++ b/docs/unraid-schema.graphql @@ -698,7 +698,8 @@ type Info implements Node { } type MetricsCpu { - used: Float + percentTotal: Float! + cpus: [CPULoad!]! } type MetricsMemory { @@ -715,7 +716,6 @@ type Metrics implements Node { type Service implements Node { id: PrefixedID! name: String - state: String online: Boolean uptime: Uptime version: String @@ -751,12 +751,6 @@ type Registration implements Node { updateExpiration: String } -type ConnectSettings { - status: String - sandbox: Boolean - flashGuid: String -} - type Owner { username: String! avatar: String! @@ -1325,9 +1319,6 @@ type Query { # Network (used by MCP tool) network: Network - - # Connect (used by MCP tool) - connect: ConnectSettings } # ============================================================================ diff --git a/tests/http_layer/test_request_construction.py b/tests/http_layer/test_request_construction.py index a93dbaf..c25d2b2 100644 --- a/tests/http_layer/test_request_construction.py +++ b/tests/http_layer/test_request_construction.py @@ -783,17 +783,6 @@ class TestStorageToolRequests: with pytest.raises(ToolError, match="log_path must start with"): await tool(action="logs", log_path="/etc/shadow") - @respx.mock - async def test_unassigned_sends_correct_query(self) -> None: - route = respx.post(API_URL).mock( - return_value=_graphql_response({"unassignedDevices": []}) - ) - tool = self._get_tool() - result = await tool(action="unassigned") - body = _extract_request_body(route.calls.last.request) - assert "GetUnassignedDevices" in body["query"] - assert "devices" in result - # =========================================================================== # Section 10: Notifications tool request construction diff --git a/tests/schema/test_query_validation.py b/tests/schema/test_query_validation.py index 59eb765..b85c64a 100644 --- a/tests/schema/test_query_validation.py +++ b/tests/schema/test_query_validation.py @@ -237,12 +237,6 @@ class TestStorageQueries: errors = _validate_operation(schema, QUERIES["disk_details"]) assert not errors, f"disk_details query validation failed: {errors}" - def test_unassigned_query(self, schema: GraphQLSchema) -> None: - from unraid_mcp.tools.storage import QUERIES - - errors = _validate_operation(schema, QUERIES["unassigned"]) - assert not errors, f"unassigned query validation failed: {errors}" - def test_log_files_query(self, schema: GraphQLSchema) -> None: from unraid_mcp.tools.storage import QUERIES @@ -258,7 +252,7 @@ class TestStorageQueries: def test_all_storage_queries_covered(self, schema: GraphQLSchema) -> None: from unraid_mcp.tools.storage import QUERIES - expected = {"shares", "disks", "disk_details", "unassigned", "log_files", "logs"} + expected = {"shares", "disks", "disk_details", "log_files", "logs"} assert set(QUERIES.keys()) == expected diff --git a/tests/test_storage.py b/tests/test_storage.py index 9cd7867..2e10141 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -154,12 +154,6 @@ class TestStorageActions: with pytest.raises(ToolError, match="not found"): await tool_fn(action="disk_details", disk_id="d:missing") - async def test_unassigned(self, _mock_graphql: AsyncMock) -> None: - _mock_graphql.return_value = {"unassignedDevices": []} - tool_fn = _make_tool() - result = await tool_fn(action="unassigned") - assert result["devices"] == [] - async def test_log_files(self, _mock_graphql: AsyncMock) -> None: _mock_graphql.return_value = {"logFiles": [{"name": "syslog", "path": "/var/log/syslog"}]} tool_fn = _make_tool() diff --git a/unraid_mcp/tools/__init__.py b/unraid_mcp/tools/__init__.py index c863e40..868f412 100644 --- a/unraid_mcp/tools/__init__.py +++ b/unraid_mcp/tools/__init__.py @@ -2,7 +2,6 @@ 10 consolidated tools with ~90 actions total: unraid_info - System information queries (19 actions) - unraid_array - Array operations and power management (12 actions) unraid_storage - Storage, disks, and logs (6 actions) unraid_docker - Docker container management (15 actions) unraid_vm - Virtual machine management (9 actions) diff --git a/unraid_mcp/tools/array.py b/unraid_mcp/tools/array.py deleted file mode 100644 index 5cf132f..0000000 --- a/unraid_mcp/tools/array.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Array parity check operations. - -Provides the `unraid_array` tool with 5 actions for parity check management. -""" - -from typing import Any, Literal - -from fastmcp import FastMCP - -from ..config.logging import logger -from ..core.client import make_graphql_request -from ..core.exceptions import ToolError - - -QUERIES: dict[str, str] = { - "parity_status": """ - query GetParityStatus { - array { parityCheckStatus { progress speed errors } } - } - """, -} - -MUTATIONS: dict[str, str] = { - "parity_start": """ - mutation StartParityCheck($correct: Boolean) { - parityCheck { start(correct: $correct) } - } - """, - "parity_pause": """ - mutation PauseParityCheck { - parityCheck { pause } - } - """, - "parity_resume": """ - mutation ResumeParityCheck { - parityCheck { resume } - } - """, - "parity_cancel": """ - mutation CancelParityCheck { - parityCheck { cancel } - } - """, -} - -ALL_ACTIONS = set(QUERIES) | set(MUTATIONS) - -ARRAY_ACTIONS = Literal[ - "parity_start", - "parity_pause", - "parity_resume", - "parity_cancel", - "parity_status", -] - - -def register_array_tool(mcp: FastMCP) -> None: - """Register the unraid_array tool with the FastMCP instance.""" - - @mcp.tool() - async def unraid_array( - action: ARRAY_ACTIONS, - correct: bool | None = None, - ) -> dict[str, Any]: - """Manage Unraid array parity checks. - - Actions: - parity_start - Start parity check (optional correct=True to fix errors) - 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 - """ - if action not in ALL_ACTIONS: - raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}") - - try: - logger.info(f"Executing unraid_array action={action}") - - 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 - - if action == "parity_start" and correct is not None: - variables = {"correct": correct} - - data = await make_graphql_request(query, variables) - - return { - "success": True, - "action": action, - "data": data, - } - - except ToolError: - raise - except Exception as e: - logger.error(f"Error in unraid_array action={action}: {e}", exc_info=True) - raise ToolError(f"Failed to execute array/{action}: {e!s}") from e - - logger.info("Array tool registered successfully") diff --git a/unraid_mcp/tools/health.py b/unraid_mcp/tools/health.py index eae568a..d6ba60b 100644 --- a/unraid_mcp/tools/health.py +++ b/unraid_mcp/tools/health.py @@ -103,7 +103,6 @@ async def _comprehensive_check() -> dict[str, Any]: query ComprehensiveHealthCheck { info { machineId time - versions { unraid } os { uptime } } array { state } diff --git a/unraid_mcp/tools/info.py b/unraid_mcp/tools/info.py index 6e3e3ef..c152ef0 100644 --- a/unraid_mcp/tools/info.py +++ b/unraid_mcp/tools/info.py @@ -63,11 +63,6 @@ QUERIES: dict[str, str] = { } } """, - "connect": """ - query GetConnectSettings { - connect { status sandbox flashGuid } - } - """, "variables": """ query GetSelectiveUnraidVariables { vars { @@ -85,12 +80,12 @@ QUERIES: dict[str, str] = { """, "metrics": """ query GetMetrics { - metrics { cpu { used } memory { used total } } + metrics { cpu { percentTotal cpus { percentTotal } } memory { used total } } } """, "services": """ query GetServices { - services { name state } + services { name online uptime } } """, "display": """ @@ -159,7 +154,6 @@ INFO_ACTIONS = Literal[ "array", "network", "registration", - "connect", "variables", "metrics", "services", @@ -327,7 +321,6 @@ def register_info_tool(mcp: FastMCP) -> None: array - Array state, capacity, disk health network - Access URLs, interfaces registration - License type, state, expiration - connect - Unraid Connect settings variables - System variables and configuration metrics - CPU and memory utilization services - Running services @@ -359,7 +352,6 @@ def register_info_tool(mcp: FastMCP) -> None: dict_actions: dict[str, str] = { "network": "network", "registration": "registration", - "connect": "connect", "variables": "vars", "metrics": "metrics", "config": "config", diff --git a/unraid_mcp/tools/keys.py b/unraid_mcp/tools/keys.py index f556a85..ee12bd7 100644 --- a/unraid_mcp/tools/keys.py +++ b/unraid_mcp/tools/keys.py @@ -16,12 +16,12 @@ from ..core.exceptions import ToolError QUERIES: dict[str, str] = { "list": """ query ListApiKeys { - apiKeys { id name roles permissions createdAt lastUsed } + apiKeys { id name roles permissions { resource actions } createdAt lastUsed } } """, "get": """ query GetApiKey($id: PrefixedID!) { - apiKey(id: $id) { id name roles permissions createdAt lastUsed } + apiKey(id: $id) { id name roles permissions { resource actions } createdAt lastUsed } } """, } diff --git a/unraid_mcp/tools/storage.py b/unraid_mcp/tools/storage.py index 60629ae..cd32531 100644 --- a/unraid_mcp/tools/storage.py +++ b/unraid_mcp/tools/storage.py @@ -1,7 +1,6 @@ """Storage and disk management. -Provides the `unraid_storage` tool with 6 actions for shares, physical disks, -unassigned devices, log files, and log content retrieval. +Provides the `unraid_storage` tool with 6 actions for shares, physical disks, log files, and log content retrieval. """ from typing import Any, Literal @@ -37,11 +36,6 @@ QUERIES: dict[str, str] = { } } """, - "unassigned": """ - query GetUnassignedDevices { - unassignedDevices { id device name size type } - } - """, "log_files": """ query ListLogFiles { logFiles { name path size modifiedAt } @@ -60,7 +54,6 @@ STORAGE_ACTIONS = Literal[ "shares", "disks", "disk_details", - "unassigned", "log_files", "logs", ] @@ -97,7 +90,6 @@ def register_storage_tool(mcp: FastMCP) -> None: shares - List all user shares with capacity info disks - List all physical disks disk_details - Detailed SMART info for a disk (requires disk_id) - unassigned - List unassigned devices log_files - List available log files logs - Retrieve log content (requires log_path, optional tail_lines) """ @@ -158,10 +150,6 @@ def register_storage_tool(mcp: FastMCP) -> None: } return {"summary": summary, "details": raw} - if action == "unassigned": - devices = data.get("unassignedDevices", []) - return {"devices": list(devices) if isinstance(devices, list) else []} - if action == "log_files": files = data.get("logFiles", []) return {"log_files": list(files) if isinstance(files, list) else []}