- 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
230 lines
8.4 KiB
Python
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)
|