Files
unraid-mcp/unraid_mcp/tools/plugins.py
Jacob Magar 252ec520d1 fix(lint): remove __future__ annotations from new tools, fix 4 failing tests
- Remove `from __future__ import annotations` from array.py, live.py,
  oidc.py, plugins.py to match existing tool pattern and resolve TC002
  ruff errors (fastmcp imports only needed in annotations under PEP 563)
- Add `# noqa: ASYNC109` to live.py timeout parameter (asyncio.timeout
  already used internally)
- Fix test_network_sends_correct_query: query name is GetNetworkInfo
- Fix test_delete_requires_confirm: match "not confirmed" not "destructive"
- Fix test_destructive_set_matches_audit[settings]: add setup_remote_access
  and enable_dynamic_remote_access to KNOWN_DESTRUCTIVE
- Fix test_logs: update mock to dict format {lines: [{timestamp, message}]}

742 tests passing, ruff clean
2026-03-15 19:57:46 -04:00

112 lines
4.0 KiB
Python

"""Plugin management for the Unraid API.
Provides the `unraid_plugins` tool with 3 actions: list, add, remove.
"""
from typing import Any, Literal, get_args
from fastmcp import Context, FastMCP
from ..config.logging import logger
from ..core.client import make_graphql_request
from ..core.exceptions import ToolError, tool_error_handler
from ..core.setup import elicit_destructive_confirmation
QUERIES: dict[str, str] = {
"list": """
query ListPlugins {
plugins { name version hasApiModule hasCliModule }
}
""",
}
MUTATIONS: dict[str, str] = {
"add": """
mutation AddPlugin($input: PluginManagementInput!) {
addPlugin(input: $input)
}
""",
"remove": """
mutation RemovePlugin($input: PluginManagementInput!) {
removePlugin(input: $input)
}
""",
}
DESTRUCTIVE_ACTIONS = {"remove"}
ALL_ACTIONS = set(QUERIES) | set(MUTATIONS)
PLUGIN_ACTIONS = Literal["add", "list", "remove"]
if set(get_args(PLUGIN_ACTIONS)) != ALL_ACTIONS:
_missing = ALL_ACTIONS - set(get_args(PLUGIN_ACTIONS))
_extra = set(get_args(PLUGIN_ACTIONS)) - ALL_ACTIONS
raise RuntimeError(
f"PLUGIN_ACTIONS and ALL_ACTIONS are out of sync. "
f"Missing: {_missing or 'none'}. Extra: {_extra or 'none'}"
)
def register_plugins_tool(mcp: FastMCP) -> None:
"""Register the unraid_plugins tool with the FastMCP instance."""
@mcp.tool()
async def unraid_plugins(
action: PLUGIN_ACTIONS,
ctx: Context | None = None,
confirm: bool = False,
names: list[str] | None = None,
bundled: bool = False,
restart: bool = True,
) -> dict[str, Any]:
"""Manage Unraid API plugins.
Actions:
list - List all installed plugins with version and module info
add - Install one or more plugins (requires names: list of package names)
remove - Remove one or more plugins (requires names, confirm=True)
Parameters:
names - List of plugin package names (required for add/remove)
bundled - Whether plugins are bundled (default: False)
restart - Whether to auto-restart API after operation (default: True)
"""
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:
_desc = f"Remove plugin(s) **{names}** from the Unraid API. This cannot be undone without re-installing."
confirmed = await elicit_destructive_confirmation(ctx, action, _desc)
if not confirmed:
raise ToolError(
f"Action '{action}' was not confirmed. "
"Re-run with confirm=True to bypass elicitation."
)
with tool_error_handler("plugins", action, logger):
logger.info(f"Executing unraid_plugins action={action}")
if action == "list":
data = await make_graphql_request(QUERIES["list"])
return {"success": True, "action": action, "data": data}
if action in ("add", "remove"):
if not names:
raise ToolError(f"names is required for '{action}' action")
input_data = {"names": names, "bundled": bundled, "restart": restart}
mutation_key = "add" if action == "add" else "remove"
data = await make_graphql_request(MUTATIONS[mutation_key], {"input": input_data})
result_key = "addPlugin" if action == "add" else "removePlugin"
restart_required = data.get(result_key)
return {
"success": True,
"action": action,
"names": names,
"manual_restart_required": restart_required,
}
raise ToolError(f"Unhandled action '{action}' — this is a bug")
logger.info("Plugins tool registered successfully")