mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
Replace 7-11 line inline guard blocks in array.py, keys.py, and plugins.py with single await gate_destructive_action(...) calls. Also fix guards.py to raise unraid_mcp.core.exceptions.ToolError (project subclass) instead of fastmcp.exceptions.ToolError so pytest.raises catches it correctly in tests.
111 lines
3.7 KiB
Python
111 lines
3.7 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.guards import gate_destructive_action
|
|
|
|
|
|
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)}")
|
|
|
|
await gate_destructive_action(
|
|
ctx,
|
|
action,
|
|
DESTRUCTIVE_ACTIONS,
|
|
confirm,
|
|
f"Remove plugin(s) **{names}** from the Unraid API. This cannot be undone without re-installing.",
|
|
)
|
|
|
|
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")
|