fix: address all 17 PR review comments

Resolves review threads:
- PRRT_kwDOO6Hdxs50fewG (setup.py): non-eliciting clients now return True
  from elicit_reset_confirmation so they can reconfigure without being blocked
- PRRT_kwDOO6Hdxs50fewM (test-tools.sh): add notification/recalculate smoke test
- PRRT_kwDOO6Hdxs50fewP (test-tools.sh): add system/array smoke test
- PRRT_kwDOO6Hdxs50fewT (resources.py): surface manager error state instead of
  reporting 'connecting' for permanently failed subscriptions
- PRRT_kwDOO6Hdxs50feAj (resources.py): use is not None check for empty cached dicts
- PRRT_kwDOO6Hdxs50fewY (integration tests): remove duplicate snapshot-registration
  tests already covered in test_resources.py
- PRRT_kwDOO6Hdxs50fewe (test_resources.py): replace brittle import-detail test
  with behavior tests for connecting/error states
- PRRT_kwDOO6Hdxs50fewh (test_customization.py): strengthen public_theme assertion
- PRRT_kwDOO6Hdxs50fewk (test_customization.py): strengthen theme assertion
- PRRT_kwDOO6Hdxs50fewo (__init__.py): correct subaction count ~88 -> ~107
- PRRT_kwDOO6Hdxs50fewx (test_oidc.py): assert providers list value directly
- PRRT_kwDOO6Hdxs50fewz (unraid.py): remove unreachable raise after vm handler
- PRRT_kwDOO6Hdxs50few2 (unraid.py): remove unreachable raise after docker handler
- PRRT_kwDOO6Hdxs50fev8 (CLAUDE.md): replace legacy 15-tool table with unified
  unraid action/subaction table
- PRRT_kwDOO6Hdxs50fev_ (test_oidc.py): assert providers + defaultAllowedOrigins
- PRRT_kwDOO6Hdxs50feAz (CLAUDE.md): update tool categories to unified API shape
- PRRT_kwDOO6Hdxs50feBE (CLAUDE.md/setup.py): update unraid_health refs to
  unraid(action=health, subaction=setup)
This commit is contained in:
Jacob Magar
2026-03-16 02:58:54 -04:00
parent dab1cd6995
commit efaab031ae
39 changed files with 844 additions and 1225 deletions

View File

@@ -31,32 +31,34 @@ This directory contains the Claude Code marketplace configuration for the Unraid
Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring.
**Features:**
- 11 tools with ~104 actions (queries and mutations)
- Real-time system metrics
- 1 consolidated `unraid` tool with ~108 actions across 15 domains
- Real-time live subscriptions (CPU, memory, logs, array state, UPS)
- Disk health and temperature monitoring
- Docker container management
- VM status and control
- Log file access
- Network share information
- Notification management
- Plugin, rclone, API key, and OIDC management
**Version:** 0.2.0
**Version:** 1.0.0
**Category:** Infrastructure
**Tags:** unraid, monitoring, homelab, graphql, docker, virtualization
## Configuration
After installation, configure your Unraid server credentials:
After installation, run setup to configure credentials interactively:
```bash
export UNRAID_API_URL="https://your-unraid-server/graphql"
export UNRAID_API_KEY="your-api-key"
```
unraid(action="health", subaction="setup")
```
Credentials are stored at `~/.unraid-mcp/.env` automatically.
**Getting an API Key:**
1. Open Unraid WebUI
2. Go to Settings → Management Access → API Keys
3. Click "Create" and select "Viewer" role
3. Click "Create" and select "Viewer" role (or appropriate roles for mutations)
4. Copy the generated API key
## Documentation

View File

@@ -5,8 +5,8 @@
"email": "jmagar@users.noreply.github.com"
},
"metadata": {
"description": "Comprehensive Unraid server management and monitoring tools via GraphQL API",
"version": "0.2.0",
"description": "Comprehensive Unraid server management and monitoring via a single consolidated MCP tool (~108 actions across 15 domains)",
"version": "1.0.0",
"homepage": "https://github.com/jmagar/unraid-mcp",
"repository": "https://github.com/jmagar/unraid-mcp"
},
@@ -14,8 +14,8 @@
{
"name": "unraid",
"source": "./",
"description": "Query and monitor Unraid servers via GraphQL API - array status, disk health, containers, VMs, system monitoring",
"version": "0.2.0",
"description": "Query and monitor Unraid servers via GraphQL API — single `unraid` tool with action+subaction routing for array, disk, docker, VM, notifications, live metrics, and more",
"version": "1.0.0",
"tags": ["unraid", "monitoring", "homelab", "graphql", "docker", "virtualization"],
"category": "infrastructure"
}

View File

@@ -88,22 +88,29 @@ docker compose down
`subscription_manager.get_resource_data(action)`. A "connecting" placeholder is returned
while the subscription starts — callers should retry in a moment.
### Tool Categories (15 Tools, ~108 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`** (13 actions): parity_start, parity_pause, parity_resume, parity_cancel, parity_status, parity_history, start_array, stop_array, add_disk, remove_disk, mount_disk, unmount_disk, clear_disk_stats
3. **`unraid_storage`** (6 actions): shares, disks, disk_details, log_files, logs, flash_backup
4. **`unraid_docker`** (7 actions): list, details, start, stop, restart, networks, network_details
5. **`unraid_vm`** (9 actions): list, details, start, stop, pause, resume, force_stop, reboot, reset
6. **`unraid_notifications`** (12 actions): overview, list, create, archive, unread, delete, delete_archived, archive_all, archive_many, unarchive_many, unarchive_all, recalculate
7. **`unraid_rclone`** (4 actions): list_remotes, config_form, create_remote, delete_remote
8. **`unraid_users`** (1 action): me
9. **`unraid_keys`** (7 actions): list, get, create, update, delete, add_role, remove_role
10. **`unraid_health`** (4 actions): check, test_connection, diagnose, setup
11. **`unraid_settings`** (2 actions): update, configure_ups
12. **`unraid_customization`** (5 actions): theme, public_theme, is_initial_setup, sso_enabled, set_theme
13. **`unraid_plugins`** (3 actions): list, add, remove
14. **`unraid_oidc`** (5 actions): providers, provider, configuration, public_providers, validate_session
15. **`unraid_live`** (11 actions): cpu, memory, cpu_telemetry, array_state, parity_progress, ups_status, notifications_overview, notification_feed, log_tail, owner, server_status
### Tool Categories (1 Tool, ~107 Subactions)
The server registers a **single consolidated `unraid` tool** with `action` (domain) + `subaction` (operation) routing. Call it as `unraid(action="docker", subaction="list")`.
| action | subactions |
|--------|-----------|
| **system** (19) | overview, array, network, registration, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config |
| **health** (4) | check, test_connection, diagnose, setup |
| **array** (13) | parity_status, parity_history, parity_start, parity_pause, parity_resume, parity_cancel, start_array, stop_array*, add_disk, remove_disk*, mount_disk, unmount_disk, clear_disk_stats* |
| **disk** (6) | shares, disks, disk_details, log_files, logs, flash_backup* |
| **docker** (7) | list, details, start, stop, restart, networks, network_details |
| **vm** (9) | list, details, start, stop, pause, resume, force_stop*, reboot, reset* |
| **notification** (12) | overview, list, create, archive, mark_unread, recalculate, archive_all, archive_many, unarchive_many, unarchive_all, delete*, delete_archived* |
| **key** (7) | list, get, create, update, delete*, add_role, remove_role |
| **plugin** (3) | list, add, remove* |
| **rclone** (4) | list_remotes, config_form, create_remote, delete_remote* |
| **setting** (2) | update, configure_ups* |
| **customization** (5) | theme, public_theme, is_initial_setup, sso_enabled, set_theme |
| **oidc** (5) | providers, provider, configuration, public_providers, validate_session |
| **user** (1) | me |
| **live** (11) | cpu, memory, cpu_telemetry, array_state, parity_progress, ups_status, notifications_overview, notification_feed, log_tail, owner, server_status |
`*` = destructive, requires `confirm=True`
### Destructive Actions (require `confirm=True`)
- **array**: stop_array, remove_disk, clear_disk_stats
@@ -194,7 +201,7 @@ When bumping the version, **always update both files** — they must stay in syn
### Credential Storage (`~/.unraid-mcp/.env`)
All runtimes (plugin, direct, Docker) load credentials from `~/.unraid-mcp/.env`.
- **Plugin/direct:** `unraid_health action=setup` writes this file automatically via elicitation,
- **Plugin/direct:** `unraid action=health subaction=setup` writes this file automatically via elicitation,
**Safe to re-run**: if credentials exist and are working, it asks before overwriting.
If credentials exist but connection fails, it silently re-configures without prompting.
or manual: `mkdir -p ~/.unraid-mcp && cp .env.example ~/.unraid-mcp/.env` then edit.

134
README.md
View File

