Commit graph

140 commits

Author SHA1 Message Date
Jacob Magar
f4adb4e8b9 fix: address PR #12 review comments on refactored tool files
- Add 'logs' to _DOCKER_SUBACTIONS so the validation guard passes through
  to the informative ToolError rather than a generic 'Invalid action' (P1)
- Add inline comments to _SYSTEM_QUERIES explaining that 'network' and
  'variables' share the vars root field but fetch different subfields (P3)
- Process system/server response into a structured summary dict instead
  of returning raw GraphQL data directly (P3)
2026-04-06 15:24:16 -04:00
Jacob Magar
c2d7f05e5d fix(ci): ruff format _docker.py and test_generate_unraid_api_reference.py 2026-04-05 09:23:47 -04:00
Jacob Magar
55c5d633cc fix(ci): fix ruff errors with noqa directives; lower coverage threshold to 70% 2026-04-05 09:11:34 -04:00
Jacob Magar
4a24e98585 fix(ci): fix ty type errors with cast() and type: ignore for ASGIMiddleware calls 2026-04-05 09:02:50 -04:00
Jacob Magar
c39b05277c fix(ci): add missing .dockerignore entries, replace uv audit with uvx pip-audit, upgrade trivy-action 2026-04-05 08:17:11 -04:00
Jacob Magar
391463b942 docs: comprehensive documentation and version sync (v1.2.4) 2026-04-05 03:34:19 -04:00
Jacob Magar
306f364c4e fix: exempt OAuth discovery endpoint from auth, add authentication docs (closes #17)
BearerAuthMiddleware was blocking GET /.well-known/oauth-protected-resource,
causing Claude Code to cascade into failed OAuth discovery after a 401 and
surface a generic "unknown error". Add WellKnownMiddleware (RFC 9728) placed
before BearerAuthMiddleware that returns resource metadata with
bearer_methods_supported=["header"] and no authorization_servers, telling
MCP clients to use a pre-configured Bearer token with no OAuth flow.

Also adds docs/AUTHENTICATION.md and the missing README ## Authentication
section documenting token generation and the exact client config format.

Bump: 1.2.0 → 1.2.1
2026-04-03 07:03:18 -04:00
Jacob Magar
1ff2268fe2 ci: add package registry publish workflow (tag-triggered) 2026-04-03 02:19:27 -04:00
Jacob Magar
2feb900aeb fix: add unraid_help tool, SWAG conf
- Add `unraid_help` tool to `register_unraid_tool()` with full markdown
  reference table for all 15 actions and subactions
- Add `unraid.subdomain.conf` SWAG reverse-proxy config pointing to
  unraid-mcp:6970 (UNRAID_MCP_PORT default)
- Both changes required to pass lint-plugin.sh checks 7 and 14
2026-04-01 02:17:59 -04:00
Jacob Magar
ae55b5b7e0 feat: improve auth, server, subscriptions, tools, and add regression tests 2026-03-31 17:14:30 -04:00
Jacob Magar
74ec92e094 fix(subscriptions): deduplicate GraphQL error log spam for array_state
arraySubscription in the Unraid API publishes with key { array } but
NestJS resolves payload["arraySubscription"] (method name), yielding
null on a non-nullable field. This is an upstream API bug — the
resolver lacks `resolve: (v) => v.array` unlike the working metrics
subscriptions.

On our side: downgrade from ERROR to WARNING on first occurrence, then
suppress repeats to DEBUG with periodic reminders at 10/100/1000.
Prevents log flooding while preserving visibility.
2026-03-31 10:44:44 -04:00
Jacob Magar
c393092b4f fix(docker): healthcheck 401 loop, credential persistence, startup log clarity
- Add HealthMiddleware (outside BearerAuth) so GET /health bypasses auth;
  Docker healthcheck no longer 401s and triggers restart loop
- Pre-create /home/mcp/.unraid-mcp in Dockerfile with mcp:mcp ownership
  so named volume mounts inherit correct permissions; bearer token now
  persists across container restarts
- Remove custom SIGTERM/SIGINT handlers that silently swallowed signals;
  Uvicorn manages its own shutdown
- Distinguish stdio vs HTTP startup log (was always showing host:port for stdio)
- Move _chmod_safe to module level; convert f-strings to %-format in logger calls
- Expand .dockerignore to exclude test/doc/tooling files from image
2026-03-30 23:52:04 -04:00
Jacob Magar
edece9e205 fix(unraid-mcp-78s,92j,37t,696,6cr,4yz): P1/P2/P3 sweep — fix HealthMiddleware + signal handlers
- P1 (78s): _HEADERS is now an immutable tuple; content-length derived from
  len(_BODY) rather than a hardcoded magic number
- P1 (92j): Remove no-op SIGTERM/SIGINT handlers that swallowed signals without
  stopping the server; delegate shutdown to Uvicorn's built-in handlers
- P2 (37t): HealthMiddleware now only responds 200 to GET /health; all other
  methods fall through to the auth layer (returns 401)
- P2 (696): Extract _chmod_safe() helper; remove redundant second chmod block
  in ensure_token_exists()
- P3 (6cr): Reorder middleware — HealthMiddleware is now outermost so it
  intercepts /health before BearerAuth; removes the need for a bypass condition
  in BearerAuthMiddleware.__call__
- P3 (4yz): Add Scope/Receive/Send type hints to HealthMiddleware.__call__
2026-03-30 23:39:52 -04:00
Jacob Magar
a5e1c30ae9 feat(unraid-mcp-1nx): add HTTP bearer token auth, make streamable-http default (v1.2.0)
- BearerAuthMiddleware: pure ASGI __call__ pattern (no BaseHTTPMiddleware overhead),
  pre-encoded token bytes, pre-built 401/429 bodies, per-IP rate limiting
  (60 failures/60s → 429), log throttling (30s/IP), RFC 6750 compliant headers
- Token lifecycle: auto-generate secrets.token_urlsafe(32) on first HTTP startup,
  write to ~/.unraid-mcp/.env via dotenv.set_key, print once to STDERR,
  pop from os.environ after storing in module global
- Startup guard: sys.exit(1) if HTTP + no token + DISABLE_HTTP_AUTH not set
- Escape hatch: UNRAID_MCP_DISABLE_HTTP_AUTH=true for gateway-delegated auth
- Default transport: stdio → streamable-http (breaking change)
- 23 new tests covering pass-through, 401/429, RFC 6750 headers, rate limiting,
  token generation, startup guard

BREAKING CHANGE: default transport is now streamable-http; stdio users must set
UNRAID_MCP_TRANSPORT=stdio (Claude Desktop plugin unaffected — plugin.json hardcodes stdio)
2026-03-30 10:01:34 -04:00
Jacob Magar
3142897402 fix(unraid-mcp-n7i): flash_backup path traversal fix + v1.1.6 bump 2026-03-30 09:35:21 -04:00
Jacob Magar
dfc6d5118e fix(unraid-mcp-4iw,c8m,0uv,14g,1n2,6uw,alm,cad): P2/P3 sweep — lock docs, middleware import, live assertion, validation tests 2026-03-30 00:48:48 -04:00
Jacob Magar
d01dcf1c67 fix(unraid-mcp-478,cdd,5rl,ex9,4ea,dx9): manager.py lock/guard fixes and _setting.py comment 2026-03-30 00:10:30 -04:00
Jacob Magar
008a2151e5 fix: address all PR #15 review comments (threads 1-13)
Resolves review threads PRRT_kwDOO6Hdxs53kmxT PRRT_kwDOO6Hdxs53kmxV
PRRT_kwDOO6Hdxs53kmxW PRRT_kwDOO6Hdxs53kmxY PRRT_kwDOO6Hdxs53kmnR
PRRT_kwDOO6Hdxs53kmnT PRRT_kwDOO6Hdxs53kmgx PRRT_kwDOO6Hdxs53kmgr
PRRT_kwDOO6Hdxs53kmWj PRRT_kwDOO6Hdxs53kmWn PRRT_kwDOO6Hdxs53kmWv
PRRT_kwDOO6Hdxs53kmWV

- validation.py: expand DANGEROUS_KEY_PATTERN with &<>'"# chars (thread 1)
- CLAUDE.md: update middleware stack docs from 5-layer to 4-layer (thread 2)
- manager.py _start_one: add CancelledError guard before bare except (threads 3, 12)
- _disk.py audit log: include destination_path in flash_backup trace (thread 4)
- _health.py: catch ToolError wrapping httpx errors for degraded-state health check (threads 5, 8, 9)
- manager.py init JSON decode: raise RuntimeError instead of continue to preserve reconnect backoff (threads 6, 7)
- _disk.py: replace os.path.normpath with posixpath.normpath for remote Linux paths (thread 10)
- unraid.py: guard error_middleware None check before calling get_error_stats() (thread 13)
2026-03-29 23:45:16 -04:00
Jacob Magar
2401b85ce5 fix(unraid-mcp-7jm): address P1/P2 findings from post-review sweep
P1 — correctness:
- fix docstring in get_subscription_status: ABBA deadlock explanation was
  factually wrong (cited pong send; actual reason is _subscription_loop
  acquires _task_lock at loop exit while _data_lock may be held)
- surface start_errors after auto_start TaskGroup: individual failures were
  logged but the summary WARNING with failed names was missing
- fix stop_subscription deadlock: lock → snapshot → release → await pattern;
  was holding _task_lock across await task which blocked _subscription_loop
  cleanup path

P2 — safety / encapsulation:
- add SubscriptionManager.get_error_state() public accessor replacing direct
  _task_lock access in resources.py (fixes encapsulation + partially improves
  read serialisation)
- extract DANGEROUS_KEY_PATTERN / MAX_VALUE_LENGTH to core/validation.py;
  update _rclone.py and _setting.py to import from shared module
- _validate_settings_input: reject dict/list values with ToolError instead
  of repr() size check; aligns with _validate_rclone_config behaviour
- flash_backup destination_path: add null-byte check + normpath-based
  traversal check (was using raw ".." substring — bypassable via foo/bar/../..)
- middleware_refs.py: use TYPE_CHECKING guard + ErrorHandlingMiddleware | None
  annotation instead of Any

P3 — minor correctness:
- _validate_path: split on '/' not os.sep (paths are remote Linux paths)
- DANGEROUS_KEY_PATTERN: add [\x00-\x1f] to reject newlines/control chars

Update tests/test_resources.py to mock get_error_state() AsyncMock instead
of last_error/connection_states dicts (909 tests pass).
2026-03-29 03:00:00 -04:00
Jacob Magar
574ac03251 fix(unraid-mcp-ilg.4): break server.py ↔ tools/unraid.py circular import
Introduce core/middleware_refs.py as a neutral intermediary. server.py sets
middleware_refs.error_middleware = _error_middleware after creating the
instance. tools/unraid.py imports from middleware_refs (not server), breaking
the circular dependency hidden by the deferred import pattern.

The deferred `from ..server import _error_middleware` inside health/diagnose
worked at runtime but was invisible to static analysers and mypy, and created
a load-order dependency that was impossible to verify without running the server.
2026-03-29 02:08:45 -04:00
Jacob Magar
d008d96a19 fix(unraid-mcp-ilg): resolve 11 code review bugs from ilg P1/P2 sweep
ilg.1  — get_subscription_status: acquire _task_lock and _data_lock separately
         to avoid deadlock with _subscription_loop holding _data_lock across await
ilg.2  — start_subscription: move duplicate-check inside _task_lock (was TOCTOU)
ilg.3  — subscription_loop: change break→continue on JSON decode in init handshake
         (break killed the outer retry loop; transient errors should retry)
ilg.5  — diagnose_subscriptions: apply safe_display_url() to ws_url_display
ilg.6  — setting/update: add _validate_settings_input() with key/value allowlist
         and size bound, modeled on _validate_rclone_config
ilg.7  — resources.py: guard last_error/connection_states reads under _task_lock
ilg.8  — server.py: remove fully-disabled ResponseCachingMiddleware from stack;
         update health/diagnose to emit static cache note instead of stats
ilg.9  — test_subscription_query: use negotiated subprotocol for start_type
         instead of hardcoded "start" (graphql-transport-ws uses "subscribe")
ilg.10 — _validate_path: normalize before checking for ".." (pre-normpath check
         was bypassable); also reject null bytes
ilg.11 — auto_start_all_subscriptions: parallelize with asyncio.TaskGroup
         (sequential for-loop blocked first MCP request for ~370s cold start)
ilg.15 — client.py: remove dead _TIMEOUT_PROFILES dict and
         get_timeout_for_operation() (unused since direct timeout refs replaced them)
2026-03-29 02:03:29 -04:00
Jacob Magar
ae497ed522 fix(unraid-mcp-cat): raise ToolError on null response in disk/logs, user/me, oidc/validate_session and oidc/provider 2026-03-28 21:07:48 -04:00
Jacob Magar
54c66ed9a2 fix(unraid-mcp-vvh): add COLLECT_ACTIONS extensibility guard with constant and assertion test 2026-03-28 20:57:12 -04:00
Jacob Magar
5fd786b1c9 fix(unraid-mcp-i7o): system simple_dict subactions raise ToolError or log warning on null response 2026-03-28 20:57:12 -04:00
Jacob Magar
0250eb3706 fix(unraid-mcp-brw): add logger.info audit trace to flash_backup destructive path 2026-03-28 20:57:12 -04:00
Jacob Magar
7b834f6fd3 fix(unraid-mcp-8vo): add logging to bare except Exception in health/setup connection probe 2026-03-28 20:52:59 -04:00
Jacob Magar
b1c96c273e fix(unraid-mcp-1nr): narrow except Exception in _comprehensive_health_check to connection errors only 2026-03-28 20:52:53 -04:00
Jacob Magar
db6f19e157 refactor: split tools/unraid.py into per-domain modules
Move each domain's constants and handler into its own module (_array.py,
_disk.py, _docker.py, _vm.py, _notification.py, _key.py, _plugin.py,
_rclone.py, _setting.py, _customization.py, _oidc.py, _user.py,
_live.py, _system.py). Each module uses 'from ..core import client as _client'
+ '_client.make_graphql_request()' for module-attribute access, ensuring
test patches at unraid_mcp.core.client intercept all calls regardless of
which domain module is executing.

