Commit Graph
100 Commits
Author SHA1 Message Date
Jacob Magar 9249950dff fix(tests): remove http_layer and property tests for removed actions
Removes tests for docker logs/remove/check_updates, storage unassigned,
and notifications warnings — all of which reference actions removed in
the stale cleanup.
2026-03-15 22:10:50 -04:00
Jacob Magar 16bb5a6146 fix(storage): remove unassigned action (unassignedDevices not in live API) 2026-03-15 21:59:06 -04:00
Jacob Magar 83df768135 fix(notifications): remove warnings action (warningsAndAlerts not in live API) 2026-03-15 21:58:56 -04:00
Jacob Magar 569956ade0 fix(docker): remove 19 stale actions absent from live API v4.29.2
Only list, details, start, stop, restart, networks, network_details
remain. Removed logs, port_conflicts, check_updates from QUERIES and all
organizer mutations + pause/unpause/remove/update/update_all from
MUTATIONS. DESTRUCTIVE_ACTIONS is now an empty set.
2026-03-15 21:58:50 -04:00
Jacob Magar f5978d67ec feat(resources): add unraid://live/{action} MCP resources for 9 snapshot subscriptions
Registers cpu, memory, cpu_telemetry, array_state, parity_progress,
ups_status, notifications_overview, owner, and server_status as MCP
resources under unraid://live/{action}. Each opens a transient WebSocket
via subscribe_once() and returns JSON; exceptions degrade gracefully to
an error JSON dict rather than raising. Skips log_tail and
notification_feed (require params, not suitable as resources).
2026-03-15 21:51:20 -04:00
Jacob Magar 7c99fe1527 refactor(subscriptions): extract SNAPSHOT_ACTIONS/COLLECT_ACTIONS to subscriptions/queries.py
Moves the subscription query dicts out of tools/live.py into a new
subscriptions/queries.py module so subscriptions/resources.py can
import them without creating a cross-layer subscriptions→tools dependency.
2026-03-15 21:43:18 -04:00
Jacob Magar e87a33ef1a test(safety): add strict no-GraphQL-call and non-destructive regression tests
Two new test classes:

TestNoGraphQLCallsWhenUnconfirmed (parametrized over all 13 destructive actions):
- test_no_graphql_call_without_confirm: make_graphql_request must NOT be
  called when confirm is absent — verifies guard fires before any I/O
- test_no_graphql_call_with_confirm_false: same with explicit confirm=False

TestNonDestructiveActionsNeverRequireConfirm (5 representative non-destructive):
- Regression guard: non-destructive mutations must work without confirm=True;
  prevents accidental over-guarding from breaking normal operations

788 tests passing
2026-03-15 20:16:23 -04:00
Jacob Magar 389b88f560 feat(settings): add update_ssh action with confirm=True guard
Enables/disables SSH and sets port via updateSshSettings mutation
(UpdateSshInput: enabled: Boolean!, port: Int!). Changing SSH config
can lock users out of the server — requires confirm=True.

- Add update_ssh to MUTATIONS, DESTRUCTIVE_ACTIONS, SETTINGS_ACTIONS
- Add ssh_enabled/ssh_port parameters to unraid_settings
- Add TestSshSettings class (4 tests: require ssh_enabled, require ssh_port, success, disable+verify vars)
- Update safety test KNOWN_DESTRUCTIVE + _DESTRUCTIVE_TEST_CASES + positive confirm test
- Update schema completeness test

757 tests passing
2026-03-15 20:13:51 -04:00
Jacob Magar 94850333e8 fix(safety): add stop_array to DESTRUCTIVE_ACTIONS, add error propagation test
stop_array can cause data loss for running containers/VMs that depend on
array shares — requires confirm=True like other destructive mutations.

- Add stop_array to DESTRUCTIVE_ACTIONS and desc_map in array.py
- Update safety audit KNOWN_DESTRUCTIVE[array] to include stop_array
- Add stop_array negative/positive tests (test_array.py, safety tests)
- Add test_snapshot_wraps_bare_exception to test_live.py (bare Exception
  from subscribe_once is wrapped by tool_error_handler into ToolError)

748 tests passing
2026-03-15 20:02:33 -04:00
Jacob Magar 252ec520d1 fix(lint): remove __future__ annotations from new tools, fix 4 failing tests
- Remove `from __future__ import annotations` from array.py, live.py,
  oidc.py, plugins.py to match existing tool pattern and resolve TC002
  ruff errors (fastmcp imports only needed in annotations under PEP 563)
