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)
This commit is contained in:
Jacob Magar
2026-03-14 04:28:34 -04:00
parent e1c80cf1da
commit 85cd173449
4 changed files with 447 additions and 88 deletions

View File

@@ -21,15 +21,26 @@ class _UnraidCredentials:
api_key: str 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. """Prompt the user for Unraid credentials via MCP elicitation.
Writes accepted credentials to .env in PROJECT_ROOT and applies them Writes accepted credentials to .env in PROJECT_ROOT and applies them
to the running process via apply_runtime_config(). 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: Returns:
True if credentials were accepted and applied, False if declined/cancelled. 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( result = await ctx.elicit(
message=( message=(
"Unraid MCP needs your Unraid server credentials to connect.\n\n" "Unraid MCP needs your Unraid server credentials to connect.\n\n"

View File

@@ -453,6 +453,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
if action == "details": if action == "details":
# Resolve name -> ID first (skips list fetch if already an ID) # Resolve name -> ID first (skips list fetch if already an ID)
actual_id = await _resolve_container_id(container_id or "") actual_id = await _resolve_container_id(container_id or "")
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"]) data = await make_graphql_request(QUERIES["details"])
containers = safe_get(data, "docker", "containers", default=[]) containers = safe_get(data, "docker", "containers", default=[])
# Match by resolved ID (exact match, no second list fetch needed) # Match by resolved ID (exact match, no second list fetch needed)
@@ -463,6 +471,16 @@ def register_docker_tool(mcp: FastMCP) -> None:
if action == "logs": if action == "logs":
actual_id = await _resolve_container_id(container_id or "") actual_id = await _resolve_container_id(container_id or "")
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( data = await make_graphql_request(
QUERIES["logs"], {"id": actual_id, "tail": tail_lines} QUERIES["logs"], {"id": actual_id, "tail": tail_lines}
) )
@@ -483,11 +501,27 @@ def register_docker_tool(mcp: FastMCP) -> None:
} }
if action == "networks": if action == "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"]) data = await make_graphql_request(QUERIES["networks"])
networks = safe_get(data, "docker", "networks", default=[]) networks = safe_get(data, "docker", "networks", default=[])
return {"networks": networks} return {"networks": networks}
if action == "network_details": if action == "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"]) data = await make_graphql_request(QUERIES["network_details"])
all_networks = safe_get(data, "docker", "networks", default=[]) all_networks = safe_get(data, "docker", "networks", default=[])
# Filter client-side by network_id since the API returns all networks # Filter client-side by network_id since the API returns all networks
@@ -497,6 +531,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
raise ToolError(f"Network '{network_id}' not found.") raise ToolError(f"Network '{network_id}' not found.")
if action == "port_conflicts": if action == "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"]) data = await make_graphql_request(QUERIES["port_conflicts"])
conflicts_data = safe_get(data, "docker", "portConflicts", default={}) conflicts_data = safe_get(data, "docker", "portConflicts", default={})
# The GraphQL response is { containerPorts: [...], lanPorts: [...] } # The GraphQL response is { containerPorts: [...], lanPorts: [...] }
@@ -511,6 +553,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
return {"port_conflicts": conflicts} return {"port_conflicts": conflicts}
if action == "check_updates": if action == "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"]) data = await make_graphql_request(QUERIES["check_updates"])
statuses = safe_get(data, "docker", "containerUpdateStatuses", default=[]) statuses = safe_get(data, "docker", "containerUpdateStatuses", default=[])
return {"update_statuses": statuses} return {"update_statuses": statuses}
@@ -519,6 +569,18 @@ def register_docker_tool(mcp: FastMCP) -> None:
if action == "restart": if action == "restart":
actual_id = await _resolve_container_id(container_id or "", strict=True) actual_id = await _resolve_container_id(container_id or "", strict=True)
# Stop (idempotent: treat "already stopped" as success) # Stop (idempotent: treat "already stopped" as success)
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( stop_data = await make_graphql_request(
MUTATIONS["stop"], MUTATIONS["stop"],
{"id": actual_id}, {"id": actual_id},
@@ -545,6 +607,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
return response return response
if action == "update_all": if action == "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"]) data = await make_graphql_request(MUTATIONS["update_all"])
results = safe_get(data, "docker", "updateAllContainers", default=[]) results = safe_get(data, "docker", "updateAllContainers", default=[])
return {"success": True, "action": "update_all", "containers": results} return {"success": True, "action": "update_all", "containers": results}
@@ -558,6 +628,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
_vars["parentId"] = parent_id _vars["parentId"] = parent_id
if children_ids is not None: if children_ids is not None:
_vars["childrenIds"] = children_ids _vars["childrenIds"] = children_ids
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) data = await make_graphql_request(MUTATIONS["create_folder"], _vars)
organizer = data.get("createDockerFolder") organizer = data.get("createDockerFolder")
if organizer is None: if organizer is None:
@@ -570,6 +648,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
_vars = {"childrenIds": children_ids} _vars = {"childrenIds": children_ids}
if folder_id is not None: if folder_id is not None:
_vars["folderId"] = folder_id _vars["folderId"] = folder_id
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) data = await make_graphql_request(MUTATIONS["set_folder_children"], _vars)
organizer = data.get("setDockerFolderChildren") organizer = data.get("setDockerFolderChildren")
if organizer is None: if organizer is None:
@@ -579,6 +665,16 @@ def register_docker_tool(mcp: FastMCP) -> None:
if action == "delete_entries": if action == "delete_entries":
if not entry_ids: if not entry_ids:
raise ToolError("entry_ids is required for 'delete_entries' action") raise ToolError("entry_ids is required for 'delete_entries' action")
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( data = await make_graphql_request(
MUTATIONS["delete_entries"], {"entryIds": entry_ids} MUTATIONS["delete_entries"], {"entryIds": entry_ids}
) )
@@ -592,13 +688,19 @@ def register_docker_tool(mcp: FastMCP) -> None:
raise ToolError("source_entry_ids is required for 'move_to_folder' action") raise ToolError("source_entry_ids is required for 'move_to_folder' action")
if not destination_folder_id: if not destination_folder_id:
raise ToolError("destination_folder_id is required for 'move_to_folder' action") raise ToolError("destination_folder_id is required for 'move_to_folder' action")
data = await make_graphql_request( _move_vars = {
MUTATIONS["move_to_folder"],
{
"sourceEntryIds": source_entry_ids, "sourceEntryIds": source_entry_ids,
"destinationFolderId": destination_folder_id, "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") organizer = data.get("moveDockerEntriesToFolder")
if organizer is None: if organizer is None:
raise ToolError("move_to_folder failed: server returned no data") 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: if position is None:
raise ToolError("position is required for 'move_to_position' action") raise ToolError("position is required for 'move_to_position' action")
data = await make_graphql_request( _mtp_vars = {
MUTATIONS["move_to_position"],
{
"sourceEntryIds": source_entry_ids, "sourceEntryIds": source_entry_ids,
"destinationFolderId": destination_folder_id, "destinationFolderId": destination_folder_id,
"position": position, "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") organizer = data.get("moveDockerItemsToPosition")
if organizer is None: if organizer is None:
raise ToolError("move_to_position failed: server returned no data") 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") raise ToolError("folder_id is required for 'rename_folder' action")
if not new_folder_name: if not new_folder_name:
raise ToolError("new_folder_name is required for 'rename_folder' action") raise ToolError("new_folder_name is required for 'rename_folder' action")
data = await make_graphql_request( _rf_vars = {"folderId": folder_id, "newName": new_folder_name}
MUTATIONS["rename_folder"], {"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") organizer = data.get("renameDockerFolder")
if organizer is None: if organizer is None:
raise ToolError("rename_folder failed: server returned no data") raise ToolError("rename_folder failed: server returned no data")
@@ -649,6 +764,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
_vars["sourceEntryIds"] = source_entry_ids _vars["sourceEntryIds"] = source_entry_ids
if position is not None: if position is not None:
_vars["position"] = position _vars["position"] = position
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) data = await make_graphql_request(MUTATIONS["create_folder_with_items"], _vars)
organizer = data.get("createDockerFolderWithItems") organizer = data.get("createDockerFolderWithItems")
if organizer is None: if organizer is None:
@@ -662,15 +785,30 @@ def register_docker_tool(mcp: FastMCP) -> None:
if action == "update_view_prefs": if action == "update_view_prefs":
if view_prefs is None: if view_prefs is None:
raise ToolError("view_prefs is required for 'update_view_prefs' action") raise ToolError("view_prefs is required for 'update_view_prefs' action")
data = await make_graphql_request( _uvp_vars = {"viewId": view_id, "prefs": view_prefs}
MUTATIONS["update_view_prefs"], {"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") organizer = data.get("updateDockerViewPreferences")
if organizer is None: if organizer is None:
raise ToolError("update_view_prefs failed: server returned no data") raise ToolError("update_view_prefs failed: server returned no data")
return {"success": True, "action": "update_view_prefs", "organizer": organizer} return {"success": True, "action": "update_view_prefs", "organizer": organizer}
if action == "sync_templates": if action == "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"]) data = await make_graphql_request(MUTATIONS["sync_templates"])
result = data.get("syncDockerTemplatePaths") result = data.get("syncDockerTemplatePaths")
if result is None: if result is None:
@@ -678,6 +816,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
return {"success": True, "action": "sync_templates", "result": result} return {"success": True, "action": "sync_templates", "result": result}
if action == "reset_template_mappings": if action == "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"]) data = await make_graphql_request(MUTATIONS["reset_template_mappings"])
return { return {
"success": True, "success": True,
@@ -686,6 +832,14 @@ def register_docker_tool(mcp: FastMCP) -> None:
} }
if action == "refresh_digests": if action == "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"]) data = await make_graphql_request(MUTATIONS["refresh_digests"])
return { return {
"success": True, "success": True,
@@ -699,6 +853,18 @@ def register_docker_tool(mcp: FastMCP) -> None:
op_context: dict[str, str] | None = ( op_context: dict[str, str] | None = (
{"operation": action} if action in ("start", "stop") else None {"operation": action} if action in ("start", "stop") else None
) )
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( data = await make_graphql_request(
MUTATIONS[action], MUTATIONS[action],
{"id": actual_id}, {"id": actual_id},

View File

@@ -248,11 +248,27 @@ def register_notifications_tool(mcp: FastMCP) -> None:
} }
if importance: if importance:
filter_vars["importance"] = importance.upper() filter_vars["importance"] = importance.upper()
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}) data = await make_graphql_request(QUERIES["list"], {"filter": filter_vars})
notifications = data.get("notifications", {}) notifications = data.get("notifications", {})
return {"notifications": notifications.get("list", [])} return {"notifications": notifications.get("list", [])}
if action == "warnings": if action == "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"]) data = await make_graphql_request(QUERIES["warnings"])
notifications = data.get("notifications", {}) notifications = data.get("notifications", {})
return {"warnings": notifications.get("warningsAndAlerts", [])} return {"warnings": notifications.get("warningsAndAlerts", [])}
@@ -279,6 +295,14 @@ def register_notifications_tool(mcp: FastMCP) -> None:
"description": description, "description": description,
"importance": importance.upper(), "importance": importance.upper(),
} }
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}) data = await make_graphql_request(MUTATIONS["create"], {"input": input_data})
notification = data.get("createNotification") notification = data.get("createNotification")
if notification is None: if notification is None:
@@ -288,19 +312,41 @@ def register_notifications_tool(mcp: FastMCP) -> None:
if action in ("archive", "unread"): if action in ("archive", "unread"):
if not notification_id: if not notification_id:
raise ToolError(f"notification_id is required for '{action}' action") raise ToolError(f"notification_id is required for '{action}' action")
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}) data = await make_graphql_request(MUTATIONS[action], {"id": notification_id})
return {"success": True, "action": action, "data": data} return {"success": True, "action": action, "data": data}
if action == "delete": if action == "delete":
if not notification_id or not notification_type: if not notification_id or not notification_type:
raise ToolError("delete requires notification_id and notification_type") raise ToolError("delete requires notification_id and notification_type")
data = await make_graphql_request( _del_vars = {"id": notification_id, "type": notification_type.upper()}
MUTATIONS["delete"], try:
{"id": notification_id, "type": notification_type.upper()}, 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} return {"success": True, "action": "delete", "data": data}
if action == "delete_archived": if action == "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"]) data = await make_graphql_request(MUTATIONS["delete_archived"])
return {"success": True, "action": "delete_archived", "data": data} return {"success": True, "action": "delete_archived", "data": data}
@@ -308,12 +354,30 @@ def register_notifications_tool(mcp: FastMCP) -> None:
variables: dict[str, Any] | None = None variables: dict[str, Any] | None = None
if importance: if importance:
variables = {"importance": importance.upper()} variables = {"importance": importance.upper()}
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) data = await make_graphql_request(MUTATIONS["archive_all"], variables)
return {"success": True, "action": "archive_all", "data": data} return {"success": True, "action": "archive_all", "data": data}
if action == "archive_many": if action == "archive_many":
if not notification_ids: if not notification_ids:
raise ToolError("notification_ids is required for 'archive_many' action") raise ToolError("notification_ids is required for 'archive_many' action")
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( data = await make_graphql_request(
MUTATIONS["archive_many"], {"ids": notification_ids} MUTATIONS["archive_many"], {"ids": notification_ids}
) )
@@ -343,7 +407,19 @@ def register_notifications_tool(mcp: FastMCP) -> None:
"description": description, "description": description,
"importance": importance.upper(), "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") notification = data.get("notifyIfUnique")
if notification is None: if notification is None:
return {"success": True, "duplicate": True, "data": None} return {"success": True, "duplicate": True, "data": None}
@@ -352,6 +428,16 @@ def register_notifications_tool(mcp: FastMCP) -> None:
if action == "unarchive_many": if action == "unarchive_many":
if not notification_ids: if not notification_ids:
raise ToolError("notification_ids is required for 'unarchive_many' action") raise ToolError("notification_ids is required for 'unarchive_many' action")
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( data = await make_graphql_request(
MUTATIONS["unarchive_many"], {"ids": notification_ids} MUTATIONS["unarchive_many"], {"ids": notification_ids}
) )
@@ -361,10 +447,26 @@ def register_notifications_tool(mcp: FastMCP) -> None:
vars_: dict[str, Any] | None = None vars_: dict[str, Any] | None = None
if importance: if importance:
vars_ = {"importance": importance.upper()} vars_ = {"importance": importance.upper()}
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_) data = await make_graphql_request(MUTATIONS["unarchive_all"], vars_)
return {"success": True, "action": "unarchive_all", "data": data} return {"success": True, "action": "unarchive_all", "data": data}
if action == "recalculate": if action == "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"]) data = await make_graphql_request(MUTATIONS["recalculate"])
return {"success": True, "action": "recalculate", "data": data} return {"success": True, "action": "recalculate", "data": data}

View File

@@ -168,6 +168,16 @@ def register_settings_tool(mcp: FastMCP) -> None:
raise ToolError( raise ToolError(
"temperature_config is required for 'update_temperature' action" "temperature_config is required for 'update_temperature' action"
) )
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( data = await make_graphql_request(
MUTATIONS["update_temperature"], {"input": temperature_config} MUTATIONS["update_temperature"], {"input": temperature_config}
) )
@@ -191,7 +201,19 @@ def register_settings_tool(mcp: FastMCP) -> None:
raise ToolError( raise ToolError(
"update_time requires at least one of: time_zone, use_ntp, ntp_servers, manual_datetime" "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 { return {
"success": True, "success": True,
"action": "update_time", "action": "update_time",
@@ -201,6 +223,16 @@ def register_settings_tool(mcp: FastMCP) -> None:
if action == "configure_ups": if action == "configure_ups":
if ups_config is None: if ups_config is None:
raise ToolError("ups_config is required for 'configure_ups' action") raise ToolError("ups_config is required for 'configure_ups' action")
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( data = await make_graphql_request(
MUTATIONS["configure_ups"], {"config": ups_config} MUTATIONS["configure_ups"], {"config": ups_config}
) )
@@ -222,6 +254,14 @@ def register_settings_tool(mcp: FastMCP) -> None:
raise ToolError( raise ToolError(
"update_api requires at least one of: access_type, forward_type, port" "update_api requires at least one of: access_type, forward_type, port"
) )
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}) data = await make_graphql_request(MUTATIONS["update_api"], {"input": api_input})
return { return {
"success": True, "success": True,
@@ -242,6 +282,16 @@ def register_settings_tool(mcp: FastMCP) -> None:
user_info["avatar"] = avatar user_info["avatar"] = avatar
if user_info: if user_info:
sign_in_input["userInfo"] = user_info sign_in_input["userInfo"] = user_info
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( data = await make_graphql_request(
MUTATIONS["connect_sign_in"], {"input": sign_in_input} MUTATIONS["connect_sign_in"], {"input": sign_in_input}
) )
@@ -252,6 +302,14 @@ def register_settings_tool(mcp: FastMCP) -> None:
} }
if action == "connect_sign_out": if action == "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"]) data = await make_graphql_request(MUTATIONS["connect_sign_out"])
return { return {
"success": True, "success": True,
@@ -267,6 +325,16 @@ def register_settings_tool(mcp: FastMCP) -> None:
remote_input["forwardType"] = forward_type remote_input["forwardType"] = forward_type
if port is not None: if port is not None:
remote_input["port"] = port remote_input["port"] = port
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( data = await make_graphql_request(
MUTATIONS["setup_remote_access"], {"input": remote_input} MUTATIONS["setup_remote_access"], {"input": remote_input}
) )
@@ -292,9 +360,21 @@ def register_settings_tool(mcp: FastMCP) -> None:
url_input["ipv4"] = access_url_ipv4 url_input["ipv4"] = access_url_ipv4
if access_url_ipv6 is not None: if access_url_ipv6 is not None:
url_input["ipv6"] = access_url_ipv6 url_input["ipv6"] = access_url_ipv6
dra_vars = {"input": {"url": url_input, "enabled": dynamic_enabled}}
try:
data = await make_graphql_request( data = await make_graphql_request(
MUTATIONS["enable_dynamic_remote_access"], MUTATIONS["enable_dynamic_remote_access"],
{"input": {"url": url_input, "enabled": dynamic_enabled}}, 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 { return {
"success": True, "success": True,