@@ -8,10 +8,10 @@
## ✨ Features
- 🔧 **11 Tools, ~104 Actions**: Complete Unraid management through MCP protocol
- 🔧 **1 Tool, ~108 Actions**: Complete Unraid management through a single consolidated MCP tool
- 🏗️ **Modular Architecture**: Clean, maintainable, and extensible codebase
-**High Performance**: Async/concurrent operations with optimized timeouts
- 🔄 **Real-time Data**: WebSocket subscriptions for live log streaming
- 🔄 **Real-time Data**: WebSocket subscriptions for live metrics, logs, array state, and more
- 📊 **Health Monitoring**: Comprehensive system diagnostics and status
- 🐳 **Docker Ready**: Full containerization support with Docker Compose
- 🔒 **Secure**: Proper SSL/TLS configuration and API key management
@@ -46,8 +46,8 @@
```
This provides instant access to Unraid monitoring and management through Claude Code with:
- **11 MCP tools** exposing **~104 actions** via the consolidated action pattern
- **10 slash commands** for quick CLI-style access (`commands/`)
- **1 MCP tool** (`unraid`) exposing **~108 actions** via `action` + `subaction` routing
- **11 slash commands** for quick CLI-style access (`commands/`)
- Real-time system metrics and health monitoring
- Docker container and VM lifecycle management
- Disk health monitoring and storage management
@@ -61,7 +61,7 @@ Claude Code plugin, direct `uv run` invocations, and Docker.
**Option 1 — Interactive (Claude Code plugin, elicitation-supported clients):**
```
unraid_health action=setup
unraid(action="health", subaction="setup")
```
The server prompts for your API URL and key, writes `~/.unraid-mcp/.env` automatically
(created with mode 700/600), and activates credentials without restart.
@@ -137,8 +137,8 @@ unraid-mcp/ # ${CLAUDE_PLUGIN_ROOT}
└── scripts/ # Validation and helper scripts
```
- **MCP Server**: 11 tools with ~104 actions via GraphQL API
- **Slash Commands**: 10 commands in `commands/` for quick CLI-style access
- **MCP Server**: 1 `unraid` tool with ~108 actions via GraphQL API
- **Slash Commands**: 11 commands in `commands/` for quick CLI-style access
- **Skill**: `/unraid` skill for monitoring and queries
- **Entry Point**: `unraid-mcp-server` defined in pyproject.toml
@@ -242,49 +242,79 @@ UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle
## 🛠️ Available Tools & Resources
Each tool uses a consolidated `action` parameter to expose multiple operations, reducing context window usage. Destructive actions require `confirm=True`.
The single `unraid` tool uses `action` (domain) + `subaction` (operation) routing to expose all operations via one MCP tool, minimizing context window usage. Destructive actions require `confirm=True`.
### Tool Categories (11 Tools, ~104 Actions)
### Single Tool, 15 Domains, ~108 Actions
| Tool | Actions | Description |
|------|---------|-------------|
| **`unraid_info`** | 21 | overview, array, network, registration, connect, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config, update_server, update_ssh |
| **`unraid_array`** | 5 | parity_start, parity_pause, parity_resume, parity_cancel, parity_status |
| **`unraid_storage`** | 7 | shares, disks, disk_details, unassigned, log_files, logs, flash_backup |
| **`unraid_docker`** | 26 | list, details, start, stop, restart, pause, unpause, remove, update, update_all, logs, networks, network_details, port_conflicts, check_updates, create_folder, set_folder_children, delete_entries, move_to_folder, move_to_position, rename_folder, create_folder_with_items, update_view_prefs, sync_templates, reset_template_mappings, refresh_digests |
| **`unraid_vm`** | 9 | list, details, start, stop, pause, resume, force_stop, reboot, reset |
| **`unraid_notifications`** | 14 | overview, list, warnings, create, create_unique, archive, archive_many, unread, unarchive_many, unarchive_all, recalculate, delete, delete_archived, archive_all |
| **`unraid_rclone`** | 4 | list_remotes, config_form, create_remote, delete_remote |
| **`unraid_users`** | 1 | me |
| **`unraid_keys`** | 5 | list, get, create, update, delete |
| **`unraid_health`** | 3 | check, test_connection, diagnose |
| **`unraid_settings`** | 9 | update, update_temperature, update_time, configure_ups, update_api, connect_sign_in, connect_sign_out, setup_remote_access, enable_dynamic_remote_access |
Call pattern: `unraid(action="<domain>", subaction="<operation>")`
### MCP Resources (Real-time Data)
- `unraid://logs/stream` - Live log streaming from `/var/log/syslog` with WebSocket subscriptions
| action= | Subactions | Description |
|---------|-----------|-------------|
| **`system`** | overview, array, network, registration, connect, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config | Server info, metrics, network, UPS (19 subactions) |
| **`health`** | check, test_connection, diagnose, setup | Health checks, connection test, diagnostics, interactive setup (4 subactions) |
| **`array`** | parity_status, parity_history, parity_start, parity_pause, parity_resume, parity_cancel, start_array, stop_array, add_disk, remove_disk, mount_disk, unmount_disk, clear_disk_stats | Parity checks, array state, disk operations (13 subactions) |
| **`disk`** | shares, disks, disk_details, log_files, logs, flash_backup | Shares, physical disks, log files (6 subactions) |
| **`docker`** | list, details, start, stop, restart, networks, network_details | Container lifecycle and network inspection (7 subactions) |
| **`vm`** | list, details, start, stop, pause, resume, force_stop, reboot, reset | Virtual machine lifecycle (9 subactions) |
| **`notification`** | overview, list, create, archive, unread, delete, delete_archived, archive_all, archive_many, unarchive_many, unarchive_all, recalculate | System notifications CRUD (12 subactions) |
| **`key`** | list, get, create, update, delete, add_role, remove_role | API key management (7 subactions) |
| **`plugin`** | list, add, remove | Plugin management (3 subactions) |
| **`rclone`** | list_remotes, config_form, create_remote, delete_remote | Cloud storage remote management (4 subactions) |
| **`setting`** | update, configure_ups | System settings and UPS config (2 subactions) |
| **`customization`** | theme, public_theme, is_initial_setup, sso_enabled, set_theme | Theme and UI customization (5 subactions) |
| **`oidc`** | providers, provider, configuration, public_providers, validate_session | OIDC/SSO provider management (5 subactions) |
| **`user`** | me | Current authenticated user (1 subaction) |
| **`live`** | cpu, memory, cpu_telemetry, array_state, parity_progress, ups_status, notifications_overview, owner, server_status, log_tail, notification_feed | Real-time WebSocket subscription snapshots (11 subactions) |
> **Note**: MCP Resources provide real-time data streams that can be accessed via MCP clients. The log stream resource automatically connects to your Unraid system logs and provides live updates.
### Destructive Actions (require `confirm=True`)
- **array**: `stop_array`, `remove_disk`, `clear_disk_stats`
- **vm**: `force_stop`, `reset`
- **notification**: `delete`, `delete_archived`
- **rclone**: `delete_remote`
- **key**: `delete`
- **disk**: `flash_backup`
- **setting**: `configure_ups`
- **plugin**: `remove`
### MCP Resources (Real-time Cached Data)
The `unraid://live/*` resources expose cached subscription data from persistent WebSocket connections:
- `unraid://live/cpu` — CPU utilization
- `unraid://live/memory` — Memory usage
- `unraid://live/cpu_telemetry` — Detailed CPU telemetry
- `unraid://live/array_state` — Array state changes
- `unraid://live/parity_progress` — Parity check progress
- `unraid://live/ups_status` — UPS status
- `unraid://live/notifications_overview` — Notification counts
- `unraid://live/owner` — Owner info changes
- `unraid://live/server_status` — Server status changes
- `unraid://live/log_tail` — Live syslog tail
- `unraid://live/notification_feed` — Real-time notification events
> **Note**: Resources return cached data from persistent WebSocket subscriptions. A `{"status": "connecting"}` placeholder is returned while the subscription initializes — retry in a moment.
---
## 💬 Custom Slash Commands
The project includes **10 custom slash commands** in `commands/` for quick access to Unraid operations:
The project includes **11 custom slash commands** in `commands/` for quick access to Unraid operations. Each command maps to a domain of the `unraid` tool.
### Available Commands
| Command | Actions | Quick Access |
|---------|---------|--------------|
| `/info` | 21 | System information, metrics, configuration |
| `/array` | 5 | Parity check management |
| `/storage` | 7 | Shares, disks, logs |
| `/docker` | 26 | Container management and monitoring |
| `/vm` | 9 | Virtual machine lifecycle |
| `/notifications` | 14 | Alert management |
| `/rclone` | 4 | Cloud storage remotes |
| `/users` | 1 | Current user query |
| `/keys` | 5 | API key management |
| `/health` | 3 | System health checks |
| Command | Domain (`action=`) | Quick Access |
|---------|-------------------|--------------|
| `/info` | `system` | System information, metrics, UPS, network |
| `/array` | `array` | Parity checks, array state, disk operations |
| `/storage` | `disk` | Shares, disks, log files |
| `/docker` | `docker` | Container lifecycle and network inspection |
| `/vm` | `vm` | Virtual machine lifecycle |
| `/notifications` | `notification` | Alert management |
| `/rclone` | `rclone` | Cloud storage remotes |
| `/users` | `user` | Current user query |
| `/keys` | `key` | API key management |
| `/health` | `health` | System health checks and setup |
| `/settings` | `setting` | System settings configuration |
### Example Usage
@@ -297,18 +327,17 @@ The project includes **10 custom slash commands** in `commands/` for quick acces
# Container management
/docker list
/docker start plex
/docker logs nginx
# VM operations
/vm list
/vm start windows-10
# Notifications
/notifications warnings
/notifications list
/notifications archive_all
# User management
/users list
# API key management
/keys list
/keys create "Automation Key" "For CI/CD"
```
@@ -339,24 +368,17 @@ unraid-mcp/
│ ├── core/ # Core infrastructure
│ │ ├── client.py # GraphQL client
│ │ ├── exceptions.py # Custom exceptions
│ │ ├── guards.py # Destructive action guards
│ │ └── types.py # Shared data types
│ ├── subscriptions/ # Real-time subscriptions
│ │ ├── manager.py # WebSocket management
│ │ ├── resources.py # MCP resources
│ │ ├── manager.py # Persistent WebSocket manager
│ │ ├── resources.py # MCP resources (unraid://live/*)
│ │ ├── snapshot.py # Transient subscribe_once helpers
│ │ └── diagnostics.py # Diagnostic tools
│ ├── tools/ # MCP tool categories (11 tools, ~104 actions)
│ │ ── info.py # System information (21 actions)
│ │ ├── array.py # Parity checks (5 actions)
│ │ ├── storage.py # Storage & monitoring (7 actions)
│ │ ├── docker.py # Container management (26 actions)
│ │ ├── virtualization.py # VM management (9 actions)
│ │ ├── notifications.py # Notification management (14 actions)
│ │ ├── rclone.py # Cloud storage (4 actions)
│ │ ├── users.py # Current user query (1 action)
│ │ ├── keys.py # API key management (5 actions)
│ │ ├── settings.py # Server settings (9 actions)
│ │ └── health.py # Health checks (3 actions)
│ ├── tools/ # Single consolidated tool (~108 actions)
│ │ ── unraid.py # All 15 domains in one file
│ └── server.py # FastMCP server setup
├── commands/ # 11 custom slash commands
├── logs/ # Log files (auto-created)
└── docker-compose.yml # Docker Compose deployment
```

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

@@ -1,48 +0,0 @@
---
description: Manage Docker containers on Unraid
argument-hint: [action] [additional-args]
---
Execute the `unraid_docker` MCP tool with action: `$1`
## Available Actions (15)
**Query Operations:**
- `list` - List all Docker containers with status
- `details` - Get detailed info for a container (requires container identifier)
- `logs` - Get container logs (requires container identifier)
- `check_updates` - Check for available container updates
- `port_conflicts` - Identify port conflicts
- `networks` - List Docker networks
- `network_details` - Get network details (requires network identifier)
**Container Lifecycle:**
- `start` - Start a stopped container (requires container identifier)
- `stop` - Stop a running container (requires container identifier)
- `restart` - Restart a container (requires container identifier)
- `pause` - Pause a running container (requires container identifier)
- `unpause` - Unpause a paused container (requires container identifier)
**Updates & Management:**
- `update` - Update a specific container (requires container identifier)
- `update_all` - Update all containers with available updates
**⚠️ Destructive:**
- `remove` - Permanently delete a container (requires container identifier + confirmation)
## Example Usage
```
/unraid-docker list
/unraid-docker details plex
/unraid-docker logs plex
/unraid-docker start nginx
/unraid-docker restart sonarr
/unraid-docker check_updates
/unraid-docker update plex
/unraid-docker port_conflicts
```
**Container Identification:** Use container name, ID, or partial match (fuzzy search supported)
Use the tool to execute the requested Docker operation and report the results.

View File

@@ -1,59 +0,0 @@
---
description: Check Unraid system health and connectivity
argument-hint: [action]
---
Execute the `unraid_health` MCP tool with action: `$1`
## Available Actions (3)
**Health Monitoring:**
- `check` - Comprehensive health check of all system components
- `test_connection` - Test basic API connectivity
- `diagnose` - Detailed diagnostic information for troubleshooting
## What Each Action Checks
### `check` - System Health
- API connectivity and response time
- Array status and disk health
- Running services status
- Docker container health
- VM status
- System resources (CPU, RAM, disk I/O)
- Network connectivity
- UPS status (if configured)
Returns: Overall health status (`HEALTHY`, `WARNING`, `CRITICAL`) with component details
### `test_connection` - Connectivity
- GraphQL endpoint availability
- Authentication validity
- Basic query execution
- Network latency
Returns: Connection status and latency metrics
### `diagnose` - Diagnostic Details
- Full system configuration
- Resource utilization trends
- Error logs and warnings
- Component-level diagnostics
- Troubleshooting recommendations
Returns: Detailed diagnostic report
## Example Usage
```
/unraid-health check
/unraid-health test_connection
/unraid-health diagnose
```
**Use Cases:**
- `check` - Quick health status (monitoring dashboards)
- `test_connection` - Verify API access (troubleshooting)
- `diagnose` - Deep dive debugging (issue resolution)
Use the tool to execute the requested health check and present results with clear severity indicators.

View File

@@ -1,50 +0,0 @@
---
description: Query Unraid server information and configuration
argument-hint: [action] [additional-args]
---
Execute the `unraid_info` MCP tool with action: `$1`
## Available Actions (19)
**System Overview:**
- `overview` - Complete system summary with all key metrics
- `server` - Server details (hostname, version, uptime)
- `servers` - List all known Unraid servers
**Array & Storage:**
- `array` - Array status, disks, and health
**Network & Registration:**
- `network` - Network configuration and interfaces
- `registration` - Registration status and license info
- `connect` - Connect service configuration
- `online` - Online status check
**Configuration:**
- `config` - System configuration settings
- `settings` - User settings and preferences
- `variables` - Environment variables
- `display` - Display settings
**Services & Monitoring:**
- `services` - Running services status
- `metrics` - System metrics (CPU, RAM, disk I/O)
- `ups_devices` - List all UPS devices
- `ups_device` - Get specific UPS device details (requires device_id)
- `ups_config` - UPS configuration
**Ownership:**
- `owner` - Server owner information
- `flash` - USB flash drive details
## Example Usage
```
/unraid-info overview
/unraid-info array
/unraid-info metrics
/unraid-info ups_device [device-id]
```
Use the tool to retrieve the requested information and present it in a clear, formatted manner.

View File

@@ -1,37 +0,0 @@
---
description: Manage Unraid API keys for authentication
argument-hint: [action] [key-id]
---
Execute the `unraid_keys` MCP tool with action: `$1`
## Available Actions (5)
**Query Operations:**
- `list` - List all API keys with metadata
- `get` - Get details for a specific API key (requires key_id)
**Management Operations:**
- `create` - Create a new API key (requires name, optional description and expiry)
- `update` - Update an existing API key (requires key_id, name, description)
**⚠️ Destructive:**
- `delete` - Permanently revoke an API key (requires key_id + confirmation)
## Example Usage
```
/unraid-keys list
/unraid-keys get [key-id]
/unraid-keys create "MCP Server Key" "Key for unraid-mcp integration"
/unraid-keys update [key-id] "Updated Name" "Updated description"
```
**Key Format:** PrefixedID (`hex64:suffix`)
**IMPORTANT:**
- Deleted keys are immediately revoked and cannot be recovered
- Store new keys securely - they're only shown once during creation
- Set expiry dates for keys used in automation
Use the tool to execute the requested API key operation and report the results.

View File

