From efaab031ae29f40af34ad311072c7ec3f7a6fcc1 Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Mon, 16 Mar 2026 02:58:54 -0400 Subject: [PATCH] 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) --- .claude-plugin/README.md | 18 +- .claude-plugin/marketplace.json | 8 +- CLAUDE.md | 41 +- README.md | 138 +++--- commands/array.md | 30 -- commands/docker.md | 48 -- commands/health.md | 59 --- commands/info.md | 50 -- commands/keys.md | 37 -- commands/notifications.md | 41 -- commands/rclone.md | 32 -- commands/settings.md | 49 -- commands/storage.md | 33 -- commands/users.md | 31 -- commands/vm.md | 41 -- docs/DESTRUCTIVE_ACTIONS.md | 255 ++++------ docs/MARKETPLACE.md | 25 +- docs/PUBLISHING.md | 4 +- skills/unraid/SKILL.md | 456 +++++++++++------- skills/unraid/references/api-reference.md | 2 + skills/unraid/references/endpoints.md | 2 + .../unraid/references/introspection-schema.md | 2 + skills/unraid/references/quick-reference.md | 280 ++++------- skills/unraid/references/troubleshooting.md | 123 +++-- tests/integration/test_subscriptions.py | 18 - tests/mcporter/test-tools.sh | 5 +- tests/property/test_input_validation.py | 2 +- tests/schema/test_query_validation.py | 8 +- tests/test_customization.py | 4 +- tests/test_live.py | 23 + tests/test_notifications.py | 8 +- tests/test_oidc.py | 5 +- tests/test_resources.py | 23 +- tests/test_setup.py | 10 +- unraid_mcp/core/setup.py | 13 +- unraid_mcp/subscriptions/queries.py | 12 + unraid_mcp/subscriptions/resources.py | 11 +- unraid_mcp/tools/__init__.py | 2 +- unraid_mcp/tools/unraid.py | 120 ++--- 39 files changed, 844 insertions(+), 1225 deletions(-) delete mode 100644 commands/array.md delete mode 100644 commands/docker.md delete mode 100644 commands/health.md delete mode 100644 commands/info.md delete mode 100644 commands/keys.md delete mode 100644 commands/notifications.md delete mode 100644 commands/rclone.md delete mode 100644 commands/settings.md delete mode 100644 commands/storage.md delete mode 100644 commands/users.md delete mode 100644 commands/vm.md diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index f89b879..26af5dd 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -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 diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c814f03..f4ee2cc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } diff --git a/CLAUDE.md b/CLAUDE.md index 17d4bea..451137d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index a02e411..88ea5a8 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ ## ✨ Features -- 🔧 **11 Tools, ~104 Actions**: Complete Unraid management through MCP protocol -- 🏗️ **Modular Architecture**: Clean, maintainable, and extensible codebase +- 🔧 **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="", subaction="")` -### 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" ``` @@ -336,27 +365,20 @@ unraid-mcp/ │ ├── config/ # Configuration management │ │ ├── settings.py # Environment & settings │ │ └── logging.py # Logging setup -│ ├── core/ # Core infrastructure +│ ├── 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 ``` diff --git a/commands/array.md b/commands/array.md deleted file mode 100644 index 1b294e9..0000000 --- a/commands/array.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -description: Manage Unraid array parity checks -argument-hint: [action] [correct=true/false] ---- - -Execute the `unraid_array` MCP tool with action: `$1` - -## Available Actions (5) - -**Parity Check Operations:** -- `parity_start` - Start parity check/sync (optional: correct=true to fix errors) -- `parity_pause` - Pause running parity operation -- `parity_resume` - Resume paused parity operation -- `parity_cancel` - Cancel running parity operation -- `parity_status` - Get current parity check status - -## Example Usage - -``` -/array parity_start -/array parity_start correct=true -/array parity_pause -/array parity_resume -/array parity_cancel -/array parity_status -``` - -**Note:** Use `correct=true` with `parity_start` to automatically fix any parity errors found during the check. - -Use the tool to execute the requested parity operation and report the results. diff --git a/commands/docker.md b/commands/docker.md deleted file mode 100644 index 95b753d..0000000 --- a/commands/docker.md +++ /dev/null @@ -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. diff --git a/commands/health.md b/commands/health.md deleted file mode 100644 index 526088a..0000000 --- a/commands/health.md +++ /dev/null @@ -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. diff --git a/commands/info.md b/commands/info.md deleted file mode 100644 index 6fd79f3..0000000 --- a/commands/info.md +++ /dev/null @@ -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. diff --git a/commands/keys.md b/commands/keys.md deleted file mode 100644 index 56bf8f8..0000000 --- a/commands/keys.md +++ /dev/null @@ -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. diff --git a/commands/notifications.md b/commands/notifications.md deleted file mode 100644 index 84716c4..0000000 --- a/commands/notifications.md +++ /dev/null @@ -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. diff --git a/commands/rclone.md b/commands/rclone.md deleted file mode 100644 index 68124e4..0000000 --- a/commands/rclone.md +++ /dev/null @@ -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. diff --git a/commands/settings.md b/commands/settings.md deleted file mode 100644 index 59316af..0000000 --- a/commands/settings.md +++ /dev/null @@ -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. diff --git a/commands/storage.md b/commands/storage.md deleted file mode 100644 index 37acb37..0000000 --- a/commands/storage.md +++ /dev/null @@ -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. diff --git a/commands/users.md b/commands/users.md deleted file mode 100644 index b4a1033..0000000 --- a/commands/users.md +++ /dev/null @@ -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. diff --git a/commands/vm.md b/commands/vm.md deleted file mode 100644 index 78923e0..0000000 --- a/commands/vm.md +++ /dev/null @@ -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. diff --git a/docs/DESTRUCTIVE_ACTIONS.md b/docs/DESTRUCTIVE_ACTIONS.md index e31be8d..b0b0913 100644 --- a/docs/DESTRUCTIVE_ACTIONS.md +++ b/docs/DESTRUCTIVE_ACTIONS.md @@ -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":"","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":"","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":[""],"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 | — | diff --git a/docs/MARKETPLACE.md b/docs/MARKETPLACE.md index 72f5415..54f1b92 100644 --- a/docs/MARKETPLACE.md +++ b/docs/MARKETPLACE.md @@ -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 diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 95bf65f..bd56168 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -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) diff --git a/skills/unraid/SKILL.md b/skills/unraid/SKILL.md index 2392073..7e84b03 100644 --- a/skills/unraid/SKILL.md +++ b/skills/unraid/SKILL.md @@ -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="", subaction="", [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 - -./skills/SKILL_NAME/scripts/SCRIPT.sh [args] -true - +### `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="") +unraid(action="vm", subaction="force_stop", vm_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 diff --git a/skills/unraid/references/api-reference.md b/skills/unraid/references/api-reference.md index dd452d0..93e353e 100644 --- a/skills/unraid/references/api-reference.md +++ b/skills/unraid/references/api-reference.md @@ -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 diff --git a/skills/unraid/references/endpoints.md b/skills/unraid/references/endpoints.md index c528846..5094297 100644 --- a/skills/unraid/references/endpoints.md +++ b/skills/unraid/references/endpoints.md @@ -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+. diff --git a/skills/unraid/references/introspection-schema.md b/skills/unraid/references/introspection-schema.md index 62676a1..75fe057 100644 --- a/skills/unraid/references/introspection-schema.md +++ b/skills/unraid/references/introspection-schema.md @@ -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`. """ diff --git a/skills/unraid/references/quick-reference.md b/skills/unraid/references/quick-reference.md index 4760bb8..c7d04f0 100644 --- a/skills/unraid/references/quick-reference.md +++ b/skills/unraid/references/quick-reference.md @@ -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="", subaction="", [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="") +unraid(action="vm", subaction="start", vm_id="") +unraid(action="vm", subaction="stop", vm_id="") +unraid(action="vm", subaction="reboot", vm_id="") +unraid(action="vm", subaction="force_stop", vm_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="") +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="", 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="", 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` | diff --git a/skills/unraid/references/troubleshooting.md b/skills/unraid/references/troubleshooting.md index 7b2df9c..c040075 100644 --- a/skills/unraid/references/troubleshooting.md +++ b/skills/unraid/references/troubleshooting.md @@ -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="", 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 ` directly. diff --git a/tests/integration/test_subscriptions.py b/tests/integration/test_subscriptions.py index fe29f7e..a5aad0b 100644 --- a/tests/integration/test_subscriptions.py +++ b/tests/integration/test_subscriptions.py @@ -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 diff --git a/tests/mcporter/test-tools.sh b/tests/mcporter/test-tools.sh index 060bcb3..9a3ebc4 100755 --- a/tests/mcporter/test-tools.sh +++ b/tests/mcporter/test-tools.sh @@ -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() { diff --git a/tests/property/test_input_validation.py b/tests/property/test_input_validation.py index b26ade3..7166deb 100644 --- a/tests/property/test_input_validation.py +++ b/tests/property/test_input_validation.py @@ -341,7 +341,7 @@ class TestNotificationsEnumFuzzing: "list", "create", "archive", - "unread", + "mark_unread", "delete", "delete_archived", "archive_all", diff --git a/tests/schema/test_query_validation.py b/tests/schema/test_query_validation.py index 327fab9..cc01a31 100644 --- a/tests/schema/test_query_validation.py +++ b/tests/schema/test_query_validation.py @@ -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", diff --git a/tests/test_customization.py b/tests/test_customization.py index 44dcfc2..fe30eae 100644 --- a/tests/test_customization.py +++ b/tests/test_customization.py @@ -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 diff --git a/tests/test_live.py b/tests/test_live.py index 56bcd14..2cdce6a 100644 --- a/tests/test_live.py +++ b/tests/test_live.py @@ -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 diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 2609a93..7a080b9 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -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 = { diff --git a/tests/test_oidc.py b/tests/test_oidc.py index 86b7a28..b2d23ea 100644 --- a/tests/test_oidc.py +++ b/tests/test_oidc.py @@ -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"] == [] diff --git a/tests/test_resources.py b/tests/test_resources.py index 3b03b3a..b930c5f 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -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: diff --git a/tests/test_setup.py b/tests/test_setup.py index 88d5df5..c0ef844 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -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 diff --git a/unraid_mcp/core/setup.py b/unraid_mcp/core/setup.py index 2d36d90..fa349ee 100644 --- a/unraid_mcp/core/setup.py +++ b/unraid_mcp/core/setup.py @@ -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 diff --git a/unraid_mcp/subscriptions/queries.py b/unraid_mcp/subscriptions/queries.py index 73b4571..d2c54b9 100644 --- a/unraid_mcp/subscriptions/queries.py +++ b/unraid_mcp/subscriptions/queries.py @@ -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 } } } diff --git a/unraid_mcp/subscriptions/resources.py b/unraid_mcp/subscriptions/resources.py index 70e0aa4..fa9cfee 100644 --- a/unraid_mcp/subscriptions/resources.py +++ b/unraid_mcp/subscriptions/resources.py @@ -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", diff --git a/unraid_mcp/tools/__init__.py b/unraid_mcp/tools/__init__.py index 01298e3..79186a4 100644 --- a/unraid_mcp/tools/__init__.py +++ b/unraid_mcp/tools/__init__.py @@ -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 diff --git a/unraid_mcp/tools/unraid.py b/unraid_mcp/tools/unraid.py index dce90ef..f0cd270 100644 --- a/unraid_mcp/tools/unraid.py +++ b/unraid_mcp/tools/unraid.py @@ -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,7 +1632,20 @@ async def _handle_live( logger.info(f"Executing unraid action=live subaction={subaction} timeout={timeout}") if subaction in SNAPSHOT_ACTIONS: - data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout) + 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} if subaction == "log_tail": @@ -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)