fix: even more changes to accommodate older GraphQL schema

This commit is contained in:
2026-02-28 16:45:19 +01:00
parent f89ed7275b
commit 88983c6736
18 changed files with 20 additions and 218 deletions

View File

@@ -84,17 +84,16 @@ docker compose down
- **Health Monitoring**: Comprehensive health check tool for system monitoring - **Health Monitoring**: Comprehensive health check tool for system monitoring
- **Real-time Subscriptions**: WebSocket-based live data streaming - **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 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 2. **`unraid_storage`** (6 actions): shares, disks, disk_details, log_files, logs
3. **`unraid_storage`** (6 actions): shares, disks, disk_details, unassigned, 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_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_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_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_rclone`** (4 actions): list_remotes, config_form, create_remote, delete_remote 7. **`unraid_users`** (1 action): me
8. **`unraid_users`** (1 action): me 8. **`unraid_keys`** (5 actions): list, get, create, update, delete
9. **`unraid_keys`** (5 actions): list, get, create, update, delete 9. **`unraid_health`** (3 actions): check, test_connection, diagnose
10. **`unraid_health`** (3 actions): check, test_connection, diagnose
### Environment Variable Hierarchy ### Environment Variable Hierarchy
The server loads environment variables from multiple locations in order: The server loads environment variables from multiple locations in order:

View File

@@ -191,7 +191,6 @@ Fix
substring matching for sensitive data redaction substring matching for sensitive data redaction
- subscriptions/: Extracted SSL context creation to shared helper in utils.py, - subscriptions/: Extracted SSL context creation to shared helper in utils.py,
replaced deprecated ssl._create_unverified_context API 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/storage.py: Fixed dict(None) risks, temperature 0 falsiness bug
- tools/notifications.py, keys.py, rclone.py: Fixed dict(None) TypeError risks - tools/notifications.py, keys.py, rclone.py: Fixed dict(None) TypeError risks
- tests/: Fixed generator type annotations, added coverage for compound keys - tests/: Fixed generator type annotations, added coverage for compound keys

View File

@@ -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`. 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 | | 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_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, log_files, logs |
| **`unraid_storage`** | 6 | shares, disks, disk_details, unassigned, 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_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_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 | | **`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 ## 💬 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 ### Available Commands
| Command | Actions | Quick Access | | Command | Actions | Quick Access |
|---------|---------|--------------| |---------|---------|--------------|
| `/info` | 19 | System information, metrics, configuration | | `/info` | 19 | System information, metrics, configuration |
| `/array` | 5 | Parity check management |
| `/storage` | 6 | Shares, disks, logs | | `/storage` | 6 | Shares, disks, logs |
| `/docker` | 15 | Container management and monitoring | | `/docker` | 15 | Container management and monitoring |
| `/vm` | 9 | Virtual machine lifecycle | | `/vm` | 9 | Virtual machine lifecycle |

View File

@@ -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.

View File

@@ -18,7 +18,6 @@ Execute the `unraid_info` MCP tool with action: `$1`
**Network & Registration:** **Network & Registration:**
- `network` - Network configuration and interfaces - `network` - Network configuration and interfaces
- `registration` - Registration status and license info - `registration` - Registration status and license info
- `connect` - Connect service configuration
- `online` - Online status check - `online` - Online status check
**Configuration:** **Configuration:**

View File

@@ -11,7 +11,6 @@ Execute the `unraid_storage` MCP tool with action: `$1`
- `shares` - List all user shares with sizes and allocation - `shares` - List all user shares with sizes and allocation
- `disks` - List all disks in the array - `disks` - List all disks in the array
- `disk_details` - Get detailed info for a specific disk (requires disk identifier) - `disk_details` - Get detailed info for a specific disk (requires disk identifier)
- `unassigned` - List unassigned devices
**Logs:** **Logs:**
- `log_files` - List available system log files - `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 shares
/unraid-storage disks /unraid-storage disks
/unraid-storage disk_details disk1 /unraid-storage disk_details disk1
/unraid-storage unassigned
/unraid-storage log_files /unraid-storage log_files
/unraid-storage logs /var/log/syslog /unraid-storage logs /var/log/syslog
``` ```

View File

