forked from HomeLab/unraid-mcp
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84675815b4 | |||
| 37013fbd81 | |||
| 202354bbc1 | |||
| 0d876564cc | |||
| df2db88e0e | |||
| 88983c6736 |
21
CLAUDE.md
21
CLAUDE.md
@@ -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, 69 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
|
||||||
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:
|
||||||
|
|||||||
34
HISTORY.md
34
HISTORY.md
@@ -5,10 +5,44 @@ Changelog
|
|||||||
(unreleased)
|
(unreleased)
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Even more schema apatations. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
|
||||||
|
0.0.5 (2026-02-28)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Runtime error due to broken import. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
0.0.4 (2026-02-28)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Fix
|
||||||
|
~~~
|
||||||
|
- Even more changes to accommodate older GraphQL schema. [Simon
|
||||||
|
Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
|
0.0.3 (2026-02-28)
|
||||||
|
------------------
|
||||||
|
|
||||||
Fix
|
Fix
|
||||||
~~~
|
~~~
|
||||||
- Adapt for supported GraphQL schema on 6.12.13. [Simon Diesenreiter]
|
- Adapt for supported GraphQL schema on 6.12.13. [Simon Diesenreiter]
|
||||||
|
|
||||||
|
Other
|
||||||
|
~~~~~
|
||||||
|
|
||||||
|
|
||||||
0.0.2 (2026-02-21)
|
0.0.2 (2026-02-21)
|
||||||
------------------
|
------------------
|
||||||
|
|||||||
10
README.md
10
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`.
|
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, 69 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 |
|
||||||
| **`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 |
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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:**
|
||||||
@@ -32,7 +31,6 @@ Execute the `unraid_info` MCP tool with action: `$1`
|
|||||||
- `metrics` - System metrics (CPU, RAM, disk I/O)
|
- `metrics` - System metrics (CPU, RAM, disk I/O)
|
||||||
- `ups_devices` - List all UPS devices
|
- `ups_devices` - List all UPS devices
|
||||||
- `ups_device` - Get specific UPS device details (requires device_id)
|
- `ups_device` - Get specific UPS device details (requires device_id)
|
||||||
- `ups_config` - UPS configuration
|
|
||||||
|
|
||||||
**Ownership:**
|
**Ownership:**
|
||||||
- `owner` - Server owner information
|
- `owner` - Server owner information
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -564,7 +564,6 @@ The current MCP server has 10 tools (76 actions) after consolidation. The follow
|
|||||||
|--------------|---------------|---------------|
|
|--------------|---------------|---------------|
|
||||||
| `list_ups_devices()` | `upsDevices` query | UPS monitoring |
|
| `list_ups_devices()` | `upsDevices` query | UPS monitoring |
|
||||||
| `get_ups_device(id)` | `upsDeviceById` query | UPS details |
|
| `get_ups_device(id)` | `upsDeviceById` query | UPS details |
|
||||||
| `get_ups_configuration()` | `upsConfiguration` query | UPS config |
|
|
||||||
| `configure_ups(config)` | `configureUps` mutation | UPS management |
|
| `configure_ups(config)` | `configureUps` mutation | UPS management |
|
||||||
|
|
||||||
#### System Metrics (0 tools currently, 1 query + 3 subscriptions)
|
#### System Metrics (0 tools currently, 1 query + 3 subscriptions)
|
||||||
|
|||||||
@@ -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]!
|
||||||
|
|||||||
@@ -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!
|
||||||
@@ -1134,7 +1128,6 @@ type ApiKey implements Node {
|
|||||||
permissions: JSON
|
permissions: JSON
|
||||||
createdAt: String!
|
createdAt: String!
|
||||||
description: String
|
description: String
|
||||||
lastUsed: String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiKeyMutations {
|
type ApiKeyMutations {
|
||||||
@@ -1325,9 +1318,6 @@ type Query {
|
|||||||
|
|
||||||
# Network (used by MCP tool)
|
# Network (used by MCP tool)
|
||||||
network: Network
|
network: Network
|
||||||
|
|
||||||
# Connect (used by MCP tool)
|
|
||||||
connect: ConnectSettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -142,12 +142,6 @@ class TestInfoQueries:
|
|||||||
errors = _validate_operation(schema, QUERIES["ups_device"])
|
errors = _validate_operation(schema, QUERIES["ups_device"])
|
||||||
assert not errors, f"ups_device query validation failed: {errors}"
|
assert not errors, f"ups_device query validation failed: {errors}"
|
||||||
|
|
||||||
def test_ups_config_query(self, schema: GraphQLSchema) -> None:
|
|
||||||
from unraid_mcp.tools.info import QUERIES
|
|
||||||
|
|
||||||
errors = _validate_operation(schema, QUERIES["ups_config"])
|
|
||||||
assert not errors, f"ups_config query validation failed: {errors}"
|
|
||||||
|
|
||||||
def test_all_info_actions_covered(self, schema: GraphQLSchema) -> None:
|
def test_all_info_actions_covered(self, schema: GraphQLSchema) -> None:
|
||||||
"""Ensure every key in QUERIES has a corresponding test."""
|
"""Ensure every key in QUERIES has a corresponding test."""
|
||||||
from unraid_mcp.tools.info import QUERIES
|
from unraid_mcp.tools.info import QUERIES
|
||||||
@@ -156,7 +150,7 @@ class TestInfoQueries:
|
|||||||
"overview", "array", "network", "registration", "connect",
|
"overview", "array", "network", "registration", "connect",
|
||||||
"variables", "metrics", "services", "display", "config",
|
"variables", "metrics", "services", "display", "config",
|
||||||
"online", "owner", "settings", "server", "servers",
|
"online", "owner", "settings", "server", "servers",
|
||||||
"flash", "ups_devices", "ups_device", "ups_config",
|
"flash", "ups_devices", "ups_device",
|
||||||
}
|
}
|
||||||
assert set(QUERIES.keys()) == expected_actions
|
assert set(QUERIES.keys()) == expected_actions
|
||||||
|
|
||||||
@@ -237,12 +231,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 +246,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from .config.settings import (
|
|||||||
VERSION,
|
VERSION,
|
||||||
)
|
)
|
||||||
from .subscriptions.resources import register_subscription_resources
|
from .subscriptions.resources import register_subscription_resources
|
||||||
from .tools.array import register_array_tool
|
|
||||||
from .tools.docker import register_docker_tool
|
from .tools.docker import register_docker_tool
|
||||||
from .tools.health import register_health_tool
|
from .tools.health import register_health_tool
|
||||||
from .tools.info import register_info_tool
|
from .tools.info import register_info_tool
|
||||||
@@ -51,7 +50,6 @@ def register_all_modules() -> None:
|
|||||||
# Register all consolidated tools
|
# Register all consolidated tools
|
||||||
registrars = [
|
registrars = [
|
||||||
register_info_tool,
|
register_info_tool,
|
||||||
register_array_tool,
|
|
||||||
register_storage_tool,
|
register_storage_tool,
|
||||||
register_docker_tool,
|
register_docker_tool,
|
||||||
register_vm_tool,
|
register_vm_tool,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
|
||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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 { timestamp } }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"display": """
|
"display": """
|
||||||
@@ -108,7 +103,7 @@ QUERIES: dict[str, str] = {
|
|||||||
""",
|
""",
|
||||||
"owner": """
|
"owner": """
|
||||||
query GetOwner {
|
query GetOwner {
|
||||||
owner { username avatar url }
|
owner { username avatar }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"settings": """
|
"settings": """
|
||||||
@@ -120,7 +115,6 @@ QUERIES: dict[str, str] = {
|
|||||||
query GetServer {
|
query GetServer {
|
||||||
info {
|
info {
|
||||||
os { hostname uptime }
|
os { hostname uptime }
|
||||||
versions { unraid }
|
|
||||||
machineId time
|
machineId time
|
||||||
}
|
}
|
||||||
array { state }
|
array { state }
|
||||||
@@ -129,27 +123,22 @@ QUERIES: dict[str, str] = {
|
|||||||
""",
|
""",
|
||||||
"servers": """
|
"servers": """
|
||||||
query GetServers {
|
query GetServers {
|
||||||
servers { id name status description ip port }
|
servers { id name status lanip wanip }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"flash": """
|
"flash": """
|
||||||
query GetFlash {
|
query GetFlash {
|
||||||
flash { id guid product vendor size }
|
flash { id guid product vendor }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"ups_devices": """
|
"ups_devices": """
|
||||||
query GetUpsDevices {
|
query GetUpsDevices {
|
||||||
upsDevices { id model status runtime charge load }
|
upsDevices { id model status name battery { chargeLevel estimatedRuntime health } }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"ups_device": """
|
"ups_device": """
|
||||||
query GetUpsDevice($id: PrefixedID!) {
|
query GetUpsDevice($id: PrefixedID!) {
|
||||||
upsDeviceById(id: $id) { id model status runtime charge load voltage frequency temperature }
|
upsDeviceById(id: $id) { id model status name battery { chargeLevel estimatedRuntime health } power {loadPercentage inputVoltage outputVoltage } }
|
||||||
}
|
|
||||||
""",
|
|
||||||
"ups_config": """
|
|
||||||
query GetUpsConfig {
|
|
||||||
upsConfiguration { enabled mode cable driver port }
|
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
@@ -159,7 +148,6 @@ INFO_ACTIONS = Literal[
|
|||||||
"array",
|
"array",
|
||||||
"network",
|
"network",
|
||||||
"registration",
|
"registration",
|
||||||
"connect",
|
|
||||||
"variables",
|
"variables",
|
||||||
"metrics",
|
"metrics",
|
||||||
"services",
|
"services",
|
||||||
@@ -173,7 +161,6 @@ INFO_ACTIONS = Literal[
|
|||||||
"flash",
|
"flash",
|
||||||
"ups_devices",
|
"ups_devices",
|
||||||
"ups_device",
|
"ups_device",
|
||||||
"ups_config",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
assert set(QUERIES.keys()) == set(INFO_ACTIONS.__args__), (
|
assert set(QUERIES.keys()) == set(INFO_ACTIONS.__args__), (
|
||||||
@@ -327,7 +314,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
|
||||||
@@ -341,7 +327,6 @@ def register_info_tool(mcp: FastMCP) -> None:
|
|||||||
flash - Flash drive info
|
flash - Flash drive info
|
||||||
ups_devices - List UPS devices
|
ups_devices - List UPS devices
|
||||||
ups_device - Single UPS device (requires device_id)
|
ups_device - Single UPS device (requires device_id)
|
||||||
ups_config - UPS configuration
|
|
||||||
"""
|
"""
|
||||||
if action not in QUERIES:
|
if action not in QUERIES:
|
||||||
raise ToolError(f"Invalid action '{action}'. Must be one of: {list(QUERIES.keys())}")
|
raise ToolError(f"Invalid action '{action}'. Must be one of: {list(QUERIES.keys())}")
|
||||||
@@ -359,14 +344,12 @@ 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",
|
||||||
"owner": "owner",
|
"owner": "owner",
|
||||||
"flash": "flash",
|
"flash": "flash",
|
||||||
"ups_device": "upsDeviceById",
|
"ups_device": "upsDeviceById",
|
||||||
"ups_config": "upsConfiguration",
|
|
||||||
}
|
}
|
||||||
# List-wrapped actions: action -> (GraphQL response key, output key)
|
# List-wrapped actions: action -> (GraphQL response key, output key)
|
||||||
list_actions: dict[str, tuple[str, str]] = {
|
list_actions: dict[str, tuple[str, str]] = {
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
"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 }
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []}
|
||||||
|
|||||||
Reference in New Issue
Block a user