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