cancel_document
Cancel documents in Frappe by changing docstatus from 1 to 2. Handles validation, linked documents, and permissions with clear error feedback.
Instructions
Cancel a document in Frappe (change docstatus from 1 to 2).
This tool handles document cancellation using Frappe's cancellation workflow,
including proper validation and error handling to provide clear feedback
for corrective action.
Args:
doctype: DocType name
name: Document name (case-sensitive)
Returns:
Success message if cancelled, or detailed error information if cancellation
fails due to validation errors, linked documents, or permission issues.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| doctype | Yes | ||
| name | Yes |
Implementation Reference
- src/tools/documents.py:642-761 (handler)The @mcp.tool()-decorated async handler function that implements the core logic of the cancel_document tool. It retrieves the document, validates its status must be submitted (docstatus=1), calls Frappe's savedocs API with action='Cancel' to change docstatus to 2, and provides detailed error handling for validation errors, permissions, and API issues.@mcp.tool() async def cancel_document( doctype: str, name: str ) -> str: """ Cancel a document in Frappe (change docstatus from 1 to 2). This tool handles document cancellation using Frappe's cancellation workflow, including proper validation and error handling to provide clear feedback for corrective action. Args: doctype: DocType name name: Document name (case-sensitive) Returns: Success message if cancelled, or detailed error information if cancellation fails due to validation errors, linked documents, or permission issues. """ try: client = get_client() # First, get the current document to check its status and get full data 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." if current_docstatus == 0: return f"Document {doctype} '{name}' is in Draft status. Only submitted documents (docstatus=1) can be cancelled." if current_docstatus == 2: return f"Document {doctype} '{name}' is already cancelled." if current_docstatus != 1: return f"Document {doctype} '{name}' has unexpected status (docstatus={current_docstatus}). Only submitted documents (docstatus=1) can be cancelled." except Exception as get_error: return f"Error retrieving document for cancellation: {get_error}" # Prepare document for cancellation by setting docstatus to 2 cancel_doc = doc_data.copy() cancel_doc['docstatus'] = 2 # Use Frappe's savedocs method which handles the cancellation workflow response = await client.post( "api/method/frappe.desk.form.save.savedocs", json_data={ "doc": json.dumps(cancel_doc), "action": "Cancel" } ) # Check if cancellation was successful if "docs" in response: cancelled_doc = response["docs"][0] if response["docs"] else {} final_docstatus = cancelled_doc.get("docstatus", 0) if final_docstatus == 2: return f"✅ Document {doctype} '{name}' successfully cancelled." else: return f"⚠️ Cancellation completed but document status is {final_docstatus} (expected 2)." # If we get here, check for success without docs if response.get("message") == "ok" or "exc" not in response: return f"✅ Document {doctype} '{name}' successfully cancelled." # If no explicit success indicator, assume it worked return f"✅ Document {doctype} '{name}' cancellation completed." 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 cancellation validation errors if "Cannot cancel" in str(exception_msg) and "linked" in str(exception_msg).lower(): return ( f"❌ Cancellation failed: Document {doctype} '{name}' cannot be cancelled because it has linked documents. " f"You may need to cancel or unlink related documents first before cancelling this document." ) elif "Cannot cancel" in str(exception_msg): return f"❌ Cancellation failed: {exception_msg}. Check document constraints and linked records." else: # Generic validation error return f"❌ Validation error: {exception_msg}. Please fix the validation issues and try again." elif "PermissionError" in str(exception_msg): return f"❌ Permission denied: You don't have sufficient permissions to cancel {doctype} documents." else: # Other exceptions return f"❌ Cancellation 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") return f"❌ Cancellation failed: {user_message}" except (json.JSONDecodeError, KeyError, IndexError): pass return f"❌ Cancellation failed: {api_error}" except Exception as error: return _format_error_response(error, "cancel_document")
- src/server.py:39-42 (registration)Top-level registration call: documents.register_tools(mcp) which invokes the register_tools function in documents.py that defines and registers the cancel_document tool using the @mcp.tool() decorator.helpers.register_tools(mcp) documents.register_tools(mcp) schema.register_tools(mcp) reports.register_tools(mcp)
- src/tools/documents.py:51-79 (helper)_format_error_response utility function used by cancel_document (and other document tools) to format error responses with diagnostic information including credential validation.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)}"
- src/tools/documents.py:81-81 (registration)The register_tools function in documents.py that defines all document-related MCP tools including cancel_document when called from server.py.def register_tools(mcp: Any) -> None: