Commit Graph

71 Commits

Author SHA1 Message Date
Jacob Magar
181ad53414 feat(subscriptions): add subscribe_once and subscribe_collect snapshot helpers 2026-03-15 18:48:42 -04:00
Jacob Magar
a3754e37c3 feat(creds): setup declined message includes manual path and variable names 2026-03-14 14:45:35 -04:00
Jacob Magar
c80ab0ca6b refactor(creds): remove per-tool elicitation from unraid_info 2026-03-14 14:20:20 -04:00
Jacob Magar
08afdcc50e refactor(creds): remove per-tool elicitation from unraid_settings 2026-03-14 14:19:42 -04:00
Jacob Magar
ba7b8dfaa6 refactor(creds): remove per-tool elicitation from unraid_keys 2026-03-14 14:18:20 -04:00
Jacob Magar
23e70e46d0 refactor(creds): remove per-tool elicitation from unraid_users 2026-03-14 14:17:14 -04:00
Jacob Magar
fe66e8742c refactor(creds): remove per-tool elicitation from unraid_rclone 2026-03-14 14:16:34 -04:00
Jacob Magar
77f3d897a3 refactor(creds): remove per-tool elicitation from unraid_notifications 2026-03-14 14:16:07 -04:00
Jacob Magar
8c67145bcc refactor(creds): remove per-tool elicitation from unraid_vm 2026-03-14 14:14:22 -04:00
Jacob Magar
9fc85ea48c refactor(creds): remove per-tool elicitation from unraid_storage 2026-03-14 14:13:52 -04:00
Jacob Magar
d99855973a refactor(creds): remove per-tool elicitation from unraid_docker 2026-03-14 14:13:14 -04:00
Jacob Magar
9435a8c534 refactor(creds): remove per-tool elicitation from unraid_array 2026-03-14 14:09:14 -04:00
Jacob Magar
81f1fe174d feat(creds): tool_error_handler converts CredentialsNotConfiguredError to ToolError with path
Converts the CredentialsNotConfiguredError sentinel to a user-facing ToolError
in tool_error_handler, including the exact CREDENTIALS_ENV_PATH so users know
where to create the .env file. Removes the now-invalid per-tool elicitation
test (replaced by 2 new tests for the handler conversion behavior).
2026-03-14 14:05:59 -04:00
Jacob Magar
e930b868e4 feat(creds): write to ~/.unraid-mcp/.env with 700/600 permissions, seed from .env.example
- _write_env now creates CREDENTIALS_DIR (mode 700) and writes credentials
  to CREDENTIALS_ENV_PATH (mode 600) instead of PROJECT_ROOT/.env
- On first run (no .env yet), seeds file content from .env.example to
  preserve comments and structure
- elicit_and_configure catches NotImplementedError from ctx.elicit() so
  clients that don't support elicitation return False gracefully instead
  of propagating the exception
- Updated test_elicit_and_configure_writes_env_file to patch CREDENTIALS_DIR
  and CREDENTIALS_ENV_PATH instead of PROJECT_ROOT
- Added 5 new tests covering dir/file permissions, .env.example seeding,
  in-place credential update, and NotImplementedError guard
2026-03-14 14:00:57 -04:00
Jacob Magar
d8ce45c0fc feat(creds): add CREDENTIALS_DIR and CREDENTIALS_ENV_PATH to settings
Introduce a version-agnostic credential directory (~/.unraid-mcp, overridable
via UNRAID_CREDENTIALS_DIR env var) and surface it as CREDENTIALS_DIR and
CREDENTIALS_ENV_PATH module-level constants. Prepend the canonical .env path to
dotenv_paths so all runtimes (plugin, uv, Docker) resolve credentials from the
same stable location without relying on versioned plugin cache paths.
2026-03-14 13:54:14 -04:00
Jacob Magar
85cd173449 fix(elicitation): guard ctx=None in elicit_and_configure, cover all settings/docker/notifications actions
- setup.py: elicit_and_configure now accepts Context | None; returns False
  immediately when ctx is None instead of crashing with AttributeError
- settings.py: added CredentialsNotConfiguredError try/except guard around
  make_graphql_request calls in all 8 previously-unguarded actions
  (update_temperature, update_time, configure_ups, update_api, connect_sign_in,
  connect_sign_out, setup_remote_access, enable_dynamic_remote_access)
- docker.py: added guards to all 20 previously-unguarded make_graphql_request
  calls (details, logs, networks, network_details, port_conflicts, check_updates,
  restart, update_all, all 11 organizer mutations, and single-container fallback)
