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.
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.
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.
- 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
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).
- _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
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.
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.
- 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>
- 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>
- 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