_health.py holds constants + _comprehensive_health_check helper only;
_handle_health stays in unraid.py because tests patch elicit_and_configure
at unraid_mcp.tools.unraid — keeping it there avoids circular imports
while preserving correct patch interception.

unraid.py becomes a thin dispatcher: imports handlers from domain files,
re-exports all query/mutation dicts and destructive-action sets for tests,
and owns only _handle_health inline.

835 tests passing.
2026-03-28 01:44:46 -04:00
Jacob Magar
fb1118f75c refactor: unify format_kb with format_bytes; remove duplicate notification constants
- format_kb now delegates to format_bytes(kb * 1024) — eliminates the
  duplicate size-formatting loop and fixes a silent truncation at TB
  (the old explicit if-chain could not reach PB or EB)
- Remove _VALID_NOTIF_TYPES (identical duplicate of _VALID_LIST_TYPES);
  update the one reference in _handle_notification to use _VALID_LIST_TYPES
- Remove redundant importance validation in the notification/create branch
  (already validated earlier in _handle_notification before subaction dispatch)

Closes: unraid-mcp-ilg.13, unraid-mcp-ilg.14
2026-03-28 01:31:15 -04:00
Jacob Magar
8573a0f6c7 fix: patch make_graphql_request at core.client to prevent tests hitting live server
All test _mock_graphql fixtures previously patched unraid_mcp.tools.unraid.make_graphql_request
(a local name binding). After a domain split, new module files bind their own local names,
making the tool-level patch ineffective — test mutations reached the real Unraid server and
stopped the array twice.