- Add `# noqa: ASYNC109` to live.py timeout parameter (asyncio.timeout
  already used internally)
- Fix test_network_sends_correct_query: query name is GetNetworkInfo
- Fix test_delete_requires_confirm: match "not confirmed" not "destructive"
- Fix test_destructive_set_matches_audit[settings]: add setup_remote_access
  and enable_dynamic_remote_access to KNOWN_DESTRUCTIVE
- Fix test_logs: update mock to dict format {lines: [{timestamp, message}]}

742 tests passing, ruff clean
2026-03-15 19:57:46 -04:00
Jacob Magar 1f35c20cdf chore: update schema tests, docs, bump version to 0.5.0
- Add schema validation tests for new tools (customization, plugins, oidc)
  and expanded array/keys actions (13 array, 7 keys)
- Update TestSchemaCompleteness to include new modules with KNOWN_SCHEMA_ISSUES
  exclusion list for 4 tool-level schema mismatches (tracked for later fix)
- Fix missing register_oidc_tool import in server.py (was causing NameError)
- Update CLAUDE.md Tool Categories section: 11 → 15 tools, ~103 actions
- Update Destructive Actions section with array/plugins additions
- Bump version 0.4.8 → 0.5.0 in pyproject.toml and .claude-plugin/plugin.json
- Schema tests: 84 passing → 119 passing (35 new tests)
- Full suite: 618 passing → 738 passing (120 net new passing)
2026-03-15 19:42:05 -04:00
Jacob Magar b68347bc1e refactor(tests): remove duplicate fixture in test_array.py, unify on _mock_graphql 2026-03-15 19:32:17 -04:00
Jacob Magar 6eafc16af7 feat(oidc): add unraid_oidc tool with providers, provider, configuration, public_providers, validate_session 2026-03-15 19:30:22 -04:00
Jacob Magar ebba60c095 fix(types): suppress ty union-attr false positives on elicitation result.data access 2026-03-15 19:27:43 -04:00
Jacob Magar 2b4b1f0395 feat(plugins): add unraid_plugins tool with list, add, remove actions
Implements the unraid_plugins MCP tool (3 actions, 1 destructive) and adds
elicit_destructive_confirmation() to core/setup to support all tools that
gate dangerous mutations behind confirm=True with optional MCP elicitation.
2026-03-15 19:26:42 -04:00
Jacob Magar d26467a4d0 feat(customization): add unraid_customization tool with theme, public_theme, is_initial_setup, sso_enabled, set_theme 2026-03-15 19:19:06 -04:00
Jacob Magar 76391b4d2b feat(keys): add add_role and remove_role actions for API key role management
Adds two new mutation actions to unraid_keys:
- add_role: calls apiKey.addRole with apiKeyId + role, requires key_id and roles
- remove_role: calls apiKey.removeRole with apiKeyId + role, requires key_id and roles

Updates safety audit to explicitly exempt remove_role from the delete/remove
heuristic (reversible action — role can be re-added). Updates schema coverage
test and adds schema validation tests for both new mutations.
2026-03-15 19:13:03 -04:00
Jacob Magar 0d4a3fa4e2 fix(live): validate log_tail path against allowlist, move guards before error handler
Add _ALLOWED_LOG_PREFIXES allowlist check to log_tail (mirrors storage.py pattern)
to prevent path traversal attacks. Move path/required guards before tool_error_handler
context so validation errors raise cleanly. Add two tests: ToolError propagation and
invalid path rejection.
2026-03-15 19:08:43 -04:00
Jacob Magar 3a72f6c6b9 feat(array): add parity_history, start/stop array, disk add/remove/mount/unmount/clear_stats
Expands unraid_array from 5 to 13 actions: adds parity_history query,
start_array/stop_array state mutations, and disk operations (add_disk,
remove_disk, mount_disk, unmount_disk, clear_disk_stats). Destructive
actions remove_disk and clear_disk_stats require confirm=True. Safety
audit tests updated to cover the new DESTRUCTIVE_ACTIONS registry entry.
2026-03-15 19:03:01 -04:00
Jacob Magar 675a466d02 feat(live): add unraid_live tool with 11 subscription snapshot actions
Creates unraid_mcp/tools/live.py with SNAPSHOT_ACTIONS (9 one-shot reads)
and COLLECT_ACTIONS (2 streaming collectors), plus tests/test_live.py
with 6 passing tests. Registers register_live_tool in server.py, bringing
the total to 12 tools.
2026-03-15 18:56:14 -04:00
Jacob Magar 5a3e8e285b fix(subscriptions): bound snapshot loops with asyncio.timeout, raise on collect errors
- Wrap async-for loops in asyncio.timeout() so both subscribe_once and subscribe_collect
  cannot hang indefinitely when no messages arrive after the handshake
