mirror of
https://github.com/jmagar/unraid-mcp.git
synced 2026-03-01 16:04:24 -08:00
**Project Configuration:** - Enhance pyproject.toml with comprehensive metadata, keywords, and classifiers - Add LICENSE file (MIT) for proper open-source distribution - Add PUBLISHING.md with comprehensive publishing guidelines - Update .gitignore to exclude tool artifacts (.cache, .pytest_cache, .ruff_cache, .ty_cache) - Ignore documentation working directories (.docs, .full-review, docs/plans, docs/sessions) **Documentation:** - Add extensive Unraid API research documentation - API source code analysis and resolver mapping - Competitive analysis and feature gap assessment - Release notes analysis (7.0.0, 7.1.0, 7.2.0) - Connect platform overview and remote access documentation - Document known API patterns, limitations, and edge cases **Testing & Code Quality:** - Expand test coverage across all tool modules - Add destructive action confirmation tests - Improve test assertions and error case validation - Refine type annotations for better static analysis **Tool Improvements:** - Enhance error handling consistency across all tools - Improve type safety with explicit type annotations - Refine GraphQL query construction patterns - Better handling of optional parameters and edge cases This commit prepares the project for v0.2.0 release with improved metadata, comprehensive documentation, and enhanced code quality. Co-authored-by: Claude <noreply@anthropic.com>
133 lines
4.9 KiB
Python
133 lines
4.9 KiB
Python
"""RClone cloud storage remote management.
|
|
|
|
Provides the `unraid_rclone` tool with 4 actions for managing
|
|
cloud storage remotes (S3, Google Drive, Dropbox, FTP, etc.).
|
|
"""
|
|
|
|
from typing import Any, Literal
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from ..config.logging import logger
|
|
from ..core.client import make_graphql_request
|
|
from ..core.exceptions import ToolError
|
|
|
|
QUERIES: dict[str, str] = {
|
|
"list_remotes": """
|
|
query ListRCloneRemotes {
|
|
rclone { remotes { name type parameters config } }
|
|
}
|
|
""",
|
|
"config_form": """
|
|
query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {
|
|
rclone { configForm(formOptions: $formOptions) { id dataSchema uiSchema } }
|
|
}
|
|
""",
|
|
}
|
|
|
|
MUTATIONS: dict[str, str] = {
|
|
"create_remote": """
|
|
mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {
|
|
rclone { createRCloneRemote(input: $input) { name type parameters } }
|
|
}
|
|
""",
|
|
"delete_remote": """
|
|
mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {
|
|
rclone { deleteRCloneRemote(input: $input) }
|
|
}
|
|
""",
|
|
}
|
|
|
|
DESTRUCTIVE_ACTIONS = {"delete_remote"}
|
|
|
|
RCLONE_ACTIONS = Literal[
|
|
"list_remotes", "config_form", "create_remote", "delete_remote",
|
|
]
|
|
|
|
|
|
def register_rclone_tool(mcp: FastMCP) -> None:
|
|
"""Register the unraid_rclone tool with the FastMCP instance."""
|
|
|
|
@mcp.tool()
|
|
async def unraid_rclone(
|
|
action: RCLONE_ACTIONS,
|
|
confirm: bool = False,
|
|
name: str | None = None,
|
|
provider_type: str | None = None,
|
|
config_data: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Manage RClone cloud storage remotes.
|
|
|
|
Actions:
|
|
list_remotes - List all configured remotes
|
|
config_form - Get config form schema (optional provider_type for specific provider)
|
|
create_remote - Create a new remote (requires name, provider_type, config_data)
|
|
delete_remote - Delete a remote (requires name, confirm=True)
|
|
"""
|
|
all_actions = set(QUERIES) | set(MUTATIONS)
|
|
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.")
|
|
|
|
try:
|
|
logger.info(f"Executing unraid_rclone action={action}")
|
|
|
|
if action == "list_remotes":
|
|
data = await make_graphql_request(QUERIES["list_remotes"])
|
|
remotes = data.get("rclone", {}).get("remotes", [])
|
|
return {"remotes": list(remotes) if isinstance(remotes, list) else []}
|
|
|
|
if action == "config_form":
|
|
variables: dict[str, Any] = {}
|
|
if provider_type:
|
|
variables["formOptions"] = {"providerType": provider_type}
|
|
data = await make_graphql_request(
|
|
QUERIES["config_form"], variables or None
|
|
)
|
|
form = data.get("rclone", {}).get("configForm", {})
|
|
if not form:
|
|
raise ToolError("No RClone config form data received")
|
|
return dict(form)
|
|
|
|
if action == "create_remote":
|
|
if name is None or provider_type is None or config_data is None:
|
|
raise ToolError(
|
|
"create_remote requires name, provider_type, and config_data"
|
|
)
|
|
data = await make_graphql_request(
|
|
MUTATIONS["create_remote"],
|
|
{"input": {"name": name, "type": provider_type, "config": config_data}},
|
|
)
|
|
remote = data.get("rclone", {}).get("createRCloneRemote", {})
|
|
return {
|
|
"success": True,
|
|
"message": f"Remote '{name}' created successfully",
|
|
"remote": remote,
|
|
}
|
|
|
|
if action == "delete_remote":
|
|
if not name:
|
|
raise ToolError("name is required for 'delete_remote' action")
|
|
data = await make_graphql_request(
|
|
MUTATIONS["delete_remote"], {"input": {"name": name}}
|
|
)
|
|
success = data.get("rclone", {}).get("deleteRCloneRemote", False)
|
|
if not success:
|
|
raise ToolError(f"Failed to delete remote '{name}'")
|
|
return {
|
|
"success": True,
|
|
"message": f"Remote '{name}' deleted successfully",
|
|
}
|
|
|
|
raise ToolError(f"Unhandled action '{action}' — this is a bug")
|
|
|
|
except ToolError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error in unraid_rclone action={action}: {e}", exc_info=True)
|
|
raise ToolError(f"Failed to execute rclone/{action}: {str(e)}") from e
|
|
|
|
logger.info("RClone tool registered successfully")
|