Skip to main content
Glama

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
NameRequiredDescriptionDefault
doctypeYes
nameYes

Implementation Reference

  • 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)
  • _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)}"
  • 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:

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