@@ -1,41 +0,0 @@
---
description: Manage Unraid system notifications and alerts
argument-hint: [action] [additional-args]
---
Execute the `unraid_notifications` MCP tool with action: `$1`
## Available Actions (9)
**Query Operations:**
- `overview` - Summary of notification counts by category
- `list` - List all notifications with details
- `warnings` - List only warning/error notifications
- `unread` - List unread notifications only
**Management Operations:**
- `create` - Create a new notification (requires title, message, severity)
- `archive` - Archive a specific notification (requires notification_id)
- `archive_all` - Archive all current notifications
**⚠️ Destructive Operations:**
- `delete` - Permanently delete a notification (requires notification_id + confirmation)
- `delete_archived` - Permanently delete all archived notifications (requires confirmation)
## Example Usage
```
/unraid-notifications overview
/unraid-notifications list
/unraid-notifications warnings
/unraid-notifications unread
/unraid-notifications create "Test Alert" "This is a test" normal
/unraid-notifications archive [notification-id]
/unraid-notifications archive_all
```
**Severity Levels:** `normal`, `warning`, `alert`, `critical`
**IMPORTANT:** Delete operations are permanent and cannot be undone.
Use the tool to execute the requested notification operation and present results clearly.

View File

@@ -1,32 +0,0 @@
---
description: Manage Rclone cloud storage remotes on Unraid
argument-hint: [action] [remote-name]
---
Execute the `unraid_rclone` MCP tool with action: `$1`
## Available Actions (4)
**Query Operations:**
- `list_remotes` - List all configured Rclone remotes
- `config_form` - Get configuration form for a remote type (requires remote_type)
**Management Operations:**
- `create_remote` - Create a new Rclone remote (requires remote_name, remote_type, config)
**⚠️ Destructive:**
- `delete_remote` - Permanently delete a remote (requires remote_name + confirmation)
## Example Usage
```
/unraid-rclone list_remotes
/unraid-rclone config_form s3
/unraid-rclone create_remote mybackup s3 {"access_key":"...","secret_key":"..."}
```
**Supported Remote Types:** s3, dropbox, google-drive, onedrive, backblaze, ftp, sftp, webdav, etc.
**IMPORTANT:** Deleting a remote does NOT delete cloud data, only the local configuration.
Use the tool to execute the requested Rclone operation and report the results.

View File

@@ -1,49 +0,0 @@
---
description: Manage Unraid system settings and configuration
argument-hint: [action] [additional-args]
---
Execute the `unraid_settings` MCP tool with action: `$1`
## Available Actions (9)
All settings actions are mutations that modify server configuration.
**General Settings:**
- `update` - Update general system settings (timezone, locale, etc.)
- `update_temperature` - Update temperature unit preference (Celsius/Fahrenheit)
- `update_time` - Update NTP and time configuration
**UPS Configuration:**
- `configure_ups` - Configure UPS settings (requires `confirm=True` — DESTRUCTIVE)
**API & Connectivity:**
- `update_api` - Update Unraid Connect API settings
**Unraid Connect (My Servers):**
- `connect_sign_in` - Sign in to Unraid Connect cloud service
- `connect_sign_out` - Sign out of Unraid Connect cloud service
**Remote Access:**
- `setup_remote_access` - Configure remote access settings (requires `confirm=True` — DESTRUCTIVE)
- `enable_dynamic_remote_access` - Enable/configure dynamic remote access (requires `confirm=True` — DESTRUCTIVE)
## Example Usage
```
/unraid-settings update
/unraid-settings update_temperature
/unraid-settings update_time
/unraid-settings update_api
/unraid-settings connect_sign_in
/unraid-settings connect_sign_out
```
**⚠️ Destructive Operations (require `confirm=True`):**
- `configure_ups` - Modifies UPS hardware configuration
- `setup_remote_access` - Changes network access policies
- `enable_dynamic_remote_access` - Changes network access policies
**IMPORTANT:** Settings changes take effect immediately and may affect server accessibility.
Use the tool to execute the requested settings operation and report the results.

View File

