Initial commit: MCP Hub Community Edition v3.0.0
Community edition generated from private repo via sync pipeline. Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n, Supabase, OpenPanel, Appwrite, Directus) with ~587 tools. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
plugins/directus/__init__.py
Normal file
6
plugins/directus/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Directus CMS Plugin - Self-Hosted Headless CMS Management"""
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
from plugins.directus.plugin import DirectusPlugin
|
||||
|
||||
__all__ = ["DirectusPlugin", "DirectusClient"]
|
||||
1335
plugins/directus/client.py
Normal file
1335
plugins/directus/client.py
Normal file
File diff suppressed because it is too large
Load Diff
38
plugins/directus/handlers/__init__.py
Normal file
38
plugins/directus/handlers/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Directus Plugin Handlers
|
||||
|
||||
Each handler module provides:
|
||||
1. Tool specifications (get_tool_specifications)
|
||||
2. Handler functions for each tool
|
||||
|
||||
Phase J.1: items (12) + collections (14) = 26 tools
|
||||
Phase J.2: files (12) + users (10) = 22 tools
|
||||
Phase J.3: access (12) + automation (12) = 24 tools
|
||||
Phase J.4: content (10) + dashboards (8) + system (10) = 28 tools
|
||||
|
||||
Total: 100 tools
|
||||
"""
|
||||
|
||||
from plugins.directus.handlers import (
|
||||
access,
|
||||
automation,
|
||||
collections,
|
||||
content,
|
||||
dashboards,
|
||||
files,
|
||||
items,
|
||||
system,
|
||||
users,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"items",
|
||||
"collections",
|
||||
"files",
|
||||
"users",
|
||||
"access",
|
||||
"automation",
|
||||
"content",
|
||||
"dashboards",
|
||||
"system",
|
||||
]
|
||||
466
plugins/directus/handlers/access.py
Normal file
466
plugins/directus/handlers/access.py
Normal file
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
Access Control Handler - Roles, Permissions, Policies
|
||||
|
||||
Phase J.3: 12 tools
|
||||
- Roles: list, get, create, update, delete (5)
|
||||
- Permissions: list, get, create, update, delete, get_my (6)
|
||||
- Policies: list (1)
|
||||
|
||||
Note: Directus v10+ uses policies for permissions.
|
||||
The 'policy' parameter is required in create_permission.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""Parse a parameter that may be a JSON string or already a native type."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# ROLES (5)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_roles",
|
||||
"method_name": "list_roles",
|
||||
"description": "List all roles in Directus.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum roles to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_role",
|
||||
"method_name": "get_role",
|
||||
"description": "Get role details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Role UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_role",
|
||||
"method_name": "create_role",
|
||||
"description": "Create a new role.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Role name"},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Material icon name",
|
||||
"default": "supervised_user_circle",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Role description",
|
||||
},
|
||||
"admin_access": {
|
||||
"type": "boolean",
|
||||
"description": "Full admin access",
|
||||
"default": False,
|
||||
},
|
||||
"app_access": {
|
||||
"type": "boolean",
|
||||
"description": "Access to admin app",
|
||||
"default": True,
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_role",
|
||||
"method_name": "update_role",
|
||||
"description": "Update role settings.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Role UUID"},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (name, icon, description, admin_access, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_role",
|
||||
"method_name": "delete_role",
|
||||
"description": "Delete a role. Users with this role will have no role.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Role UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# PERMISSIONS (6)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_permissions",
|
||||
"method_name": "list_permissions",
|
||||
"description": "List all permissions.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"role": {"_eq": "role-uuid"}})',
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum permissions to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_permission",
|
||||
"method_name": "get_permission",
|
||||
"description": "Get permission details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Permission ID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_permission",
|
||||
"method_name": "create_permission",
|
||||
"description": "Create a new permission rule. NOTE: In Directus v10+, 'policy' is required.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["create", "read", "update", "delete", "share"],
|
||||
"description": "Permission action",
|
||||
},
|
||||
"policy": {
|
||||
"type": "string",
|
||||
"description": "Policy UUID (REQUIRED in Directus v10+). Use list_policies to get available policies.",
|
||||
},
|
||||
"role": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Role UUID (null for public) - deprecated in v10+, use policy instead",
|
||||
},
|
||||
"permissions": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter rules for this permission",
|
||||
},
|
||||
"validation": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Validation rules for create/update",
|
||||
},
|
||||
"presets": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Default values for create",
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Allowed fields (* for all)",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "action", "policy"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_permission",
|
||||
"method_name": "update_permission",
|
||||
"description": "Update a permission rule.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Permission ID"},
|
||||
"data": {"type": "object", "description": "Fields to update"},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_permission",
|
||||
"method_name": "delete_permission",
|
||||
"description": "Delete a permission rule.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Permission ID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "get_my_permissions",
|
||||
"method_name": "get_my_permissions",
|
||||
"description": "Get the current user's effective permissions.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
# =====================
|
||||
# POLICIES (1)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_policies",
|
||||
"method_name": "list_policies",
|
||||
"description": "List all access policies.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum policies to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_roles(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List roles."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_roles(filter=parsed_filter, sort=parsed_sort, limit=limit)
|
||||
roles = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(roles), "roles": roles}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_role(client: DirectusClient, id: str) -> str:
|
||||
"""Get role by ID."""
|
||||
try:
|
||||
result = await client.get_role(id)
|
||||
return json.dumps(
|
||||
{"success": True, "role": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_role(
|
||||
client: DirectusClient,
|
||||
name: str,
|
||||
icon: str = "supervised_user_circle",
|
||||
description: str | None = None,
|
||||
admin_access: bool = False,
|
||||
app_access: bool = True,
|
||||
) -> str:
|
||||
"""Create a new role."""
|
||||
try:
|
||||
result = await client.create_role(
|
||||
name=name,
|
||||
icon=icon,
|
||||
description=description,
|
||||
admin_access=admin_access,
|
||||
app_access=app_access,
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Role '{name}' created", "role": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_role(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update role."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_role(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Role updated", "role": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_role(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a role."""
|
||||
try:
|
||||
await client.delete_role(id)
|
||||
return json.dumps({"success": True, "message": f"Role {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_permissions(
|
||||
client: DirectusClient, filter: dict | None = None, limit: int = 100
|
||||
) -> str:
|
||||
"""List permissions."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
result = await client.list_permissions(filter=parsed_filter, limit=limit)
|
||||
permissions = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(permissions), "permissions": permissions},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_permission(client: DirectusClient, id: str) -> str:
|
||||
"""Get permission by ID."""
|
||||
try:
|
||||
result = await client.get_permission(id)
|
||||
return json.dumps(
|
||||
{"success": True, "permission": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_permission(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
action: str,
|
||||
policy: str,
|
||||
role: str | None = None,
|
||||
permissions: dict | None = None,
|
||||
validation: dict | None = None,
|
||||
presets: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Create a new permission. Requires 'policy' in Directus v10+."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_permissions = _parse_json_param(permissions, "permissions")
|
||||
parsed_validation = _parse_json_param(validation, "validation")
|
||||
parsed_presets = _parse_json_param(presets, "presets")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
|
||||
result = await client.create_permission(
|
||||
collection=collection,
|
||||
action=action,
|
||||
policy=policy,
|
||||
role=role,
|
||||
permissions=parsed_permissions,
|
||||
validation=parsed_validation,
|
||||
presets=parsed_presets,
|
||||
fields=parsed_fields,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Permission created for {collection}.{action}",
|
||||
"permission": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_permission(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update permission."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_permission(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Permission updated", "permission": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_permission(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a permission."""
|
||||
try:
|
||||
await client.delete_permission(id)
|
||||
return json.dumps({"success": True, "message": f"Permission {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_my_permissions(client: DirectusClient) -> str:
|
||||
"""Get current user's permissions."""
|
||||
try:
|
||||
result = await client.get_my_permissions()
|
||||
return json.dumps(
|
||||
{"success": True, "permissions": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_policies(
|
||||
client: DirectusClient, filter: dict | None = None, limit: int = 100
|
||||
) -> str:
|
||||
"""List policies."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
result = await client.list_policies(filter=parsed_filter, limit=limit)
|
||||
policies = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(policies), "policies": policies},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
573
plugins/directus/handlers/automation.py
Normal file
573
plugins/directus/handlers/automation.py
Normal file
@@ -0,0 +1,573 @@
|
||||
"""
|
||||
Automation Handler - Flows, Operations, Webhooks
|
||||
|
||||
Phase J.3: 12 tools
|
||||
- Flows: list, get, create, update, delete, trigger (6)
|
||||
- Operations: list, create (2)
|
||||
- Webhooks: list, create, update, delete (4)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""Parse a parameter that may be a JSON string or already a native type."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# FLOWS (6)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_flows",
|
||||
"method_name": "list_flows",
|
||||
"description": "List all automation flows.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum flows to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_flow",
|
||||
"method_name": "get_flow",
|
||||
"description": "Get flow details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Flow UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_flow",
|
||||
"method_name": "create_flow",
|
||||
"description": "Create a new automation flow.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Flow name"},
|
||||
"trigger": {
|
||||
"type": "string",
|
||||
"enum": ["event", "schedule", "operation", "webhook", "manual"],
|
||||
"description": "Trigger type",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive"],
|
||||
"default": "active",
|
||||
"description": "Flow status",
|
||||
},
|
||||
"icon": {"type": "string", "description": "Material icon", "default": "bolt"},
|
||||
"options": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Trigger-specific options",
|
||||
},
|
||||
"accountability": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"enum": ["all", "activity", None],
|
||||
"description": "Accountability tracking",
|
||||
},
|
||||
"description": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Flow description",
|
||||
},
|
||||
},
|
||||
"required": ["name", "trigger"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_flow",
|
||||
"method_name": "update_flow",
|
||||
"description": "Update flow settings.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Flow UUID"},
|
||||
"data": {"type": "object", "description": "Fields to update"},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_flow",
|
||||
"method_name": "delete_flow",
|
||||
"description": "Delete a flow and its operations.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Flow UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "trigger_flow",
|
||||
"method_name": "trigger_flow",
|
||||
"description": "Manually trigger a flow.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Flow UUID"},
|
||||
"data": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Data to pass to the flow",
|
||||
},
|
||||
},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# =====================
|
||||
# OPERATIONS (2)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_operations",
|
||||
"method_name": "list_operations",
|
||||
"description": "List all operations in flows.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"flow": {"_eq": "flow-uuid"}})',
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum operations to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_operation",
|
||||
"method_name": "create_operation",
|
||||
"description": "Create a new operation in a flow.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"flow": {"type": "string", "description": "Flow UUID"},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Operation type (e.g., 'log', 'mail', 'request', 'item-create')",
|
||||
},
|
||||
"name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Operation name",
|
||||
},
|
||||
"key": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Operation key (for referencing in other operations)",
|
||||
},
|
||||
"options": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Operation-specific options",
|
||||
},
|
||||
"position_x": {
|
||||
"type": "integer",
|
||||
"description": "X position in flow editor",
|
||||
"default": 0,
|
||||
},
|
||||
"position_y": {
|
||||
"type": "integer",
|
||||
"description": "Y position in flow editor",
|
||||
"default": 0,
|
||||
},
|
||||
},
|
||||
"required": ["flow", "type"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# WEBHOOKS (4)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_webhooks",
|
||||
"method_name": "list_webhooks",
|
||||
"description": "[DEPRECATED] List all webhooks. Webhooks are deprecated in Directus 10+, use Flows instead.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum webhooks to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_webhook",
|
||||
"method_name": "create_webhook",
|
||||
"description": "[DEPRECATED] Create a new webhook. Use Flows instead in Directus 10+. See create_flow and create_operation for the recommended alternative.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Webhook name"},
|
||||
"url": {"type": "string", "description": "Target URL"},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Actions to trigger (create, update, delete)",
|
||||
},
|
||||
"collections": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Collections to watch",
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"],
|
||||
"default": "POST",
|
||||
"description": "HTTP method",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive"],
|
||||
"default": "active",
|
||||
"description": "Webhook status",
|
||||
},
|
||||
"headers": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Custom headers",
|
||||
},
|
||||
},
|
||||
"required": ["name", "url", "actions", "collections"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_webhook",
|
||||
"method_name": "update_webhook",
|
||||
"description": "[DEPRECATED] Update webhook settings. Use Flows instead in Directus 10+.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Webhook ID"},
|
||||
"data": {"type": "object", "description": "Fields to update"},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_webhook",
|
||||
"method_name": "delete_webhook",
|
||||
"description": "[DEPRECATED] Delete a webhook. Use Flows instead in Directus 10+.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Webhook ID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_flows(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List flows."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_flows(filter=parsed_filter, sort=parsed_sort, limit=limit)
|
||||
flows = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(flows), "flows": flows}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_flow(client: DirectusClient, id: str) -> str:
|
||||
"""Get flow by ID."""
|
||||
try:
|
||||
result = await client.get_flow(id)
|
||||
return json.dumps(
|
||||
{"success": True, "flow": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_flow(
|
||||
client: DirectusClient,
|
||||
name: str,
|
||||
trigger: str,
|
||||
status: str = "active",
|
||||
icon: str = "bolt",
|
||||
options: dict | None = None,
|
||||
accountability: str | None = None,
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""Create a new flow."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_options = _parse_json_param(options, "options")
|
||||
|
||||
result = await client.create_flow(
|
||||
name=name,
|
||||
trigger=trigger,
|
||||
status=status,
|
||||
icon=icon,
|
||||
options=parsed_options,
|
||||
accountability=accountability,
|
||||
description=description,
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Flow '{name}' created", "flow": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_flow(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update flow."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_flow(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Flow updated", "flow": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_flow(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a flow."""
|
||||
try:
|
||||
await client.delete_flow(id)
|
||||
return json.dumps({"success": True, "message": f"Flow {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def trigger_flow(client: DirectusClient, id: str, data: dict | None = None) -> str:
|
||||
"""Trigger a flow manually."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.trigger_flow(id, parsed_data)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Flow {id} triggered",
|
||||
"result": result.get("data") if result else None,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_operations(
|
||||
client: DirectusClient, filter: dict | None = None, limit: int = 100
|
||||
) -> str:
|
||||
"""List operations."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
result = await client.list_operations(filter=parsed_filter, limit=limit)
|
||||
operations = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(operations), "operations": operations},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_operation(
|
||||
client: DirectusClient,
|
||||
flow: str,
|
||||
type: str,
|
||||
name: str | None = None,
|
||||
key: str | None = None,
|
||||
options: dict | None = None,
|
||||
position_x: int = 0,
|
||||
position_y: int = 0,
|
||||
) -> str:
|
||||
"""Create a new operation."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_options = _parse_json_param(options, "options")
|
||||
|
||||
result = await client.create_operation(
|
||||
flow=flow,
|
||||
type=type,
|
||||
name=name,
|
||||
key=key,
|
||||
options=parsed_options,
|
||||
position_x=position_x,
|
||||
position_y=position_y,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Operation '{type}' created in flow",
|
||||
"operation": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_webhooks(
|
||||
client: DirectusClient, filter: dict | None = None, limit: int = 100
|
||||
) -> str:
|
||||
"""List webhooks. DEPRECATED: Use Flows instead in Directus 10+."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
result = await client.list_webhooks(filter=parsed_filter, limit=limit)
|
||||
webhooks = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"total": len(webhooks),
|
||||
"webhooks": webhooks,
|
||||
"warning": "DEPRECATED: Webhooks are deprecated in Directus 10+. Use Flows instead.",
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_webhook(
|
||||
client: DirectusClient,
|
||||
name: str,
|
||||
url: str,
|
||||
actions: list[str],
|
||||
collections: list[str],
|
||||
method: str = "POST",
|
||||
status: str = "active",
|
||||
headers: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new webhook. DEPRECATED: Use Flows instead in Directus 10+."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_actions = _parse_json_param(actions, "actions")
|
||||
parsed_collections = _parse_json_param(collections, "collections")
|
||||
parsed_headers = _parse_json_param(headers, "headers")
|
||||
|
||||
result = await client.create_webhook(
|
||||
name=name,
|
||||
url=url,
|
||||
actions=parsed_actions,
|
||||
collections=parsed_collections,
|
||||
method=method,
|
||||
status=status,
|
||||
headers=parsed_headers,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Webhook '{name}' created",
|
||||
"warning": "DEPRECATED: Webhooks are deprecated in Directus 10+. Use Flows instead for better reliability. See create_flow and create_operation tools.",
|
||||
"webhook": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": error_msg,
|
||||
"hint": "Webhooks are deprecated in Directus 10+. Consider using Flows instead: 1) create_flow with trigger='event', 2) create_operation with type='request' for HTTP calls.",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def update_webhook(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update webhook. DEPRECATED: Use Flows instead in Directus 10+."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_webhook(id, parsed_data)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Webhook updated",
|
||||
"warning": "DEPRECATED: Webhooks are deprecated in Directus 10+. Use Flows instead.",
|
||||
"webhook": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"hint": "Webhooks are deprecated in Directus 10+. Consider migrating to Flows.",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
|
||||
async def delete_webhook(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a webhook. DEPRECATED: Use Flows instead in Directus 10+."""
|
||||
try:
|
||||
await client.delete_webhook(id)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Webhook {id} deleted",
|
||||
"note": "Consider using Flows instead of Webhooks in Directus 10+.",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
544
plugins/directus/handlers/collections.py
Normal file
544
plugins/directus/handlers/collections.py
Normal file
@@ -0,0 +1,544 @@
|
||||
"""
|
||||
Collections & Fields Handler - Schema management
|
||||
|
||||
Phase J.1: 14 tools
|
||||
- Collections: list, get, create, update, delete (5)
|
||||
- Fields: list, get, create, update, delete (5)
|
||||
- Relations: list, get, create, delete (4)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""
|
||||
Parse a parameter that may be a JSON string or already a native type.
|
||||
|
||||
MCP tools may receive object/array parameters as JSON strings.
|
||||
This function safely converts them to proper Python types.
|
||||
|
||||
Args:
|
||||
value: The value to parse (may be string, dict, list, or None)
|
||||
param_name: Name of parameter for error messages
|
||||
|
||||
Returns:
|
||||
Parsed value (dict, list, or original value if not JSON)
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Already the correct type
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
|
||||
# Try to parse JSON string
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
# Check if it looks like JSON (starts with { or [)
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (14 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# COLLECTIONS (5)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_collections",
|
||||
"method_name": "list_collections",
|
||||
"description": "List all collections (tables) in Directus.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_collection",
|
||||
"method_name": "get_collection",
|
||||
"description": "Get collection details including schema and meta information.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"collection": {"type": "string", "description": "Collection name"}},
|
||||
"required": ["collection"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_collection",
|
||||
"method_name": "create_collection",
|
||||
"description": "Create a new collection (table) in Directus.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name (table name)"},
|
||||
"meta": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Collection meta (icon, note, hidden, singleton, etc.)",
|
||||
},
|
||||
"schema": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Schema options (name, comment)",
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "object"}}, {"type": "null"}],
|
||||
"description": "Initial fields to create with collection",
|
||||
},
|
||||
},
|
||||
"required": ["collection"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_collection",
|
||||
"method_name": "update_collection",
|
||||
"description": "Update collection meta information.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"description": "Meta fields to update (icon, note, hidden, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "meta"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_collection",
|
||||
"method_name": "delete_collection",
|
||||
"description": "Delete a collection and all its data. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name to delete"}
|
||||
},
|
||||
"required": ["collection"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# FIELDS (5)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_fields",
|
||||
"method_name": "list_fields",
|
||||
"description": "List all fields, optionally filtered by collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Collection name (optional, lists all fields if not provided)",
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_field",
|
||||
"method_name": "get_field",
|
||||
"description": "Get field details including schema, meta, and type information.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name"},
|
||||
},
|
||||
"required": ["collection", "field"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_field",
|
||||
"method_name": "create_field",
|
||||
"description": "Create a new field in a collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name"},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Field type (string, integer, boolean, uuid, datetime, json, etc.)",
|
||||
},
|
||||
"meta": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Field meta (interface, display, options, etc.)",
|
||||
},
|
||||
"schema": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Schema options (is_nullable, default_value, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "field", "type"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_field",
|
||||
"method_name": "update_field",
|
||||
"description": "Update field configuration.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name"},
|
||||
"meta": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Meta fields to update",
|
||||
},
|
||||
"schema": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Schema fields to update",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "field"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_field",
|
||||
"method_name": "delete_field",
|
||||
"description": "Delete a field from a collection. This removes the column and all data.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name to delete"},
|
||||
},
|
||||
"required": ["collection", "field"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# RELATIONS (4)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_relations",
|
||||
"method_name": "list_relations",
|
||||
"description": "List all relations (foreign keys), optionally filtered by collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Collection name (optional)",
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_relation",
|
||||
"method_name": "get_relation",
|
||||
"description": "Get relation details.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name"},
|
||||
},
|
||||
"required": ["collection", "field"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_relation",
|
||||
"method_name": "create_relation",
|
||||
"description": "Create a new relation (foreign key) between collections.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name (many side)"},
|
||||
"field": {"type": "string", "description": "Field name for the relation"},
|
||||
"related_collection": {
|
||||
"type": "string",
|
||||
"description": "Related collection name (one side)",
|
||||
},
|
||||
"meta": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Relation meta (one_field, junction_field, etc.)",
|
||||
},
|
||||
"schema": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Schema options (on_delete, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "field", "related_collection"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_relation",
|
||||
"method_name": "delete_relation",
|
||||
"description": "Delete a relation.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"field": {"type": "string", "description": "Field name"},
|
||||
},
|
||||
"required": ["collection", "field"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_collections(client: DirectusClient) -> str:
|
||||
"""List all collections."""
|
||||
try:
|
||||
result = await client.list_collections()
|
||||
collections = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(collections), "collections": collections},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_collection(client: DirectusClient, collection: str) -> str:
|
||||
"""Get collection details."""
|
||||
try:
|
||||
result = await client.get_collection(collection)
|
||||
return json.dumps(
|
||||
{"success": True, "collection": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_collection(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
meta: dict | None = None,
|
||||
schema: dict | None = None,
|
||||
fields: list[dict] | None = None,
|
||||
) -> str:
|
||||
"""Create a new collection."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_meta = _parse_json_param(meta, "meta")
|
||||
parsed_schema = _parse_json_param(schema, "schema")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
|
||||
# Auto-fill schema.name if schema is provided but missing name
|
||||
# This ensures a real database table is created, not just a folder collection
|
||||
if parsed_schema is not None:
|
||||
if not isinstance(parsed_schema, dict):
|
||||
parsed_schema = {}
|
||||
if "name" not in parsed_schema:
|
||||
parsed_schema["name"] = collection
|
||||
|
||||
result = await client.create_collection(
|
||||
collection=collection, meta=parsed_meta, schema=parsed_schema, fields=parsed_fields
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Collection '{collection}' created",
|
||||
"collection": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_collection(client: DirectusClient, collection: str, meta: dict) -> str:
|
||||
"""Update collection meta."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_meta = _parse_json_param(meta, "meta")
|
||||
result = await client.update_collection(collection, parsed_meta)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Collection '{collection}' updated",
|
||||
"collection": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_collection(client: DirectusClient, collection: str) -> str:
|
||||
"""Delete a collection."""
|
||||
try:
|
||||
await client.delete_collection(collection)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Collection '{collection}' deleted"}, indent=2
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_fields(client: DirectusClient, collection: str | None = None) -> str:
|
||||
"""List fields."""
|
||||
try:
|
||||
result = await client.list_fields(collection)
|
||||
fields = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "collection": collection, "total": len(fields), "fields": fields},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_field(client: DirectusClient, collection: str, field: str) -> str:
|
||||
"""Get field details."""
|
||||
try:
|
||||
result = await client.get_field(collection, field)
|
||||
return json.dumps(
|
||||
{"success": True, "field": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_field(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
field: str,
|
||||
type: str,
|
||||
meta: dict | None = None,
|
||||
schema: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new field."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_meta = _parse_json_param(meta, "meta")
|
||||
parsed_schema = _parse_json_param(schema, "schema")
|
||||
|
||||
result = await client.create_field(
|
||||
collection=collection, field=field, type=type, meta=parsed_meta, schema=parsed_schema
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Field '{field}' created in {collection}",
|
||||
"field": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_field(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
field: str,
|
||||
meta: dict | None = None,
|
||||
schema: dict | None = None,
|
||||
) -> str:
|
||||
"""Update field configuration."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_meta = _parse_json_param(meta, "meta")
|
||||
parsed_schema = _parse_json_param(schema, "schema")
|
||||
|
||||
result = await client.update_field(
|
||||
collection=collection, field=field, meta=parsed_meta, schema=parsed_schema
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Field '{field}' updated", "field": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_field(client: DirectusClient, collection: str, field: str) -> str:
|
||||
"""Delete a field."""
|
||||
try:
|
||||
await client.delete_field(collection, field)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Field '{field}' deleted from {collection}"}, indent=2
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_relations(client: DirectusClient, collection: str | None = None) -> str:
|
||||
"""List relations."""
|
||||
try:
|
||||
result = await client.list_relations(collection)
|
||||
relations = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"collection": collection,
|
||||
"total": len(relations),
|
||||
"relations": relations,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_relation(client: DirectusClient, collection: str, field: str) -> str:
|
||||
"""Get relation details."""
|
||||
try:
|
||||
result = await client.get_relation(collection, field)
|
||||
return json.dumps(
|
||||
{"success": True, "relation": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_relation(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
field: str,
|
||||
related_collection: str,
|
||||
meta: dict | None = None,
|
||||
schema: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new relation."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_meta = _parse_json_param(meta, "meta")
|
||||
parsed_schema = _parse_json_param(schema, "schema")
|
||||
|
||||
result = await client.create_relation(
|
||||
collection=collection,
|
||||
field=field,
|
||||
related_collection=related_collection,
|
||||
meta=parsed_meta,
|
||||
schema=parsed_schema,
|
||||
)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Relation created: {collection}.{field} -> {related_collection}",
|
||||
"relation": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_relation(client: DirectusClient, collection: str, field: str) -> str:
|
||||
"""Delete a relation."""
|
||||
try:
|
||||
await client.delete_relation(collection, field)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Relation {collection}.{field} deleted"}, indent=2
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
370
plugins/directus/handlers/content.py
Normal file
370
plugins/directus/handlers/content.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
Content Handler - Revisions, Versions, Comments
|
||||
|
||||
Phase J.4: 10 tools
|
||||
- Revisions: list, get (2)
|
||||
- Versions: list, get, create, update, delete, promote (6)
|
||||
- Comments: list, create (2)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""Parse a parameter that may be a JSON string or already a native type."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# REVISIONS (2)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_revisions",
|
||||
"method_name": "list_revisions",
|
||||
"description": "List revisions (history) of items.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"collection": {"_eq": "posts"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields (default: ['-activity'])",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum revisions to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_revision",
|
||||
"method_name": "get_revision",
|
||||
"description": "Get revision details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Revision ID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# =====================
|
||||
# VERSIONS (6)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_versions",
|
||||
"method_name": "list_versions",
|
||||
"description": "List content versions (drafts).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum versions to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_version",
|
||||
"method_name": "get_version",
|
||||
"description": "Get version details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Version UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_version",
|
||||
"method_name": "create_version",
|
||||
"description": "Create a new content version (draft).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Version name"},
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"item": {"type": "string", "description": "Item ID"},
|
||||
"key": {"type": "string", "description": "Version key (unique identifier)"},
|
||||
},
|
||||
"required": ["name", "collection", "item", "key"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_version",
|
||||
"method_name": "update_version",
|
||||
"description": "Update a version.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Version UUID"},
|
||||
"data": {"type": "object", "description": "Fields to update (name, delta)"},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_version",
|
||||
"method_name": "delete_version",
|
||||
"description": "Delete a version.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Version UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "promote_version",
|
||||
"method_name": "promote_version",
|
||||
"description": "Promote a version to main (apply changes to item).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Version UUID to promote"},
|
||||
"mainHash": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Expected main content hash (for conflict detection)",
|
||||
},
|
||||
},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# =====================
|
||||
# COMMENTS (2)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_comments",
|
||||
"method_name": "list_comments",
|
||||
"description": "List comments on items.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"collection": {"_eq": "posts"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum comments to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_comment",
|
||||
"method_name": "create_comment",
|
||||
"description": "Create a comment on an item.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"item": {"type": "string", "description": "Item ID"},
|
||||
"comment": {"type": "string", "description": "Comment text"},
|
||||
},
|
||||
"required": ["collection", "item", "comment"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_revisions(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List revisions."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_revisions(filter=parsed_filter, sort=parsed_sort, limit=limit)
|
||||
revisions = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(revisions), "revisions": revisions},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_revision(client: DirectusClient, id: str) -> str:
|
||||
"""Get revision by ID."""
|
||||
try:
|
||||
result = await client.get_revision(id)
|
||||
return json.dumps(
|
||||
{"success": True, "revision": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_versions(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List versions."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_versions(filter=parsed_filter, sort=parsed_sort, limit=limit)
|
||||
versions = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(versions), "versions": versions},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_version(client: DirectusClient, id: str) -> str:
|
||||
"""Get version by ID."""
|
||||
try:
|
||||
result = await client.get_version(id)
|
||||
return json.dumps(
|
||||
{"success": True, "version": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_version(
|
||||
client: DirectusClient, name: str, collection: str, item: str, key: str
|
||||
) -> str:
|
||||
"""Create a new version."""
|
||||
try:
|
||||
result = await client.create_version(name=name, collection=collection, item=item, key=key)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Version '{name}' created",
|
||||
"version": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_version(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update version."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_version(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Version updated", "version": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_version(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a version."""
|
||||
try:
|
||||
await client.delete_version(id)
|
||||
return json.dumps({"success": True, "message": f"Version {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def promote_version(client: DirectusClient, id: str, mainHash: str | None = None) -> str:
|
||||
"""Promote version to main."""
|
||||
try:
|
||||
result = await client.promote_version(id, mainHash)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Version {id} promoted to main",
|
||||
"result": result.get("data") if result else None,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_comments(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List comments."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_comments(filter=parsed_filter, sort=parsed_sort, limit=limit)
|
||||
comments = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(comments), "comments": comments},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_comment(client: DirectusClient, collection: str, item: str, comment: str) -> str:
|
||||
"""Create a comment."""
|
||||
try:
|
||||
result = await client.create_comment(collection, item, comment)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Comment created", "comment": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
332
plugins/directus/handlers/dashboards.py
Normal file
332
plugins/directus/handlers/dashboards.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Dashboards Handler - Dashboards & Panels
|
||||
|
||||
Phase J.4: 8 tools
|
||||
- Dashboards: list, get, create, update, delete (5)
|
||||
- Panels: list, create, delete (3)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (8 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# DASHBOARDS (5)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_dashboards",
|
||||
"method_name": "list_dashboards",
|
||||
"description": "List all insights dashboards.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum dashboards to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_dashboard",
|
||||
"method_name": "get_dashboard",
|
||||
"description": "Get dashboard details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Dashboard UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_dashboard",
|
||||
"method_name": "create_dashboard",
|
||||
"description": "Create a new insights dashboard.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Dashboard name"},
|
||||
"icon": {
|
||||
"type": "string",
|
||||
"description": "Material icon",
|
||||
"default": "dashboard",
|
||||
},
|
||||
"note": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Dashboard description",
|
||||
},
|
||||
"color": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Accent color",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_dashboard",
|
||||
"method_name": "update_dashboard",
|
||||
"description": "Update dashboard settings.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Dashboard UUID"},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (name, icon, note, color)",
|
||||
},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_dashboard",
|
||||
"method_name": "delete_dashboard",
|
||||
"description": "Delete a dashboard and its panels.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Dashboard UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
# =====================
|
||||
# PANELS (3)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_panels",
|
||||
"method_name": "list_panels",
|
||||
"description": "List all panels, optionally filtered by dashboard.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"dashboard": {"_eq": "dashboard-uuid"}})',
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum panels to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_panel",
|
||||
"method_name": "create_panel",
|
||||
"description": "Create a new panel in a dashboard.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dashboard": {"type": "string", "description": "Dashboard UUID"},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Panel type (label, list, metric, time-series, etc.)",
|
||||
},
|
||||
"name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Panel name",
|
||||
},
|
||||
"icon": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Material icon",
|
||||
},
|
||||
"color": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Accent color",
|
||||
},
|
||||
"note": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Panel description",
|
||||
},
|
||||
"width": {
|
||||
"type": "integer",
|
||||
"description": "Panel width (1-12 grid units)",
|
||||
"default": 12,
|
||||
},
|
||||
"height": {
|
||||
"type": "integer",
|
||||
"description": "Panel height (grid units)",
|
||||
"default": 6,
|
||||
},
|
||||
"position_x": {
|
||||
"type": "integer",
|
||||
"description": "X position in dashboard",
|
||||
"default": 0,
|
||||
},
|
||||
"position_y": {
|
||||
"type": "integer",
|
||||
"description": "Y position in dashboard",
|
||||
"default": 0,
|
||||
},
|
||||
"options": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Panel-specific options",
|
||||
},
|
||||
},
|
||||
"required": ["dashboard", "type"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_panel",
|
||||
"method_name": "delete_panel",
|
||||
"description": "Delete a panel from a dashboard.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Panel UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_dashboards(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List dashboards."""
|
||||
try:
|
||||
result = await client.list_dashboards(filter=filter, sort=sort, limit=limit)
|
||||
dashboards = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(dashboards), "dashboards": dashboards},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_dashboard(client: DirectusClient, id: str) -> str:
|
||||
"""Get dashboard by ID."""
|
||||
try:
|
||||
result = await client.get_dashboard(id)
|
||||
return json.dumps(
|
||||
{"success": True, "dashboard": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_dashboard(
|
||||
client: DirectusClient,
|
||||
name: str,
|
||||
icon: str = "dashboard",
|
||||
note: str | None = None,
|
||||
color: str | None = None,
|
||||
) -> str:
|
||||
"""Create a new dashboard."""
|
||||
try:
|
||||
result = await client.create_dashboard(name=name, icon=icon, note=note, color=color)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Dashboard '{name}' created",
|
||||
"dashboard": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_dashboard(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update dashboard."""
|
||||
try:
|
||||
result = await client.update_dashboard(id, data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Dashboard updated", "dashboard": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_dashboard(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a dashboard."""
|
||||
try:
|
||||
await client.delete_dashboard(id)
|
||||
return json.dumps({"success": True, "message": f"Dashboard {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_panels(client: DirectusClient, filter: dict | None = None, limit: int = 100) -> str:
|
||||
"""List panels."""
|
||||
try:
|
||||
result = await client.list_panels(filter=filter, limit=limit)
|
||||
panels = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(panels), "panels": panels}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_panel(
|
||||
client: DirectusClient,
|
||||
dashboard: str,
|
||||
type: str,
|
||||
name: str | None = None,
|
||||
icon: str | None = None,
|
||||
color: str | None = None,
|
||||
note: str | None = None,
|
||||
width: int = 12,
|
||||
height: int = 6,
|
||||
position_x: int = 0,
|
||||
position_y: int = 0,
|
||||
options: dict | None = None,
|
||||
) -> str:
|
||||
"""Create a new panel."""
|
||||
try:
|
||||
result = await client.create_panel(
|
||||
dashboard=dashboard,
|
||||
type=type,
|
||||
name=name,
|
||||
icon=icon,
|
||||
color=color,
|
||||
note=note,
|
||||
width=width,
|
||||
height=height,
|
||||
position_x=position_x,
|
||||
position_y=position_y,
|
||||
options=options,
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Panel '{type}' created", "panel": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_panel(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a panel."""
|
||||
try:
|
||||
await client.delete_panel(id)
|
||||
return json.dumps({"success": True, "message": f"Panel {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
406
plugins/directus/handlers/files.py
Normal file
406
plugins/directus/handlers/files.py
Normal file
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
Files & Folders Handler - Asset management
|
||||
|
||||
Phase J.2: 12 tools
|
||||
- Files: list, get, update, delete, delete_files, import_url (6)
|
||||
- Folders: list, get, create, update, delete (5)
|
||||
- Note: File upload requires multipart form - import_url is the alternative
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""Parse a parameter that may be a JSON string or already a native type."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# FILES (7)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_files",
|
||||
"method_name": "list_files",
|
||||
"description": "List all files in Directus storage with filtering options.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"type": {"_contains": "image"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields (e.g., ['-uploaded_on'])",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum files to return",
|
||||
"default": 100,
|
||||
},
|
||||
"offset": {"type": "integer", "description": "Files to skip", "default": 0},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Search term",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_file",
|
||||
"method_name": "get_file",
|
||||
"description": "Get file metadata by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "File UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "update_file",
|
||||
"method_name": "update_file",
|
||||
"description": "Update file metadata (title, description, tags, folder).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "File UUID"},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (title, description, tags, folder, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_file",
|
||||
"method_name": "delete_file",
|
||||
"description": "Delete a file. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "File UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_files",
|
||||
"method_name": "delete_files",
|
||||
"description": "Delete multiple files. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of file UUIDs to delete",
|
||||
}
|
||||
},
|
||||
"required": ["ids"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "import_file_url",
|
||||
"method_name": "import_file_url",
|
||||
"description": "Import a file from a URL into Directus storage.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "URL of the file to import"},
|
||||
"data": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Additional file data (title, description, folder, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["url"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "get_file_url",
|
||||
"method_name": "get_file_url",
|
||||
"description": "Get the direct URL to access a file.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "File UUID"},
|
||||
"download": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to force download instead of display",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
# =====================
|
||||
# FOLDERS (5)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_folders",
|
||||
"method_name": "list_folders",
|
||||
"description": "List all folders in Directus storage.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter object",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum folders to return",
|
||||
"default": 100,
|
||||
},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Search term",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_folder",
|
||||
"method_name": "get_folder",
|
||||
"description": "Get folder details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Folder UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_folder",
|
||||
"method_name": "create_folder",
|
||||
"description": "Create a new folder.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "Folder name"},
|
||||
"parent": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Parent folder UUID (null for root)",
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_folder",
|
||||
"method_name": "update_folder",
|
||||
"description": "Update folder name or parent.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "Folder UUID"},
|
||||
"data": {"type": "object", "description": "Fields to update (name, parent)"},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_folder",
|
||||
"method_name": "delete_folder",
|
||||
"description": "Delete a folder. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "Folder UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_files(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
search: str | None = None,
|
||||
) -> str:
|
||||
"""List files."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_files(
|
||||
filter=parsed_filter, sort=parsed_sort, limit=limit, offset=offset, search=search
|
||||
)
|
||||
files = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(files), "files": files}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_file(client: DirectusClient, id: str) -> str:
|
||||
"""Get file metadata."""
|
||||
try:
|
||||
result = await client.get_file(id)
|
||||
return json.dumps(
|
||||
{"success": True, "file": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_file(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update file metadata."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_file(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "File updated", "file": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_file(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a file."""
|
||||
try:
|
||||
await client.delete_file(id)
|
||||
return json.dumps({"success": True, "message": f"File {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_files(client: DirectusClient, ids: list[str]) -> str:
|
||||
"""Delete multiple files."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_ids = _parse_json_param(ids, "ids")
|
||||
await client.delete_files(parsed_ids)
|
||||
return json.dumps({"success": True, "message": f"Deleted {len(ids)} files"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def import_file_url(client: DirectusClient, url: str, data: dict | None = None) -> str:
|
||||
"""Import file from URL."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.import_file_url(url, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "File imported", "file": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_file_url(client: DirectusClient, id: str, download: bool = False) -> str:
|
||||
"""Get file URL."""
|
||||
try:
|
||||
base_url = client.base_url
|
||||
url = f"{base_url}/assets/{id}"
|
||||
if download:
|
||||
url += "?download=true"
|
||||
return json.dumps({"success": True, "id": id, "url": url, "download": download}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_folders(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
search: str | None = None,
|
||||
) -> str:
|
||||
"""List folders."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_folders(
|
||||
filter=parsed_filter, sort=parsed_sort, limit=limit, search=search
|
||||
)
|
||||
folders = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(folders), "folders": folders},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_folder(client: DirectusClient, id: str) -> str:
|
||||
"""Get folder details."""
|
||||
try:
|
||||
result = await client.get_folder(id)
|
||||
return json.dumps(
|
||||
{"success": True, "folder": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_folder(client: DirectusClient, name: str, parent: str | None = None) -> str:
|
||||
"""Create a folder."""
|
||||
try:
|
||||
result = await client.create_folder(name, parent)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Folder '{name}' created", "folder": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_folder(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update folder."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.update_folder(id, parsed_data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Folder updated", "folder": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_folder(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a folder."""
|
||||
try:
|
||||
await client.delete_folder(id)
|
||||
return json.dumps({"success": True, "message": f"Folder {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
576
plugins/directus/handlers/items.py
Normal file
576
plugins/directus/handlers/items.py
Normal file
@@ -0,0 +1,576 @@
|
||||
"""
|
||||
Items Handler - CRUD operations for any collection
|
||||
|
||||
Phase J.1: 12 tools
|
||||
- list_items, get_item, create_item, create_items
|
||||
- update_item, update_items, delete_item, delete_items
|
||||
- search_items, aggregate_items, export_items, import_items
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def _parse_json_param(value: Any, param_name: str = "parameter") -> Any:
|
||||
"""Parse a parameter that may be a JSON string or already a native type."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
if stripped.startswith("{") or stripped.startswith("["):
|
||||
try:
|
||||
return json.loads(stripped)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid JSON in '{param_name}': {e}")
|
||||
return value
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (12 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "list_items",
|
||||
"method_name": "list_items",
|
||||
"description": "List items from any Directus collection with filtering, sorting, and pagination.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {
|
||||
"type": "string",
|
||||
"description": "Collection name (e.g., 'posts', 'products')",
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return (e.g., ['id', 'title', 'author.*'])",
|
||||
},
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"status": {"_eq": "published"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields (e.g., ['-date_created', 'title'])",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum items to return",
|
||||
"default": 100,
|
||||
},
|
||||
"offset": {"type": "integer", "description": "Items to skip", "default": 0},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Full-text search query",
|
||||
},
|
||||
},
|
||||
"required": ["collection"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_item",
|
||||
"method_name": "get_item",
|
||||
"description": "Get a single item by ID from any collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"id": {"type": "string", "description": "Item ID"},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_item",
|
||||
"method_name": "create_item",
|
||||
"description": "Create a new item in any collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"data": {"type": "object", "description": "Item data (field values)"},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return in response",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "create_items",
|
||||
"method_name": "create_items",
|
||||
"description": "Create multiple items in a collection at once.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Array of item data objects",
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return in response",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_item",
|
||||
"method_name": "update_item",
|
||||
"description": "Update an existing item by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"id": {"type": "string", "description": "Item ID"},
|
||||
"data": {"type": "object", "description": "Fields to update"},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return in response",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "id", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "update_items",
|
||||
"method_name": "update_items",
|
||||
"description": "Update multiple items by their IDs.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of item IDs to update",
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (applied to all items)",
|
||||
},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return in response",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "keys", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "delete_item",
|
||||
"method_name": "delete_item",
|
||||
"description": "Delete an item by ID. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"id": {"type": "string", "description": "Item ID to delete"},
|
||||
},
|
||||
"required": ["collection", "id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_items",
|
||||
"method_name": "delete_items",
|
||||
"description": "Delete multiple items by their IDs. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of item IDs to delete",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "keys"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "search_items",
|
||||
"method_name": "search_items",
|
||||
"description": "Full-text search across items in a collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to return",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum items to return",
|
||||
"default": 25,
|
||||
},
|
||||
},
|
||||
"required": ["collection", "query"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "aggregate_items",
|
||||
"method_name": "aggregate_items",
|
||||
"description": "Perform aggregate operations (count, sum, avg, min, max) on items.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"aggregate": {
|
||||
"type": "object",
|
||||
"description": 'Aggregate functions (e.g., {"count": "*", "sum": "price"})',
|
||||
},
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter to apply before aggregation",
|
||||
},
|
||||
"groupBy": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to group by",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "aggregate"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "export_items",
|
||||
"method_name": "export_items",
|
||||
"description": "Export items from a collection with filtering options.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"fields": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Fields to export",
|
||||
},
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "Filter to apply",
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum items to export",
|
||||
"default": 1000,
|
||||
},
|
||||
},
|
||||
"required": ["collection"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "import_items",
|
||||
"method_name": "import_items",
|
||||
"description": "Import multiple items into a collection.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collection": {"type": "string", "description": "Collection name"},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {"type": "object"},
|
||||
"description": "Array of items to import",
|
||||
},
|
||||
},
|
||||
"required": ["collection", "data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
fields: list[str] | None = None,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
search: str | None = None,
|
||||
) -> str:
|
||||
"""List items from a collection."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_items(
|
||||
collection=collection,
|
||||
fields=parsed_fields,
|
||||
filter=parsed_filter,
|
||||
sort=parsed_sort,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
search=search,
|
||||
)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "collection": collection, "total": len(items), "items": items},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_item(
|
||||
client: DirectusClient, collection: str, id: str, fields: list[str] | None = None
|
||||
) -> str:
|
||||
"""Get item by ID."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.get_item(collection, id, fields=parsed_fields)
|
||||
return json.dumps(
|
||||
{"success": True, "item": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_item(
|
||||
client: DirectusClient, collection: str, data: dict[str, Any], fields: list[str] | None = None
|
||||
) -> str:
|
||||
"""Create a new item."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.create_item(collection, parsed_data, fields=parsed_fields)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Item created in {collection}",
|
||||
"item": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
data: list[dict[str, Any]],
|
||||
fields: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Create multiple items."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.create_items(collection, parsed_data, fields=parsed_fields)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Created {len(items)} items in {collection}",
|
||||
"items": items,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_item(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
id: str,
|
||||
data: dict[str, Any],
|
||||
fields: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Update an item."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.update_item(collection, id, parsed_data, fields=parsed_fields)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Item {id} updated", "item": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
keys: list[str],
|
||||
data: dict[str, Any],
|
||||
fields: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Update multiple items."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_keys = _parse_json_param(keys, "keys")
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.update_items(
|
||||
collection, parsed_keys, parsed_data, fields=parsed_fields
|
||||
)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Updated {len(items)} items in {collection}",
|
||||
"items": items,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_item(client: DirectusClient, collection: str, id: str) -> str:
|
||||
"""Delete an item."""
|
||||
try:
|
||||
await client.delete_item(collection, id)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Item {id} deleted from {collection}"}, indent=2
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_items(client: DirectusClient, collection: str, keys: list[str]) -> str:
|
||||
"""Delete multiple items."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_keys = _parse_json_param(keys, "keys")
|
||||
await client.delete_items(collection, parsed_keys)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"Deleted {len(keys)} items from {collection}"}, indent=2
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def search_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
query: str,
|
||||
fields: list[str] | None = None,
|
||||
limit: int = 25,
|
||||
) -> str:
|
||||
"""Full-text search items."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
result = await client.list_items(
|
||||
collection=collection, fields=parsed_fields, search=query, limit=limit
|
||||
)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "query": query, "total": len(items), "items": items},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def aggregate_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
aggregate: dict[str, Any],
|
||||
filter: dict | None = None,
|
||||
groupBy: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Aggregate items."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_aggregate = _parse_json_param(aggregate, "aggregate")
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
_parse_json_param(groupBy, "groupBy")
|
||||
|
||||
result = await client.list_items(
|
||||
collection=collection, filter=parsed_filter, aggregate=parsed_aggregate
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "collection": collection, "result": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def export_items(
|
||||
client: DirectusClient,
|
||||
collection: str,
|
||||
fields: list[str] | None = None,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 1000,
|
||||
) -> str:
|
||||
"""Export items from a collection."""
|
||||
try:
|
||||
# Parse JSON string parameters
|
||||
parsed_fields = _parse_json_param(fields, "fields")
|
||||
parsed_filter = _parse_json_param(filter, "filter")
|
||||
parsed_sort = _parse_json_param(sort, "sort")
|
||||
|
||||
result = await client.list_items(
|
||||
collection=collection,
|
||||
fields=parsed_fields,
|
||||
filter=parsed_filter,
|
||||
sort=parsed_sort,
|
||||
limit=limit,
|
||||
)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"collection": collection,
|
||||
"exported_count": len(items),
|
||||
"data": items,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def import_items(client: DirectusClient, collection: str, data: list[dict[str, Any]]) -> str:
|
||||
"""Import items into a collection."""
|
||||
try:
|
||||
# Parse JSON string parameter
|
||||
parsed_data = _parse_json_param(data, "data")
|
||||
result = await client.create_items(collection, parsed_data)
|
||||
items = result.get("data", [])
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Imported {len(items)} items into {collection}",
|
||||
"imported_count": len(items),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
259
plugins/directus/handlers/system.py
Normal file
259
plugins/directus/handlers/system.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
System Handler - Settings, Server, Schema, Activity, Presets, Notifications
|
||||
|
||||
Phase J.4: 10 tools
|
||||
- Settings: get, update (2)
|
||||
- Server: info, health, graphql_sdl, openapi_spec (4)
|
||||
- Schema: snapshot, diff, apply (3)
|
||||
- Activity: list (1)
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
||||
return [
|
||||
# =====================
|
||||
# SETTINGS (2)
|
||||
# =====================
|
||||
{
|
||||
"name": "get_settings",
|
||||
"method_name": "get_settings",
|
||||
"description": "Get system settings.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "update_settings",
|
||||
"method_name": "update_settings",
|
||||
"description": "Update system settings (project name, logo, colors, etc.).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"data": {"type": "object", "description": "Settings to update"}},
|
||||
"required": ["data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# SERVER (4)
|
||||
# =====================
|
||||
{
|
||||
"name": "get_server_info",
|
||||
"method_name": "get_server_info",
|
||||
"description": "Get Directus server information (version, extensions, etc.).",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "health_check",
|
||||
"method_name": "health_check",
|
||||
"description": "Check Directus server health status.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_graphql_sdl",
|
||||
"method_name": "get_graphql_sdl",
|
||||
"description": "Get the GraphQL SDL schema.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_openapi_spec",
|
||||
"method_name": "get_openapi_spec",
|
||||
"description": "Get the OpenAPI specification.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
# =====================
|
||||
# SCHEMA (3)
|
||||
# =====================
|
||||
{
|
||||
"name": "get_schema_snapshot",
|
||||
"method_name": "get_schema_snapshot",
|
||||
"description": "Get complete schema snapshot for migration/backup.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "schema_diff",
|
||||
"method_name": "schema_diff",
|
||||
"description": "Get diff between current schema and provided snapshot.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"snapshot": {
|
||||
"type": "object",
|
||||
"description": "Schema snapshot to compare against",
|
||||
}
|
||||
},
|
||||
"required": ["snapshot"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "schema_apply",
|
||||
"method_name": "schema_apply",
|
||||
"description": "Apply schema diff to database.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"diff": {"type": "object", "description": "Schema diff to apply"}},
|
||||
"required": ["diff"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
# =====================
|
||||
# ACTIVITY (1)
|
||||
# =====================
|
||||
{
|
||||
"name": "list_activity",
|
||||
"method_name": "list_activity",
|
||||
"description": "List activity log (all actions performed in Directus).",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"action": {"_eq": "create"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields (default: ['-timestamp'])",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum activities to return",
|
||||
"default": 100,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def get_settings(client: DirectusClient) -> str:
|
||||
"""Get system settings."""
|
||||
try:
|
||||
result = await client.get_settings()
|
||||
return json.dumps(
|
||||
{"success": True, "settings": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_settings(client: DirectusClient, data: dict[str, Any]) -> str:
|
||||
"""Update system settings."""
|
||||
try:
|
||||
result = await client.update_settings(data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Settings updated", "settings": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_server_info(client: DirectusClient) -> str:
|
||||
"""Get server info."""
|
||||
try:
|
||||
result = await client.get_server_info()
|
||||
return json.dumps(
|
||||
{"success": True, "server": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def health_check(client: DirectusClient) -> str:
|
||||
"""Check server health."""
|
||||
try:
|
||||
result = await client.health_check()
|
||||
return json.dumps(
|
||||
{"success": True, "status": result.get("status", "unknown"), "health": result},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_graphql_sdl(client: DirectusClient) -> str:
|
||||
"""Get GraphQL SDL."""
|
||||
try:
|
||||
result = await client.get_graphql_sdl()
|
||||
# GraphQL SDL is usually returned as text
|
||||
if isinstance(result, dict):
|
||||
sdl = result.get("data", result)
|
||||
else:
|
||||
sdl = result
|
||||
return json.dumps({"success": True, "sdl": sdl}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_openapi_spec(client: DirectusClient) -> str:
|
||||
"""Get OpenAPI specification."""
|
||||
try:
|
||||
result = await client.get_openapi_spec()
|
||||
return json.dumps({"success": True, "spec": result}, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_schema_snapshot(client: DirectusClient) -> str:
|
||||
"""Get schema snapshot."""
|
||||
try:
|
||||
result = await client.get_schema_snapshot()
|
||||
return json.dumps(
|
||||
{"success": True, "snapshot": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def schema_diff(client: DirectusClient, snapshot: dict[str, Any]) -> str:
|
||||
"""Get schema diff."""
|
||||
try:
|
||||
result = await client.schema_diff(snapshot)
|
||||
return json.dumps(
|
||||
{"success": True, "diff": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def schema_apply(client: DirectusClient, diff: dict[str, Any]) -> str:
|
||||
"""Apply schema diff."""
|
||||
try:
|
||||
result = await client.schema_apply(diff)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Schema applied",
|
||||
"result": result.get("data") if result else None,
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def list_activity(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""List activity log."""
|
||||
try:
|
||||
result = await client.list_activity(filter=filter, sort=sort, limit=limit)
|
||||
activities = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(activities), "activities": activities},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
357
plugins/directus/handlers/users.py
Normal file
357
plugins/directus/handlers/users.py
Normal file
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
Users Handler - User management
|
||||
|
||||
Phase J.2: 10 tools
|
||||
- list_users, get_user, get_current_user
|
||||
- create_user, update_user, delete_user, delete_users
|
||||
- invite_user, accept_invite, update_current_user
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""Return tool specifications for ToolGenerator (10 tools)"""
|
||||
return [
|
||||
{
|
||||
"name": "list_users",
|
||||
"method_name": "list_users",
|
||||
"description": "List all users in Directus with filtering options.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": 'Filter object (e.g., {"status": {"_eq": "active"}})',
|
||||
},
|
||||
"sort": {
|
||||
"anyOf": [{"type": "array", "items": {"type": "string"}}, {"type": "null"}],
|
||||
"description": "Sort fields (e.g., ['email'])",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum users to return",
|
||||
"default": 100,
|
||||
},
|
||||
"offset": {"type": "integer", "description": "Users to skip", "default": 0},
|
||||
"search": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Search term",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_user",
|
||||
"method_name": "get_user",
|
||||
"description": "Get user details by ID.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "User UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "get_current_user",
|
||||
"method_name": "get_current_user",
|
||||
"description": "Get the currently authenticated user.",
|
||||
"schema": {"type": "object", "properties": {}, "required": []},
|
||||
"scope": "read",
|
||||
},
|
||||
{
|
||||
"name": "create_user",
|
||||
"method_name": "create_user",
|
||||
"description": "Create a new user.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "User email address",
|
||||
},
|
||||
"password": {"type": "string", "minLength": 8, "description": "User password"},
|
||||
"role": {"type": "string", "description": "Role UUID"},
|
||||
"first_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "First name",
|
||||
},
|
||||
"last_name": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Last name",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["draft", "invited", "active", "suspended", "archived"],
|
||||
"default": "active",
|
||||
"description": "User status",
|
||||
},
|
||||
},
|
||||
"required": ["email", "password", "role"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_user",
|
||||
"method_name": "update_user",
|
||||
"description": "Update user details.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "User UUID"},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (email, first_name, last_name, role, status, etc.)",
|
||||
},
|
||||
},
|
||||
"required": ["id", "data"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_user",
|
||||
"method_name": "delete_user",
|
||||
"description": "Delete a user. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "User UUID to delete"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "delete_users",
|
||||
"method_name": "delete_users",
|
||||
"description": "Delete multiple users. This action is irreversible.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Array of user UUIDs to delete",
|
||||
}
|
||||
},
|
||||
"required": ["ids"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "invite_user",
|
||||
"method_name": "invite_user",
|
||||
"description": "Invite a user by email.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "Email to invite",
|
||||
},
|
||||
"role": {"type": "string", "description": "Role UUID for the invited user"},
|
||||
"invite_url": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"description": "Custom invite URL",
|
||||
},
|
||||
},
|
||||
"required": ["email", "role"],
|
||||
},
|
||||
"scope": "admin",
|
||||
},
|
||||
{
|
||||
"name": "update_current_user",
|
||||
"method_name": "update_current_user",
|
||||
"description": "Update the currently authenticated user's profile.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Fields to update (first_name, last_name, language, theme, etc.)",
|
||||
}
|
||||
},
|
||||
"required": ["data"],
|
||||
},
|
||||
"scope": "write",
|
||||
},
|
||||
{
|
||||
"name": "get_user_role",
|
||||
"method_name": "get_user_role",
|
||||
"description": "Get the role of a specific user.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"id": {"type": "string", "description": "User UUID"}},
|
||||
"required": ["id"],
|
||||
},
|
||||
"scope": "read",
|
||||
},
|
||||
]
|
||||
|
||||
# =====================
|
||||
# HANDLER FUNCTIONS
|
||||
# =====================
|
||||
|
||||
async def list_users(
|
||||
client: DirectusClient,
|
||||
filter: dict | None = None,
|
||||
sort: list[str] | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
search: str | None = None,
|
||||
) -> str:
|
||||
"""List users."""
|
||||
try:
|
||||
result = await client.list_users(
|
||||
filter=filter, sort=sort, limit=limit, offset=offset, search=search
|
||||
)
|
||||
users = result.get("data", [])
|
||||
return json.dumps(
|
||||
{"success": True, "total": len(users), "users": users}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_user(client: DirectusClient, id: str) -> str:
|
||||
"""Get user by ID."""
|
||||
try:
|
||||
result = await client.get_user(id)
|
||||
return json.dumps(
|
||||
{"success": True, "user": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_current_user(client: DirectusClient) -> str:
|
||||
"""Get current user."""
|
||||
try:
|
||||
result = await client.get_current_user()
|
||||
return json.dumps(
|
||||
{"success": True, "user": result.get("data")}, indent=2, ensure_ascii=False
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def create_user(
|
||||
client: DirectusClient,
|
||||
email: str,
|
||||
password: str,
|
||||
role: str,
|
||||
first_name: str | None = None,
|
||||
last_name: str | None = None,
|
||||
status: str = "active",
|
||||
) -> str:
|
||||
"""Create a new user."""
|
||||
try:
|
||||
result = await client.create_user(
|
||||
email=email,
|
||||
password=password,
|
||||
role=role,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
status=status,
|
||||
)
|
||||
return json.dumps(
|
||||
{"success": True, "message": f"User {email} created", "user": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_user(client: DirectusClient, id: str, data: dict[str, Any]) -> str:
|
||||
"""Update user."""
|
||||
try:
|
||||
result = await client.update_user(id, data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "User updated", "user": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_user(client: DirectusClient, id: str) -> str:
|
||||
"""Delete a user."""
|
||||
try:
|
||||
await client.delete_user(id)
|
||||
return json.dumps({"success": True, "message": f"User {id} deleted"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def delete_users(client: DirectusClient, ids: list[str]) -> str:
|
||||
"""Delete multiple users."""
|
||||
try:
|
||||
await client.delete_users(ids)
|
||||
return json.dumps({"success": True, "message": f"Deleted {len(ids)} users"}, indent=2)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def invite_user(
|
||||
client: DirectusClient, email: str, role: str, invite_url: str | None = None
|
||||
) -> str:
|
||||
"""Invite a user."""
|
||||
try:
|
||||
result = await client.invite_user(email, role, invite_url)
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Invitation sent to {email}",
|
||||
"result": result.get("data"),
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def update_current_user(client: DirectusClient, data: dict[str, Any]) -> str:
|
||||
"""Update current user profile."""
|
||||
try:
|
||||
# Get current user first to get ID
|
||||
current = await client.get_current_user()
|
||||
user_id = current.get("data", {}).get("id")
|
||||
if not user_id:
|
||||
return json.dumps(
|
||||
{"success": False, "error": "Cannot determine current user"}, indent=2
|
||||
)
|
||||
|
||||
result = await client.update_user(user_id, data)
|
||||
return json.dumps(
|
||||
{"success": True, "message": "Profile updated", "user": result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
|
||||
async def get_user_role(client: DirectusClient, id: str) -> str:
|
||||
"""Get user's role."""
|
||||
try:
|
||||
result = await client.get_user(id)
|
||||
user = result.get("data", {})
|
||||
role_id = user.get("role")
|
||||
|
||||
if role_id:
|
||||
role_result = await client.get_role(role_id)
|
||||
return json.dumps(
|
||||
{"success": True, "user_id": id, "role": role_result.get("data")},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
else:
|
||||
return json.dumps(
|
||||
{
|
||||
"success": True,
|
||||
"user_id": id,
|
||||
"role": None,
|
||||
"message": "User has no role assigned",
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "error": str(e)}, indent=2)
|
||||
166
plugins/directus/plugin.py
Normal file
166
plugins/directus/plugin.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""
|
||||
Directus Plugin - Self-Hosted Headless CMS Management
|
||||
|
||||
Complete Directus Self-Hosted management through REST APIs.
|
||||
Provides tools for Items, Collections, Fields, Files, Users,
|
||||
Roles, Permissions, Flows, Versions, Dashboards, and System.
|
||||
|
||||
For Self-Hosted instances deployed on Coolify.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
from plugins.directus import handlers
|
||||
from plugins.directus.client import DirectusClient
|
||||
|
||||
class DirectusPlugin(BasePlugin):
|
||||
"""
|
||||
Directus Self-Hosted Plugin - Complete CMS management.
|
||||
|
||||
Provides comprehensive Directus management capabilities including:
|
||||
- Items operations (CRUD for any collection)
|
||||
- Collections & Fields (schema management)
|
||||
- Files & Folders (asset management)
|
||||
- Users management (CRUD, invite)
|
||||
- Access Control (Roles, Permissions, Policies)
|
||||
- Automation (Flows, Operations, Webhooks)
|
||||
- Content Management (Revisions, Versions, Comments)
|
||||
- Dashboards (Dashboards, Panels)
|
||||
- System operations (Settings, Server, Schema, Activity)
|
||||
|
||||
Phase J.1: Items (12) + Collections (14) = 26 tools
|
||||
Phase J.2: Files (12) + Users (10) = 22 tools
|
||||
Phase J.3: Access (12) + Automation (12) = 24 tools
|
||||
Phase J.4: Content (10) + Dashboards (8) + System (10) = 28 tools
|
||||
|
||||
Total: 100 tools - Complete!
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_plugin_name() -> str:
|
||||
"""Return plugin type identifier"""
|
||||
return "directus"
|
||||
|
||||
@staticmethod
|
||||
def get_required_config_keys() -> list[str]:
|
||||
"""Return required configuration keys"""
|
||||
return ["url", "token"]
|
||||
|
||||
def __init__(self, config: dict[str, Any], project_id: str | None = None):
|
||||
"""
|
||||
Initialize Directus plugin with client.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary containing:
|
||||
- url: Directus instance URL (e.g., https://directus.example.com)
|
||||
- token: Static admin token
|
||||
project_id: Optional MCP project ID (auto-generated if not provided)
|
||||
"""
|
||||
super().__init__(config, project_id=project_id)
|
||||
|
||||
# Create Directus API client
|
||||
self.client = DirectusClient(base_url=config["url"], token=config["token"])
|
||||
|
||||
@staticmethod
|
||||
def get_tool_specifications() -> list[dict[str, Any]]:
|
||||
"""
|
||||
Return all tool specifications for ToolGenerator.
|
||||
|
||||
This method is called by ToolGenerator to create unified tools
|
||||
with site parameter routing.
|
||||
|
||||
Returns:
|
||||
List of tool specification dictionaries
|
||||
"""
|
||||
specs = []
|
||||
|
||||
# Phase J.1: Core (26 tools)
|
||||
specs.extend(handlers.items.get_tool_specifications()) # 12 tools
|
||||
specs.extend(handlers.collections.get_tool_specifications()) # 14 tools
|
||||
|
||||
# Phase J.2: Assets & Users (22 tools)
|
||||
specs.extend(handlers.files.get_tool_specifications()) # 12 tools
|
||||
specs.extend(handlers.users.get_tool_specifications()) # 10 tools
|
||||
|
||||
# Phase J.3: Access & Automation (24 tools)
|
||||
specs.extend(handlers.access.get_tool_specifications()) # 12 tools
|
||||
specs.extend(handlers.automation.get_tool_specifications()) # 12 tools
|
||||
|
||||
# Phase J.4: Advanced (28 tools)
|
||||
specs.extend(handlers.content.get_tool_specifications()) # 10 tools
|
||||
specs.extend(handlers.dashboards.get_tool_specifications()) # 8 tools
|
||||
specs.extend(handlers.system.get_tool_specifications()) # 10 tools
|
||||
|
||||
return specs
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
"""
|
||||
Dynamically delegate method calls to appropriate handlers.
|
||||
|
||||
This allows ToolGenerator to call methods like plugin.list_items()
|
||||
without explicitly defining each method.
|
||||
|
||||
Args:
|
||||
name: Method name being called
|
||||
|
||||
Returns:
|
||||
Handler function from the appropriate handler module
|
||||
"""
|
||||
# Try to find the method in handler modules
|
||||
handler_modules = [
|
||||
# Phase J.1: Core
|
||||
handlers.items,
|
||||
handlers.collections,
|
||||
# Phase J.2: Assets & Users
|
||||
handlers.files,
|
||||
handlers.users,
|
||||
# Phase J.3: Access & Automation
|
||||
handlers.access,
|
||||
handlers.automation,
|
||||
# Phase J.4: Advanced
|
||||
handlers.content,
|
||||
handlers.dashboards,
|
||||
handlers.system,
|
||||
]
|
||||
|
||||
for handler_module in handler_modules:
|
||||
if hasattr(handler_module, name):
|
||||
func = getattr(handler_module, name)
|
||||
|
||||
# Create wrapper that passes self.client
|
||||
async def wrapper(_func=func, **kwargs):
|
||||
return await _func(self.client, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
# Method not found in any handler
|
||||
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||
|
||||
async def check_health(self) -> dict[str, Any]:
|
||||
"""
|
||||
Check if Directus instance is accessible (internal use).
|
||||
|
||||
Note: This is named check_health to avoid shadowing the handler's
|
||||
health_check function which is exposed as an MCP tool.
|
||||
|
||||
Returns:
|
||||
Dict containing health check result
|
||||
"""
|
||||
try:
|
||||
result = await self.client.health_check()
|
||||
is_healthy = result.get("status") == "ok"
|
||||
return {
|
||||
"healthy": is_healthy,
|
||||
"message": f"Directus instance at {self.client.base_url} is {'accessible' if is_healthy else 'not accessible'}",
|
||||
}
|
||||
except Exception as e:
|
||||
return {"healthy": False, "message": f"Directus health check failed: {str(e)}"}
|
||||
|
||||
async def health_check(self, **kwargs) -> str:
|
||||
"""
|
||||
Override BasePlugin.health_check to use handler function.
|
||||
|
||||
This ensures the MCP tool returns a JSON string, not a Dict.
|
||||
"""
|
||||
return await handlers.system.health_check(self.client)
|
||||
Reference in New Issue
Block a user