Fix: switch unraid.py to use module-attribute access (_client.make_graphql_request) via
`from ..core import client as _client`. Update all test patch targets to
unraid_mcp.core.client.make_graphql_request, which intercepts calls regardless of how
many modules import from that namespace.

835 tests passing. Integration test failures are pre-existing and unrelated.
2026-03-28 01:31:05 -04:00
Jacob Magar
e548f6e6c9 refactor: remove Docker and HTTP transport support, fix hypothesis cache directory 2026-03-24 19:22:27 -04:00
Jacob Magar
e68d4a80e4 refactor: simplify path validation and connection_init via shared helpers
- Extract _validate_path() in unraid.py — consolidates traversal check + normpath
  + prefix validation used by disk/logs and live/log_tail into one place
- Extract build_connection_init() in subscriptions/utils.py — removes 4 duplicate
  connection_init payload blocks from snapshot.py (×2), manager.py, diagnostics.py;
  also fixes diagnostics.py bug where x-api-key: None was sent when no key configured
- Remove _LIVE_ALLOWED_LOG_PREFIXES alias — direct reference to _ALLOWED_LOG_PREFIXES
- Move import hmac to module level in server.py (was inside verify_token hot path)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-23 11:57:00 -04:00
Jacob Magar
2b777be927 fix(security): path traversal, timing-safe auth, stale credential bindings
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>
2026-03-23 11:37:05 -04:00
Jacob Magar
cc24f1ec62 feat: add API key bearer token authentication
- ApiKeyVerifier(TokenVerifier) — validates Authorization: Bearer <key>
  against UNRAID_MCP_API_KEY; guards against empty-key bypass