- notifications.py: added guards to all 11 previously-unguarded calls
  (list, warnings, create, archive/unread, delete, delete_archived, archive_all,
  archive_many, create_unique, unarchive_many, unarchive_all, recalculate)
2026-03-14 04:28:34 -04:00
Jacob Magar
e1c80cf1da feat(elicitation): add ctx + credential elicitation to unraid_settings 2026-03-14 04:19:08 -04:00
Jacob Magar
ba14a8d341 feat(elicitation): add ctx + credential elicitation to unraid_keys 2026-03-14 04:18:06 -04:00
Jacob Magar
cec254b432 feat(elicitation): add ctx + credential elicitation to unraid_users 2026-03-14 04:17:17 -04:00
Jacob Magar
dec80832ea feat(elicitation): add ctx + credential elicitation to unraid_rclone 2026-03-14 04:16:54 -04:00
Jacob Magar
4b4c8ddf63 feat(elicitation): add ctx + credential elicitation to unraid_notifications 2026-03-14 04:16:08 -04:00
Jacob Magar
dfcaa37614 feat(elicitation): add ctx + credential elicitation to unraid_vm 2026-03-14 04:14:43 -04:00
Jacob Magar
060acab239 feat(elicitation): add ctx + credential elicitation to unraid_storage 2026-03-14 04:14:15 -04:00
Jacob Magar
be186dc2d7 feat(elicitation): add ctx + credential elicitation to unraid_docker 2026-03-14 04:13:34 -04:00
Jacob Magar
13f85bd499 feat(elicitation): add ctx + credential elicitation to unraid_array 2026-03-14 04:11:38 -04:00
Jacob Magar
49264550b1 feat(elicitation): auto-elicit credentials on CredentialsNotConfiguredError in unraid_info 2026-03-14 04:07:51 -04:00
Jacob Magar
9be46750b8 feat(elicitation): add setup action to unraid_health 2026-03-14 04:02:15 -04:00
Jacob Magar
61604b313f fix(elicitation): pass CredentialsNotConfiguredError through tool_error_handler 2026-03-14 03:58:44 -04:00
Jacob Magar
8a986a84c2 feat(elicitation): raise CredentialsNotConfiguredError in client when creds absent
make_graphql_request now reads credentials from the settings module at call
time (via a local import) instead of relying on module-level names captured at
import time. When either credential is missing it raises CredentialsNotConfiguredError
(not ToolError), allowing callers to trigger elicitation rather than surfacing a
generic error to the MCP client.

Updated tests/test_client.py and tests/http_layer/test_request_construction.py
to patch unraid_mcp.config.settings.* instead of the now-removed client-module
attrs, and to expect CredentialsNotConfiguredError on missing credentials.
2026-03-14 03:55:57 -04:00
Jacob Magar
02e61b4290 refactor(elicitation): use PROJECT_ROOT directly (already a Path) 2026-03-14 03:50:45 -04:00
Jacob Magar
e73f791fd3 feat(elicitation): add elicit_and_configure() with .env persistence 2026-03-14 03:49:11 -04:00
Jacob Magar
42bfcc1998 feat(elicitation): degrade gracefully when credentials are missing at startup 2026-03-14 03:45:42 -04:00
Jacob Magar
520d92af57 feat(elicitation): add is_configured() and apply_runtime_config() to settings 2026-03-14 03:43:20 -04:00
Jacob Magar
ea839ec09c feat(elicitation): add CredentialsNotConfiguredError sentinel 2026-03-14 03:39:49 -04:00
Jacob Magar
91bce1dbd5 fix: address PR review threads (test-actions, diagnostics, docker, health, storage, plugin)
Resolves review threads:
- PRRT_kwDOO6Hdxs50R8VI (test-actions.sh: remove || echo "000" curl fallback)
- PRRT_kwDOO6Hdxs50R8VJ (test-actions.sh: JSON parse failures → FAIL not silent)
- PRRT_kwDOO6Hdxs50QdKd (diagnostics.py: sanitize raw exception text from ToolError)
- PRRT_kwDOO6Hdxs50QdKs (storage.py: unassigned uses unassignedDevices query)
- PRRT_kwDOO6Hdxs50Mwlk (docker.py: port_conflicts returns flat merged list)
- PRRT_kwDOO6Hdxs50Mwlo (docker.py: logs returns plain string not dict)
- PRRT_kwDOO6Hdxs50Mt5K (docker.py: unraid_docker logs format compatibility)
- PRRT_kwDOO6Hdxs50Mt5L (health.py: or {} null guards throughout)
- PRRT_kwDOO6Hdxs50Mt5r (docker.py: port_conflicts flat list backward compat)
- plugin.json: version synced to 0.4.4 to match pyproject.toml

