fix: address all 17 PR review comments

Resolves review threads:
- PRRT_kwDOO6Hdxs50fewG (setup.py): non-eliciting clients now return True
  from elicit_reset_confirmation so they can reconfigure without being blocked
- PRRT_kwDOO6Hdxs50fewM (test-tools.sh): add notification/recalculate smoke test
- PRRT_kwDOO6Hdxs50fewP (test-tools.sh): add system/array smoke test
- PRRT_kwDOO6Hdxs50fewT (resources.py): surface manager error state instead of
  reporting 'connecting' for permanently failed subscriptions
- PRRT_kwDOO6Hdxs50feAj (resources.py): use is not None check for empty cached dicts
- PRRT_kwDOO6Hdxs50fewY (integration tests): remove duplicate snapshot-registration
  tests already covered in test_resources.py
- PRRT_kwDOO6Hdxs50fewe (test_resources.py): replace brittle import-detail test
  with behavior tests for connecting/error states
- PRRT_kwDOO6Hdxs50fewh (test_customization.py): strengthen public_theme assertion
- PRRT_kwDOO6Hdxs50fewk (test_customization.py): strengthen theme assertion
- PRRT_kwDOO6Hdxs50fewo (__init__.py): correct subaction count ~88 -> ~107
- PRRT_kwDOO6Hdxs50fewx (test_oidc.py): assert providers list value directly
- PRRT_kwDOO6Hdxs50fewz (unraid.py): remove unreachable raise after vm handler
- PRRT_kwDOO6Hdxs50few2 (unraid.py): remove unreachable raise after docker handler
- PRRT_kwDOO6Hdxs50fev8 (CLAUDE.md): replace legacy 15-tool table with unified
  unraid action/subaction table
- PRRT_kwDOO6Hdxs50fev_ (test_oidc.py): assert providers + defaultAllowedOrigins
- PRRT_kwDOO6Hdxs50feAz (CLAUDE.md): update tool categories to unified API shape
- PRRT_kwDOO6Hdxs50feBE (CLAUDE.md/setup.py): update unraid_health refs to
  unraid(action=health, subaction=setup)
This commit is contained in:
Jacob Magar
2026-03-16 02:58:54 -04:00
parent dab1cd6995
commit efaab031ae
39 changed files with 844 additions and 1225 deletions

View File