- subscribe_once: TimeoutError → ToolError("Subscription timed out after Xs")
- subscribe_collect: TimeoutError → pass (return events collected so far)
- Remove manual deadline checks inside the loops (now redundant)
- subscribe_collect now raises ToolError on GraphQL payload errors instead of silently dropping them
- subscribe_collect handshake now distinguishes connection_error (auth) from unexpected type
2026-03-15 18:52:27 -04:00
Jacob Magar 181ad53414 feat(subscriptions): add subscribe_once and subscribe_collect snapshot helpers 2026-03-15 18:48:42 -04:00
Jacob MagarandClaude 72ccf9b074 chore: add settings slash command and bump to 0.4.8
- Add commands/settings.md slash command for unraid_settings tool
- Sync uv.lock after version bump

Co-authored-by: Claude <claude@anthropic.com>
2026-03-14 16:40:04 -04:00
Jacob Magar 5740f4848a chore: bump version to 0.4.7 2026-03-14 14:49:13 -04:00
Jacob Magar 4ce3edd423 docs: update credential setup to reflect ~/.unraid-mcp/.env flow 2026-03-14 14:48:06 -04:00
Jacob Magar 2c0f4a1730 feat(creds): add optional env_file from ~/.unraid-mcp/.env to docker-compose 2026-03-14 14:46:10 -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 c4f1b2eb00 test(creds): add replacement test for credentials propagation through tool_error_handler 2026-03-14 14:21:21 -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 14e9dca8bc chore: bump version to 0.4.6 2026-03-14 04:30:01 -04:00
Jacob Magar f0a97edbf7 docs: update credential setup docs to reflect elicitation flow 2026-03-14 04:29:35 -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 7458409147 test(elicitation): assert warning is logged when creds missing at startup 2026-03-14 03:47:29 -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 a7988e1eae test(elicitation): fix os.environ leak in apply_runtime_config test 2026-03-14 03:44:34 -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 1952720ef9 test(elicitation): fix test_setup.py style and add ToolError contract test 2026-03-14 03:41:24 -04:00
Jacob Magar ea839ec09c feat(elicitation): add CredentialsNotConfiguredError sentinel 2026-03-14 03:39:49 -04:00
Jacob MagarandClaude b734eff902 docs: add version bump reminder to CLAUDE.md
Always update both pyproject.toml and .claude-plugin/plugin.json
when bumping versions — missed in 0.4.4→0.4.5 bump.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-14 03:11:04 -04:00
Jacob MagarandClaude 3f13cf89c8 chore: bump plugin.json version to 0.4.5
Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-14 03:09:58 -04:00
Jacob MagarandClaude af3b5818dc refactor: merge comprehensive code review fixes branch
Merges 35 commits from refactor/comprehensive-code-review-fixes:
- Critical/high/medium/low PR review findings addressed
- Test suite reorganized (safety, http_layer, schema, integration)
- Destructive action guard tests added
- Rclone bug fix, diagnostics improvements
- Version bump to 0.4.5

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-14 03:01:59 -04:00
Jacob MagarandClaude d47101f8f7 chore: bump version to 0.4.5, update lock file
Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-14 03:01:21 -04:00
Jacob MagarandClaude d0cc99711a fix: address remaining PR review threads (docs, test-destructive, rclone test)
Resolves review threads:
- PRRT_kwDOO6Hdxs50T0Wp (test-destructive.sh: add key cleanup on failure path)
- PRRT_kwDOO6Hdxs50T0Ws (test-destructive.sh: pick last notification match by title)
- PRRT_kwDOO6Hdxs50T0Wt (README.md: correct test-actions.sh coverage description)
- PRRT_kwDOO6Hdxs50T0Wv (CLAUDE.md: add info.update_ssh to destructive actions list)
- PRRT_kwDOO6Hdxs50T0Wy (http_layer test: inp["config"] -> inp["parameters"])
- PRRT_kwDOO6Hdxs50T0Wz (DESTRUCTIVE_ACTIONS.md: key ID extraction key.id not top-level)
- PRRT_kwDOO6Hdxs50T0W2 (DESTRUCTIVE_ACTIONS.md: delete_archived — add archive step)
- PRRT_kwDOO6Hdxs50T0W3 (DESTRUCTIVE_ACTIONS.md: rclone params provider_type/config_data/name)
- PRRT_kwDOO6Hdxs50T0W4 (DESTRUCTIVE_ACTIONS.md: notification delete list+match pattern)
- PRRT_kwDOO6Hdxs50T0W5 (DESTRUCTIVE_ACTIONS.md: create_folder uses folder_name param)
- PRRT_kwDOO6Hdxs50T0W7 (README.md: cleanup note — test-tools.sh may write tmp log file)

