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:
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)
|
||||
Reference in New Issue
Block a user