From 85cd173449fe63cdfd4022b5725b2114fb17f2fc Mon Sep 17 00:00:00 2001 From: Jacob Magar Date: Sat, 14 Mar 2026 04:28:34 -0400 Subject: [PATCH] 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) --- unraid_mcp/core/setup.py | 13 +- unraid_mcp/tools/docker.py | 264 ++++++++++++++++++++++++------ unraid_mcp/tools/notifications.py | 140 +++++++++++++--- unraid_mcp/tools/settings.py | 118 ++++++++++--- 4 files changed, 447 insertions(+), 88 deletions(-) diff --git a/unraid_mcp/core/setup.py b/unraid_mcp/core/setup.py index e37ac7e..e452b50 100644 --- a/unraid_mcp/core/setup.py +++ b/unraid_mcp/core/setup.py @@ -21,15 +21,26 @@ class _UnraidCredentials: api_key: str -async def elicit_and_configure(ctx: Context) -> bool: +async def elicit_and_configure(ctx: Context | None) -> bool: """Prompt the user for Unraid credentials via MCP elicitation. Writes accepted credentials to .env in PROJECT_ROOT and applies them to the running process via apply_runtime_config(). + Args: + ctx: The MCP context for elicitation. If None, returns False immediately + (no context available to prompt the user). + Returns: True if credentials were accepted and applied, False if declined/cancelled. """ + if ctx is None: + logger.warning( + "Cannot elicit credentials: no MCP context available. " + "Run unraid_health action=setup to configure credentials." + ) + return False + result = await ctx.elicit( message=( "Unraid MCP needs your Unraid server credentials to connect.\n\n" diff --git a/unraid_mcp/tools/docker.py b/unraid_mcp/tools/docker.py index 094e5ab..f575726 100644 --- a/unraid_mcp/tools/docker.py +++ b/unraid_mcp/tools/docker.py @@ -453,7 +453,15 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "details": # Resolve name -> ID first (skips list fetch if already an ID) actual_id = await _resolve_container_id(container_id or "") - data = await make_graphql_request(QUERIES["details"]) + try: + data = await make_graphql_request(QUERIES["details"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["details"]) containers = safe_get(data, "docker", "containers", default=[]) # Match by resolved ID (exact match, no second list fetch needed) for c in containers: @@ -463,9 +471,19 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "logs": actual_id = await _resolve_container_id(container_id or "") - data = await make_graphql_request( - QUERIES["logs"], {"id": actual_id, "tail": tail_lines} - ) + try: + data = await make_graphql_request( + QUERIES["logs"], {"id": actual_id, "tail": tail_lines} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + QUERIES["logs"], {"id": actual_id, "tail": tail_lines} + ) logs_data = safe_get(data, "docker", "logs") if logs_data is None: raise ToolError(f"No logs returned for container '{container_id}'") @@ -483,12 +501,28 @@ def register_docker_tool(mcp: FastMCP) -> None: } if action == "networks": - data = await make_graphql_request(QUERIES["networks"]) + try: + data = await make_graphql_request(QUERIES["networks"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["networks"]) networks = safe_get(data, "docker", "networks", default=[]) return {"networks": networks} if action == "network_details": - data = await make_graphql_request(QUERIES["network_details"]) + try: + data = await make_graphql_request(QUERIES["network_details"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["network_details"]) all_networks = safe_get(data, "docker", "networks", default=[]) # Filter client-side by network_id since the API returns all networks for net in all_networks: @@ -497,7 +531,15 @@ def register_docker_tool(mcp: FastMCP) -> None: raise ToolError(f"Network '{network_id}' not found.") if action == "port_conflicts": - data = await make_graphql_request(QUERIES["port_conflicts"]) + try: + data = await make_graphql_request(QUERIES["port_conflicts"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["port_conflicts"]) conflicts_data = safe_get(data, "docker", "portConflicts", default={}) # The GraphQL response is { containerPorts: [...], lanPorts: [...] } # but callers expect result["port_conflicts"] to be a flat list. @@ -511,7 +553,15 @@ def register_docker_tool(mcp: FastMCP) -> None: return {"port_conflicts": conflicts} if action == "check_updates": - data = await make_graphql_request(QUERIES["check_updates"]) + try: + data = await make_graphql_request(QUERIES["check_updates"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["check_updates"]) statuses = safe_get(data, "docker", "containerUpdateStatuses", default=[]) return {"update_statuses": statuses} @@ -519,11 +569,23 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "restart": actual_id = await _resolve_container_id(container_id or "", strict=True) # Stop (idempotent: treat "already stopped" as success) - stop_data = await make_graphql_request( - MUTATIONS["stop"], - {"id": actual_id}, - operation_context={"operation": "stop"}, - ) + try: + stop_data = await make_graphql_request( + MUTATIONS["stop"], + {"id": actual_id}, + operation_context={"operation": "stop"}, + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + stop_data = await make_graphql_request( + MUTATIONS["stop"], + {"id": actual_id}, + operation_context={"operation": "stop"}, + ) stop_was_idempotent = stop_data.get("idempotent_success", False) # Start (idempotent: treat "already running" as success) start_data = await make_graphql_request( @@ -545,7 +607,15 @@ def register_docker_tool(mcp: FastMCP) -> None: return response if action == "update_all": - data = await make_graphql_request(MUTATIONS["update_all"]) + try: + data = await make_graphql_request(MUTATIONS["update_all"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["update_all"]) results = safe_get(data, "docker", "updateAllContainers", default=[]) return {"success": True, "action": "update_all", "containers": results} @@ -558,7 +628,15 @@ def register_docker_tool(mcp: FastMCP) -> None: _vars["parentId"] = parent_id if children_ids is not None: _vars["childrenIds"] = children_ids - data = await make_graphql_request(MUTATIONS["create_folder"], _vars) + try: + data = await make_graphql_request(MUTATIONS["create_folder"], _vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["create_folder"], _vars) organizer = data.get("createDockerFolder") if organizer is None: raise ToolError("create_folder failed: server returned no data") @@ -570,7 +648,15 @@ def register_docker_tool(mcp: FastMCP) -> None: _vars = {"childrenIds": children_ids} if folder_id is not None: _vars["folderId"] = folder_id - data = await make_graphql_request(MUTATIONS["set_folder_children"], _vars) + try: + data = await make_graphql_request(MUTATIONS["set_folder_children"], _vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["set_folder_children"], _vars) organizer = data.get("setDockerFolderChildren") if organizer is None: raise ToolError("set_folder_children failed: server returned no data") @@ -579,9 +665,19 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "delete_entries": if not entry_ids: raise ToolError("entry_ids is required for 'delete_entries' action") - data = await make_graphql_request( - MUTATIONS["delete_entries"], {"entryIds": entry_ids} - ) + try: + data = await make_graphql_request( + MUTATIONS["delete_entries"], {"entryIds": entry_ids} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["delete_entries"], {"entryIds": entry_ids} + ) organizer = data.get("deleteDockerEntries") if organizer is None: raise ToolError("delete_entries failed: server returned no data") @@ -592,13 +688,19 @@ def register_docker_tool(mcp: FastMCP) -> None: raise ToolError("source_entry_ids is required for 'move_to_folder' action") if not destination_folder_id: raise ToolError("destination_folder_id is required for 'move_to_folder' action") - data = await make_graphql_request( - MUTATIONS["move_to_folder"], - { - "sourceEntryIds": source_entry_ids, - "destinationFolderId": destination_folder_id, - }, - ) + _move_vars = { + "sourceEntryIds": source_entry_ids, + "destinationFolderId": destination_folder_id, + } + try: + data = await make_graphql_request(MUTATIONS["move_to_folder"], _move_vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["move_to_folder"], _move_vars) organizer = data.get("moveDockerEntriesToFolder") if organizer is None: raise ToolError("move_to_folder failed: server returned no data") @@ -613,14 +715,20 @@ def register_docker_tool(mcp: FastMCP) -> None: ) if position is None: raise ToolError("position is required for 'move_to_position' action") - data = await make_graphql_request( - MUTATIONS["move_to_position"], - { - "sourceEntryIds": source_entry_ids, - "destinationFolderId": destination_folder_id, - "position": position, - }, - ) + _mtp_vars = { + "sourceEntryIds": source_entry_ids, + "destinationFolderId": destination_folder_id, + "position": position, + } + try: + data = await make_graphql_request(MUTATIONS["move_to_position"], _mtp_vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["move_to_position"], _mtp_vars) organizer = data.get("moveDockerItemsToPosition") if organizer is None: raise ToolError("move_to_position failed: server returned no data") @@ -631,9 +739,16 @@ def register_docker_tool(mcp: FastMCP) -> None: raise ToolError("folder_id is required for 'rename_folder' action") if not new_folder_name: raise ToolError("new_folder_name is required for 'rename_folder' action") - data = await make_graphql_request( - MUTATIONS["rename_folder"], {"folderId": folder_id, "newName": new_folder_name} - ) + _rf_vars = {"folderId": folder_id, "newName": new_folder_name} + try: + data = await make_graphql_request(MUTATIONS["rename_folder"], _rf_vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["rename_folder"], _rf_vars) organizer = data.get("renameDockerFolder") if organizer is None: raise ToolError("rename_folder failed: server returned no data") @@ -649,7 +764,15 @@ def register_docker_tool(mcp: FastMCP) -> None: _vars["sourceEntryIds"] = source_entry_ids if position is not None: _vars["position"] = position - data = await make_graphql_request(MUTATIONS["create_folder_with_items"], _vars) + try: + data = await make_graphql_request(MUTATIONS["create_folder_with_items"], _vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["create_folder_with_items"], _vars) organizer = data.get("createDockerFolderWithItems") if organizer is None: raise ToolError("create_folder_with_items failed: server returned no data") @@ -662,23 +785,46 @@ def register_docker_tool(mcp: FastMCP) -> None: if action == "update_view_prefs": if view_prefs is None: raise ToolError("view_prefs is required for 'update_view_prefs' action") - data = await make_graphql_request( - MUTATIONS["update_view_prefs"], {"viewId": view_id, "prefs": view_prefs} - ) + _uvp_vars = {"viewId": view_id, "prefs": view_prefs} + try: + data = await make_graphql_request(MUTATIONS["update_view_prefs"], _uvp_vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["update_view_prefs"], _uvp_vars) organizer = data.get("updateDockerViewPreferences") if organizer is None: raise ToolError("update_view_prefs failed: server returned no data") return {"success": True, "action": "update_view_prefs", "organizer": organizer} if action == "sync_templates": - data = await make_graphql_request(MUTATIONS["sync_templates"]) + try: + data = await make_graphql_request(MUTATIONS["sync_templates"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["sync_templates"]) result = data.get("syncDockerTemplatePaths") if result is None: raise ToolError("sync_templates failed: server returned no data") return {"success": True, "action": "sync_templates", "result": result} if action == "reset_template_mappings": - data = await make_graphql_request(MUTATIONS["reset_template_mappings"]) + try: + data = await make_graphql_request(MUTATIONS["reset_template_mappings"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["reset_template_mappings"]) return { "success": True, "action": "reset_template_mappings", @@ -686,7 +832,15 @@ def register_docker_tool(mcp: FastMCP) -> None: } if action == "refresh_digests": - data = await make_graphql_request(MUTATIONS["refresh_digests"]) + try: + data = await make_graphql_request(MUTATIONS["refresh_digests"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["refresh_digests"]) return { "success": True, "action": "refresh_digests", @@ -699,11 +853,23 @@ def register_docker_tool(mcp: FastMCP) -> None: op_context: dict[str, str] | None = ( {"operation": action} if action in ("start", "stop") else None ) - data = await make_graphql_request( - MUTATIONS[action], - {"id": actual_id}, - operation_context=op_context, - ) + try: + data = await make_graphql_request( + MUTATIONS[action], + {"id": actual_id}, + operation_context=op_context, + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS[action], + {"id": actual_id}, + operation_context=op_context, + ) # Handle idempotent success if data.get("idempotent_success"): diff --git a/unraid_mcp/tools/notifications.py b/unraid_mcp/tools/notifications.py index 67aa2cb..29c67ed 100644 --- a/unraid_mcp/tools/notifications.py +++ b/unraid_mcp/tools/notifications.py @@ -248,12 +248,28 @@ def register_notifications_tool(mcp: FastMCP) -> None: } if importance: filter_vars["importance"] = importance.upper() - data = await make_graphql_request(QUERIES["list"], {"filter": filter_vars}) + try: + data = await make_graphql_request(QUERIES["list"], {"filter": filter_vars}) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["list"], {"filter": filter_vars}) notifications = data.get("notifications", {}) return {"notifications": notifications.get("list", [])} if action == "warnings": - data = await make_graphql_request(QUERIES["warnings"]) + try: + data = await make_graphql_request(QUERIES["warnings"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(QUERIES["warnings"]) notifications = data.get("notifications", {}) return {"warnings": notifications.get("warningsAndAlerts", [])} @@ -279,7 +295,15 @@ def register_notifications_tool(mcp: FastMCP) -> None: "description": description, "importance": importance.upper(), } - data = await make_graphql_request(MUTATIONS["create"], {"input": input_data}) + try: + data = await make_graphql_request(MUTATIONS["create"], {"input": input_data}) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["create"], {"input": input_data}) notification = data.get("createNotification") if notification is None: raise ToolError("Notification creation failed: server returned no data") @@ -288,35 +312,75 @@ def register_notifications_tool(mcp: FastMCP) -> None: if action in ("archive", "unread"): if not notification_id: raise ToolError(f"notification_id is required for '{action}' action") - data = await make_graphql_request(MUTATIONS[action], {"id": notification_id}) + try: + data = await make_graphql_request(MUTATIONS[action], {"id": notification_id}) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS[action], {"id": notification_id}) return {"success": True, "action": action, "data": data} if action == "delete": if not notification_id or not notification_type: raise ToolError("delete requires notification_id and notification_type") - data = await make_graphql_request( - MUTATIONS["delete"], - {"id": notification_id, "type": notification_type.upper()}, - ) + _del_vars = {"id": notification_id, "type": notification_type.upper()} + try: + data = await make_graphql_request(MUTATIONS["delete"], _del_vars) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["delete"], _del_vars) return {"success": True, "action": "delete", "data": data} if action == "delete_archived": - data = await make_graphql_request(MUTATIONS["delete_archived"]) + try: + data = await make_graphql_request(MUTATIONS["delete_archived"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["delete_archived"]) return {"success": True, "action": "delete_archived", "data": data} if action == "archive_all": variables: dict[str, Any] | None = None if importance: variables = {"importance": importance.upper()} - data = await make_graphql_request(MUTATIONS["archive_all"], variables) + try: + data = await make_graphql_request(MUTATIONS["archive_all"], variables) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["archive_all"], variables) return {"success": True, "action": "archive_all", "data": data} if action == "archive_many": if not notification_ids: raise ToolError("notification_ids is required for 'archive_many' action") - data = await make_graphql_request( - MUTATIONS["archive_many"], {"ids": notification_ids} - ) + try: + data = await make_graphql_request( + MUTATIONS["archive_many"], {"ids": notification_ids} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["archive_many"], {"ids": notification_ids} + ) return {"success": True, "action": "archive_many", "data": data} if action == "create_unique": @@ -343,7 +407,19 @@ def register_notifications_tool(mcp: FastMCP) -> None: "description": description, "importance": importance.upper(), } - data = await make_graphql_request(MUTATIONS["create_unique"], {"input": input_data}) + try: + data = await make_graphql_request( + MUTATIONS["create_unique"], {"input": input_data} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["create_unique"], {"input": input_data} + ) notification = data.get("notifyIfUnique") if notification is None: return {"success": True, "duplicate": True, "data": None} @@ -352,20 +428,46 @@ def register_notifications_tool(mcp: FastMCP) -> None: if action == "unarchive_many": if not notification_ids: raise ToolError("notification_ids is required for 'unarchive_many' action") - data = await make_graphql_request( - MUTATIONS["unarchive_many"], {"ids": notification_ids} - ) + try: + data = await make_graphql_request( + MUTATIONS["unarchive_many"], {"ids": notification_ids} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["unarchive_many"], {"ids": notification_ids} + ) return {"success": True, "action": "unarchive_many", "data": data} if action == "unarchive_all": vars_: dict[str, Any] | None = None if importance: vars_ = {"importance": importance.upper()} - data = await make_graphql_request(MUTATIONS["unarchive_all"], vars_) + try: + data = await make_graphql_request(MUTATIONS["unarchive_all"], vars_) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["unarchive_all"], vars_) return {"success": True, "action": "unarchive_all", "data": data} if action == "recalculate": - data = await make_graphql_request(MUTATIONS["recalculate"]) + try: + data = await make_graphql_request(MUTATIONS["recalculate"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["recalculate"]) return {"success": True, "action": "recalculate", "data": data} raise ToolError(f"Unhandled action '{action}' — this is a bug") diff --git a/unraid_mcp/tools/settings.py b/unraid_mcp/tools/settings.py index 25156ba..a38a81a 100644 --- a/unraid_mcp/tools/settings.py +++ b/unraid_mcp/tools/settings.py @@ -168,9 +168,19 @@ def register_settings_tool(mcp: FastMCP) -> None: raise ToolError( "temperature_config is required for 'update_temperature' action" ) - data = await make_graphql_request( - MUTATIONS["update_temperature"], {"input": temperature_config} - ) + try: + data = await make_graphql_request( + MUTATIONS["update_temperature"], {"input": temperature_config} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["update_temperature"], {"input": temperature_config} + ) return { "success": True, "action": "update_temperature", @@ -191,7 +201,19 @@ def register_settings_tool(mcp: FastMCP) -> None: raise ToolError( "update_time requires at least one of: time_zone, use_ntp, ntp_servers, manual_datetime" ) - data = await make_graphql_request(MUTATIONS["update_time"], {"input": time_input}) + try: + data = await make_graphql_request( + MUTATIONS["update_time"], {"input": time_input} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["update_time"], {"input": time_input} + ) return { "success": True, "action": "update_time", @@ -201,9 +223,19 @@ def register_settings_tool(mcp: FastMCP) -> None: if action == "configure_ups": if ups_config is None: raise ToolError("ups_config is required for 'configure_ups' action") - data = await make_graphql_request( - MUTATIONS["configure_ups"], {"config": ups_config} - ) + try: + data = await make_graphql_request( + MUTATIONS["configure_ups"], {"config": ups_config} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["configure_ups"], {"config": ups_config} + ) return { "success": True, "action": "configure_ups", @@ -222,7 +254,15 @@ def register_settings_tool(mcp: FastMCP) -> None: raise ToolError( "update_api requires at least one of: access_type, forward_type, port" ) - data = await make_graphql_request(MUTATIONS["update_api"], {"input": api_input}) + try: + data = await make_graphql_request(MUTATIONS["update_api"], {"input": api_input}) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["update_api"], {"input": api_input}) return { "success": True, "action": "update_api", @@ -242,9 +282,19 @@ def register_settings_tool(mcp: FastMCP) -> None: user_info["avatar"] = avatar if user_info: sign_in_input["userInfo"] = user_info - data = await make_graphql_request( - MUTATIONS["connect_sign_in"], {"input": sign_in_input} - ) + try: + data = await make_graphql_request( + MUTATIONS["connect_sign_in"], {"input": sign_in_input} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["connect_sign_in"], {"input": sign_in_input} + ) return { "success": True, "action": "connect_sign_in", @@ -252,7 +302,15 @@ def register_settings_tool(mcp: FastMCP) -> None: } if action == "connect_sign_out": - data = await make_graphql_request(MUTATIONS["connect_sign_out"]) + try: + data = await make_graphql_request(MUTATIONS["connect_sign_out"]) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request(MUTATIONS["connect_sign_out"]) return { "success": True, "action": "connect_sign_out", @@ -267,9 +325,19 @@ def register_settings_tool(mcp: FastMCP) -> None: remote_input["forwardType"] = forward_type if port is not None: remote_input["port"] = port - data = await make_graphql_request( - MUTATIONS["setup_remote_access"], {"input": remote_input} - ) + try: + data = await make_graphql_request( + MUTATIONS["setup_remote_access"], {"input": remote_input} + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["setup_remote_access"], {"input": remote_input} + ) return { "success": True, "action": "setup_remote_access", @@ -292,10 +360,22 @@ def register_settings_tool(mcp: FastMCP) -> None: url_input["ipv4"] = access_url_ipv4 if access_url_ipv6 is not None: url_input["ipv6"] = access_url_ipv6 - data = await make_graphql_request( - MUTATIONS["enable_dynamic_remote_access"], - {"input": {"url": url_input, "enabled": dynamic_enabled}}, - ) + dra_vars = {"input": {"url": url_input, "enabled": dynamic_enabled}} + try: + data = await make_graphql_request( + MUTATIONS["enable_dynamic_remote_access"], + dra_vars, + ) + except CredentialsNotConfiguredError: + configured = await elicit_and_configure(ctx) + if not configured: + raise ToolError( + "Credentials required. Run `unraid_health action=setup` to configure." + ) + data = await make_graphql_request( + MUTATIONS["enable_dynamic_remote_access"], + dra_vars, + ) return { "success": True, "action": "enable_dynamic_remote_access",