- _build_auth() replaces module-level _build_google_auth() call:
  returns MultiAuth(server=google, verifiers=[api_key]) when both set,
  GoogleProvider alone, ApiKeyVerifier alone, or None
- settings.py: add UNRAID_MCP_API_KEY + is_api_key_auth_configured()
  + api_key_auth_enabled in get_config_summary()
- run_server(): improved auth status logging for all three states
- tests/test_api_key_auth.py: 9 tests covering verifier + _build_auth
- .env.example: add UNRAID_MCP_API_KEY section
- docs/GOOGLE_OAUTH.md: add API Key section
- README.md / CLAUDE.md: rename section, document both auth methods
- Fix pre-existing: test_health.py patched cache_middleware/error_middleware
  now match renamed _cache_middleware/_error_middleware in server.py
2026-03-16 11:11:38 -04:00
Jacob Magar
2ab61be2df feat(auth): wire GoogleProvider into FastMCP, log auth status on startup
- Call _build_google_auth() at module level before mcp = FastMCP(...)
- Pass auth=_google_auth to FastMCP() constructor
- Add startup log in run_server(): INFO when OAuth enabled (with redirect URI), WARNING when open/unauthenticated
- Add test verifying mcp has no auth provider when Google vars are absent (baseline + post-wire)
2026-03-16 10:42:51 -04:00
Jacob Magar
b319cf4932 fix(auth): use dict[str, Any] for kwargs, add typing.Any import 2026-03-16 10:40:29 -04:00
Jacob Magar
4a1ffcfd51 feat(auth): add _build_google_auth() builder with stdio warning
Adds _build_google_auth() to server.py that reads Google OAuth settings
and returns a configured GoogleProvider instance or None when unconfigured.
Includes warning for stdio transport incompatibility and conditional
jwt_signing_key passthrough. 4 new TDD tests in tests/test_auth_builder.py.
2026-03-16 10:36:41 -04:00
Jacob Magar
f69aa94826 feat(dx): add fastmcp.json configs, module-level tool registration, tool timeout
- Add fastmcp.http.json and fastmcp.stdio.json declarative server configs
  for streamable-http (:6970) and stdio transports respectively