@@ -1,33 +0,0 @@
---
description: Query Unraid storage, shares, and disk information
argument-hint: [action] [additional-args]
---
Execute the `unraid_storage` MCP tool with action: `$1`
## Available Actions (6)
**Shares & Disks:**
- `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
- `logs` - Read log file contents (requires log file path)
## Example Usage
```
/unraid-storage shares
/unraid-storage disks
/unraid-storage disk_details disk1
/unraid-storage unassigned
/unraid-storage log_files
/unraid-storage logs /var/log/syslog
```
**Note:** Log file paths must start with `/var/log/`, `/boot/logs/`, or `/mnt/`
Use the tool to retrieve the requested storage information and present it clearly.

View File

@@ -1,31 +0,0 @@
---
description: Query current authenticated Unraid user
argument-hint: [action]
---
Execute the `unraid_users` MCP tool with action: `$1`
## Available Actions (1)
**Query Operation:**
- `me` - Get current authenticated user info (id, name, description, roles)
## Example Usage
```
/users me
```
## API Limitation
⚠️ **Note:** The Unraid GraphQL API does not support user management operations. Only the `me` query is available, which returns information about the currently authenticated user (the API key holder).
**Not supported:**
- Listing all users
- Getting other user details
- Adding/deleting users
- Cloud/remote access queries
For user management, use the Unraid web UI.
Use the tool to query the current authenticated user and report the results.

View File

@@ -1,41 +0,0 @@
---
description: Manage virtual machines on Unraid
argument-hint: [action] [vm-id]
---
Execute the `unraid_vm` MCP tool with action: `$1` and vm_id: `$2`
## Available Actions (9)
**Query Operations:**
- `list` - List all VMs with status and resource allocation
- `details` - Get detailed info for a VM (requires vm_id)
**Lifecycle Operations:**
- `start` - Start a stopped VM (requires vm_id)
- `stop` - Gracefully stop a running VM (requires vm_id)
- `pause` - Pause a running VM (requires vm_id)
- `resume` - Resume a paused VM (requires vm_id)
- `reboot` - Gracefully reboot a VM (requires vm_id)
**⚠️ Destructive Operations:**
- `force_stop` - Forcefully power off VM (like pulling power cord - requires vm_id + confirmation)
- `reset` - Hard reset VM (power cycle without graceful shutdown - requires vm_id + confirmation)
## Example Usage
```
/unraid-vm list
/unraid-vm details windows-10
/unraid-vm start ubuntu-server
/unraid-vm stop windows-10
/unraid-vm pause debian-vm
/unraid-vm resume debian-vm
/unraid-vm reboot ubuntu-server
```
**VM Identification:** Use VM ID (PrefixedID format: `hex64:suffix`)
**IMPORTANT:** `force_stop` and `reset` bypass graceful shutdown and may corrupt VM filesystem. Use `stop` instead for safe shutdowns.
Use the tool to execute the requested VM operation and report the results.

View File

@@ -1,78 +1,52 @@
# Destructive Actions
**Last Updated:** 2026-03-13
**Total destructive actions:** 15 across 7 tools
**Last Updated:** 2026-03-16
**Total destructive actions:** 12 across 8 domains (single `unraid` tool)
All destructive actions require `confirm=True` at the call site. There is no additional environment variable gate — `confirm` is the sole guard.
> **mcporter commands below** use `$MCP_URL` (default: `http://localhost:6970/mcp`). Run `test-actions.sh` for automated non-destructive coverage; destructive actions are always skipped there and tested manually per the strategies below.
>
> **Calling convention (v1.0.0+):** All operations use the single `unraid` tool with `action` (domain) + `subaction` (operation). For example:
> `mcporter call --http-url "$MCP_URL" --tool unraid --args '{"action":"docker","subaction":"list"}'`
---
## `unraid_docker`
## `array`
### `remove` — Delete a container permanently
### `stop_array` — Stop the Unraid array
**Strategy: mock/safety audit only.**
Stopping the array unmounts all shares and can interrupt running containers and VMs accessing array data. Test via `tests/safety/` confirming the `confirm=False` guard raises `ToolError`. Do not run live unless all containers and VMs are shut down first.
---
### `remove_disk` — Remove a disk from the array
```bash
# 1. Provision a throwaway canary container
docker run -d --name mcp-test-canary alpine sleep 3600
# Prerequisite: array must already be stopped; use a disk you intend to remove
# 2. Discover its MCP-assigned ID
CID=$(mcporter call --http-url "$MCP_URL" --tool unraid_docker \
--args '{"action":"list"}' --output json \
| python3 -c "import json,sys; cs=json.load(sys.stdin).get('containers',[]); print(next(c['id'] for c in cs if 'mcp-test-canary' in c.get('name','')))")
# 3. Remove via MCP
mcporter call --http-url "$MCP_URL" --tool unraid_docker \
--args "{\"action\":\"remove\",\"container_id\":\"$CID\",\"confirm\":true}" --output json
# 4. Verify
docker ps -a | grep mcp-test-canary # should return nothing
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"array","subaction":"remove_disk","disk_id":"<DISK_ID>","confirm":true}' --output json
```
---
### `update_all` — Pull latest images and restart all containers
**Strategy: mock/safety audit only.**
No safe live isolation — this hits every running container. Test via `tests/safety/` confirming the `confirm=False` guard raises `ToolError`. Do not run live unless all containers can tolerate a simultaneous restart.
---
### `delete_entries` — Delete Docker organizer folders/entries
### `clear_disk_stats` — Clear I/O statistics for a disk (irreversible)
```bash
# 1. Create a throwaway organizer folder
# Parameter: folder_name (str); ID is in organizer.views.flatEntries[type==FOLDER]
FOLDER=$(mcporter call --http-url "$MCP_URL" --tool unraid_docker \
--args '{"action":"create_folder","folder_name":"mcp-test-delete-me"}' --output json)
FID=$(echo "$FOLDER" | python3 -c "
import json,sys
data=json.load(sys.stdin)
entries=(data.get('organizer',{}).get('views',{}).get('flatEntries') or [])
match=next((e['id'] for e in entries if e.get('type')=='FOLDER' and 'mcp-test' in e.get('name','')),'' )
print(match)")
# Discover disk IDs
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"disk","subaction":"disks"}' --output json
# 2. Delete it
mcporter call --http-url "$MCP_URL" --tool unraid_docker \
--args "{\"action\":\"delete_entries\",\"entry_ids\":[\"$FID\"],\"confirm\":true}" --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid_docker \
--args '{"action":"list"}' --output json | python3 -c \
"import json,sys; folders=[x for x in json.load(sys.stdin).get('folders',[]) if 'mcp-test' in x.get('name','')]; print('clean' if not folders else folders)"
# Clear stats for a specific disk
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"array","subaction":"clear_disk_stats","disk_id":"<DISK_ID>","confirm":true}' --output json
```
---
### `reset_template_mappings` — Wipe all template-to-container associations
**Strategy: mock/safety audit only.**
Global state — wipes all template mappings, requires full remapping afterward. No safe isolation. Test via `tests/safety/` confirming the `confirm=False` guard raises `ToolError`.
---
## `unraid_vm`
## `vm`
### `force_stop` — Hard power-off a VM (potential data corruption)
@@ -80,16 +54,16 @@ Global state — wipes all template mappings, requires full remapping afterward.
# Prerequisite: create a minimal Alpine test VM in Unraid VM manager
# (Alpine ISO, 512MB RAM, no persistent disk, name contains "mcp-test")
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid_vm \
--args '{"action":"list"}' --output json \
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"vm","subaction":"list"}' --output json \
| python3 -c "import json,sys; vms=json.load(sys.stdin).get('vms',[]); print(next(v.get('uuid',v.get('id','')) for v in vms if 'mcp-test' in v.get('name','')))")
mcporter call --http-url "$MCP_URL" --tool unraid_vm \
--args "{\"action\":\"force_stop\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"force_stop\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
# Verify: VM state should return to stopped
mcporter call --http-url "$MCP_URL" --tool unraid_vm \
--args "{\"action\":\"details\",\"vm_id\":\"$VID\"}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"details\",\"vm_id\":\"$VID\"}" --output json
```
---
@@ -98,27 +72,27 @@ mcporter call --http-url "$MCP_URL" --tool unraid_vm \
```bash
# Same minimal Alpine test VM as above
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid_vm \
--args '{"action":"list"}' --output json \
VID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"vm","subaction":"list"}' --output json \
| python3 -c "import json,sys; vms=json.load(sys.stdin).get('vms',[]); print(next(v.get('uuid',v.get('id','')) for v in vms if 'mcp-test' in v.get('name','')))")
mcporter call --http-url "$MCP_URL" --tool unraid_vm \
--args "{\"action\":\"reset\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"vm\",\"subaction\":\"reset\",\"vm_id\":\"$VID\",\"confirm\":true}" --output json
```
---
## `unraid_notifications`
## `notification`
### `delete` — Permanently delete a notification
```bash
# 1. Create a test notification, then list to get the real stored ID (create response
# ID is ULID-based; stored filename uses a unix timestamp, so IDs differ)
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"create","title":"mcp-test-delete","subject":"safe to delete","description":"MCP destructive action test","importance":"INFO"}' --output json
NID=$(mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"list","notification_type":"UNREAD"}' --output json \
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"create","title":"mcp-test-delete","subject":"safe to delete","description":"MCP destructive action test","importance":"INFO"}' --output json
NID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"list","notification_type":"UNREAD"}' --output json \
| python3 -c "
import json,sys
notifs=json.load(sys.stdin).get('notifications',[])
@@ -126,12 +100,12 @@ matches=[n['id'] for n in reversed(notifs) if n.get('title')=='mcp-test-delete']
print(matches[0] if matches else '')")
# 2. Delete it (notification_type required)
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args "{\"action\":\"delete\",\"notification_id\":\"$NID\",\"notification_type\":\"UNREAD\",\"confirm\":true}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"notification\",\"subaction\":\"delete\",\"notification_id\":\"$NID\",\"notification_type\":\"UNREAD\",\"confirm\":true}" --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"list"}' --output json | python3 -c \
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"list"}' --output json | python3 -c \
"import json,sys; ns=[n for n in json.load(sys.stdin).get('notifications',[]) if 'mcp-test' in n.get('title','')]; print('clean' if not ns else ns)"
```
@@ -141,45 +115,45 @@ mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
```bash
# 1. Create and archive a test notification
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"create","title":"mcp-test-archive-wipe","subject":"archive me","description":"safe to delete","importance":"INFO"}' --output json
AID=$(mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"list","notification_type":"UNREAD"}' --output json \
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"create","title":"mcp-test-archive-wipe","subject":"archive me","description":"safe to delete","importance":"INFO"}' --output json
AID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"list","notification_type":"UNREAD"}' --output json \
| python3 -c "
import json,sys
notifs=json.load(sys.stdin).get('notifications',[])
matches=[n['id'] for n in reversed(notifs) if n.get('title')=='mcp-test-archive-wipe']
print(matches[0] if matches else '')")
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args "{\"action\":\"archive\",\"notification_id\":\"$AID\"}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"notification\",\"subaction\":\"archive\",\"notification_id\":\"$AID\"}" --output json
# 2. Wipe all archived
# NOTE: this deletes ALL archived notifications, not just the test one
mcporter call --http-url "$MCP_URL" --tool unraid_notifications \
--args '{"action":"delete_archived","confirm":true}' --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"notification","subaction":"delete_archived","confirm":true}' --output json
```
> Run on `shart` if archival history on `tootie` matters.
---
## `unraid_rclone`
## `rclone`
### `delete_remote` — Remove an rclone remote configuration
```bash
# 1. Create a throwaway local remote (points to /tmp — no real data)
# Parameters: name (str), provider_type (str), config_data (dict)
mcporter call --http-url "$MCP_URL" --tool unraid_rclone \
--args '{"action":"create_remote","name":"mcp-test-remote","provider_type":"local","config_data":{"root":"/tmp"}}' --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"rclone","subaction":"create_remote","name":"mcp-test-remote","provider_type":"local","config_data":{"root":"/tmp"}}' --output json
# 2. Delete it
mcporter call --http-url "$MCP_URL" --tool unraid_rclone \
--args '{"action":"delete_remote","name":"mcp-test-remote","confirm":true}' --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"rclone","subaction":"delete_remote","name":"mcp-test-remote","confirm":true}' --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid_rclone \
--args '{"action":"list_remotes"}' --output json | python3 -c \
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"rclone","subaction":"list_remotes"}' --output json | python3 -c \
"import json,sys; remotes=json.load(sys.stdin).get('remotes',[]); print('clean' if 'mcp-test-remote' not in remotes else 'FOUND — cleanup failed')"
```
@@ -187,29 +161,29 @@ mcporter call --http-url "$MCP_URL" --tool unraid_rclone \
---
## `unraid_keys`
## `key`
### `delete` — Delete an API key (immediately revokes access)
```bash
# 1. Create a test key (names cannot contain hyphens; ID is at key.id)
KID=$(mcporter call --http-url "$MCP_URL" --tool unraid_keys \
--args '{"action":"create","name":"mcp test key","roles":["VIEWER"]}' --output json \
KID=$(mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"key","subaction":"create","name":"mcp test key","roles":["VIEWER"]}' --output json \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('key',{}).get('id',''))")
# 2. Delete it
mcporter call --http-url "$MCP_URL" --tool unraid_keys \
--args "{\"action\":\"delete\",\"key_id\":\"$KID\",\"confirm\":true}" --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args "{\"action\":\"key\",\"subaction\":\"delete\",\"key_id\":\"$KID\",\"confirm\":true}" --output json
# 3. Verify
mcporter call --http-url "$MCP_URL" --tool unraid_keys \
--args '{"action":"list"}' --output json | python3 -c \
"import json,sys; ks=json.load(sys.stdin).get('keys',[]); print('clean' if not any('mcp-test-key' in k.get('name','') for k in ks) else 'FOUND — cleanup failed')"
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"key","subaction":"list"}' --output json | python3 -c \
"import json,sys; ks=json.load(sys.stdin).get('keys',[]); print('clean' if not any('mcp test key' in k.get('name','') for k in ks) else 'FOUND — cleanup failed')"
```
---
## `unraid_storage`
## `disk`
### `flash_backup` — Rclone backup of flash drive (overwrites destination)
@@ -217,70 +191,34 @@ mcporter call --http-url "$MCP_URL" --tool unraid_keys \
# Prerequisite: create a dedicated test remote pointing away from real backup destination
# (use rclone create_remote first, or configure mcp-test-remote manually)
mcporter call --http-url "$MCP_URL" --tool unraid_storage \
--args '{"action":"flash_backup","remote_name":"mcp-test-remote","source_path":"/boot","destination_path":"/flash-backup-test","confirm":true}' --output json
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"disk","subaction":"flash_backup","remote_name":"mcp-test-remote","source_path":"/boot","destination_path":"/flash-backup-test","confirm":true}' --output json
```
> Never point at the same destination as your real flash backup. Create a dedicated `mcp-test-remote` (see `rclone: delete_remote` above for provisioning pattern).
---
## `unraid_settings`
## `setting`
### `configure_ups` — Overwrite UPS monitoring configuration
**Strategy: mock/safety audit only.**
Wrong config can break UPS integration. If live testing is required: read current config via `unraid_info ups_config`, save values, re-apply identical values (no-op), verify response matches. Test via `tests/safety/` for guard behavior.
Wrong config can break UPS integration. If live testing is required: read current config via `unraid(action="system", subaction="ups_config")`, save values, re-apply identical values (no-op), verify response matches. Test via `tests/safety/` for guard behavior.
---
### `setup_remote_access` — Modify remote access configuration
## `plugin`
### `remove` — Uninstall a plugin (irreversible without re-install)
**Strategy: mock/safety audit only.**
Misconfiguration can break remote connectivity and lock you out. Do not run live. Test via `tests/safety/` confirming `confirm=False` raises `ToolError`.
---
### `enable_dynamic_remote_access` — Toggle dynamic remote access
Removing a plugin cannot be undone without a full re-install. Test via `tests/safety/` confirming the `confirm=False` guard raises `ToolError`. Do not run live unless the plugin is intentionally being uninstalled.
```bash
# Strategy: toggle to false (disabling is reversible) on shart only, then restore
# Step 1: Read current state
CURRENT=$(mcporter call --http-url "$SHART_MCP_URL" --tool unraid_info \
--args '{"action":"settings"}' --output json)
# Step 2: Disable (safe — can be re-enabled)
mcporter call --http-url "$SHART_MCP_URL" --tool unraid_settings \
--args '{"action":"enable_dynamic_remote_access","access_url_type":"SUBDOMAINS","dynamic_enabled":false,"confirm":true}' --output json
# Step 3: Restore to previous state
mcporter call --http-url "$SHART_MCP_URL" --tool unraid_settings \
--args '{"action":"enable_dynamic_remote_access","access_url_type":"SUBDOMAINS","dynamic_enabled":true,"confirm":true}' --output json
```
> Run on `shart` (10.1.0.3) only — never `tootie`.
---
## `unraid_info`
### `update_ssh` — Change SSH enabled state and port
```bash
# Strategy: read current config, re-apply same values (no-op change)
# 1. Read current SSH settings
CURRENT=$(mcporter call --http-url "$MCP_URL" --tool unraid_info \
--args '{"action":"settings"}' --output json)
SSH_ENABLED=$(echo "$CURRENT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ssh',{}).get('enabled', True))")
SSH_PORT=$(echo "$CURRENT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ssh',{}).get('port', 22))")
# 2. Re-apply same values (no-op)
mcporter call --http-url "$MCP_URL" --tool unraid_info \
--args "{\"action\":\"update_ssh\",\"ssh_enabled\":$SSH_ENABLED,\"ssh_port\":$SSH_PORT,\"confirm\":true}" --output json
# 3. Verify SSH connectivity still works
ssh root@"$UNRAID_HOST" -p "$SSH_PORT" exit
# If live testing is necessary (intentional removal only):
mcporter call --http-url "$MCP_URL" --tool unraid \
--args '{"action":"plugin","subaction":"remove","names":["<plugin-name>"],"confirm":true}' --output json
```
---
@@ -290,7 +228,9 @@ ssh root@"$UNRAID_HOST" -p "$SSH_PORT" exit
The `tests/safety/` directory contains pytest tests that verify:
- Every destructive action raises `ToolError` when called with `confirm=False`
- Every destructive action raises `ToolError` when called without the `confirm` parameter
- The `DESTRUCTIVE_ACTIONS` set in each tool file stays in sync with the actions listed above
- The `_*_DESTRUCTIVE` sets in `unraid_mcp/tools/unraid.py` stay in sync with the actions listed above
- No GraphQL request reaches the network layer when confirmation is missing (`TestNoGraphQLCallsWhenUnconfirmed`)
- Non-destructive actions never require `confirm` (`TestNonDestructiveActionsNeverRequireConfirm`)
These run as part of the standard test suite:
@@ -302,20 +242,17 @@ uv run pytest tests/safety/ -v
## Summary Table
| Tool | Action | Strategy | Target Server |
|------|--------|----------|---------------|
| `unraid_docker` | `remove` | Pre-existing stopped container on Unraid server (skipped in test-destructive.sh) | either |
| `unraid_docker` | `update_all` | Mock/safety audit only | — |
| `unraid_docker` | `delete_entries` | Create folder → destroy | either |
| `unraid_docker` | `reset_template_mappings` | Mock/safety audit only | — |
| `unraid_vm` | `force_stop` | Minimal Alpine test VM | either |
| `unraid_vm` | `reset` | Minimal Alpine test VM | either |
| `unraid_notifications` | `delete` | Create notification → destroy | either |
| `unraid_notifications` | `delete_archived` | Create → archive → wipe | shart preferred |
| `unraid_rclone` | `delete_remote` | Create local:/tmp remote → destroy | either |
| `unraid_keys` | `delete` | Create test key → destroy | either |
| `unraid_storage` | `flash_backup` | Dedicated test remote, isolated path | either |
| `unraid_settings` | `configure_ups` | Mock/safety audit only | — |
| `unraid_settings` | `setup_remote_access` | Mock/safety audit only | — |
| `unraid_settings` | `enable_dynamic_remote_access` | Toggle false → restore | shart only |
| `unraid_info` | `update_ssh` | Read → re-apply same values (no-op) | either |
| Domain (`action=`) | Subaction | Strategy | Target Server |
|--------------------|-----------|----------|---------------|
| `array` | `stop_array` | Mock/safety audit only | — |
| `array` | `remove_disk` | Array must be stopped; use intended disk | either |
| `array` | `clear_disk_stats` | Discover disk ID → clear | either |
| `vm` | `force_stop` | Minimal Alpine test VM | either |
| `vm` | `reset` | Minimal Alpine test VM | either |
| `notification` | `delete` | Create notification → destroy | either |
| `notification` | `delete_archived` | Create → archive → wipe | shart preferred |
| `rclone` | `delete_remote` | Create local:/tmp remote → destroy | either |
| `key` | `delete` | Create test key → destroy | either |
| `disk` | `flash_backup` | Dedicated test remote, isolated path | either |
| `setting` | `configure_ups` | Mock/safety audit only | — |
| `plugin` | `remove` | Mock/safety audit only | — |

View File