Changes:
- test-actions.sh: curl exit code captured directly; JSON failures surface as FAIL
- diagnostics.py: 4 ToolError sites log exc_info=True, raise sanitized messages
- storage.py: unassigned action queries unassignedDevices instead of disks
- docker.py: logs action returns newline-joined string; port_conflicts merges
  containerPorts + lanPorts into a flat list for backward compatibility
- health.py: all nested dict lookups use `or {}` instead of `.get(k, {})` to
  handle explicit GraphQL null values

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 23:19:50 -04:00
Jacob Magar
7bb9d93bd5 chore: reorganize test scripts, add destructive action tests, fix rclone bug
- Move scripts/test-tools.sh and scripts/test-actions.sh → tests/mcporter/
  - Fix PROJECT_DIR path in test-tools.sh (SCRIPT_DIR/.. → SCRIPT_DIR/../..)
- Add tests/mcporter/test-destructive.sh: 2 live + 13 skipped destructive tests
  - stdio transport (no running server required)
  - notifications:delete (create→list→delete), keys:delete (create→delete→verify)
  - 3 new skips: createDockerFolder/updateSshSettings/createRCloneRemote not in API
  - Requires --confirm flag; dry-run by default
- Add tests/mcporter/README.md documenting both scripts and coverage
- Rewrite docs/DESTRUCTIVE_ACTIONS.md: merge test guide, all 15 actions with commands
- Delete docs/test-actions.md (merged into tests/mcporter/README.md)
- Fix rclone.py create_remote: send "parameters" not "config" (API field name)
- Update README.md and CLAUDE.md: 11 tools/~104 actions, new script paths
- Add AGENTS.md and GEMINI.md symlinks to CLAUDE.md
- Bump version 0.4.3 → 0.4.4

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 22:35:52 -04:00
Jacob Magar
a07dbd2294 fix: address PR review critical and high findings
- Remove duplicate _cap_log_content definition (dead code merge artifact)
  from manager.py; keep byte-count version that correctly handles multibyte UTF-8
- Fix storage.py unassigned handler reading wrong key (unassignedDevices → disks)
  — query already fetched `disks {}` but handler returned empty list every call
- Add null checks to all 8 Docker organizer object mutations; raise ToolError
  instead of silently returning success=True with organizer=None
- Raise ToolError in docker logs when server returns no log data
- Extract notification object from create response (was returning raw GraphQL
  wrapper dict instead of the notification itself)
- Raise ToolError in test_subscription_query on connection failure and unexpected
  exceptions (was returning error dicts, bypassing error handling)
- Remove stale "Bug N fix" inline comments from diagnostics.py
- Update docker.py module docstring to reflect 26 actions (was 15)
- Bump version 0.4.1 → 0.4.2

Co-authored-by: Claude <claude@anthropic.com>
2026-03-13 15:23:12 -04:00
Jacob Magar
8eab5992ba fix: resolve 21 pre-existing schema field drift failures
- Fix InfoOs: remove codepage (not in schema, codename already queried)
- Fix InfoVersions: use core { unraid api kernel } and packages { ... }
  subtype structure instead of flat field list; remove non-existent fields
- Fix Info: remove apps field from overview query (not in Info type)
- Fix Connect query: replace missing status/sandbox/flashGuid with
  dynamicRemoteAccess { enabledType runningType error }
- Fix CpuUtilization: replace used with percentTotal
- Fix Service: remove state field, add online and version
- Fix Server: replace ip/port with wanip/lanip/localurl/remoteurl
- Fix Flash: remove size field (not in schema)
- Fix UPSDevice: replace flat runtime/charge/load/voltage/frequency/temperature
  with nested battery { chargeLevel estimatedRuntime health } and
  power { loadPercentage inputVoltage outputVoltage } sub-types
- Fix ups_device variable type: PrefixedID! -> String! (schema uses String!)
- Fix UPSConfiguration: replace enabled/mode/cable/driver/port with
  service/upsCable/upsType/device/batteryLevel/minutes/timeout/killUps/upsName
