MCP-Frappe/frappe_mcp/tools/document_lifecycle.py
MOHAN 2ee93048e1 feat: Add tools for managing server scripts, client scripts, translations, assignment rules, user permissions, webhooks, API keys, and workflows
- Implemented server and client script management tools in `frappe_mcp/tools/scripts.py`
- Added translation and user permission management tools in `frappe_mcp/tools/translations.py`
- Created user and role management tools in `frappe_mcp/tools/users.py`
- Developed webhook and API key management tools in `frappe_mcp/tools/webhooks.py`
- Introduced workflow management tools in `frappe_mcp/tools/workflow_tools.py`
- Added `pyproject.toml` for project metadata and dependencies
2026-04-21 20:26:45 +05:30

230 lines
8.4 KiB
Python

"""
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)