fix(tools): remove 10 dead actions referencing mutations absent from live API

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.
This commit is contained in:
Jacob Magar
2026-03-15 23:21:25 -04:00
parent c37d4b1c5a
commit 4b43c47091
8 changed files with 59 additions and 661 deletions

View File

@@ -153,22 +153,9 @@ QUERIES: dict[str, str] = {
""",
}
MUTATIONS: dict[str, str] = {
"update_server": """
mutation UpdateServerIdentity($name: String!, $comment: String, $sysModel: String) {
updateServerIdentity(name: $name, comment: $comment, sysModel: $sysModel) {
id name comment status
}
}
""",
"update_ssh": """
mutation UpdateSshSettings($input: UpdateSshInput!) {
updateSshSettings(input: $input) { id useSsh portssh }
}
""",
}
MUTATIONS: dict[str, str] = {}
DESTRUCTIVE_ACTIONS = {"update_ssh"}
DESTRUCTIVE_ACTIONS: set[str] = set()
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
INFO_ACTIONS = Literal[
@@ -191,8 +178,6 @@ INFO_ACTIONS = Literal[
"ups_devices",
"ups_device",
"ups_config",
"update_server",
"update_ssh",
]
if set(get_args(INFO_ACTIONS)) != ALL_ACTIONS:
@@ -324,13 +309,7 @@ def register_info_tool(mcp: FastMCP) -> None:
@mcp.tool()
async def unraid_info(
action: INFO_ACTIONS,
confirm: bool = False,
device_id: str | None = None,
server_name: str | None = None,
server_comment: str | None = None,
sys_model: str | None = None,
ssh_enabled: bool | None = None,
ssh_port: int | None = None,
) -> dict[str, Any]:
"""Query Unraid system information.
@@ -354,52 +333,13 @@ def register_info_tool(mcp: FastMCP) -> None:
ups_devices - List UPS devices
ups_device - Single UPS device (requires device_id)
ups_config - UPS configuration
update_server - Update server name, comment, and model (requires server_name)
update_ssh - Enable/disable SSH and set port (requires ssh_enabled, ssh_port)
"""
if action not in ALL_ACTIONS:
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
if action in DESTRUCTIVE_ACTIONS and not confirm:
raise ToolError(f"Action '{action}' is destructive. Set confirm=True to proceed.")
if action == "ups_device" and not device_id:
raise ToolError("device_id is required for ups_device action")
# Mutation handlers — must return before query = QUERIES[action]
if action == "update_server":
if server_name is None:
raise ToolError("server_name is required for 'update_server' action")
variables_mut: dict[str, Any] = {"name": server_name}
if server_comment is not None:
variables_mut["comment"] = server_comment
if sys_model is not None:
variables_mut["sysModel"] = sys_model
with tool_error_handler("info", action, logger):
logger.info("Executing unraid_info action=update_server")
data = await make_graphql_request(MUTATIONS["update_server"], variables_mut)
return {
"success": True,
"action": "update_server",
"data": data.get("updateServerIdentity"),
}
if action == "update_ssh":
if ssh_enabled is None:
raise ToolError("ssh_enabled is required for 'update_ssh' action")
if ssh_port is None:
raise ToolError("ssh_port is required for 'update_ssh' action")
with tool_error_handler("info", action, logger):
logger.info("Executing unraid_info action=update_ssh")
data = await make_graphql_request(
MUTATIONS["update_ssh"], {"input": {"enabled": ssh_enabled, "port": ssh_port}}
)
return {
"success": True,
"action": "update_ssh",
"data": data.get("updateSshSettings"),
}
# connect is not available on all Unraid API versions
if action == "connect":
raise ToolError(

View File

@@ -83,11 +83,6 @@ MUTATIONS: dict[str, str] = {
}
}
""",
"create_unique": """
mutation NotifyIfUnique($input: NotificationData!) {
notifyIfUnique(input: $input) { id title importance }
}
""",
"unarchive_many": """
mutation UnarchiveNotifications($ids: [PrefixedID!]!) {
unarchiveNotifications(ids: $ids) {
@@ -128,7 +123,6 @@ NOTIFICATION_ACTIONS = Literal[
"delete_archived",
"archive_all",
"archive_many",
"create_unique",
"unarchive_many",
"unarchive_all",
"recalculate",
@@ -173,7 +167,6 @@ def register_notifications_tool(mcp: FastMCP) -> None:
delete_archived - Delete all archived notifications (requires confirm=True)
archive_all - Archive all notifications (optional importance filter)
archive_many - Archive multiple notifications by ID (requires notification_ids)
create_unique - Create notification only if no equivalent unread exists (requires title, subject, description, importance)
unarchive_many - Move notifications back to unread (requires notification_ids)
unarchive_all - Move all archived notifications to unread (optional importance filter)
recalculate - Recompute overview counts from disk
@@ -284,36 +277,6 @@ def register_notifications_tool(mcp: FastMCP) -> None:
)
return {"success": True, "action": "archive_many", "data": data}
if action == "create_unique":
if title is None or subject is None or description is None or importance is None:
raise ToolError(
"create_unique requires title, subject, description, and importance"
)
if importance.upper() not in _VALID_IMPORTANCE:
raise ToolError(
f"importance must be one of: {', '.join(sorted(_VALID_IMPORTANCE))}. "
f"Got: '{importance}'"
)
if len(title) > 200:
raise ToolError(f"title must be at most 200 characters (got {len(title)})")
if len(subject) > 500:
raise ToolError(f"subject must be at most 500 characters (got {len(subject)})")
if len(description) > 2000:
raise ToolError(
f"description must be at most 2000 characters (got {len(description)})"
)
input_data = {
"title": title,
"subject": subject,
"description": description,
"importance": importance.upper(),
}
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}
return {"success": True, "duplicate": False, "data": notification}
if action == "unarchive_many":
if not notification_ids:
raise ToolError("notification_ids is required for 'unarchive_many' action")

View File

@@ -1,7 +1,7 @@
"""System settings, time, UPS, and remote access mutations.
"""System settings and UPS mutations.
Provides the `unraid_settings` tool with 9 actions for updating system
configuration, time settings, UPS, API settings, and Unraid Connect.
Provides the `unraid_settings` tool with 2 actions for updating system
configuration and UPS monitoring.
"""
from typing import Any, Literal, get_args
@@ -19,72 +19,21 @@ MUTATIONS: dict[str, str] = {
updateSettings(input: $input) { restartRequired values warnings }
}
""",
"update_temperature": """
mutation UpdateTemperatureConfig($input: TemperatureConfigInput!) {
updateTemperatureConfig(input: $input)
}
""",
"update_time": """
mutation UpdateSystemTime($input: UpdateSystemTimeInput!) {
updateSystemTime(input: $input) { currentTime timeZone useNtp ntpServers }
}
""",
"configure_ups": """
mutation ConfigureUps($config: UPSConfigInput!) {
configureUps(config: $config)
}
""",
"update_api": """
mutation UpdateApiSettings($input: ConnectSettingsInput!) {
updateApiSettings(input: $input) { accessType forwardType port }
}
""",
"connect_sign_in": """
mutation ConnectSignIn($input: ConnectSignInInput!) {
connectSignIn(input: $input)
}
""",
"connect_sign_out": """
mutation ConnectSignOut {
connectSignOut
}
""",
"setup_remote_access": """
mutation SetupRemoteAccess($input: SetupRemoteAccessInput!) {
setupRemoteAccess(input: $input)
}
""",
"enable_dynamic_remote_access": """
mutation EnableDynamicRemoteAccess($input: EnableDynamicRemoteAccessInput!) {
enableDynamicRemoteAccess(input: $input)
}
""",
"update_ssh": """
mutation UpdateSshSettings($input: UpdateSshInput!) {
updateSshSettings(input: $input) { useSsh portssh }
}
""",
}
DESTRUCTIVE_ACTIONS = {
"configure_ups",
"setup_remote_access",
"enable_dynamic_remote_access",
"update_ssh",
}
ALL_ACTIONS = set(MUTATIONS)
SETTINGS_ACTIONS = Literal[
"configure_ups",
"connect_sign_in",
"connect_sign_out",
"enable_dynamic_remote_access",
"setup_remote_access",
"update",
"update_api",
"update_ssh",
"update_temperature",
"update_time",
]
if set(get_args(SETTINGS_ACTIONS)) != ALL_ACTIONS:
@@ -104,40 +53,13 @@ def register_settings_tool(mcp: FastMCP) -> None:
action: SETTINGS_ACTIONS,
confirm: bool = False,
settings_input: dict[str, Any] | None = None,
temperature_config: dict[str, Any] | None = None,
time_zone: str | None = None,
use_ntp: bool | None = None,
ntp_servers: list[str] | None = None,
manual_datetime: str | None = None,
ups_config: dict[str, Any] | None = None,
access_type: str | None = None,
forward_type: str | None = None,
port: int | None = None,
api_key: str | None = None,
username: str | None = None,
email: str | None = None,
avatar: str | None = None,
access_url_type: str | None = None,
access_url_name: str | None = None,
access_url_ipv4: str | None = None,
access_url_ipv6: str | None = None,
dynamic_enabled: bool | None = None,
ssh_enabled: bool | None = None,
ssh_port: int | None = None,
) -> dict[str, Any]:
"""Update Unraid system settings, time, UPS, and remote access configuration.
"""Update Unraid system settings and UPS configuration.
Actions:
update - Update system settings (requires settings_input dict)
update_temperature - Update temperature sensor config (requires temperature_config dict)
update_time - Update time/timezone/NTP (requires at least one of: time_zone, use_ntp, ntp_servers, manual_datetime)
configure_ups - Configure UPS monitoring (requires ups_config dict, confirm=True)
update_api - Update API/Connect settings (requires at least one of: access_type, forward_type, port)
connect_sign_in - Sign in to Unraid Connect (requires api_key)
connect_sign_out - Sign out from Unraid Connect
setup_remote_access - Configure remote access (requires access_type, confirm=True)
enable_dynamic_remote_access - Enable/disable dynamic remote access (requires access_url_type, dynamic_enabled, confirm=True)
update_ssh - Enable/disable SSH and set port (requires ssh_enabled, ssh_port, confirm=True)
"""
if action not in ALL_ACTIONS:
raise ToolError(f"Invalid action '{action}'. Must be one of: {sorted(ALL_ACTIONS)}")
@@ -154,41 +76,6 @@ def register_settings_tool(mcp: FastMCP) -> None:
data = await make_graphql_request(MUTATIONS["update"], {"input": settings_input})
return {"success": True, "action": "update", "data": data.get("updateSettings")}
if action == "update_temperature":
if temperature_config is None:
raise ToolError(
"temperature_config is required for 'update_temperature' action"
)
data = await make_graphql_request(
MUTATIONS["update_temperature"], {"input": temperature_config}
)
return {
"success": True,
"action": "update_temperature",
"result": data.get("updateTemperatureConfig"),
}
if action == "update_time":
time_input: dict[str, Any] = {}
if time_zone is not None:
time_input["timeZone"] = time_zone
if use_ntp is not None:
time_input["useNtp"] = use_ntp
if ntp_servers is not None:
time_input["ntpServers"] = ntp_servers
if manual_datetime is not None:
time_input["manualDateTime"] = manual_datetime
if not time_input:
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})
return {
"success": True,
"action": "update_time",
"data": data.get("updateSystemTime"),
}
if action == "configure_ups":
if ups_config is None:
raise ToolError("ups_config is required for 'configure_ups' action")
@@ -201,114 +88,6 @@ def register_settings_tool(mcp: FastMCP) -> None:
"result": data.get("configureUps"),
}
if action == "update_api":
api_input: dict[str, Any] = {}
if access_type is not None:
api_input["accessType"] = access_type
if forward_type is not None:
api_input["forwardType"] = forward_type
if port is not None:
api_input["port"] = port
if not api_input:
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})
return {
"success": True,
"action": "update_api",
"data": data.get("updateApiSettings"),
}
if action == "connect_sign_in":
if not api_key:
raise ToolError("api_key is required for 'connect_sign_in' action")
sign_in_input: dict[str, Any] = {"apiKey": api_key}
user_info: dict[str, Any] = {}
if username:
user_info["preferred_username"] = username
if email:
user_info["email"] = email
if avatar:
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}
)
return {
"success": True,
"action": "connect_sign_in",
"result": data.get("connectSignIn"),
}
if action == "connect_sign_out":
data = await make_graphql_request(MUTATIONS["connect_sign_out"])
return {
"success": True,
"action": "connect_sign_out",
"result": data.get("connectSignOut"),
}
if action == "setup_remote_access":
if not access_type:
raise ToolError("access_type is required for 'setup_remote_access' action")
remote_input: dict[str, Any] = {"accessType": access_type}
if forward_type is not 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}
)
return {
"success": True,
"action": "setup_remote_access",
"result": data.get("setupRemoteAccess"),
}
if action == "enable_dynamic_remote_access":
if not access_url_type:
raise ToolError(
"access_url_type is required for 'enable_dynamic_remote_access' action"
)
if dynamic_enabled is None:
raise ToolError(
"dynamic_enabled is required for 'enable_dynamic_remote_access' action"
)
url_input: dict[str, Any] = {"type": access_url_type}
if access_url_name is not None:
url_input["name"] = access_url_name
if access_url_ipv4 is not None:
url_input["ipv4"] = access_url_ipv4
if access_url_ipv6 is not None:
url_input["ipv6"] = access_url_ipv6
dra_vars = {"input": {"url": url_input, "enabled": dynamic_enabled}}
data = await make_graphql_request(
MUTATIONS["enable_dynamic_remote_access"],
dra_vars,
)
return {
"success": True,
"action": "enable_dynamic_remote_access",
"result": data.get("enableDynamicRemoteAccess"),
}
if action == "update_ssh":
if ssh_enabled is None:
raise ToolError("ssh_enabled is required for 'update_ssh' action")
if ssh_port is None:
raise ToolError("ssh_port is required for 'update_ssh' action")
data = await make_graphql_request(
MUTATIONS["update_ssh"],
{"input": {"enabled": ssh_enabled, "port": ssh_port}},
)
return {
"success": True,
"action": "update_ssh",
"data": data.get("updateSshSettings"),
}
raise ToolError(f"Unhandled action '{action}' — this is a bug")
logger.info("Settings tool registered successfully")