Changes:
- test-destructive.sh keys test: attempt key delete cleanup when delete step fails
- test-destructive.sh notifications test: reverse list to pick most-recent title match
- tests/mcporter/README.md: accurate coverage claim; accurate cleanup section
- CLAUDE.md: info.update_ssh added to destructive actions list
- tests/http_layer/test_request_construction.py: assert parameters not config field
- docs/DESTRUCTIVE_ACTIONS.md: all 5 example code blocks corrected with right
  parameter names, correct ID extraction paths, and proper sequencing

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 23:29:14 -04:00
Jacob MagarandClaude 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 MagarandClaude 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 MagarandClaude 5a0b99d138 docs: add non-destructive action smoke test script
Adds scripts/test-actions.sh — a mcporter-based smoke test that exercises
all 42 non-destructive MCP actions across 10 tools. Two-phase design:
phase 1 runs param-free reads; phase 2 extracts resource IDs from those
responses to test the ID-required reads (container details/logs, network
details, disk details, log content, VM details, API key get).

Also adds docs/test-actions.md with full usage, coverage table, skip
rationale, and cleanup notes.

Fixed three bugs discovered during test run:
- Connectivity check used curl -f which treats 406 (correct MCP response
  to plain GET) as failure
- run_test_capture wrote status lines to stdout causing captured $() to
  contain mixed text+JSON, breaking all Phase 2 ID extraction
- run_test echoed full JSON response to terminal on every call

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-13 17:26:36 -04:00
Jacob MagarandClaude 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 MagarandClaude 85d52094ea chore: bump version 0.4.0 → 0.4.1
Comprehensive PR review pass — all 36 review threads resolved:
- Fixed docker organizer variable wiring, empty children_ids, short-ID matching
- Added confirm guard for update_ssh, fixed avatar field handling in settings
- Redacted API URL from client logs
- Fixed subscription validator (field-based allow-list), byte-count log cap,
  partial autostart state, URL scheme validation, stale connection_issues
- Fixed logging.py stream restore bug, attached root logger to file handler
- Fixed keys.py create mutation fields, ToolError on failed ops
- Fixed notifications create_unique length validation
- Fixed flash_backup response validation
- Resolved 21 pre-existing schema field drift failures across info, docker,
  storage, keys, and health tools
- Pinned uv Docker image tag to 0.9.25
- Gitignored docs/research, docs/superpowers, .claude/worktrees

Co-authored-by: Claude <claude@anthropic.com>
2026-03-13 13:18:22 -04:00
Jacob Magar 7ef21b8051 chore: gitignore .claude/worktrees to prevent agent dirs being staged 2026-03-13 11:23:39 -04:00
Jacob Magar a2c9cbfbeb chore: remove accidentally staged worktree submodule reference 2026-03-13 11:23:31 -04:00
Jacob Magar 3ae85e1df7 fix: resolve 21 pre-existing schema field drift failures
- info.py: fix InfoVersions queries (core/packages nesting), Connect fields,
  CpuUtilization.percentTotal, Service fields, Server fields, Flash, UPS
- health.py: align versions.core.unraid path in health check query/handler
- docker.py: logs subfield selection, networks nesting, port_conflicts fields,
  check_updates fields, network_details client-side filtering
- storage.py: replace non-existent unassignedDevices with disks query
- keys.py: add permissions subfields, remove lastUsed (not on ApiKey)
- test_query_validation.py: update docker mutations coverage to include 11
  organizer mutations; fix comprehensive check query shape
- test_docker.py: update networks mock to match new docker.networks nesting
2026-03-13 11:23:24 -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 4ed78b4867 fix: pin uv image tag to 0.9.25 instead of floating 0.6 minor tag 2026-03-13 10:59:53 -04:00
Jacob Magar a5ed5aab5f fix: update tests for confirm guard on update_ssh and field-based subscription allow-list 2026-03-13 10:55:54 -04:00
Jacob Magar e24ef5e85d fix: update test import _ALLOWED_SUBSCRIPTION_NAMES → _ALLOWED_SUBSCRIPTION_FIELDS 2026-03-13 10:51:53 -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 0283006374 chore: gitignore docs/research and docs/superpowers subdirs, untrack files
These directories contain session-specific research artifacts and plan
documents — ephemeral content that doesn't belong in the repo history.