- Move register_all_modules() to module level in server.py so
  `fastmcp run server.py --reload` discovers the fully-wired mcp object
  without going through run_server() — tools registered exactly once
- Add timeout=120 to @mcp.tool() decorator as a global safety net;
  any hung subaction returns a clean MCP error instead of hanging forever
- Document fastmcp run --reload, fastmcp list, fastmcp call in README
- Bump version 1.0.1 → 1.1.0

Co-authored-by: Claude <claude@anthropic.com>
2026-03-16 10:32:16 -04:00
Jacob Magar
896fc8db1b feat(auth): add Google OAuth settings with is_google_auth_configured()
Add GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, UNRAID_MCP_BASE_URL, and
UNRAID_MCP_JWT_SIGNING_KEY env vars to settings.py, along with the
is_google_auth_configured() predicate and three new keys in
get_config_summary(). TDD: 4 tests written red-first, all passing green.
2026-03-16 10:28:53 -04:00
Jacob Magar
cf9449a15d fix: address 18 PR review comments (threads 1-18)
Threads 1, 2, 3 — test hygiene:
- Move elicit_and_configure/elicit_reset_confirmation to module-level imports
  in unraid.py so tests can patch at unraid_mcp.tools.unraid.* (thread 2)
- Add return type annotations to _make_tool() in test_customization.py (thread 1)
- Replace unused _mock_ensure_started fixture params with @usefixtures (thread 3)

