Files
unraid-mcp/CLAUDE.md

11 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

Environment Setup

Copy .env.example to .env and configure:

Required:

  • UNRAID_API_URL: Unraid GraphQL endpoint
  • UNRAID_API_KEY: Unraid API key

Server:

  • 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; set false for 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

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
    • guards.py — destructive action gating via MCP elicitation
    • utils.py — shared helpers (safe_get, safe_display_url, path validation)
    • setup.py — elicitation-based credential setup flow
  • 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
  • Version Helper: unraid_mcp/version.py - Reads version from package metadata via importlib

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: QUERIES and MUTATIONS dicts prevent GraphQL injection and organize operations
  • Destructive Action Safety: DESTRUCTIVE_ACTIONS sets require confirm=True for 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: live action subactions use a shared SubscriptionManager that maintains persistent WebSocket connections. Resources serve cached data via subscription_manager.get_resource_data(action). A "connecting" placeholder is returned while the subscription starts — callers should retry in a moment. When UNRAID_AUTO_START_SUBSCRIPTIONS=false, resources fall back to on-demand subscribe_once.

Tool Categories (3 Tools: 1 Primary + 2 Diagnostic)

The server registers 3 MCP tools:

  • unraid — primary tool with action (domain) + subaction (operation) routing, 107 subactions. Call it as unraid(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
  • notification: delete, delete_archived
  • rclone: delete_remote
  • key: delete
  • disk: flash_backup
  • setting: configure_ups
  • plugin: remove

Environment Variable Hierarchy

The server loads environment variables from multiple locations in order:

  1. ~/.unraid-mcp/.env (primary — canonical credentials dir, all runtimes)
  2. ~/.unraid-mcp/.env.local (local overrides, only used if primary is absent)
  3. ../.env.local (project root local overrides)
  4. ../.env (project root fallback)
  5. unraid_mcp/.env (last resort)

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

Middleware Chain

server.py wraps all tools in a 5-layer stack (order matters — outermost first):

  1. LoggingMiddleware — logs every tools/call and resources/read with duration
  2. ErrorHandlingMiddleware — converts unhandled exceptions to proper MCP errors
  3. SlidingWindowRateLimitingMiddleware — 540 req/min sliding window
  4. ResponseLimitingMiddleware — truncates responses > 512 KB with a clear suffix
  5. ResponseCachingMiddleware — caching disabled entirely for unraid tool (mutations and reads share one tool name, so no per-subaction exclusion is possible)

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's mock_graphql_request patches the core module — wrong for tool-level tests
  • Use conftest.py's make_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 (119 tests)
├── contract/             # Response shape contract tests
└── property/             # Input validation property-based tests

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 reference
  • docs/UNRAID_API_OPERATIONS.md — All supported operations with examples
  • docs/MARKETPLACE.md — Plugin marketplace listing and publishing guide
  • docs/PUBLISHING.md — Step-by-step instructions for publishing to Claude plugin registry

Use these when adding new queries/mutations.

Version Bumps

When bumping the version, always update both files — they must stay in sync:

  • pyproject.tomlversion = "X.Y.Z" under [project]
  • .claude-plugin/plugin.json"version": "X.Y.Z"

Credential Storage (~/.unraid-mcp/.env)

All runtimes (plugin, direct uv run) load credentials from ~/.unraid-mcp/.env.

  • Plugin/direct: unraid action=health subaction=setup writes 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/.env then edit.
  • 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).

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