Skip to main content
Glama
schema.py11.2 kB
""" Schema MCP tools for Frappe DocType introspection. This module provides tools for examining DocType schemas, field definitions, and database structure. """ from typing import Any, Dict, List, Optional, Union import json from ..frappe_api import get_client, FrappeApiError from ..auth import validate_api_credentials def _format_error_response(error: Exception, operation: str) -> str: """Format error response with detailed information.""" credentials_check = validate_api_credentials() # Check for missing credentials first if not credentials_check["valid"]: error_msg = f"Authentication failed: {credentials_check['message']}. " error_msg += "API key/secret is the only supported authentication method." return error_msg # Handle FrappeApiError if isinstance(error, FrappeApiError): error_msg = f"Frappe API error: {error}" if error.status_code in (401, 403): error_msg += " Please check your API key and secret." return error_msg # Default error handling return f"Error in {operation}: {str(error)}" def register_tools(mcp: Any) -> None: """Register schema tools with the MCP server.""" @mcp.tool() async def get_doctype_schema(doctype: str) -> str: """ Get the complete schema for a DocType including field definitions, validations, and linked DocTypes. Use this to understand the structure of a DocType before creating or updating documents. Args: doctype: DocType name """ try: client = get_client() # Get DocType schema response = await client.get(f"api/resource/DocType/{doctype}") if "data" in response: schema_data = response["data"] # Format the response to highlight key information fields = schema_data.get("fields", []) field_summary = [] for field in fields: field_info = { "label": field.get("label"), "fieldname": field.get("fieldname"), "fieldtype": field.get("fieldtype"), "reqd": field.get("reqd", 0) == 1, "options": field.get("options"), "default": field.get("default") } if field.get("description"): field_info["description"] = field.get("description") field_summary.append(field_info) formatted_response = { "doctype": doctype, "module": schema_data.get("module"), "naming_rule": schema_data.get("autoname"), "is_submittable": schema_data.get("is_submittable", 0) == 1, "is_tree": schema_data.get("is_tree", 0) == 1, "track_changes": schema_data.get("track_changes", 0) == 1, "allow_rename": schema_data.get("allow_rename", 0) == 1, "fields": field_summary, "permissions": schema_data.get("permissions", []) } return json.dumps(formatted_response, indent=2) else: return json.dumps(response, indent=2) except Exception as error: return _format_error_response(error, "get_doctype_schema") @mcp.tool() async def get_field_options( doctype: str, fieldname: str, limit: Optional[int] = 20 ) -> str: """ Get available options for a Link or Select field. For Link fields, returns documents from the linked DocType. For Select fields, returns the predefined options. Args: doctype: DocType name fieldname: Field name limit: Maximum number of options to return (default: 20) """ try: client = get_client() # First get the field definition to understand its type schema_response = await client.get(f"api/resource/DocType/{doctype}") if "data" not in schema_response: return f"Could not get schema for DocType: {doctype}" fields = schema_response["data"].get("fields", []) target_field = None for field in fields: if field.get("fieldname") == fieldname: target_field = field break if not target_field: return f"Field '{fieldname}' not found in DocType '{doctype}'" fieldtype = target_field.get("fieldtype") options = target_field.get("options", "") if fieldtype == "Link": # Get documents from linked DocType if not options: return f"Link field '{fieldname}' has no linked DocType defined" params = { "fields": json.dumps(["name", "title"]), "limit": str(limit) } response = await client.get(f"api/resource/{options}", params=params) if "data" in response: documents = response["data"] return f"Available {options} documents for field '{fieldname}':\n\n" + json.dumps(documents, indent=2) else: return json.dumps(response, indent=2) elif fieldtype == "Select": # Parse select options if not options: return f"Select field '{fieldname}' has no options defined" select_options = [opt.strip() for opt in options.split("\n") if opt.strip()] return f"Select options for field '{fieldname}':\n\n" + json.dumps(select_options, indent=2) else: return f"Field '{fieldname}' is of type '{fieldtype}' which doesn't have predefined options" except Exception as error: return _format_error_response(error, "get_field_options") @mcp.tool() async def get_doctype_list( module: Optional[str] = None, limit: Optional[int] = 50 ) -> str: """ Get a list of available DocTypes, optionally filtered by module. Args: module: Module name to filter by (optional) limit: Maximum number of DocTypes to return (default: 50) """ try: client = get_client() # Build parameters params = { "fields": json.dumps(["name", "module", "is_submittable", "is_tree", "description"]), "limit": str(limit), "order_by": "name" } if module: params["filters"] = json.dumps({"module": module}) # Get DocType list response = await client.get("api/resource/DocType", params=params) if "data" in response: doctypes = response["data"] count = len(doctypes) filter_text = f" in module '{module}'" if module else "" return f"Found {count} DocTypes{filter_text}:\n\n" + json.dumps(doctypes, indent=2) else: return json.dumps(response, indent=2) except Exception as error: return _format_error_response(error, "get_doctype_list") @mcp.tool() async def get_frappe_usage_info( doctype: Optional[str] = None, workflow: Optional[str] = None ) -> str: """ Get combined information about a DocType or workflow, including schema metadata and usage guidance. Args: doctype: DocType name (optional if workflow is provided) workflow: Workflow name (optional if doctype is provided) """ try: if not doctype and not workflow: return "Please provide either a doctype or workflow parameter" client = get_client() info_parts = [] if doctype: # Get DocType schema information schema_response = await client.get(f"api/resource/DocType/{doctype}") if "data" in schema_response: schema_data = schema_response["data"] # Extract key information info = { "doctype": doctype, "module": schema_data.get("module"), "description": schema_data.get("description"), "is_submittable": schema_data.get("is_submittable", 0) == 1, "is_tree": schema_data.get("is_tree", 0) == 1, "naming_rule": schema_data.get("autoname"), "required_fields": [] } # Get required fields for field in schema_data.get("fields", []): if field.get("reqd", 0) == 1: info["required_fields"].append({ "fieldname": field.get("fieldname"), "label": field.get("label"), "fieldtype": field.get("fieldtype"), "options": field.get("options") }) info_parts.append(f"DocType Information:\n{json.dumps(info, indent=2)}") if workflow: # Get workflow information try: workflow_response = await client.get(f"api/resource/Workflow/{workflow}") if "data" in workflow_response: workflow_data = workflow_response["data"] workflow_info = { "workflow": workflow, "document_type": workflow_data.get("document_type"), "is_active": workflow_data.get("is_active", 0) == 1, "workflow_states": workflow_data.get("states", []), "transitions": workflow_data.get("transitions", []) } info_parts.append(f"Workflow Information:\n{json.dumps(workflow_info, indent=2)}") except: info_parts.append(f"Could not retrieve workflow information for: {workflow}") return "\n\n".join(info_parts) if info_parts else "No information found" except Exception as error: return _format_error_response(error, "get_frappe_usage_info")

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/appliedrelevance/frappe-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server