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.
- 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>
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.
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)
- 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
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.
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.
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.
The Unraid graphql-ws server expects the API key directly in connectionParams
as `x-api-key`, not nested under `headers`. The old format caused the server
to fall through to cookie auth and crash on `undefined.csrf_token`.
Fixed in snapshot.py (×2), manager.py, diagnostics.py, and updated the
integration test assertion to match the correct payload shape.
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.
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.