delete_document
Remove documents from Frappe with detailed error handling and corrective guidance for linked records, permissions, or constraints.
Instructions
Delete a document from Frappe.
This tool handles document deletion with comprehensive error handling,
providing detailed feedback about why deletions fail and actionable
guidance for resolving common deletion blockers.
Args:
doctype: DocType name
name: Document name (case-sensitive)
Returns:
Success message if deleted, or detailed error information with
corrective actions for linked documents, permissions, or constraints.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doctype | Yes | ||
| name | Yes |
Implementation Reference
- src/tools/documents.py:180-348 (handler)The main execution handler for the 'delete_document' tool. Decorated with @mcp.tool(), it deletes Frappe documents via API with sophisticated error handling for linked docs, permissions, docstatus, extracting actionable insights from errors.@mcp.tool() async def delete_document( doctype: str, name: str ) -> str: """ Delete a document from Frappe. This tool handles document deletion with comprehensive error handling, providing detailed feedback about why deletions fail and actionable guidance for resolving common deletion blockers. Args: doctype: DocType name name: Document name (case-sensitive) Returns: Success message if deleted, or detailed error information with corrective actions for linked documents, permissions, or constraints. """ try: client = get_client() # First, get the current document to check its status and understand constraints try: doc_response = await client.get(f"api/resource/{doctype}/{name}") doc_data = doc_response.get("data", {}) current_docstatus = doc_data.get("docstatus", None) if current_docstatus is None: return f"Error: Could not retrieve document {doctype} '{name}'. Document may not exist." # Provide guidance based on document status if current_docstatus == 1: return ( f"⚠️ Document {doctype} '{name}' is submitted (docstatus=1). " f"Submitted documents cannot be deleted. You must cancel it first using cancel_document, " f"then delete the cancelled document." ) elif current_docstatus == 2: # Cancelled documents can usually be deleted, but may have constraints pass # Continue with deletion attempt elif current_docstatus == 0: # Draft documents should be deletable, but may have linked documents pass # Continue with deletion attempt except Exception as get_error: # Document might not exist, which is fine for deletion pass # Attempt to delete the document response = await client.delete(f"api/resource/{doctype}/{name}") if response.get("message") == "ok": return f"✅ Document {doctype} '{name}' successfully deleted." else: return f"⚠️ Deletion may have succeeded but response format unexpected: {json.dumps(response, indent=2)}" except FrappeApiError as api_error: # Handle specific Frappe API errors with detailed information if api_error.response_data: error_data = api_error.response_data # Check for validation errors in the response if "exception" in error_data: exception_msg = error_data["exception"] # Extract user-friendly error messages if "ValidationError" in str(exception_msg): # Common deletion validation errors if "linked" in str(exception_msg).lower() or "referenced" in str(exception_msg).lower(): # Extract linked document information directly from the error message linked_docs = _extract_linked_docs_from_error(str(exception_msg)) if linked_docs: linked_info = "\n".join([f" - {doc['doctype']} '{doc['name']}'" for doc in linked_docs]) return ( f"❌ Deletion failed: Document {doctype} '{name}' cannot be deleted because it is linked to other documents.\n" f"Blocking document(s):\n{linked_info}\n" f"To resolve: Delete, cancel, or unlink these documents first, then retry deletion." ) else: return ( f"❌ Deletion failed: Document {doctype} '{name}' cannot be deleted because it is linked to other documents. " f"Error details: {exception_msg}. " f"To resolve: Find and delete/cancel the documents that reference this one, or remove the links first." ) elif "Cannot delete" in str(exception_msg): return ( f"❌ Deletion failed: {exception_msg}. " f"This may be due to business rules, data integrity constraints, or linked transactions. " f"Check for related documents that need to be handled first." ) elif "submitted" in str(exception_msg).lower(): return ( f"❌ Deletion failed: Cannot delete submitted documents. " f"Use cancel_document first to cancel {doctype} '{name}', then try deleting the cancelled document." ) else: # Generic validation error return f"❌ Validation error: {exception_msg}. Please resolve the validation issues before deleting." elif "PermissionError" in str(exception_msg): return ( f"❌ Permission denied: You don't have sufficient permissions to delete {doctype} documents. " f"Contact your system administrator to request Delete permission for {doctype}." ) elif "IntegrityError" in str(exception_msg) or "foreign key" in str(exception_msg).lower(): return ( f"❌ Database constraint violation: Document {doctype} '{name}' cannot be deleted due to foreign key constraints. " f"Error: {exception_msg}. " f"This usually means other records reference this document. Find and handle those references first." ) else: # Other exceptions with helpful context return f"❌ Deletion failed: {exception_msg}" # Check for server messages with more details if "_server_messages" in error_data: try: messages = json.loads(error_data["_server_messages"]) if messages: msg_data = json.loads(messages[0]) user_message = msg_data.get("message", "Unknown error") # Parse common server messages for actionable guidance if "linked" in user_message.lower(): return ( f"❌ Deletion failed: {user_message}. " f"To resolve: Identify and handle the linked documents first, then retry deletion." ) else: return f"❌ Deletion failed: {user_message}" except (json.JSONDecodeError, KeyError, IndexError): pass # Handle HTTP status codes for additional context if api_error.status_code == 403: return ( f"❌ Access forbidden: You don't have permission to delete {doctype} '{name}'. " f"Contact your administrator for Delete permissions on {doctype}." ) elif api_error.status_code == 404: return f"✅ Document {doctype} '{name}' not found - it may already be deleted or never existed." elif api_error.status_code == 417: # HTTP 417 errors may contain linked document information in the response error_text = str(api_error) if api_error.response_data: error_text += " " + str(api_error.response_data) linked_docs = _extract_linked_docs_from_error(error_text) if linked_docs: linked_info = "\n".join([f" - {doc['doctype']} '{doc['name']}'" for doc in linked_docs]) return ( f"❌ Deletion failed with HTTP 417 error. Found blocking document(s):\n{linked_info}\n" f"These documents may be preventing deletion. Try deleting, cancelling, or unlinking them first." ) else: return ( f"❌ Deletion failed with HTTP 417 error. This may be due to server configuration issues " f"or complex validation constraints. Error: {api_error}. " f"Check for business rule violations, permissions, or linked documents." ) else: return f"❌ Deletion failed: {api_error}" except Exception as error: return _format_error_response(error, "delete_document")
- src/server.py:40-40 (registration)Invocation of documents.register_tools(mcp) which defines and registers the delete_document tool via its @mcp.tool() decorator within the register_tools function.documents.register_tools(mcp)
- src/tools/documents.py:16-48 (helper)Utility function called by delete_document to parse error messages and identify specific linked documents blocking deletion, enabling user-friendly feedback.def _extract_linked_docs_from_error(error_message: str) -> List[Dict[str, str]]: """ Extract linked document information from Frappe error messages. Frappe's LinkExistsError and similar messages often contain specific information about which documents are linked, which we can parse directly. """ import re linked_docs = [] # Common patterns in Frappe error messages for linked documents patterns = [ # "is linked with DocType <a href="...">Name</a>" r'is linked with (\w+(?:\s+\w+)*)\s+<a[^>]*>([^<]+)</a>', # "Cannot delete ... because ... is linked with DocType Name" r'is linked with (\w+(?:\s+\w+)*)\s+([A-Z0-9-]+)', # "referenced by DocType: Name" r'referenced by (\w+(?:\s+\w+)*):?\s+([A-Z0-9-]+)', ] for pattern in patterns: matches = re.findall(pattern, error_message, re.IGNORECASE) for match in matches: doctype, name = match # Clean up doctype (remove extra spaces, standardize) doctype = ' '.join(doctype.split()) linked_docs.append({ "doctype": doctype, "name": name, "source": "error_message" }) return linked_docs
- src/tools/documents.py:51-79 (helper)Fallback error formatter used in delete_document's outermost exception handler to provide diagnostic information including credential status.def _format_error_response(error: Exception, operation: str) -> str: """Format error response with detailed information.""" credentials_check = validate_api_credentials() # Build diagnostic information diagnostics = [ f"Error in {operation}", f"Error type: {type(error).__name__}", f"Is FrappeApiError: {isinstance(error, FrappeApiError)}", f"API Key available: {credentials_check['details']['api_key_available']}", f"API Secret available: {credentials_check['details']['api_secret_available']}" ] # 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)}"