@@ -14,10 +14,10 @@ The marketplace catalog that lists all available plugins in this repository.
- Plugin catalog with the "unraid" skill
- Categories and tags for discoverability
### 2. Plugin Manifest (`skills/unraid/.claude-plugin/plugin.json`)
### 2. Plugin Manifest (`.claude-plugin/plugin.json`)
The individual plugin configuration for the Unraid skill.
**Location:** `skills/unraid/.claude-plugin/plugin.json`
**Location:** `.claude-plugin/plugin.json`
**Contents:**
- Plugin name, version, author
@@ -73,12 +73,11 @@ Users can also install from a specific commit or branch:
```text
unraid-mcp/
├── .claude-plugin/ # Marketplace manifest
│ ├── marketplace.json
── README.md
├── skills/unraid/ # Plugin directory
│ ├── .claude-plugin/ # Plugin manifest
│ │ └── plugin.json
├── .claude-plugin/ # Plugin manifest + marketplace manifest
│ ├── plugin.json # Plugin configuration (name, version, mcpServers)
── marketplace.json # Marketplace catalog
│ └── README.md # Marketplace installation guide
├── skills/unraid/ # Skill documentation and helpers
│ ├── SKILL.md # Skill documentation
│ ├── README.md # Plugin documentation
│ ├── examples/ # Example scripts
@@ -112,7 +111,7 @@ Before publishing to GitHub:
2. **Update Version Numbers**
- Bump version in `.claude-plugin/marketplace.json`
- Bump version in `skills/unraid/.claude-plugin/plugin.json`
- Bump version in `.claude-plugin/plugin.json`
- Update version in `README.md` if needed
3. **Test Locally**
@@ -123,15 +122,15 @@ Before publishing to GitHub:
4. **Commit and Push**
```bash
git add .claude-plugin/ skills/unraid/.claude-plugin/
git add .claude-plugin/
git commit -m "feat: add Claude Code marketplace configuration"
git push origin main
```
5. **Create Release Tag** (Optional)
```bash
git tag -a v0.2.0 -m "Release v0.2.0"
git push origin v0.2.0
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
```
## User Experience
@@ -159,7 +158,7 @@ After installation, users will:
To release a new version:
1. Make changes to the plugin
2. Update version in `skills/unraid/.claude-plugin/plugin.json`
2. Update version in `.claude-plugin/plugin.json`
3. Update marketplace catalog in `.claude-plugin/marketplace.json`
4. Run validation: `./scripts/validate-marketplace.sh`
5. Commit and push

View File

@@ -40,7 +40,7 @@ Before publishing, update the version in `pyproject.toml`:
```toml
[project]
version = "0.2.1" # Follow semantic versioning: MAJOR.MINOR.PATCH
version = "1.0.0" # Follow semantic versioning: MAJOR.MINOR.PATCH
```
**Semantic Versioning Guide:**
@@ -156,7 +156,7 @@ UNRAID_API_URL=https://your-server uvx unraid-mcp-server
**Benefits of uvx:**
- No installation required
- Automatic virtual environment management
- Always uses the latest version (or specify version: `uvx unraid-mcp-server@0.2.0`)
- Always uses the latest version (or specify version: `uvx unraid-mcp-server@1.0.0`)
- Clean execution environment
## Automation with GitHub Actions (Future)

View File

@@ -1,210 +1,292 @@
---
name: unraid
description: "Query and monitor Unraid servers via the GraphQL API. Use when the user asks to 'check Unraid', 'monitor Unraid', 'Unraid API', 'get Unraid status', 'check disk temperatures', 'read Unraid logs', 'list Unraid shares', 'Unraid array status', 'Unraid containers', 'Unraid VMs', or mentions Unraid system monitoring, disk health, parity checks, or server status."
description: "This skill should be used when the user mentions Unraid, asks to check server health, monitor array or disk status, list or restart Docker containers, start or stop VMs, read system logs, check parity status, view notifications, manage API keys, configure rclone remotes, check UPS or power status, get live CPU or memory data, force stop a VM, check disk temperatures, or perform any operation on an Unraid NAS server. Also use when the user needs to set up or configure Unraid MCP credentials."
---
# Unraid API Skill
# Unraid MCP Skill
**⚠️ MANDATORY SKILL INVOCATION ⚠️**
Use the single `unraid` MCP tool with `action` (domain) + `subaction` (operation) for all Unraid operations.
**YOU MUST invoke this skill (NOT optional) when the user mentions ANY of these triggers:**
- "Unraid status", "disk health", "array status"
- "Unraid containers", "VMs on Unraid", "Unraid logs"
- "check Unraid", "Unraid monitoring", "server health"
- Any mention of Unraid servers or system monitoring
## Setup
**Failure to invoke this skill when triggers occur violates your operational requirements.**
First time? Run setup to configure credentials:
Query and monitor Unraid servers using the GraphQL API. Access all 27 read-only endpoints for system monitoring, disk health, logs, containers, VMs, and more.
## Quick Start
Set your Unraid server credentials:
```bash
export UNRAID_URL="https://your-unraid-server/graphql"
export UNRAID_API_KEY="your-api-key"
```
unraid(action="health", subaction="setup")
```
**Get API Key:** Settings → Management Access → API Keys → Create (select "Viewer" role)
Credentials are stored at `~/.unraid-mcp/.env`. Re-run `setup` any time to update or verify.
Use the helper script for any query:
## Calling Convention
```bash
./scripts/unraid-query.sh -q "{ online }"
```
unraid(action="<domain>", subaction="<operation>", [additional params])
```
Or run example scripts:
```bash
./scripts/dashboard.sh # Complete multi-server dashboard
./examples/disk-health.sh # Disk temperatures & health
./examples/read-logs.sh syslog 20 # Read system logs
**Examples:**
```
## Core Concepts
### GraphQL API Structure
Unraid 7.2+ uses GraphQL (not REST). Key differences:
- **Single endpoint:** `/graphql` for all queries
- **Request exactly what you need:** Specify fields in query
- **Strongly typed:** Use introspection to discover fields
- **No container logs:** Docker container output logs not accessible
### Two Resources for Stats
- **`info`** - Static hardware specs (CPU model, cores, OS version)
- **`metrics`** - Real-time usage (CPU %, memory %, current load)
Always use `metrics` for monitoring, `info` for specifications.
## Common Tasks
### System Monitoring
**Check if server is online:**
```bash
./scripts/unraid-query.sh -q "{ online }"
```
**Get CPU and memory usage:**
```bash
./scripts/unraid-query.sh -q "{ metrics { cpu { percentTotal } memory { used total percentTotal } } }"
```
**Complete dashboard:**
```bash
./scripts/dashboard.sh
```
### Disk Management
**Check disk health and temperatures:**
```bash
./examples/disk-health.sh
```
**Get array status:**
```bash
./scripts/unraid-query.sh -q "{ array { state parityCheckStatus { status progress errors } } }"
```
**List all physical disks (including cache/USB):**
```bash
./scripts/unraid-query.sh -q "{ disks { name } }"
```
### Storage Shares
**List network shares:**
```bash
./scripts/unraid-query.sh -q "{ shares { name comment } }"
```
### Logs
**List available logs:**
```bash
./scripts/unraid-query.sh -q "{ logFiles { name size modifiedAt } }"
```
**Read log content:**
```bash
./examples/read-logs.sh syslog 20
```
### Containers & VMs
**List Docker containers:**
```bash
./scripts/unraid-query.sh -q "{ docker { containers { names image state status } } }"
```
**List VMs:**
```bash
./scripts/unraid-query.sh -q "{ vms { domain { name state } } }"
```
**Note:** Container output logs are NOT accessible via API. Use `docker logs` via SSH.
### Notifications
**Get notification counts:**
```bash
./scripts/unraid-query.sh -q "{ notifications { overview { unread { info warning alert total } } } }"
```
## Helper Script Usage
The `scripts/unraid-query.sh` helper supports:
```bash
# Basic usage
./scripts/unraid-query.sh -u URL -k API_KEY -q "QUERY"
# Use environment variables
export UNRAID_URL="https://unraid.local/graphql"
export UNRAID_API_KEY="your-key"
./scripts/unraid-query.sh -q "{ online }"
# Format options
-f json # Raw JSON (default)
-f pretty # Pretty-printed JSON
-f raw # Just the data (no wrapper)
```
## Additional Resources
### Reference Files
For detailed documentation, consult:
- **`references/endpoints.md`** - Complete list of all 27 API endpoints
- **`references/troubleshooting.md`** - Common errors and solutions
- **`references/api-reference.md`** - Detailed field documentation
### Helper Scripts
- **`scripts/unraid-query.sh`** - Main GraphQL query tool
- **`scripts/dashboard.sh`** - Automated multi-server inventory reporter
## Quick Command Reference
```bash
# System status
./scripts/unraid-query.sh -q "{ online metrics { cpu { percentTotal } } }"
# Disk health
./examples/disk-health.sh
# Array status
./scripts/unraid-query.sh -q "{ array { state } }"
# Read logs
./examples/read-logs.sh syslog 20
# Complete dashboard
./scripts/dashboard.sh
# List shares
./scripts/unraid-query.sh -q "{ shares { name } }"
# List containers
./scripts/unraid-query.sh -q "{ docker { containers { names state } } }"
unraid(action="system", subaction="overview")
unraid(action="docker", subaction="list")
unraid(action="health", subaction="check")
unraid(action="array", subaction="parity_status")
unraid(action="disk", subaction="disks")
unraid(action="vm", subaction="list")
unraid(action="notification", subaction="overview")
unraid(action="live", subaction="cpu")
```
---
## 🔧 Agent Tool Usage Requirements
## All Domains and Subactions
**CRITICAL:** When invoking scripts from this skill via the zsh-tool, **ALWAYS use `pty: true`**.
### `system` — Server Information
| Subaction | Description |
|-----------|-------------|
| `overview` | Complete system summary (recommended starting point) |
| `server` | Hostname, version, uptime |
| `servers` | All known Unraid servers |
| `array` | Array status and disk list |
| `network` | Network interfaces and config |
| `registration` | License and registration status |
| `variables` | Environment variables |
| `metrics` | Real-time CPU, memory, I/O usage |
| `services` | Running services status |
| `display` | Display settings |
| `config` | System configuration |
| `online` | Quick online status check |
| `owner` | Server owner information |
| `settings` | User settings and preferences |
| `flash` | USB flash drive details |
| `ups_devices` | List all UPS devices |
| `ups_device` | Single UPS device (requires `device_id`) |
| `ups_config` | UPS configuration |
Without PTY mode, command output will not be visible even though commands execute successfully.
### `health` — Diagnostics
| Subaction | Description |
|-----------|-------------|
| `check` | Comprehensive health check — connectivity, array, disks, containers, VMs, resources |
| `test_connection` | Test API connectivity and authentication |
| `diagnose` | Detailed diagnostic report with troubleshooting recommendations |
| `setup` | Configure credentials interactively (stores to `~/.unraid-mcp/.env`) |
**Correct invocation pattern:**
```typescript
<invoke name="mcp__plugin_zsh-tool_zsh-tool__zsh">
<parameter name="command">./skills/SKILL_NAME/scripts/SCRIPT.sh [args]</parameter>
<parameter name="pty">true</parameter>
</invoke>
### `array` — Array & Parity
| Subaction | Description |
|-----------|-------------|
| `parity_status` | Current parity check progress and status |
| `parity_history` | Historical parity check results |
| `parity_start` | Start a parity check |
| `parity_pause` | Pause a running parity check |
| `parity_resume` | Resume a paused parity check |
| `parity_cancel` | Cancel a running parity check |
| `start_array` | Start the array |
| `stop_array` | ⚠️ Stop the array (requires `confirm=True`) |
| `add_disk` | Add a disk to the array (requires `slot`, `id`) |
| `remove_disk` | ⚠️ Remove a disk (requires `slot`, `confirm=True`) |
| `mount_disk` | Mount a disk |
| `unmount_disk` | Unmount a disk |
| `clear_disk_stats` | ⚠️ Clear disk statistics (requires `confirm=True`) |
### `disk` — Storage & Logs
| Subaction | Description |
|-----------|-------------|
| `shares` | List network shares |
| `disks` | All physical disks with health and temperatures |
| `disk_details` | Detailed info for a specific disk (requires `disk_id`) |
| `log_files` | List available log files |
| `logs` | Read log content (requires `path`; optional `lines`) |
| `flash_backup` | ⚠️ Trigger a flash backup (requires `confirm=True`) |
### `docker` — Containers
| Subaction | Description |
|-----------|-------------|
| `list` | All containers with status, image, state |
| `details` | Single container details (requires container identifier) |
| `start` | Start a container (requires container identifier) |
| `stop` | Stop a container (requires container identifier) |
| `restart` | Restart a container (requires container identifier) |
| `networks` | List Docker networks |
| `network_details` | Details for a specific network (requires `network_id`) |
**Container Identification:** Name, ID, or partial name (fuzzy match supported).
### `vm` — Virtual Machines
| Subaction | Description |
|-----------|-------------|
| `list` | All VMs with state |
| `details` | Single VM details (requires `vm_id`) |
| `start` | Start a VM (requires `vm_id`) |
| `stop` | Gracefully stop a VM (requires `vm_id`) |
| `pause` | Pause a VM (requires `vm_id`) |
| `resume` | Resume a paused VM (requires `vm_id`) |
| `reboot` | Reboot a VM (requires `vm_id`) |
| `force_stop` | ⚠️ Force stop a VM (requires `vm_id`, `confirm=True`) |
| `reset` | ⚠️ Hard reset a VM (requires `vm_id`, `confirm=True`) |
### `notification` — Notifications
| Subaction | Description |
|-----------|-------------|
| `overview` | Notification counts (unread, archived by type) |
| `list` | List notifications (optional `filter`, `limit`, `offset`) |
| `mark_unread` | Mark a notification as unread (requires `notification_id`) |
| `create` | Create a notification (requires `title`, `subject`, `description`, `importance`) |
| `archive` | Archive a notification (requires `notification_id`) |
| `delete` | ⚠️ Delete a notification (requires `notification_id`, `notification_type`, `confirm=True`) |
| `delete_archived` | ⚠️ Delete all archived (requires `confirm=True`) |
| `archive_all` | Archive all unread notifications |
| `archive_many` | Archive multiple (requires `ids` list) |
| `unarchive_many` | Unarchive multiple (requires `ids` list) |
| `unarchive_all` | Unarchive all archived notifications |
| `recalculate` | Recalculate notification counts |
### `key` — API Keys
| Subaction | Description |
|-----------|-------------|
| `list` | All API keys |
| `get` | Single key details (requires `key_id`) |
| `create` | Create a new key (requires `name`, `roles`) |
| `update` | Update a key (requires `key_id`) |
| `delete` | ⚠️ Delete a key (requires `key_id`, `confirm=True`) |
| `add_role` | Add a role to a key (requires `key_id`, `role`) |
| `remove_role` | Remove a role from a key (requires `key_id`, `role`) |
### `plugin` — Plugins
| Subaction | Description |
|-----------|-------------|
| `list` | All installed plugins |
| `add` | Install plugins (requires `names` — list of plugin names) |
| `remove` | ⚠️ Uninstall plugins (requires `names` — list of plugin names, `confirm=True`) |
### `rclone` — Cloud Storage
| Subaction | Description |
|-----------|-------------|
| `list_remotes` | List configured rclone remotes |
| `config_form` | Get configuration form for a remote type |
| `create_remote` | Create a new remote (requires `name`, `type`, `fields`) |
| `delete_remote` | ⚠️ Delete a remote (requires `name`, `confirm=True`) |
### `setting` — System Settings
| Subaction | Description |
|-----------|-------------|
| `update` | Update system settings (requires `settings` object) |
| `configure_ups` | ⚠️ Configure UPS settings (requires `confirm=True`) |
### `customization` — Theme & Appearance
| Subaction | Description |
|-----------|-------------|
| `theme` | Current theme settings |
| `public_theme` | Public-facing theme |
| `is_initial_setup` | Check if initial setup is complete |
| `sso_enabled` | Check SSO status |
| `set_theme` | Update theme (requires theme parameters) |
### `oidc` — SSO / OpenID Connect
| Subaction | Description |
|-----------|-------------|
| `providers` | List configured OIDC providers |
| `provider` | Single provider details (requires `provider_id`) |
| `configuration` | OIDC configuration |
| `public_providers` | Public-facing provider list |
| `validate_session` | Validate current SSO session |
### `user` — Current User
| Subaction | Description |
|-----------|-------------|
| `me` | Current authenticated user info |
### `live` — Real-Time Subscriptions
These use persistent WebSocket connections. Returns a "connecting" placeholder on the first call — retry momentarily for live data.
| Subaction | Description |
|-----------|-------------|
| `cpu` | Live CPU utilization |
| `memory` | Live memory usage |
| `cpu_telemetry` | Detailed CPU telemetry |
| `array_state` | Live array state changes |
| `parity_progress` | Live parity check progress |
| `ups_status` | Live UPS status |
| `notifications_overview` | Live notification counts |
| `owner` | Live owner info |
| `server_status` | Live server status |
| `log_tail` | Live log tail stream |
| `notification_feed` | Live notification feed |
---
## Destructive Actions
All require `confirm=True` as an explicit parameter. Without it, the action is blocked and elicitation is triggered.
| Domain | Subaction | Risk |
|--------|-----------|------|
| `array` | `stop_array` | Stops array while containers/VMs may use shares |
| `array` | `remove_disk` | Removes disk from array |
| `array` | `clear_disk_stats` | Clears disk statistics permanently |
| `vm` | `force_stop` | Hard kills VM without graceful shutdown |
| `vm` | `reset` | Hard resets VM |
| `notification` | `delete` | Permanently deletes a notification |
| `notification` | `delete_archived` | Permanently deletes all archived notifications |
| `rclone` | `delete_remote` | Removes a cloud storage remote |
| `key` | `delete` | Permanently deletes an API key |
| `disk` | `flash_backup` | Triggers flash backup operation |
| `setting` | `configure_ups` | Modifies UPS configuration |
| `plugin` | `remove` | Uninstalls a plugin |
---
## Common Workflows
### First-time setup
```
unraid(action="health", subaction="setup")
unraid(action="health", subaction="check")
```
### System health overview
```
unraid(action="system", subaction="overview")
unraid(action="health", subaction="check")
```
### Container management
```
unraid(action="docker", subaction="list")
unraid(action="docker", subaction="details", container_id="plex")
unraid(action="docker", subaction="restart", container_id="sonarr")
```
### Array and disk status
```
unraid(action="array", subaction="parity_status")
unraid(action="disk", subaction="disks")
unraid(action="system", subaction="array")
```
### Read logs
```
unraid(action="disk", subaction="log_files")
unraid(action="disk", subaction="logs", path="syslog", lines=50)
```
### Live monitoring
```
unraid(action="live", subaction="cpu")
unraid(action="live", subaction="memory")
unraid(action="live", subaction="array_state")
```
### VM operations
```
unraid(action="vm", subaction="list")
unraid(action="vm", subaction="start", vm_id="<id>")
unraid(action="vm", subaction="force_stop", vm_id="<id>", confirm=True)
```
---
## Notes
- **Rate limit:** 100 requests / 10 seconds
- **Log path validation:** Only `/var/log/`, `/boot/logs/`, `/mnt/` prefixes accepted
- **Container logs:** Docker container stdout/stderr are NOT accessible via API — use SSH + `docker logs`
- **`arraySubscription`:** Known Unraid API bug — `live/array_state` may show "connecting" indefinitely
- **Event-driven subs** (`notifications_overview`, `owner`, `server_status`, `ups_status`): Only populate cache on first real server event

