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
- **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:

View File

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

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`.
### 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 |

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` - Network configuration and interfaces
- `registration` - Registration status and license info
- `connect` - Connect service configuration
- `online` - Online status check
**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
- `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
```

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

View File

@@ -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]!

View File

@@ -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
}
# ============================================================================

View File

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

View File

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

View File

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

View File

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

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 {
info {
machineId time
versions { unraid }
os { uptime }
}
array { state }

View File

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

View File

@@ -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 }
}
""",
}

View File

@@ -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 []}