@@ -871,8 +871,6 @@ async def _handle_docker(
"container": (data.get("docker") or {}).get(subaction),
}
raise ToolError(f"Unhandled docker subaction '{subaction}' — this is a bug")
# ===========================================================================
# VM
@@ -950,8 +948,6 @@ async def _handle_vm(
return {"success": data["vm"][field], "subaction": subaction, "vm_id": vm_id}
raise ToolError(f"Failed to {subaction} VM or unexpected response")
raise ToolError(f"Unhandled vm subaction '{subaction}' — this is a bug")
# ===========================================================================
# NOTIFICATION
@@ -965,7 +961,7 @@ _NOTIFICATION_QUERIES: dict[str, str] = {
_NOTIFICATION_MUTATIONS: dict[str, str] = {
"create": "mutation CreateNotification($input: NotificationData!) { createNotification(input: $input) { id title importance } }",
"archive": "mutation ArchiveNotification($id: PrefixedID!) { archiveNotification(id: $id) { id title importance } }",
"unread": "mutation UnreadNotification($id: PrefixedID!) { unreadNotification(id: $id) { id title importance } }",
"mark_unread": "mutation UnreadNotification($id: PrefixedID!) { unreadNotification(id: $id) { id title importance } }",
"delete": "mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) { deleteNotification(id: $id, type: $type) { unread { info warning alert total } archive { info warning alert total } } }",
"delete_archived": "mutation DeleteArchivedNotifications { deleteArchivedNotifications { unread { info warning alert total } archive { info warning alert total } } }",
"archive_all": "mutation ArchiveAllNotifications($importance: NotificationImportance) { archiveAll(importance: $importance) { unread { info warning alert total } archive { info warning alert total } } }",
@@ -1077,7 +1073,7 @@ async def _handle_notification(
raise ToolError("Notification creation failed: server returned no data")
return {"success": True, "notification": notif}
if subaction in ("archive", "unread"):
if subaction in ("archive", "mark_unread"):
if not notification_id:
raise ToolError(f"notification_id is required for notification/{subaction}")
data = await make_graphql_request(
@@ -1615,7 +1611,7 @@ _LIVE_ALLOWED_LOG_PREFIXES = ("/var/log/", "/boot/logs/", "/mnt/")
async def _handle_live(
subaction: str, path: str | None, collect_for: float, timeout: float
) -> dict[str, Any]:
from ..subscriptions.queries import COLLECT_ACTIONS, SNAPSHOT_ACTIONS
from ..subscriptions.queries import COLLECT_ACTIONS, EVENT_DRIVEN_ACTIONS, SNAPSHOT_ACTIONS
from ..subscriptions.snapshot import subscribe_collect, subscribe_once
all_live = set(SNAPSHOT_ACTIONS) | set(COLLECT_ACTIONS)
@@ -1636,7 +1632,20 @@ async def _handle_live(
logger.info(f"Executing unraid action=live subaction={subaction} timeout={timeout}")
if subaction in SNAPSHOT_ACTIONS:
data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout)
if subaction in EVENT_DRIVEN_ACTIONS:
try:
data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout)
except ToolError as e:
if "timed out" in str(e):
return {
"success": True,
"subaction": subaction,
"status": "no_recent_events",
"message": f"No events received in {timeout:.0f}s — this subscription only emits on state changes",
}
raise
else:
data = await subscribe_once(SNAPSHOT_ACTIONS[subaction], timeout=timeout)
return {"success": True, "subaction": subaction, "data": data}
if subaction == "log_tail":
@@ -1761,58 +1770,51 @@ def register_unraid_tool(mcp: FastMCP) -> None:
Use action + subaction to select an operation. All params are optional
except those required by the specific subaction.
action="system" - Server info, metrics, network, UPS
subactions: overview, array, network, registration, variables, metrics,
services, display, config, online, owner, settings, server,
servers, flash, ups_devices, ups_device, ups_config
┌─────────────────┬──────────────────────────────────────────────────────────────────────┐
action │ subactions │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ system │ overview, array, network, registration, variables, metrics,
│ │ services, display, config, online, owner, settings, server, │
│ │ servers, flash, ups_devices, ups_device, ups_config │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ health │ check, test_connection, diagnose, setup │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ array │ parity_status, parity_history, parity_start, parity_pause, │
│ │ parity_resume, parity_cancel, start_array*, stop_array*, │
│ │ add_disk, remove_disk*, mount_disk, unmount_disk, clear_disk_stats* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ disk │ shares, disks, disk_details, log_files, logs, flash_backup* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ docker │ list, details, start, stop, restart, networks, network_details │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ vm │ list, details, start, stop, pause, resume, │
│ │ force_stop*, reboot, reset* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ notification │ overview, list, create, archive, mark_unread, recalculate, │
│ │ archive_all, archive_many, unarchive_many, unarchive_all, │
│ │ delete*, delete_archived* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ key │ list, get, create, update, delete*, add_role, remove_role │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ plugin │ list, add, remove* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ rclone │ list_remotes, config_form, create_remote, delete_remote* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ setting │ update, configure_ups* │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ customization │ theme, public_theme, is_initial_setup, sso_enabled, set_theme │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ oidc │ providers, provider, configuration, public_providers, │
│ │ validate_session │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ user │ me │
├─────────────────┼──────────────────────────────────────────────────────────────────────┤
│ live │ cpu, memory, cpu_telemetry, array_state, parity_progress, │
│ │ ups_status, notifications_overview, owner, server_status, │
│ │ log_tail (requires path=), notification_feed │
└─────────────────┴──────────────────────────────────────────────────────────────────────┘
action="health" - MCP server and API health
subactions: check, test_connection, diagnose, setup
action="array" - Array and parity management
subactions: parity_status, parity_history, parity_start, parity_pause,
parity_resume, parity_cancel, start_array, stop_array,
add_disk, remove_disk, mount_disk, unmount_disk, clear_disk_stats
action="disk" - Shares, physical disks, logs
subactions: shares, disks, disk_details, log_files, logs, flash_backup
action="docker" - Container lifecycle and networks
subactions: list, details, start, stop, restart, networks, network_details
action="vm" - Virtual machine lifecycle
subactions: list, details, start, stop, pause, resume, force_stop, reboot, reset
action="notification" - System notifications
subactions: overview, list, create, archive, unread, delete,
delete_archived, archive_all, archive_many,
unarchive_many, unarchive_all, recalculate
action="key" - API key management
subactions: list, get, create, update, delete, add_role, remove_role
action="plugin" - Plugin management
subactions: list, add, remove
action="rclone" - Cloud storage remotes
subactions: list_remotes, config_form, create_remote, delete_remote
action="setting" - System settings mutations
subactions: update, configure_ups
action="customization" - Theme and UI
subactions: theme, public_theme, is_initial_setup, sso_enabled, set_theme
action="oidc" - OIDC/SSO providers
subactions: providers, provider, configuration, public_providers, validate_session
action="user" - Current authenticated user
subactions: me
action="live" - Real-time WebSocket subscription snapshots
subactions: cpu, memory, cpu_telemetry, array_state, parity_progress,
ups_status, notifications_overview, owner, server_status,
log_tail, notification_feed
* Destructive — requires confirm=True
"""
if action == "system":
return await _handle_system(subaction, device_id)