- Fix storage unassigned query: unassignedDevices not in schema, use disks
- Fix docker logs: add subfield selection for DockerContainerLogs type
- Fix docker networks/network_details: move from root dockerNetworks/dockerNetwork
  to docker { networks { ... } }; filter by ID client-side for network_details
- Fix docker port_conflicts: replace containerName/port/conflictsWith with
  containerPorts { privatePort type containers { id name } } and lanPorts
- Fix docker check_updates: replace id/updateAvailable/currentVersion/latestVersion
  with name/updateStatus per ExplicitStatusItem schema type
- Fix keys queries: add subfield selection for permissions { resource actions },
  remove lastUsed (not on ApiKey type)
- Fix health.py comprehensive check: use versions { core { unraid } }
- Update docker mutations coverage assertion to include 11 organizer mutations
- Update test_networks mock to match new docker { networks } response shape
- Update health.py runtime accessor to follow new versions.core.unraid path
2026-03-13 11:19:40 -04:00
Jacob Magar
6fe6e30f1e merge: subscription fixes from worktree (validation, byte cap, autostart, URL scheme) 2026-03-13 10:48:05 -04:00
Jacob Magar
37b6150aed fix: safe_get returns default correctly, redact API URL in client logs
- core/utils.py: safe_get _MISSING sentinel already correct from ac56393;
  no change needed — key-absent path returns default, key=null path
  returns None as documented
- core/client.py: redact API URL in request log via safe_display_url(),
  exposing only scheme+host:port (strips path, credentials, query params)

Resolves review threads PRRT_kwDOO6Hdxs50FgPj PRRT_kwDOO6Hdxs50E50O
2026-03-13 10:44:39 -04:00
Jacob Magar
482da4485d fix: flash_backup validation, smoke test assertions, docker/notification test coverage
- storage.py: validate initiateFlashBackup response before returning success=True
- test-tools.sh: remove set -e/inherit_errexit; add success assertion to smoke tests
- test_destructive_guards.py: add confirm-guard tests for new docker destructive actions
- test_docker.py: assert mutation variables in organizer tests; add items branch test
- test_query_validation.py: add 5 missing notification mutation schema test methods
- test_notifications.py: use lowercase importance to test uppercasing logic

Resolves review threads PRRT_kwDOO6Hdxs50FgPb PRRT_kwDOO6Hdxs50FgO4 PRRT_kwDOO6Hdxs50FgO8 PRRT_kwDOO6Hdxs50FgPI PRRT_kwDOO6Hdxs50FgPL PRRT_kwDOO6Hdxs50FgPm PRRT_kwDOO6Hdxs50E2iK PRRT_kwDOO6Hdxs50E2im
2026-03-13 10:41:43 -04:00
Jacob Magar
0e4365bd4b fix: logging.py - no closed stream restore, attach root logger to file handler
- Don't restore old_stream reference after closing it; use None/raise on failure
- Attach root logger to shared file handler so library logs reach the log file

Resolves review threads PRRT_kwDOO6Hdxs50E50K PRRT_kwDOO6Hdxs50E2iQ PRRT_kwDOO6Hdxs50E2iT
2026-03-13 10:39:49 -04:00
Jacob Magar
9026faaa7c fix: correct subscription validation, byte-based log cap, partial autostart, URL scheme
- diagnostics.py: fix allow-list vs field name mismatch in subscription validator
  (_ALLOWED_SUBSCRIPTION_FIELDS now contains schema field names like "logFile",
  not operation names like "logFileSubscription", matching what _SUBSCRIPTION_NAME_PATTERN
  extracts); add _validate_subscription_query() called before any network I/O;
  replace chained .replace() URL building with build_ws_url(); gate connection_issues
  on current failure state via _analyze_subscription_status()
- manager.py: add _cap_log_content() with byte-count pre-check
  (len(value.encode("utf-8", errors="replace")) > _MAX_RESOURCE_DATA_BYTES) so
  multibyte UTF-8 content cannot bypass the 1 MB cap
- resources.py: add double-checked locking (_startup_lock) in ensure_subscriptions_started();
  propagate exception from auto_start_all_subscriptions() via raise so
  _subscriptions_started=True is never set after a failed init
- utils.py: add build_ws_url() that raises ValueError on unknown/missing URL scheme
  instead of silently falling through; add _analyze_subscription_status() helper
  that gates connection_issues on current failure state