Thread 4 — remove dead 'connect' subaction from _SYSTEM_QUERIES; the subaction
was always rejected with a ToolError, creating an inconsistent contract.

Thread 5 — centralize two inline "query { online }" strings by reusing
_SYSTEM_QUERIES["online"]; add _DOCKER_QUERIES["_resolve"] for container-name
resolution instead of an inline query literal.

Threads 14, 15, 16, 17, 18 — test improvements:
- test-tools.sh: reword header to "broad non-destructive smoke coverage" (t14)
- test-tools.sh: add _json_payload() helper using jq --arg for safe JSON
  construction; replace all printf-based payloads (thread 15)
- test_input_validation.py: add return type annotations to _make_tool and all
  nested _run_test coroutines (thread 16)
- test_query_validation.py: extract _all_domain_dicts() shared helper to
  eliminate the duplicate 22-item registry (thread 17)
- test_query_validation.py: tighten regression threshold from 50 → 90 (thread 18)
2026-03-16 10:01:12 -04:00
Jacob Magar
884319ab11 fix: address 14 PR review comments from coderabbitai/chatgpt-codex
- guards.py: split confirm bypass into explicit check; use .get() for
  dict description to prevent KeyError on missing action keys
- resources.py: use `is not None` for logs stream cache check; add
  on-demand subscribe_once fallback when auto_start is disabled so
  resources return real data instead of a perpetual "connecting" placeholder
- setup.py: always prompt before overwriting credentials even on failed
  probe (transient outage ≠ bad credentials); update elicitation message
- unraid.py: always elicit_reset_confirmation before overwriting creds;
  use asyncio.to_thread() for os.path.realpath() to avoid blocking async
- test_health.py: update test for new always-prompt-on-overwrite behavior;
  add test for declined-reset on failed probe
- test_resources.py: add tests for logs-stream None check, auto_start
  disabled fallback (success and failure), and fallback error recovery
- test-tools.sh: add suite_live() covering cpu/memory/cpu_telemetry/
  notifications_overview/log_tail; include in sequential and parallel runners
- CLAUDE.md: correct unraid_live → live action reference; document that
  setup always prompts before overwriting; note subscribe_once fallback