View File

@@ -1,3 +1,5 @@
> **⚠️ DEVELOPER REFERENCE ONLY** — This file documents the raw GraphQL API schema for development and maintenance purposes (adding new queries/mutations). Do NOT use these curl/GraphQL examples for MCP tool usage. Use `unraid(action=..., subaction=...)` calls instead. See `SKILL.md` for the correct calling convention.
# Unraid API - Complete Reference Guide
**Tested on:** Unraid 7.2 x86_64

View File

@@ -1,3 +1,5 @@
> **⚠️ DEVELOPER REFERENCE ONLY** — This file documents raw GraphQL endpoints for development purposes. For MCP tool usage, use `unraid(action=..., subaction=...)` calls as documented in `SKILL.md`.
# Unraid API Endpoints Reference
Complete list of available GraphQL read-only endpoints in Unraid 7.2+.

View File

@@ -1,3 +1,5 @@
> **⚠️ DEVELOPER REFERENCE ONLY** — Full GraphQL SDL from live API introspection. Use this to verify field names and types when adding new queries/mutations to the MCP server. Not for runtime agent usage.
"""
Indicates exactly one field must be supplied and this field must not be `null`.
"""

View File

@@ -1,219 +1,115 @@
# Unraid API Quick Reference
# Unraid MCP — Quick Reference
Quick reference for the most common Unraid GraphQL API queries.
All operations use: `unraid(action="<domain>", subaction="<operation>", [params])`
## Setup
## Most Common Operations
```bash
# Set environment variables
export UNRAID_URL="https://your-unraid-server/graphql"
export UNRAID_API_KEY="your-api-key-here"
# Or use the helper script directly
./scripts/unraid-query.sh -u "$UNRAID_URL" -k "$UNRAID_API_KEY" -q "{ online }"
### Health & Status
```
unraid(action="health", subaction="setup") # First-time credential setup
unraid(action="health", subaction="check") # Full health check
unraid(action="health", subaction="test_connection") # Quick connectivity test
unraid(action="system", subaction="overview") # Complete server summary
unraid(action="system", subaction="metrics") # CPU / RAM / I/O usage
unraid(action="system", subaction="online") # Online status
```
## Common Queries
### System Status
```graphql
{
online
metrics {
cpu { percentTotal }
memory { total used free percentTotal }
}
}
### Array & Disks
```
unraid(action="system", subaction="array") # Array status overview
unraid(action="disk", subaction="disks") # All disks with temps & health
unraid(action="array", subaction="parity_status") # Current parity check
unraid(action="array", subaction="parity_history") # Past parity results
unraid(action="array", subaction="parity_start") # Start parity check
unraid(action="array", subaction="stop_array", confirm=True) # ⚠️ Stop array
```
### Array Status
```graphql
{
array {
state
parityCheckStatus { status progress errors }
}
}
### Logs
```
### Disk List with Temperatures
```graphql
{
array {
disks {
name
device
temp
status
fsSize
fsFree
isSpinning
}
}
}
```
### All Physical Disks (including USB/SSDs)
```graphql
{
disks {
id
name
}
}
```
### Network Shares
```graphql
{
shares {
name
comment
}
}
unraid(action="disk", subaction="log_files") # List available logs
unraid(action="disk", subaction="logs", path="syslog", lines=50) # Read syslog
unraid(action="disk", subaction="logs", path="/var/log/syslog") # Full path also works
```
### Docker Containers
```graphql
{
docker {
containers {
id
names
image
state
status
}
}
}
```
unraid(action="docker", subaction="list")
unraid(action="docker", subaction="details", container_id="plex")
unraid(action="docker", subaction="start", container_id="nginx")
unraid(action="docker", subaction="stop", container_id="nginx")
unraid(action="docker", subaction="restart", container_id="sonarr")
unraid(action="docker", subaction="networks")
```
### Virtual Machines
```graphql
{
vms {
id
name
state
cpus
memory
}
}
```
### List Log Files
```graphql
{
logFiles {
name
size
modifiedAt
}
}
```
### Read Log Content
```graphql
{
logFile(path: "syslog", lines: 20) {
content
totalLines
}
}
```
### System Info
```graphql
{
info {
time
cpu { model cores threads }
os { distro release }
system { manufacturer model }
}
}
```
### UPS Devices
```graphql
{
upsDevices {
id
name
status
charge
load
}
}
unraid(action="vm", subaction="list")
unraid(action="vm", subaction="details", vm_id="<id>")
unraid(action="vm", subaction="start", vm_id="<id>")
unraid(action="vm", subaction="stop", vm_id="<id>")
unraid(action="vm", subaction="reboot", vm_id="<id>")
unraid(action="vm", subaction="force_stop", vm_id="<id>", confirm=True) # ⚠️
```
### Notifications
**Counts:**
```graphql
{
notifications {
overview {
unread { info warning alert total }
archive { info warning alert total }
}
}
}
```
unraid(action="notification", subaction="overview")
unraid(action="notification", subaction="unread")
unraid(action="notification", subaction="list", filter="UNREAD", limit=10)
unraid(action="notification", subaction="archive", notification_id="<id>")
unraid(action="notification", subaction="create", title="Test", subject="Subject",
description="Body", importance="normal")
```
**List Unread:**
```graphql
{
notifications {
list(filter: { type: UNREAD, offset: 0, limit: 10 }) {
id
subject
description
timestamp
}
}
}
### API Keys
```
unraid(action="key", subaction="list")
unraid(action="key", subaction="create", name="my-key", roles=["viewer"])
unraid(action="key", subaction="delete", key_id="<id>", confirm=True) # ⚠️
```
**List Archived:**
```graphql
{
notifications {
list(filter: { type: ARCHIVE, offset: 0, limit: 10 }) {
id
subject
description
timestamp
}
}
}
### Plugins
```
unraid(action="plugin", subaction="list")
unraid(action="plugin", subaction="add", names=["community.applications"])
unraid(action="plugin", subaction="remove", names=["old.plugin"], confirm=True) # ⚠️
```
## Field Name Notes
- Use `metrics` for real-time usage (CPU/memory percentages)
- Use `info` for hardware specs (cores, model, etc.)
- Temperature field is `temp` (not `temperature`)
- Status field is `state` for array (not `status`)
- Sizes are in kilobytes
- Temperatures are in Celsius
## Response Structure
All responses follow this pattern:
```json
{
"data": {
"queryName": { ... }
}
}
### rclone
```
unraid(action="rclone", subaction="list_remotes")
unraid(action="rclone", subaction="delete_remote", name="<remote>", confirm=True) # ⚠️
```
Errors appear in:
```json
{
"errors": [
{ "message": "..." }
]
}
### Live Subscriptions (real-time)
```
unraid(action="live", subaction="cpu")
unraid(action="live", subaction="memory")
unraid(action="live", subaction="parity_progress")
unraid(action="live", subaction="log_tail")
unraid(action="live", subaction="notification_feed")
unraid(action="live", subaction="ups_status")
```
> Returns `{"status": "connecting"}` on first call — retry momentarily.
---
## Domain → action= Mapping
| Old tool name (pre-v1.0) | New `action=` |
|--------------------------|---------------|
| `unraid_info` | `system` |
| `unraid_health` | `health` |
| `unraid_array` | `array` |
| `unraid_storage` | `disk` |
| `unraid_docker` | `docker` |
| `unraid_vm` | `vm` |
| `unraid_notifications` | `notification` |
| `unraid_keys` | `key` |
| `unraid_plugins` | `plugin` |
| `unraid_rclone` | `rclone` |
| `unraid_settings` | `setting` |
| `unraid_customization` | `customization` |
| `unraid_oidc` | `oidc` |
| `unraid_users` | `user` |
| `unraid_live` | `live` |