Resolves review threads PRRT_kwDOO6Hdxs50E50H PRRT_kwDOO6Hdxs50E2iX
PRRT_kwDOO6Hdxs50E2if PRRT_kwDOO6Hdxs50E2ig PRRT_kwDOO6Hdxs50E2ii
PRRT_kwDOO6Hdxs50E2iq
2026-03-13 09:57:20 -04:00
Jacob Magar cd33ee2dda chore: sync versions, update uv, expand dockerignore
- plugin.json: bump version 0.2.0 → 0.4.0 to match pyproject.toml
- Dockerfile: update uv 0.5.4 → 0.6 (global CLAUDE.md requires 0.10+)
- .dockerignore: exclude tests/, docs/, scripts/, commands/, .full-review/,
  .claude-plugin/, *.md (keep README.md) — reduces image size
2026-03-13 03:08:01 -04:00
Jacob MagarandClaude 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 bdb2155366 fix: update TestQueryCache tests to await async cache methods
_QueryCache.get/put/invalidate_all are async (use asyncio.Lock internally).
Updated 6 sync test methods to async def with proper await calls so they
test the actual async interface rather than calling unawaited coroutines.
2026-03-13 02:40:43 -04:00
Jacob MagarandClaude 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
Jacob MagarandClaude 06f18f32fc chore: update gitignore, bump to 0.2.1, apply CodeRabbit fixes
- Add .windsurf/, *.bak*, .1code/, .emdash.json to .gitignore
- Sync standard gitignore entries per project conventions
- Apply final test/tool fixes from CodeRabbit review threads
- Update GraphQL schema to latest introspection snapshot
- Bump version 0.2.0 → 0.2.1

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 00:53:51 -04:00
Jacob Magar 2a5b19c42f test: address final 3 CodeRabbit review threads
- http_layer/test_request_construction.py: tighten JSON error match from
  "invalid response" to "invalid response.*not valid JSON" to prevent
  false positives
- safety/test_destructive_guards.py: add test_docker_update_all_with_confirm
  to TestConfirmAllowsExecution (was missing positive coverage for update_all)
- safety/test_destructive_guards.py: expand conftest import comment to explain
  why the direct conftest import is intentional and correct
2026-02-19 02:25:21 -05:00
Jacob Magar 1751bc2984 fix: apply all PR review agent findings (silent failures, type safety, test gaps)
Addresses issues found by 4 parallel review agents (code-reviewer,
silent-failure-hunter, type-design-analyzer, pr-test-analyzer).

Source fixes:
- core/utils.py: add public safe_display_url() (moved from tools/health.py)
- core/client.py: rename _redact_sensitive → redact_sensitive (public API)
- core/types.py: add SubscriptionData.__post_init__ for tz-aware datetime
  enforcement; remove 6 unused type aliases (SystemHealth, APIResponse, etc.)
- subscriptions/manager.py: add exc_info=True to both except-Exception blocks;
  add except ValueError break-on-config-error before retry loop; import
  redact_sensitive by new public name
- subscriptions/resources.py: re-raise in autostart_subscriptions() so
  ensure_subscriptions_started() doesn't permanently set _subscriptions_started
- subscriptions/diagnostics.py: except ToolError: raise before broad except;
  use safe_display_url() instead of raw URL slice
- tools/health.py: move _safe_display_url to core/utils; add exc_info=True;
  raise ToolError (not return dict) on ImportError
- tools/info.py: use get_args(INFO_ACTIONS) instead of INFO_ACTIONS.__args__
- tools/{array,docker,keys,notifications,rclone,storage,virtualization}.py:
  add Literal-vs-ALL_ACTIONS sync check at import time

Test fixes:
- test_health.py: import safe_display_url from core.utils; update
  test_diagnose_import_error_internal to expect ToolError (not error dict)
- test_storage.py: add 3 safe_get tests for zero/False/empty-string values
- test_subscription_manager.py: add TestCapLogContentSingleMassiveLine (2 tests)
- test_client.py: rename _redact_sensitive → redact_sensitive; add tests for
  new sensitive keys and is_cacheable explicit-keyword form
2026-02-19 02:23:04 -05:00