@@ -355,7 +355,6 @@ The project's documentation explicitly compares SSH vs API capabilities:
| Network config | Y | Y | Y | Y | N | N | N | | Network config | Y | Y | Y | Y | N | N | N |
| Network bandwidth | N | Y | N | Y | N | N | N | | Network bandwidth | N | Y | N | Y | N | N | N |
| Registration/license info | Y | Y | Y | N | 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 | | Unraid variables | Y | Y | Y | N | N | N | N |
| System services status | N | 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 | | Flash drive info | N | Y | Y | N | N | Y | N |

View File

@@ -665,7 +665,6 @@ type Query {
servers: [Server!]! servers: [Server!]!
services: [Service!]! services: [Service!]!
shares: [Share] shares: [Share]
unassignedDevices: [UnassignedDevice]
me: Me me: Me
user(id: ID!): User user(id: ID!): User
users(input: usersInput): [User!]! users(input: usersInput): [User!]!
@@ -743,7 +742,6 @@ type Subscription {
service(name: String!): [Service!] service(name: String!): [Service!]
share(id: ID!): Share! share(id: ID!): Share!
shares: [Share!] shares: [Share!]
unassignedDevices: [UnassignedDevice!]
me: Me me: Me
user(id: ID!): User! user(id: ID!): User!
users: [User]! users: [User]!

View File

@@ -698,7 +698,8 @@ type Info implements Node {
} }
type MetricsCpu { type MetricsCpu {
used: Float percentTotal: Float!
cpus: [CPULoad!]!
} }
type MetricsMemory { type MetricsMemory {
@@ -715,7 +716,6 @@ type Metrics implements Node {
type Service implements Node { type Service implements Node {
id: PrefixedID! id: PrefixedID!
name: String name: String
state: String
online: Boolean online: Boolean
uptime: Uptime uptime: Uptime
version: String version: String
@@ -751,12 +751,6 @@ type Registration implements Node {
updateExpiration: String updateExpiration: String
} }
type ConnectSettings {
status: String
sandbox: Boolean
flashGuid: String
}
type Owner { type Owner {
username: String! username: String!
avatar: String! avatar: String!
@@ -1325,9 +1319,6 @@ type Query {
# Network (used by MCP tool) # Network (used by MCP tool)
network: Network network: Network
# Connect (used by MCP tool)
connect: ConnectSettings
} }
# ============================================================================ # ============================================================================

View File

@@ -783,17 +783,6 @@ class TestStorageToolRequests:
with pytest.raises(ToolError, match="log_path must start with"): with pytest.raises(ToolError, match="log_path must start with"):
await tool(action="logs", log_path="/etc/shadow") 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 # Section 10: Notifications tool request construction

View File

@@ -237,12 +237,6 @@ class TestStorageQueries:
errors = _validate_operation(schema, QUERIES["disk_details"]) errors = _validate_operation(schema, QUERIES["disk_details"])
assert not errors, f"disk_details query validation failed: {errors}" 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: def test_log_files_query(self, schema: GraphQLSchema) -> None:
from unraid_mcp.tools.storage import QUERIES from unraid_mcp.tools.storage import QUERIES
@@ -258,7 +252,7 @@ class TestStorageQueries:
def test_all_storage_queries_covered(self, schema: GraphQLSchema) -> None: def test_all_storage_queries_covered(self, schema: GraphQLSchema) -> None:
from unraid_mcp.tools.storage import QUERIES 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 assert set(QUERIES.keys()) == expected

View File

@@ -154,12 +154,6 @@ class TestStorageActions:
with pytest.raises(ToolError, match="not found"): with pytest.raises(ToolError, match="not found"):
await tool_fn(action="disk_details", disk_id="d:missing") 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: async def test_log_files(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"logFiles": [{"name": "syslog", "path": "/var/log/syslog"}]} _mock_graphql.return_value = {"logFiles": [{"name": "syslog", "path": "/var/log/syslog"}]}
tool_fn = _make_tool() tool_fn = _make_tool()

View File

@@ -2,7 +2,6 @@
10 consolidated tools with ~90 actions total: 10 consolidated tools with ~90 actions total:
unraid_info - System information queries (19 actions) 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_storage - Storage, disks, and logs (6 actions)
unraid_docker - Docker container management (15 actions) unraid_docker - Docker container management (15 actions)
unraid_vm - Virtual machine management (9 actions) unraid_vm - Virtual machine management (9 actions)

View File

@@ -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")

View File

@@ -103,7 +103,6 @@ async def _comprehensive_check() -> dict[str, Any]:
query ComprehensiveHealthCheck { query ComprehensiveHealthCheck {
info { info {
machineId time machineId time
versions { unraid }
os { uptime } os { uptime }
} }
array { state } array { state }

View File

@@ -63,11 +63,6 @@ QUERIES: dict[str, str] = {
} }
} }
""", """,
"connect": """
query GetConnectSettings {
connect { status sandbox flashGuid }
}
""",
"variables": """ "variables": """
query GetSelectiveUnraidVariables { query GetSelectiveUnraidVariables {
vars { vars {
@@ -85,12 +80,12 @@ QUERIES: dict[str, str] = {
""", """,
"metrics": """ "metrics": """
query GetMetrics { query GetMetrics {
metrics { cpu { used } memory { used total } } metrics { cpu { percentTotal cpus { percentTotal } } memory { used total } }
} }
""", """,
"services": """ "services": """
query GetServices { query GetServices {
services { name state } services { name online uptime }
} }
""", """,
"display": """ "display": """
@@ -159,7 +154,6 @@ INFO_ACTIONS = Literal[
"array", "array",
"network", "network",
"registration", "registration",
"connect",
"variables", "variables",
"metrics", "metrics",
"services", "services",
@@ -327,7 +321,6 @@ def register_info_tool(mcp: FastMCP) -> None:
array - Array state, capacity, disk health array - Array state, capacity, disk health
network - Access URLs, interfaces network - Access URLs, interfaces
registration - License type, state, expiration registration - License type, state, expiration
connect - Unraid Connect settings
variables - System variables and configuration variables - System variables and configuration
metrics - CPU and memory utilization metrics - CPU and memory utilization
services - Running services services - Running services
@@ -359,7 +352,6 @@ def register_info_tool(mcp: FastMCP) -> None:
dict_actions: dict[str, str] = { dict_actions: dict[str, str] = {
"network": "network", "network": "network",
"registration": "registration", "registration": "registration",
"connect": "connect",
"variables": "vars", "variables": "vars",
"metrics": "metrics", "metrics": "metrics",
"config": "config", "config": "config",

View File

@@ -16,12 +16,12 @@ from ..core.exceptions import ToolError
QUERIES: dict[str, str] = { QUERIES: dict[str, str] = {
"list": """ "list": """
query ListApiKeys { query ListApiKeys {
apiKeys { id name roles permissions createdAt lastUsed } apiKeys { id name roles permissions { resource actions } createdAt lastUsed }
} }
""", """,
"get": """ "get": """
query GetApiKey($id: PrefixedID!) { query GetApiKey($id: PrefixedID!) {
apiKey(id: $id) { id name roles permissions createdAt lastUsed } apiKey(id: $id) { id name roles permissions { resource actions } createdAt lastUsed }
} }
""", """,
} }

View File

@@ -1,7 +1,6 @@
"""Storage and disk management. """Storage and disk management.
Provides the `unraid_storage` tool with 6 actions for shares, physical disks, Provides the `unraid_storage` tool with 6 actions for shares, physical disks, log files, and log content retrieval.
unassigned devices, log files, and log content retrieval.
""" """
from typing import Any, Literal from typing import Any, Literal
@@ -37,11 +36,6 @@ QUERIES: dict[str, str] = {
} }
} }
""", """,
"unassigned": """
query GetUnassignedDevices {
unassignedDevices { id device name size type }
}
""",
"log_files": """ "log_files": """
query ListLogFiles { query ListLogFiles {
logFiles { name path size modifiedAt } logFiles { name path size modifiedAt }
@@ -60,7 +54,6 @@ STORAGE_ACTIONS = Literal[
"shares", "shares",
"disks", "disks",
"disk_details", "disk_details",
"unassigned",
"log_files", "log_files",
"logs", "logs",
] ]
@@ -97,7 +90,6 @@ def register_storage_tool(mcp: FastMCP) -> None:
shares - List all user shares with capacity info shares - List all user shares with capacity info
disks - List all physical disks disks - List all physical disks
disk_details - Detailed SMART info for a disk (requires disk_id) disk_details - Detailed SMART info for a disk (requires disk_id)
unassigned - List unassigned devices
log_files - List available log files log_files - List available log files
logs - Retrieve log content (requires log_path, optional tail_lines) 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} 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": if action == "log_files":
files = data.get("logFiles", []) files = data.get("logFiles", [])
return {"log_files": list(files) if isinstance(files, list) else []} return {"log_files": list(files) if isinstance(files, list) else []}