""" Level 3-4 — Document lifecycle tools. Child row operations, rename, amend, duplicate, status, draft save. """ import json from mcp.types import Tool from frappe_mcp.client.frappe_api import FrappeClient def tools() -> list[Tool]: return [ # --- Child row ops --- Tool( name="frappe_append_child_row", description="Append a new row to a child table in a document.", inputSchema={ "type": "object", "required": ["doctype", "name", "child_fieldname", "row_data"], "properties": { "doctype": {"type": "string", "description": "Parent DocType"}, "name": {"type": "string", "description": "Parent document name"}, "child_fieldname": {"type": "string", "description": "Fieldname of the child table e.g. 'items'"}, "row_data": {"type": "object", "description": "Fields for the new row"}, }, }, ), Tool( name="frappe_update_child_row", description="Update a specific row in a child table by its row name.", inputSchema={ "type": "object", "required": ["child_doctype", "row_name", "updates"], "properties": { "child_doctype": {"type": "string", "description": "Child table DocType e.g. 'Sales Order Item'"}, "row_name": {"type": "string", "description": "Child row document name"}, "updates": {"type": "object"}, }, }, ), Tool( name="frappe_delete_child_row", description="Delete a specific row from a child table.", inputSchema={ "type": "object", "required": ["child_doctype", "row_name"], "properties": { "child_doctype": {"type": "string"}, "row_name": {"type": "string"}, }, }, ), # --- Rename --- Tool( name="frappe_rename_document", description="Rename a document (changes its primary key/name). Only works if DocType allows renaming.", inputSchema={ "type": "object", "required": ["doctype", "old_name", "new_name"], "properties": { "doctype": {"type": "string"}, "old_name": {"type": "string"}, "new_name": {"type": "string"}, "merge": {"type": "boolean", "default": False, "description": "Merge into existing document if new_name exists"}, }, }, ), # --- Amend --- Tool( name="frappe_amend_document", description=( "Create an amendment of a cancelled/submitted document. " "Returns a new draft with amended_ prefix and links to original." ), inputSchema={ "type": "object", "required": ["doctype", "name"], "properties": { "doctype": {"type": "string"}, "name": {"type": "string"}, }, }, ), # --- Duplicate --- Tool( name="frappe_duplicate_document", description="Clone a document — creates a new draft copy with the same field values.", inputSchema={ "type": "object", "required": ["doctype", "name"], "properties": { "doctype": {"type": "string"}, "name": {"type": "string"}, "field_overrides": { "type": "object", "description": "Fields to override in the copy e.g. {\"customer\": \"New Customer\"}", }, }, }, ), # --- Status --- Tool( name="frappe_get_document_status", description=( "Get the normalized status of a document: " "draft | saved | submitted | cancelled | workflow_state." ), inputSchema={ "type": "object", "required": ["doctype", "name"], "properties": { "doctype": {"type": "string"}, "name": {"type": "string"}, }, }, ), # --- Draft save --- Tool( name="frappe_save_document_draft", description=( "Explicitly save a document as draft (docstatus=0). " "Use when you want to save partial data without validating all required fields." ), inputSchema={ "type": "object", "required": ["doctype", "name"], "properties": { "doctype": {"type": "string"}, "name": {"type": "string"}, "updates": {"type": "object", "description": "Optional field updates to apply before saving"}, }, }, ), ] def handlers() -> dict: return { "frappe_append_child_row": _append_child_row, "frappe_update_child_row": _update_child_row, "frappe_delete_child_row": _delete_child_row, "frappe_rename_document": _rename_document, "frappe_amend_document": _amend_document, "frappe_duplicate_document": _duplicate_document, "frappe_get_document_status": _get_document_status, "frappe_save_document_draft": _save_document_draft, } _DOCSTATUS_MAP = {0: "draft/saved", 1: "submitted", 2: "cancelled"} async def _append_child_row(args: dict) -> str: client = FrappeClient() doc = await client.get_doc(args["doctype"], args["name"]) rows = doc.get(args["child_fieldname"], []) rows.append(args["row_data"]) result = await client.update_doc(args["doctype"], args["name"], {args["child_fieldname"]: rows}) return json.dumps(result, indent=2) async def _update_child_row(args: dict) -> str: client = FrappeClient() result = await client.update_doc(args["child_doctype"], args["row_name"], args["updates"]) return json.dumps(result, indent=2) async def _delete_child_row(args: dict) -> str: client = FrappeClient() result = await client.delete_doc(args["child_doctype"], args["row_name"]) return json.dumps(result, indent=2) async def _rename_document(args: dict) -> str: client = FrappeClient() result = await client.call_method( "frappe.client.rename_doc", doctype=args["doctype"], old_name=args["old_name"], new_name=args["new_name"], merge=args.get("merge", False), ) return json.dumps({"renamed_to": result}, indent=2) async def _amend_document(args: dict) -> str: client = FrappeClient() doc = await client.get_doc(args["doctype"], args["name"]) amended = {k: v for k, v in doc.items() if not k.startswith("__")} amended.pop("name", None) amended["amended_from"] = args["name"] amended["docstatus"] = 0 result = await client.create_doc(args["doctype"], amended) return json.dumps(result, indent=2) async def _duplicate_document(args: dict) -> str: client = FrappeClient() doc = await client.get_doc(args["doctype"], args["name"]) copy = {k: v for k, v in doc.items() if not k.startswith("__")} copy.pop("name", None) copy["docstatus"] = 0 copy.update(args.get("field_overrides", {})) result = await client.create_doc(args["doctype"], copy) return json.dumps(result, indent=2) async def _get_document_status(args: dict) -> str: client = FrappeClient() doc = await client.get_doc(args["doctype"], args["name"]) docstatus = doc.get("docstatus", 0) workflow_state = doc.get("workflow_state", None) status = doc.get("status", None) return json.dumps({ "name": args["name"], "doctype": args["doctype"], "docstatus": docstatus, "docstatus_label": _DOCSTATUS_MAP.get(docstatus, "unknown"), "status": status, "workflow_state": workflow_state, }, indent=2) async def _save_document_draft(args: dict) -> str: client = FrappeClient() updates = args.get("updates", {}) updates["docstatus"] = 0 result = await client.update_doc(args["doctype"], args["name"], updates) return json.dumps(result, indent=2)