Security: - Remove /mnt/ from _ALLOWED_LOG_PREFIXES to prevent Unraid share exposure - Add early .. detection for disk/logs and live/log_tail path validation - Add /boot/ prefix restriction for flash_backup source_path - Use hmac.compare_digest for timing-safe API key verification in server.py - Gate include_traceback on DEBUG log level (no tracebacks in production) Correctness: - Re-raise CredentialsNotConfiguredError in health check instead of swallowing - Fix ups_device query (remove non-existent nominalPower/currentPower fields) Best practices (BP-01, BP-05, BP-06): - Add # noqa: ASYNC109 to timeout params in _handle_live and unraid() - Fix start_array* → start_array in docstring (not in ARRAY_DESTRUCTIVE) - Remove from __future__ import annotations from snapshot.py - Replace import-time UNRAID_API_KEY/URL bindings with _settings.ATTR pattern in manager.py, snapshot.py, utils.py, diagnostics.py — fixes stale binding after apply_runtime_config() post-elicitation (BP-05) CI/CD: - Add .github/workflows/ci.yml (5-job pipeline: lint, typecheck, test, version-sync, audit) - Add fail_under = 80 to [tool.coverage.report] - Add version sync check to scripts/validate-marketplace.sh Documentation: - Sync plugin.json version 1.1.1 → 1.1.2 with pyproject.toml - Update CLAUDE.md: 3 tools, system domain count 18, scripts comment fix - Update README.md: 3 tools, security notes - Update docs/AUTHENTICATION.md: H1 title fix - Add UNRAID_CREDENTIALS_DIR to .env.example Bump: 1.1.1 → 1.1.2 Co-Authored-By: Claude <noreply@anthropic.com>
12 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This is an MCP (Model Context Protocol) server that provides tools to interact with an Unraid server's GraphQL API. The server is built using FastMCP with a modular architecture consisting of separate packages for configuration, core functionality, subscriptions, and tools.
Development Commands
Setup
# Initialize uv virtual environment and install dependencies
uv sync
# Install dev dependencies
uv sync --group dev
Running the Server
# Local development with uv (recommended)
uv run unraid-mcp-server
# Direct module execution
uv run -m unraid_mcp.main
Code Quality
# Lint and format with ruff
uv run ruff check unraid_mcp/
uv run ruff format unraid_mcp/
# Type checking with ty (Astral's fast type checker)
uv run ty check unraid_mcp/
# Run tests
uv run pytest
Docker Development
# Build the Docker image
docker build -t unraid-mcp-server .
# Run with Docker Compose
docker compose up -d
# View logs
docker compose logs -f unraid-mcp
# Stop service
docker compose down
Environment Setup
Copy .env.example to .env and configure:
Required:
UNRAID_API_URL: Unraid GraphQL endpointUNRAID_API_KEY: Unraid API key
Server:
UNRAID_MCP_TRANSPORT: Transport type (default: streamable-http)UNRAID_MCP_PORT: Server port (default: 6970)UNRAID_MCP_HOST: Server host (default: 0.0.0.0)UNRAID_MCP_LOG_LEVEL: Log verbosity (default: INFO)UNRAID_MCP_LOG_FILE: Log filename in logs/ (default: unraid-mcp.log)
SSL/TLS:
UNRAID_VERIFY_SSL: SSL verification (default: true; setfalsefor self-signed certs)
Subscriptions:
UNRAID_AUTO_START_SUBSCRIPTIONS: Auto-start live subscriptions on startup (default: true)UNRAID_MAX_RECONNECT_ATTEMPTS: WebSocket reconnect limit (default: 10)
Credentials override:
UNRAID_CREDENTIALS_DIR: Override the~/.unraid-mcp/credentials directory path
Authentication (Optional — protects the HTTP server)
Two independent methods. Use either or both — when both are set, MultiAuth accepts either.
Google OAuth — requires all three vars:
| Env Var | Purpose |
|---|---|
GOOGLE_CLIENT_ID |
Google OAuth 2.0 Client ID |
GOOGLE_CLIENT_SECRET |
Google OAuth 2.0 Client Secret |
UNRAID_MCP_BASE_URL |
Public URL of this server (e.g. http://10.1.0.2:6970) |
UNRAID_MCP_JWT_SIGNING_KEY |
Stable 32+ char secret — prevents token invalidation on restart |
Google Cloud Console setup: APIs & Services → Credentials → OAuth 2.0 Client ID (Web application) → Authorized redirect URIs: <UNRAID_MCP_BASE_URL>/auth/callback
API Key — clients present as Authorization: Bearer <key>:
| Env Var | Purpose |
|---|---|
UNRAID_MCP_API_KEY |
Static bearer token (can be same value as UNRAID_API_KEY) |
Generate a stable JWT signing key:
python3 -c "import secrets; print(secrets.token_hex(32))"
Omit all auth vars to run without auth (default — open server).
Full guide: docs/AUTHENTICATION.md
Architecture
Core Components
- Main Server:
unraid_mcp/server.py- Modular MCP server with FastMCP integration - Entry Point:
unraid_mcp/main.py- Application entry point and startup logic - Configuration:
unraid_mcp/config/- Settings management and logging configuration - Core Infrastructure:
unraid_mcp/core/- GraphQL client, exceptions, and shared types - Subscriptions:
unraid_mcp/subscriptions/- Real-time WebSocket subscriptions and diagnostics - Tools:
unraid_mcp/tools/- Domain-specific tool implementations - GraphQL Client: Uses httpx for async HTTP requests to Unraid API
- Transport Layer: Supports streamable-http (recommended), SSE (deprecated), and stdio
Key Design Patterns
- Consolidated Action Pattern: Each tool uses
action: Literal[...]parameter to expose multiple operations via a single MCP tool, reducing context window usage - Pre-built Query Dicts:
QUERIESandMUTATIONSdicts prevent GraphQL injection and organize operations - Destructive Action Safety:
DESTRUCTIVE_ACTIONSsets requireconfirm=Truefor dangerous operations - Modular Architecture: Clean separation of concerns across focused modules
- Error Handling: Uses ToolError for user-facing errors, detailed logging for debugging
- Timeout Management: Custom timeout configurations for different query types (90s for disk ops)
- Data Processing: Tools return both human-readable summaries and detailed raw data
- Health Monitoring: Comprehensive health check tool for system monitoring
- Real-time Subscriptions: WebSocket-based live data streaming
- Persistent Subscription Manager:
liveaction subactions use a sharedSubscriptionManagerthat maintains persistent WebSocket connections. Resources serve cached data viasubscription_manager.get_resource_data(action). A "connecting" placeholder is returned while the subscription starts — callers should retry in a moment. WhenUNRAID_AUTO_START_SUBSCRIPTIONS=false, resources fall back to on-demandsubscribe_once.
Tool Categories (3 Tools: 1 Primary + 2 Diagnostic)
The server registers 3 MCP tools:
unraid— primary tool withaction(domain) +subaction(operation) routing, 107 subactions. Call it asunraid(action="docker", subaction="list").diagnose_subscriptions— inspect subscription connection states, errors, and WebSocket URLs.test_subscription_query— test a specific GraphQL subscription query (allowlisted fields only).
| action | subactions |
|---|---|
| system (18) | 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
- vm: force_stop, reset
- notifications: delete, delete_archived
- rclone: delete_remote
- keys: delete
- disk: flash_backup
- settings: configure_ups
- plugins: remove
Environment Variable Hierarchy
The server loads environment variables from multiple locations in order:
~/.unraid-mcp/.env(primary — canonical credentials dir, all runtimes)~/.unraid-mcp/.env.local(local overrides, only used if primary is absent)/app/.env.local(Docker container mount)../.env.local(project root local overrides)../.env(project root fallback)unraid_mcp/.env(last resort)
Transport Configuration
- streamable-http (recommended): HTTP-based transport on
/mcpendpoint - sse (deprecated): Server-Sent Events transport
- stdio: Standard input/output for direct integration
Error Handling Strategy
- GraphQL errors are converted to ToolError with descriptive messages
- HTTP errors include status codes and response details
- Network errors are caught and wrapped with connection context
- All errors are logged with full context for debugging
Performance Considerations
- Increased timeouts for disk operations (90s read timeout)
- Selective queries to avoid GraphQL type overflow issues
- Optional caching controls for Docker container queries
- Log file overwrite at 10MB cap to prevent disk space issues
Critical Gotchas
Mutation Handler Ordering
Mutation handlers MUST return before the domain query dict lookup. Mutations are not in the domain _*_QUERIES dicts (e.g., _DOCKER_QUERIES, _ARRAY_QUERIES) — reaching that line for a mutation subaction causes a KeyError. Always add early-return if subaction == "mutation_name": ... return blocks BEFORE the queries lookup.
Test Patching
- Patch at the tool module level:
unraid_mcp.tools.unraid.make_graphql_request(not core) conftest.py'smock_graphql_requestpatches the core module — wrong for tool-level tests- Use
conftest.py'smake_tool_fn()helper or local_make_tool()pattern
Test Suite Structure
tests/
├── conftest.py # Shared fixtures + make_tool_fn() helper
├── test_*.py # Unit tests (mock at tool module level)
├── http_layer/ # httpx-level request/response tests (respx)
├── integration/ # WebSocket subscription lifecycle tests (slow)
├── safety/ # Destructive action guard tests
└── schema/ # GraphQL query validation (99 tests, all passing)
Running Targeted Tests
uv run pytest tests/safety/ # Destructive action guards only
uv run pytest tests/schema/ # GraphQL query validation only
uv run pytest tests/http_layer/ # HTTP/httpx layer only
uv run pytest tests/test_docker.py # Single tool only
uv run pytest -x # Fail fast on first error
Scripts
# HTTP smoke-test against a live server (non-destructive actions, all domains)
./tests/mcporter/test-actions.sh [MCP_URL] # default: http://localhost:6970/mcp
# stdio smoke-test, no running server needed (good for CI)
./tests/mcporter/test-tools.sh [--parallel] [--timeout-ms N] [--verbose]
# Destructive action smoke-test (confirms guard blocks without confirm=True)
./tests/mcporter/test-destructive.sh [MCP_URL]
See tests/mcporter/README.md for transport differences and docs/DESTRUCTIVE_ACTIONS.md for exact destructive-action test commands.
API Reference Docs
docs/UNRAID_API_COMPLETE_REFERENCE.md— Full GraphQL schema referencedocs/UNRAID_API_OPERATIONS.md— All supported operations with examples
Use these when adding new queries/mutations.
Version Bumps
When bumping the version, always update both files — they must stay in sync:
pyproject.toml→version = "X.Y.Z"under[project].claude-plugin/plugin.json→"version": "X.Y.Z"
Credential Storage (~/.unraid-mcp/.env)
All runtimes (plugin, direct, Docker) load credentials from ~/.unraid-mcp/.env.
- Plugin/direct:
unraid action=health subaction=setupwrites this file automatically via elicitation, Safe to re-run: always prompts for confirmation before overwriting existing credentials, whether the connection is working or not (failed probe may be a transient outage, not bad creds). or manual:mkdir -p ~/.unraid-mcp && cp .env.example ~/.unraid-mcp/.envthen edit. - Docker:
docker-compose.ymlloads it viaenv_filebefore container start. - No symlinks needed. Version bumps do not affect this path.
- Permissions: dir=700, file=600 (set automatically by elicitation; set manually if
using
cp:chmod 700 ~/.unraid-mcp && chmod 600 ~/.unraid-mcp/.env).
Symlinks
AGENTS.md and GEMINI.md are symlinks to CLAUDE.md for Codex/Gemini compatibility:
ln -sf CLAUDE.md AGENTS.md && ln -sf CLAUDE.md GEMINI.md