diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 26af5dd..f4136ac 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -49,7 +49,7 @@ Query and monitor Unraid servers via GraphQL API - array status, disk health, co After installation, run setup to configure credentials interactively: -``` +```python unraid(action="health", subaction="setup") ``` diff --git a/README.md b/README.md index 88ea5a8..3935e76 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 🚀 Unraid MCP Server [![Python Version](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) -[![FastMCP](https://img.shields.io/badge/FastMCP-2.11.2+-green.svg)](https://github.com/jlowin/fastmcp) +[![FastMCP](https://img.shields.io/badge/FastMCP-3.x-green.svg)](https://github.com/jlowin/fastmcp) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) **A powerful MCP (Model Context Protocol) server that provides comprehensive tools to interact with an Unraid server's GraphQL API.** @@ -26,7 +26,6 @@ - [Installation](#-installation) - [Configuration](#-configuration) - [Available Tools & Resources](#-available-tools--resources) -- [Custom Slash Commands](#-custom-slash-commands) - [Development](#-development) - [Architecture](#-architecture) - [Troubleshooting](#-troubleshooting) @@ -47,7 +46,6 @@ This provides instant access to Unraid monitoring and management through Claude Code with: - **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 @@ -97,8 +95,13 @@ cd unraid-mcp ### 2. Configure Environment ```bash +# For Docker/production use — canonical credential location (all runtimes) +mkdir -p ~/.unraid-mcp && chmod 700 ~/.unraid-mcp +cp .env.example ~/.unraid-mcp/.env && chmod 600 ~/.unraid-mcp/.env +# Edit ~/.unraid-mcp/.env with your values + +# For local development only cp .env.example .env -# Edit .env with your Unraid API details ``` ### 3. Deploy with Docker (Recommended) @@ -130,7 +133,6 @@ unraid-mcp/ # ${CLAUDE_PLUGIN_ROOT} ├── .claude-plugin/ │ ├── marketplace.json # Marketplace catalog │ └── plugin.json # Plugin manifest -├── commands/ # 10 custom slash commands ├── unraid_mcp/ # MCP server Python package ├── skills/unraid/ # Skill and documentation ├── pyproject.toml # Dependencies and entry points @@ -138,7 +140,6 @@ unraid-mcp/ # ${CLAUDE_PLUGIN_ROOT} ``` - **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 @@ -223,11 +224,15 @@ UNRAID_MCP_PORT=6970 UNRAID_MCP_LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR UNRAID_MCP_LOG_FILE=unraid-mcp.log -# SSL/TLS Configuration +# SSL/TLS Configuration UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle +# Subscription Configuration +UNRAID_AUTO_START_SUBSCRIPTIONS=true # Auto-start WebSocket subscriptions on startup (default: true) +UNRAID_MAX_RECONNECT_ATTEMPTS=5 # Max WebSocket reconnection attempts (default: 5) + # Optional: Log Stream Configuration -# UNRAID_AUTOSTART_LOG_PATH=/var/log/syslog # Path for log streaming resource +# UNRAID_AUTOSTART_LOG_PATH=/var/log/syslog # Path for log streaming resource (unraid://logs/stream) ``` ### Transport Options @@ -250,13 +255,13 @@ Call pattern: `unraid(action="", subaction="")` | 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) | +| **`system`** | overview, array, network, registration, variables, metrics, services, display, config, online, owner, settings, server, servers, flash, ups_devices, ups_device, ups_config | Server info, metrics, network, UPS (18 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) | +| **`notification`** | overview, list, create, archive, mark_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) | @@ -278,8 +283,9 @@ Call pattern: `unraid(action="", subaction="")` ### MCP Resources (Real-time Cached Data) -The `unraid://live/*` resources expose cached subscription data from persistent WebSocket connections: +The server exposes two classes of MCP resources backed by persistent WebSocket connections: +**`unraid://live/*` — 9 snapshot resources** (auto-started, always-cached): - `unraid://live/cpu` — CPU utilization - `unraid://live/memory` — Memory usage - `unraid://live/cpu_telemetry` — Detailed CPU telemetry @@ -289,68 +295,12 @@ The `unraid://live/*` resources expose cached subscription data from persistent - `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 + +**`unraid://logs/stream`** — Live log file tail (path controlled by `UNRAID_AUTOSTART_LOG_PATH`) > **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 **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 | 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 - -```bash -# System monitoring -/info overview -/health check -/storage shares - -# Container management -/docker list -/docker start plex - -# VM operations -/vm list -/vm start windows-10 - -# Notifications -/notifications list -/notifications archive_all - -# API key management -/keys list -/keys create "Automation Key" "For CI/CD" -``` - -### Command Features - -Each slash command provides: -- **Comprehensive documentation** of all available actions -- **Argument hints** for required parameters -- **Safety warnings** for destructive operations (⚠️) -- **Usage examples** for common scenarios -- **Action categorization** (Query, Lifecycle, Management, Destructive) - -Run any command without arguments to see full documentation, or type `/help` to list all available commands. +> **`log_tail` and `notification_feed`** are accessible as tool subactions (`unraid(action="live", subaction="log_tail")`) but are not registered as MCP resources — they use transient one-shot subscriptions and require parameters. --- @@ -362,6 +312,8 @@ Run any command without arguments to see full documentation, or type `/help` to unraid-mcp/ ├── unraid_mcp/ # Main package │ ├── main.py # Entry point +│ ├── server.py # FastMCP server setup +│ ├── version.py # Version management (importlib.metadata) │ ├── config/ # Configuration management │ │ ├── settings.py # Environment & settings │ │ └── logging.py # Logging setup @@ -369,18 +321,34 @@ unraid-mcp/ │ │ ├── client.py # GraphQL client │ │ ├── exceptions.py # Custom exceptions │ │ ├── guards.py # Destructive action guards -│ │ └── types.py # Shared data types +│ │ ├── setup.py # Interactive credential setup +│ │ ├── types.py # Shared data types +│ │ └── utils.py # Utility functions │ ├── subscriptions/ # Real-time subscriptions │ │ ├── manager.py # Persistent WebSocket manager │ │ ├── resources.py # MCP resources (unraid://live/*) │ │ ├── snapshot.py # Transient subscribe_once helpers -│ │ └── diagnostics.py # Diagnostic tools -│ ├── 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 +│ │ ├── queries.py # Subscription query constants +│ │ ├── diagnostics.py # Diagnostic tools +│ │ └── utils.py # Subscription utility functions +│ └── tools/ # Single consolidated tool (~108 actions) +│ └── unraid.py # All 15 domains in one file +├── tests/ # Test suite +│ ├── conftest.py # Shared fixtures +│ ├── test_*.py # Unit tests (per domain) +│ ├── http_layer/ # httpx-level request tests +│ ├── integration/ # WebSocket lifecycle tests +│ ├── safety/ # Destructive action guard tests +│ └── schema/ # GraphQL query validation +├── docs/ # Documentation & API references +├── scripts/ # Build and utility scripts +├── skills/unraid/ # Claude skill assets +├── .claude-plugin/ # Plugin manifest & marketplace config +├── .env.example # Environment template +├── Dockerfile # Container image definition +├── docker-compose.yml # Docker Compose deployment +├── pyproject.toml # Project config & dependencies +└── logs/ # Log files (auto-created, gitignored) ``` ### Code Quality Commands diff --git a/skills/unraid/references/api-reference.md b/skills/unraid/references/api-reference.md index 93e353e..1a5759a 100644 --- a/skills/unraid/references/api-reference.md +++ b/skills/unraid/references/api-reference.md @@ -1,7 +1,7 @@ -> **⚠️ 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 +> **⚠️ 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. + **Tested on:** Unraid 7.2 x86_64 **Date:** 2026-01-21 **API Type:** GraphQL diff --git a/skills/unraid/references/quick-reference.md b/skills/unraid/references/quick-reference.md index c7d04f0..d39b8d6 100644 --- a/skills/unraid/references/quick-reference.md +++ b/skills/unraid/references/quick-reference.md @@ -5,7 +5,8 @@ All operations use: `unraid(action="", subaction="", [params] ## Most Common Operations ### Health & Status -``` + +```python 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 @@ -15,7 +16,8 @@ unraid(action="system", subaction="online") # Online status ``` ### Array & Disks -``` + +```python 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 @@ -25,14 +27,17 @@ unraid(action="array", subaction="stop_array", confirm=True) # ⚠️ Stop ``` ### Logs -``` -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 + +```python +unraid(action="disk", subaction="log_files") # List available logs +unraid(action="disk", subaction="logs", log_path="syslog", tail_lines=50) # Read syslog +unraid(action="disk", subaction="logs", log_path="/var/log/syslog") # Full path also works +unraid(action="live", subaction="log_tail", log_path="/var/log/syslog") # Live tail ``` ### Docker Containers -``` + +```python unraid(action="docker", subaction="list") unraid(action="docker", subaction="details", container_id="plex") unraid(action="docker", subaction="start", container_id="nginx") @@ -42,7 +47,8 @@ unraid(action="docker", subaction="networks") ``` ### Virtual Machines -``` + +```python unraid(action="vm", subaction="list") unraid(action="vm", subaction="details", vm_id="") unraid(action="vm", subaction="start", vm_id="") @@ -52,37 +58,41 @@ unraid(action="vm", subaction="force_stop", vm_id="", confirm=True) # ⚠ ``` ### Notifications -``` + +```python unraid(action="notification", subaction="overview") -unraid(action="notification", subaction="unread") -unraid(action="notification", subaction="list", filter="UNREAD", limit=10) +unraid(action="notification", subaction="list", list_type="UNREAD", limit=10) unraid(action="notification", subaction="archive", notification_id="") unraid(action="notification", subaction="create", title="Test", subject="Subject", description="Body", importance="normal") ``` ### API Keys -``` + +```python unraid(action="key", subaction="list") unraid(action="key", subaction="create", name="my-key", roles=["viewer"]) unraid(action="key", subaction="delete", key_id="", confirm=True) # ⚠️ ``` ### Plugins -``` + +```python unraid(action="plugin", subaction="list") unraid(action="plugin", subaction="add", names=["community.applications"]) unraid(action="plugin", subaction="remove", names=["old.plugin"], confirm=True) # ⚠️ ``` ### rclone -``` + +```python unraid(action="rclone", subaction="list_remotes") unraid(action="rclone", subaction="delete_remote", name="", confirm=True) # ⚠️ ``` ### Live Subscriptions (real-time) -``` + +```python unraid(action="live", subaction="cpu") unraid(action="live", subaction="memory") unraid(action="live", subaction="parity_progress") @@ -90,6 +100,7 @@ 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. --- diff --git a/skills/unraid/references/troubleshooting.md b/skills/unraid/references/troubleshooting.md index c040075..2e99703 100644 --- a/skills/unraid/references/troubleshooting.md +++ b/skills/unraid/references/troubleshooting.md @@ -5,7 +5,8 @@ **Error:** `CredentialsNotConfiguredError` or message containing `~/.unraid-mcp/.env` **Fix:** Run setup to configure credentials interactively: -``` + +```python unraid(action="health", subaction="setup") ``` @@ -20,12 +21,14 @@ This writes `UNRAID_API_URL` and `UNRAID_API_KEY` to `~/.unraid-mcp/.env`. Re-ru **Diagnostic steps:** 1. Test basic connectivity: -``` + +```python unraid(action="health", subaction="test_connection") ``` 2. Full diagnostic report: -``` + +```python unraid(action="health", subaction="diagnose") ``` @@ -57,7 +60,8 @@ unraid(action="health", subaction="diagnose") **Error:** `Action 'X' was not confirmed. Re-run with confirm=True to bypass elicitation.` **Fix:** Add `confirm=True` to the call: -``` + +```python unraid(action="array", subaction="stop_array", confirm=True) unraid(action="vm", subaction="force_stop", vm_id="", confirm=True) ``` diff --git a/tests/mcporter/test-tools.sh b/tests/mcporter/test-tools.sh index 973b84b..0b43d1d 100755 --- a/tests/mcporter/test-tools.sh +++ b/tests/mcporter/test-tools.sh @@ -2,7 +2,7 @@ # ============================================================================= # test-tools.sh — Integration smoke-test for unraid-mcp MCP server tools # -# Exercises every non-destructive action using the consolidated `unraid` tool +# Exercises broad non-destructive smoke coverage of the consolidated `unraid` tool # (action + subaction pattern). The server is launched ad-hoc via mcporter's # --stdio flag so no persistent process or registered server entry is required. # @@ -298,6 +298,24 @@ skip_test() { SKIP_COUNT=$(( SKIP_COUNT + 1 )) } +# --------------------------------------------------------------------------- +# Safe JSON payload builder +# Usage: _json_payload '' key1=value1 key2=value2 ... +# Uses jq --arg to safely encode shell values into JSON, preventing injection +# via special characters in variable values (e.g., quotes, backslashes). +# --------------------------------------------------------------------------- +_json_payload() { + local template="${1:?template required}"; shift + local jq_args=() + local pair k v + for pair in "$@"; do + k="${pair%%=*}" + v="${pair#*=}" + jq_args+=(--arg "$k" "$v") + done + jq -n "${jq_args[@]}" "$template" +} + # --------------------------------------------------------------------------- # ID extractors # --------------------------------------------------------------------------- @@ -441,9 +459,9 @@ suite_system() { ups_id="$(get_ups_id)" || ups_id='' if [[ -n "${ups_id}" ]]; then run_test "system: ups_device" \ - "$(printf '{"action":"system","subaction":"ups_device","device_id":"%s"}' "${ups_id}")" + "$(_json_payload '{"action":"system","subaction":"ups_device","device_id":$v}' v="${ups_id}")" run_test "system: ups_config" \ - "$(printf '{"action":"system","subaction":"ups_config","device_id":"%s"}' "${ups_id}")" + "$(_json_payload '{"action":"system","subaction":"ups_config","device_id":$v}' v="${ups_id}")" else skip_test "system: ups_device" "no UPS devices found" skip_test "system: ups_config" "no UPS devices found" @@ -469,7 +487,7 @@ suite_disk() { disk_id="$(get_disk_id)" || disk_id='' if [[ -n "${disk_id}" ]]; then run_test "disk: disk_details" \ - "$(printf '{"action":"disk","subaction":"disk_details","disk_id":"%s"}' "${disk_id}")" + "$(_json_payload '{"action":"disk","subaction":"disk_details","disk_id":$v}' v="${disk_id}")" else skip_test "disk: disk_details" "no disks found" fi @@ -478,7 +496,7 @@ suite_disk() { log_path="$(get_log_path)" || log_path='' if [[ -n "${log_path}" ]]; then run_test "disk: logs" \ - "$(printf '{"action":"disk","subaction":"logs","log_path":"%s","tail_lines":20}' "${log_path}")" + "$(_json_payload '{"action":"disk","subaction":"logs","log_path":$v,"tail_lines":20}' v="${log_path}")" else skip_test "disk: logs" "no log files found" fi @@ -495,7 +513,7 @@ suite_docker() { container_id="$(get_docker_id)" || container_id='' if [[ -n "${container_id}" ]]; then run_test "docker: details" \ - "$(printf '{"action":"docker","subaction":"details","container_id":"%s"}' "${container_id}")" + "$(_json_payload '{"action":"docker","subaction":"details","container_id":$v}' v="${container_id}")" else skip_test "docker: details" "no containers found" fi @@ -504,7 +522,7 @@ suite_docker() { network_id="$(get_network_id)" || network_id='' if [[ -n "${network_id}" ]]; then run_test "docker: network_details" \ - "$(printf '{"action":"docker","subaction":"network_details","network_id":"%s"}' "${network_id}")" + "$(_json_payload '{"action":"docker","subaction":"network_details","network_id":$v}' v="${network_id}")" else skip_test "docker: network_details" "no networks found" fi @@ -520,7 +538,7 @@ suite_vm() { vm_id="$(get_vm_id)" || vm_id='' if [[ -n "${vm_id}" ]]; then run_test "vm: details" \ - "$(printf '{"action":"vm","subaction":"details","vm_id":"%s"}' "${vm_id}")" + "$(_json_payload '{"action":"vm","subaction":"details","vm_id":$v}' v="${vm_id}")" else skip_test "vm: details" "no VMs found (or VM service unavailable)" fi @@ -558,7 +576,7 @@ suite_key() { key_id="$(get_key_id)" || key_id='' if [[ -n "${key_id}" ]]; then run_test "key: get" \ - "$(printf '{"action":"key","subaction":"get","key_id":"%s"}' "${key_id}")" + "$(_json_payload '{"action":"key","subaction":"get","key_id":$v}' v="${key_id}")" else skip_test "key: get" "no API keys found" fi