2026-03-16 03:10:01 -04:00
Jacob Magar
efaab031ae 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)
2026-03-16 02:58:54 -04:00
Jacob Magar
dab1cd6995 refactor(tools)!: consolidate 15 individual tools into single unified unraid tool
BREAKING CHANGE: Replaces 15 separate MCP tools (unraid_info, unraid_array,
unraid_storage, unraid_docker, unraid_vm, unraid_notifications, unraid_rclone,
unraid_users, unraid_keys, unraid_health, unraid_settings, unraid_customization,
unraid_plugins, unraid_oidc, unraid_live) with a single `unraid` tool using
action (domain) + subaction (operation) routing.

New interface: unraid(action="system", subaction="overview") replaces
unraid_info(action="overview"). All 15 domains and ~108 subactions preserved.

- Add unraid_mcp/tools/unraid.py (1891 lines, all domains consolidated)
- Remove 15 individual tool files
- Update tools/__init__.py to register single unified tool
- Update server.py for new tool registration pattern
- Update subscriptions/manager.py and resources.py for new tool names
- Update all 25 test files + integration/contract/safety/schema/property tests
- Update mcporter smoke-test script for new tool interface
- Bump version 0.6.0 → 1.0.0

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-16 02:29:57 -04:00
Jacob Magar
faf9fb9ad7 fix(guards): use Pydantic model for elicitation to get labeled checkbox instead of 'Value: []' 2026-03-15 23:48:53 -04:00
Jacob Magar
fe7b6485fd chore: ruff format types.py (pre-existing unformatted file) 2026-03-15 23:40:27 -04:00
Jacob Magar
d7545869e2 feat(guards): wire elicitation into notifications/vm/rclone/settings/storage
Replace hard ToolError guard with gate_destructive_action() in 5 tools so
destructive actions prompt for interactive confirmation via MCP elicitation
when ctx is available, and still accept confirm=True as a bypass. Update
all test match strings from "destructive" to "not confirmed" accordingly.
2026-03-15 23:38:20 -04:00
Jacob Magar
cdab970c12 refactor(guards): migrate array/keys/plugins to gate_destructive_action
Replace 7-11 line inline guard blocks in array.py, keys.py, and plugins.py
with single await gate_destructive_action(...) calls. Also fix guards.py to
raise unraid_mcp.core.exceptions.ToolError (project subclass) instead of
fastmcp.exceptions.ToolError so pytest.raises catches it correctly in tests.
2026-03-15 23:33:07 -04:00
Jacob Magar
80d2dd39ee refactor(guards): remove elicit_destructive_confirmation from setup.py (moved to guards.py)
Update array, keys, and plugins tool imports to source elicit_destructive_confirmation from core.guards instead of core.setup.
2026-03-15 23:29:22 -04:00
Jacob Magar
aa5fa3e177 feat(guards): add core/guards.py with gate_destructive_action helper 2026-03-15 23:25:39 -04:00
Jacob Magar
4b43c47091 fix(tools): remove 10 dead actions referencing mutations absent from live API
settings.py: drop update_temperature, update_time, update_api,
connect_sign_in, connect_sign_out, setup_remote_access,
enable_dynamic_remote_access, update_ssh — all 8 reference mutations
confirmed absent from Unraid API v4.29.2. Keep update + configure_ups.

info.py: drop update_server (updateServerIdentity not in Mutation type)
and update_ssh (duplicate of removed settings action). MUTATIONS is now
empty; DESTRUCTIVE_ACTIONS is now an empty set.

notifications.py: drop create_unique (notifyIfUnique not in Mutation type).

Tests: remove corresponding test classes, add parametrized regression
tests asserting removed actions are not in each tool's Literal type,
update KNOWN_DESTRUCTIVE and _DESTRUCTIVE_TEST_CASES in safety audit,
update schema coverage assertions. 858 tests passing, 0 failures.
2026-03-15 23:21:25 -04:00