View File

@@ -1,36 +1,105 @@
# Unraid API Troubleshooting Guide
# Unraid MCP — Troubleshooting Guide
Common issues and solutions when working with the Unraid GraphQL API.
## Credentials Not Configured
## "Cannot query field" error
**Error:** `CredentialsNotConfiguredError` or message containing `~/.unraid-mcp/.env`
Field name doesn't exist in your Unraid version. Use introspection to find valid fields:
```bash
./scripts/unraid-query.sh -q "{ __type(name: \"TypeName\") { fields { name } } }"
**Fix:** Run setup to configure credentials interactively:
```
unraid(action="health", subaction="setup")
```
## "API key validation failed"
- Check API key is correct and not truncated
- Verify key has appropriate permissions (use "Viewer" role)
- Ensure URL includes `/graphql` endpoint (e.g. `http://host/graphql`)
This writes `UNRAID_API_URL` and `UNRAID_API_KEY` to `~/.unraid-mcp/.env`. Re-run at any time to update or rotate credentials.
## Empty results
Many queries return empty arrays when no data exists:
- `docker.containers` - No containers running
- `vms` - No VMs configured (or VM service disabled)
- `notifications` - No active alerts
- `plugins` - No plugins installed
---
This is normal behavior, not an error. Ensure your scripts handle empty arrays gracefully.
## Connection Failed / API Unreachable
## "VMs are not available" (GraphQL Error)
If the VM manager is disabled in Unraid settings, querying `{ vms { ... } }` will return a GraphQL error.
**Solution:** Check if VM service is enabled before querying, or use error handling (like `IGNORE_ERRORS=true` in dashboard scripts) to process partial data.
**Symptoms:** Timeout, connection refused, network error
## URL connection issues
- Use HTTPS (not HTTP) for remote access if configured
- For local access: `http://unraid-server-ip/graphql`
- For Unraid Connect: Use provided URL with token in hostname
- Use `-k` (insecure) with curl if using self-signed certs on local HTTPS
- Use `-L` (follow redirects) if Unraid redirects HTTP to HTTPS
**Diagnostic steps:**
1. Test basic connectivity:
```
unraid(action="health", subaction="test_connection")
```
2. Full diagnostic report:
```
unraid(action="health", subaction="diagnose")
```
3. Check that `UNRAID_API_URL` in `~/.unraid-mcp/.env` points to the correct Unraid GraphQL endpoint.
4. Verify the API key has the required roles. Get a new key: **Unraid UI → Settings → Management Access → API Keys → Create** (select "Viewer" role for read-only, or appropriate roles for mutations).
---
## Invalid Action / Subaction
**Error:** `Invalid action 'X'` or `Invalid subaction 'X' for action 'Y'`
**Fix:** Check the domain table in `SKILL.md` for the exact `action=` and `subaction=` strings. Common mistakes:
| Wrong | Correct |
|-------|---------|
| `action="info"` | `action="system"` |
| `action="notifications"` | `action="notification"` |
| `action="keys"` | `action="key"` |
| `action="plugins"` | `action="plugin"` |
| `action="settings"` | `action="setting"` |
| `subaction="unread"` | `subaction="mark_unread"` |
---
## Destructive Action Blocked
**Error:** `Action 'X' was not confirmed. Re-run with confirm=True to bypass elicitation.`
**Fix:** Add `confirm=True` to the call:
```
unraid(action="array", subaction="stop_array", confirm=True)
unraid(action="vm", subaction="force_stop", vm_id="<id>", confirm=True)
```
See the Destructive Actions table in `SKILL.md` for the full list.
---
## Live Subscription Returns "Connecting"
**Symptoms:** `unraid(action="live", ...)` returns `{"status": "connecting"}`
**Explanation:** The persistent WebSocket subscription has not yet received its first event. Retry in a moment.
**Known issue:** `live/array_state` uses `arraySubscription` which has a known Unraid API bug (returns null for a non-nullable field). This subscription will always show "connecting."
**Event-driven subscriptions** (`live/notifications_overview`, `live/owner`, `live/server_status`, `live/ups_status`) only populate when the server emits a change event. If the server is idle, these may never populate during a session.
**Workaround for array state:** Use `unraid(action="system", subaction="array")` for a synchronous snapshot instead.
---
## Rate Limit Exceeded
**Limit:** 100 requests / 10 seconds
**Symptoms:** HTTP 429 or rate limit error
**Fix:** Space out requests. Avoid polling in tight loops. Use `live/` subscriptions for real-time data instead of polling `system/metrics` repeatedly.
---
## Log Path Rejected
**Error:** `Invalid log path`
**Valid log path prefixes:** `/var/log/`, `/boot/logs/`, `/mnt/`
Use `unraid(action="disk", subaction="log_files")` to list available logs before reading.
---
## Container Logs Not Available
Docker container stdout/stderr are **not accessible via the Unraid API**. SSH to the Unraid server and use `docker logs <container>` directly.

View File

@@ -125,24 +125,6 @@ class TestSubscriptionManagerInit:
cfg = mgr.subscription_configs["logFileSubscription"]
assert cfg.get("auto_start") is False
def test_subscription_configs_contain_all_snapshot_actions(self) -> None:
from unraid_mcp.subscriptions.queries import SNAPSHOT_ACTIONS
mgr = SubscriptionManager()
for action in SNAPSHOT_ACTIONS:
assert action in mgr.subscription_configs, (
f"'{action}' missing from subscription_configs"
)
def test_snapshot_actions_all_auto_start(self) -> None:
from unraid_mcp.subscriptions.queries import SNAPSHOT_ACTIONS
mgr = SubscriptionManager()
for action in SNAPSHOT_ACTIONS:
assert mgr.subscription_configs[action].get("auto_start") is True, (
f"'{action}' missing auto_start=True"
)
# ---------------------------------------------------------------------------
# Connection Lifecycle

View File

