""" 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)