Resolves review threads PRRT_kwDOO6Hdxs50E50Y PRRT_kwDOO6Hdxs50E50a PRRT_kwDOO6Hdxs50E50c PRRT_kwDOO6Hdxs50E50d PRRT_kwDOO6Hdxs50E2iN PRRT_kwDOO6Hdxs50E2h8
2026-03-13 10:38:17 -04:00
Jacob Magar
ac745bec42 fix: keys create mutation field, ToolError on failed ops, create_unique validation
- keys.py: fix create mutation to use correct ApiKey/ApiKeyWithSecret fields
- keys.py: raise ToolError when create/update response contains no key data
- notifications.py: add length validation to create_unique matching create action

Resolves review threads PRRT_kwDOO6Hdxs50E50f PRRT_kwDOO6Hdxs50E50h PRRT_kwDOO6Hdxs50E50i PRRT_kwDOO6Hdxs50E2iB
2026-03-13 10:35:16 -04:00
Jacob Magar
d76bfb889d fix: add confirm guard for update_ssh, fix avatar dropped without username/email
- info.py: add DESTRUCTIVE_ACTIONS set with update_ssh, add confirm param to
  unraid_info signature, add destructive guard before mutation handlers
- settings.py: build user_info dict unconditionally so avatar is included
  even when username/email are absent; only attach userInfo when non-empty

Resolves review threads PRRT_kwDOO6Hdxs50FgO0 PRRT_kwDOO6Hdxs50FgPC
2026-03-13 10:33:56 -04:00
Jacob Magar
c913e6bce9 fix: correct source_entry_ids var, allow empty children_ids, fix strict short-ID
- create_folder_with_items: forward source_entry_ids not entry_ids to sourceEntryIds
- set_folder_children: use `is not None` guard to allow children_ids=[]
- _resolve_container_id: allow short hex ID matching independent of strict mode

Resolves review threads PRRT_kwDOO6Hdxs50FgOr PRRT_kwDOO6Hdxs50FgPO PRRT_kwDOO6Hdxs50E2iH
2026-03-13 10:33:35 -04:00
Jacob Magar
9aee3a2448 feat: add 28 GraphQL mutations across storage, info, docker, and new settings tool
- storage: flash_backup mutation (initiates rclone flash backup, destructive)
- info: update_server and update_ssh mutations
- docker: 11 organizer mutations (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); delete_entries and
  reset_template_mappings added to DESTRUCTIVE_ACTIONS
- settings: new unraid_settings tool with 9 mutations (update,
  update_temperature, update_time, configure_ups, update_api,
  connect_sign_in, connect_sign_out, setup_remote_access,
  enable_dynamic_remote_access); registered in server.py
- tests: 82 new tests (28 settings, 23 docker organizer, 7 info, 6 storage
  + 18 existing fixes for notification regex and safety audit list)
- bump version 0.3.0 → 0.4.0 (11 tools, ~104 actions)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-13 03:03:37 -04:00
Jacob Magar
4af1e74b4a fix: rename notification enum validation vars to lowercase per ruff N806 2026-03-13 02:44:45 -04:00
Jacob Magar
ac5639301c fix: split subscription_lock, fix safe_get None semantics, validate notification enums
P-01: Replace single subscription_lock with two fine-grained locks:
- _task_lock guards active_subscriptions (task lifecycle operations)
- _data_lock guards resource_data (WebSocket message writes and reads)
Eliminates serialization between WebSocket updates and tool reads.

CQ-05: safe_get now preserves explicit None at terminal key.
Uses sentinel _MISSING to distinguish "key absent" (returns default)
from "key=null" (returns None). Fixes conflation that masked
intentional null values from the Unraid API.

SEC-M04: Validate list_type, importance, and notification_type against
known enums before dispatching to GraphQL. Prevents wasting rate-limited
requests on invalid values and avoids leaking schema details in errors.
2026-03-13 02:44:26 -04:00
Jacob Magar
60defc35ca feat: add 5 notification mutations + comprehensive refactors from PR review
New notification actions (archive_many, create_unique, unarchive_many,
unarchive_all, recalculate) bring unraid_notifications to 14 actions.

Also includes continuation of CodeRabbit/PR review fixes:
- Remove redundant try-except in virtualization.py (silent failure fix)
- Add QueryCache protocol with get/put/invalidate_all to core/client.py
- Refactor subscriptions (manager, diagnostics, resources, utils)
- Update config (logging, settings) for improved structure
- Expand test coverage: http_layer, safety guards, schema validation
- Minor cleanups: array, docker, health, keys tools

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 01:54:55 -04:00