@@ -421,6 +421,7 @@ suite_system() {
printf '\n%b== system (info/metrics/UPS) ==%b\n' "${C_BOLD}" "${C_RESET}" | tee -a "${LOG_FILE}"
run_test "system: overview" '{"action":"system","subaction":"overview"}'
run_test "system: array" '{"action":"system","subaction":"array"}'
run_test "system: network" '{"action":"system","subaction":"network"}'
run_test "system: registration" '{"action":"system","subaction":"registration"}'
run_test "system: variables" '{"action":"system","subaction":"variables"}'
@@ -531,8 +532,8 @@ suite_notification() {
run_test "notification: overview" '{"action":"notification","subaction":"overview"}'
run_test "notification: list" '{"action":"notification","subaction":"list"}'
run_test "notification: unread" '{"action":"notification","subaction":"unread"}'
# Mutating: create/archive/delete/delete_archived/archive_all/etc. — skipped
run_test "notification: recalculate" '{"action":"notification","subaction":"recalculate"}'
# Mutating: create/archive/mark_unread/delete/delete_archived/archive_all/etc. — skipped
}
suite_rclone() {

View File

@@ -341,7 +341,7 @@ class TestNotificationsEnumFuzzing:
"list",
"create",
"archive",
"unread",
"mark_unread",
"delete",
"delete_archived",
"archive_all",

View File

@@ -522,11 +522,11 @@ class TestNotificationMutations:
errors = _validate_operation(schema, MUTATIONS["archive"])
assert not errors, f"archive mutation validation failed: {errors}"
def test_unread_mutation(self, schema: GraphQLSchema) -> None:
def test_mark_unread_mutation(self, schema: GraphQLSchema) -> None:
from unraid_mcp.tools.unraid import _NOTIFICATION_MUTATIONS as MUTATIONS
errors = _validate_operation(schema, MUTATIONS["unread"])
assert not errors, f"unread mutation validation failed: {errors}"
errors = _validate_operation(schema, MUTATIONS["mark_unread"])
assert not errors, f"mark_unread mutation validation failed: {errors}"
def test_delete_mutation(self, schema: GraphQLSchema) -> None:
from unraid_mcp.tools.unraid import _NOTIFICATION_MUTATIONS as MUTATIONS
@@ -576,7 +576,7 @@ class TestNotificationMutations:
expected = {
"create",
"archive",
"unread",
"mark_unread",
"delete",
"delete_archived",
"archive_all",

View File

@@ -25,14 +25,14 @@ async def test_theme_returns_customization(_mock_graphql):
"customization": {"theme": {"name": "azure"}, "partnerInfo": None, "activationCode": None}
}
result = await _make_tool()(action="customization", subaction="theme")
assert "customization" in result
assert result["customization"]["theme"]["name"] == "azure"
@pytest.mark.asyncio
async def test_public_theme(_mock_graphql):
_mock_graphql.return_value = {"publicTheme": {"name": "black"}}
result = await _make_tool()(action="customization", subaction="public_theme")
assert "publicTheme" in result
assert result["publicTheme"]["name"] == "black"
@pytest.mark.asyncio

View File

@@ -83,6 +83,7 @@ async def test_invalid_subaction_raises():
@pytest.mark.asyncio
async def test_snapshot_propagates_tool_error(_mock_subscribe_once):
"""Non-event-driven (streaming) actions still propagate timeout as ToolError."""
from unraid_mcp.core.exceptions import ToolError
_mock_subscribe_once.side_effect = ToolError("Subscription timed out after 10s")
@@ -90,6 +91,28 @@ async def test_snapshot_propagates_tool_error(_mock_subscribe_once):
await _make_tool()(action="live", subaction="cpu")
@pytest.mark.asyncio
async def test_event_driven_timeout_returns_no_recent_events(_mock_subscribe_once):
"""Event-driven subscriptions return a graceful no_recent_events response on timeout."""
from unraid_mcp.core.exceptions import ToolError
_mock_subscribe_once.side_effect = ToolError("Subscription timed out after 10s")
result = await _make_tool()(action="live", subaction="notifications_overview")
assert result["success"] is True
assert result["status"] == "no_recent_events"
assert "No events received" in result["message"]
@pytest.mark.asyncio
async def test_event_driven_non_timeout_error_propagates(_mock_subscribe_once):
"""Non-timeout ToolErrors from event-driven subscriptions still propagate."""
from unraid_mcp.core.exceptions import ToolError
_mock_subscribe_once.side_effect = ToolError("Subscription auth failed")
with pytest.raises(ToolError, match="auth failed"):
await _make_tool()(action="live", subaction="owner")
@pytest.mark.asyncio
async def test_log_tail_rejects_invalid_path(_mock_subscribe_collect):
from unraid_mcp.core.exceptions import ToolError

View File

@@ -122,12 +122,14 @@ class TestNotificationsActions:
result = await tool_fn(action="notification", subaction="archive_all")
assert result["success"] is True
async def test_unread_notification(self, _mock_graphql: AsyncMock) -> None:
async def test_mark_unread_notification(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {"unreadNotification": {"id": "n:1"}}
tool_fn = _make_tool()
result = await tool_fn(action="notification", subaction="unread", notification_id="n:1")
result = await tool_fn(
action="notification", subaction="mark_unread", notification_id="n:1"
)
assert result["success"] is True
assert result["subaction"] == "unread"
assert result["subaction"] == "mark_unread"
async def test_list_with_importance_filter(self, _mock_graphql: AsyncMock) -> None:
_mock_graphql.return_value = {

View File

@@ -35,7 +35,7 @@ async def test_providers_returns_list(_mock_graphql):
async def test_public_providers(_mock_graphql):
_mock_graphql.return_value = {"publicOidcProviders": []}
result = await _make_tool()(action="oidc", subaction="public_providers")
assert "providers" in result
assert result["providers"] == []
@pytest.mark.asyncio
@@ -60,4 +60,5 @@ async def test_configuration(_mock_graphql):
"oidcConfiguration": {"providers": [], "defaultAllowedOrigins": []}
}
result = await _make_tool()(action="oidc", subaction="configuration")
assert "providers" in result
assert result["providers"] == []
assert result["defaultAllowedOrigins"] == []

View File

@@ -42,22 +42,31 @@ class TestLiveResourcesUseManagerCache:
assert json.loads(result) == cached
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_resource_returns_status_when_no_cache(
async def test_resource_returns_connecting_when_no_cache_and_no_error(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
mock_mgr.last_error = {}
mcp = _make_resources()
resource = mcp.providers[0]._components[f"resource:unraid://live/{action}@"]
result = await resource.fn()
parsed = json.loads(result)
assert "status" in parsed
assert parsed["status"] == "connecting"
def test_subscribe_once_not_imported(self) -> None:
"""subscribe_once must not be imported — resources use manager cache exclusively."""
import unraid_mcp.subscriptions.resources as res_module
assert not hasattr(res_module, "subscribe_once")
@pytest.mark.parametrize("action", list(SNAPSHOT_ACTIONS.keys()))
async def test_resource_returns_error_status_on_permanent_failure(
self, action: str, _mock_ensure_started: AsyncMock
) -> None:
with patch("unraid_mcp.subscriptions.resources.subscription_manager") as mock_mgr:
mock_mgr.get_resource_data = AsyncMock(return_value=None)
mock_mgr.last_error = {action: "WebSocket auth failed"}
mcp = _make_resources()
resource = mcp.providers[0]._components[f"resource:unraid://live/{action}@"]
result = await resource.fn()
parsed = json.loads(result)
assert parsed["status"] == "error"
assert "auth failed" in parsed["message"]
class TestSnapshotSubscriptionsRegistered:

View File

@@ -468,8 +468,12 @@ async def test_elicit_reset_confirmation_returns_false_when_cancelled():
@pytest.mark.asyncio
async def test_elicit_reset_confirmation_returns_false_when_not_implemented():
"""Returns False when the MCP client does not support elicitation."""
async def test_elicit_reset_confirmation_returns_true_when_not_implemented():
"""Returns True (proceed with reset) when the MCP client does not support elicitation.
Non-interactive clients (stdio, CI) must not be permanently blocked from
reconfiguring credentials just because they can't ask the user a yes/no question.
"""
from unittest.mock import AsyncMock, MagicMock
from unraid_mcp.core.setup import elicit_reset_confirmation
@@ -478,7 +482,7 @@ async def test_elicit_reset_confirmation_returns_false_when_not_implemented():
mock_ctx.elicit = AsyncMock(side_effect=NotImplementedError("elicitation not supported"))
result = await elicit_reset_confirmation(mock_ctx, "https://example.com")
assert result is False
assert result is True
@pytest.mark.asyncio

View File

@@ -52,8 +52,13 @@ async def elicit_reset_confirmation(ctx: Context | None, current_url: str) -> bo
response_type=bool,
)
except NotImplementedError:
logger.warning("MCP client does not support elicitation for reset confirmation.")
return False
# Client doesn't support elicitation — treat as "proceed with reset" so
# non-interactive clients (stdio, CI) are not permanently blocked from
# reconfiguring credentials.
logger.warning(
"MCP client does not support elicitation for reset confirmation — proceeding with reset."
)
return True
if result.action != "accept":
logger.info("Credential reset declined by user (%s).", result.action)
@@ -80,7 +85,7 @@ async def elicit_and_configure(ctx: Context | None) -> bool:
if ctx is None:
logger.warning(
"Cannot elicit credentials: no MCP context available. "
"Run unraid_health action=setup to configure credentials."
"Run unraid(action=health, subaction=setup) to configure credentials."
)
return False
@@ -97,7 +102,7 @@ async def elicit_and_configure(ctx: Context | None) -> bool:
except NotImplementedError:
logger.warning(
"MCP client does not support elicitation. "
"Use unraid_health action=setup or create %s manually.",
"Use unraid(action=health, subaction=setup) or create %s manually.",
CREDENTIALS_ENV_PATH,
)
return False

View File

@@ -1,5 +1,17 @@
"""GraphQL subscription query strings for snapshot and collect operations."""
# Subscriptions that only emit on state changes (not on a regular interval).
# When subscribe_once times out for these, it means no recent change — not an error.
EVENT_DRIVEN_ACTIONS: frozenset[str] = frozenset(
{
"parity_progress",
"ups_status",
"notifications_overview",
"owner",
"server_status",
}
)
SNAPSHOT_ACTIONS = {
"cpu": """
subscription { systemMetricsCpu { id percentTotal cpus { percentTotal percentUser percentSystem percentIdle } } }

View File

@@ -107,8 +107,17 @@ def register_subscription_resources(mcp: FastMCP) -> None:
async def _live_resource() -> str:
await ensure_subscriptions_started()
data = await subscription_manager.get_resource_data(action)
if data:
if data is not None:
return json.dumps(data, indent=2)
# Surface permanent errors instead of reporting "connecting" indefinitely
last_error = subscription_manager.last_error.get(action)
if last_error:
return json.dumps(
{
"status": "error",
"message": f"Subscription '{action}' failed: {last_error}",
}
)
return json.dumps(
{
"status": "connecting",

View File

@@ -1,6 +1,6 @@
"""MCP tools — single consolidated unraid tool with action + subaction routing.
unraid - All Unraid operations (15 actions, ~88 subactions)
unraid - All Unraid operations (15 actions, ~107 subactions)
system - System info, metrics, UPS, network, registration
health - Health checks, connection test, diagnostics, setup
array - Parity, array state, disk add/remove/mount

View File

@@ -871,8 +871,6 @@ async def _handle_docker(
"container": (data.get("docker") or {}).get(subaction),
}
raise ToolError(f"Unhandled docker subaction '{subaction}' — this is a bug")
# ===========================================================================
# VM
@@ -950,8 +948,6 @@ async def _handle_vm(
return {"success": data["vm"][field], "subaction": subaction, "vm_id": vm_id}
raise ToolError(f"Failed to {subaction} VM or unexpected response")
raise ToolError(f"Unhandled vm subaction '{subaction}' — this is a bug")
# ===========================================================================
# NOTIFICATION
@@ -965,7 +961,7 @@ _NOTIFICATION_QUERIES: dict[str, str] = {
_NOTIFICATION_MUTATIONS: dict[str, str] = {
"create": "mutation CreateNotification($input: NotificationData!) { createNotification(input: $input) { id title importance } }",
"archive": "mutation ArchiveNotification($id: PrefixedID!) { archiveNotification(id: $id) { id title importance } }",
"unread": "mutation UnreadNotification($id: PrefixedID!) { unreadNotification(id: $id) { id title importance } }",
"mark_unread": "mutation UnreadNotification($id: PrefixedID!) { unreadNotification(id: $id) { id title importance } }",
"delete": "mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) { deleteNotification(id: $id, type: $type) { unread { info warning alert total } archive { info warning alert total } } }",
"delete_archived": "mutation DeleteArchivedNotifications { deleteArchivedNotifications { unread { info warning alert total } archive { info warning alert total } } }",
"archive_all": "mutation ArchiveAllNotifications($importance: NotificationImportance) { archiveAll(importance: $importance) { unread { info warning alert total } archive { info warning alert total } } }",
@@ -1077,7 +1073,7 @@ async def _handle_notification(
raise ToolError("Notification creation failed: server returned no data")
return {"success": True, "notification": notif}
if subaction in ("archive", "unread"):
if subaction in ("archive", "mark_unread"):
if not notification_id:
raise ToolError(f"notification_id is required for notification/{subaction}")
data = await make_graphql_request(
@@ -1615,7 +1611,7 @@ _LIVE_ALLOWED_LOG_PREFIXES = ("/var/log/", "/boot/logs/", "/mnt/")
async def _handle_live(
subaction: str, path: str | None, collect_for: float, timeout: float
) -> dict[str, Any]:
from ..subscriptions.queries import COLLECT_ACTIONS, SNAPSHOT_ACTIONS
from ..subscriptions.queries import COLLECT_ACTIONS, EVENT_DRIVEN_ACTIONS, SNAPSHOT_ACTIONS
from ..subscriptions.snapshot import subscribe_collect, subscribe_once
all_live = set(SNAPSHOT_ACTIONS) | set(COLLECT_ACTIONS)
@@ -1636,6 +1632,19 @@ async def _handle_live(
logger.info(f"Executing unraid action=live subaction={subaction} timeout={timeout}")
if subaction in SNAPSHOT_ACTIONS:
if subaction in EVENT_DRIVEN_ACTIONS:
try:
data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout)
except ToolError as e:
if "timed out" in str(e):
return {
"success": True,
"subaction": subaction,
"status": "no_recent_events",
"message": f"No events received in {timeout:.0f}s — this subscription only emits on state changes",
}
raise
else:
data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout)
return {"success": True, "subaction": subaction, "data": data}
@@ -1761,58 +1770,51 @@ def register_unraid_tool(mcp: FastMCP) -> None:
Use action + subaction to select an operation. All params are optional
except those required by the specific subaction.
action="system" - Server info, metrics, network, UPS
subactions: overview, array, network, registration, variables, metrics,
services, display, config, online, owner, settings, server,
servers, flash, ups_devices, ups_device, ups_config
┌─────────────────┬──────────────────────────────────────────────────────────────────────┐
action │ subactions │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ system │ overview, array, network, registration, variables, metrics,
│ │ services, display, config, online, owner, settings, server, │
│ │ servers, flash, ups_devices, ups_device, ups_config │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ health │ check, test_connection, diagnose, setup │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ array │ parity_status, parity_history, parity_start, parity_pause, │
│ │ parity_resume, parity_cancel, start_array*, stop_array*, │
│ │ add_disk, remove_disk*, mount_disk, unmount_disk, clear_disk_stats* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ disk │ shares, disks, disk_details, log_files, logs, flash_backup* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ docker │ list, details, start, stop, restart, networks, network_details │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ vm │ list, details, start, stop, pause, resume, │
│ │ force_stop*, reboot, reset* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ notification │ overview, list, create, archive, mark_unread, recalculate, │
│ │ archive_all, archive_many, unarchive_many, unarchive_all, │
│ │ delete*, delete_archived* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ key │ list, get, create, update, delete*, add_role, remove_role │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ plugin │ list, add, remove* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ rclone │ list_remotes, config_form, create_remote, delete_remote* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ setting │ update, configure_ups* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ customization │ theme, public_theme, is_initial_setup, sso_enabled, set_theme │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ oidc │ providers, provider, configuration, public_providers, │
│ │ validate_session │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ user │ me │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ live │ cpu, memory, cpu_telemetry, array_state, parity_progress, │
│ │ ups_status, notifications_overview, owner, server_status, │
│ │ log_tail (requires path=), notification_feed │
└─────────────────┴──────────────────────────────────────────────────────────────────────┘
action="health" - MCP server and API health
subactions: check, test_connection, diagnose, setup
action="array" - Array and parity management
subactions: parity_status, parity_history, parity_start, parity_pause,
parity_resume, parity_cancel, start_array, stop_array,
add_disk, remove_disk, mount_disk, unmount_disk, clear_disk_stats
action="disk" - Shares, physical disks, logs
subactions: shares, disks, disk_details, log_files, logs, flash_backup
action="docker" - Container lifecycle and networks
subactions: list, details, start, stop, restart, networks, network_details
action="vm" - Virtual machine lifecycle
subactions: list, details, start, stop, pause, resume, force_stop, reboot, reset
action="notification" - System notifications
subactions: overview, list, create, archive, unread, delete,
delete_archived, archive_all, archive_many,
unarchive_many, unarchive_all, recalculate
action="key" - API key management
subactions: list, get, create, update, delete, add_role, remove_role
action="plugin" - Plugin management
subactions: list, add, remove
action="rclone" - Cloud storage remotes
subactions: list_remotes, config_form, create_remote, delete_remote
action="setting" - System settings mutations
subactions: update, configure_ups
action="customization" - Theme and UI
subactions: theme, public_theme, is_initial_setup, sso_enabled, set_theme
action="oidc" - OIDC/SSO providers
subactions: providers, provider, configuration, public_providers, validate_session
action="user" - Current authenticated user
subactions: me
action="live" - Real-time WebSocket subscription snapshots
subactions: cpu, memory, cpu_telemetry, array_state, parity_progress,
ups_status, notifications_overview, owner, server_status,
log_tail, notification_feed
* Destructive — requires confirm=True
"""
if action == "system":
return await _handle_system(subaction, device_id)