mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-23 12:39:24 -07:00
- 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
112 lines
4.0 KiB
Python
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")
|