- 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
478 lines
20 KiB
Python
478 lines
20 KiB
Python
"""
|
|
Level 8 — Business action tools (ERPNext shortcuts).
|
|
High-value operations: convert documents, approve, transfer stock, etc.
|
|
These call ERPNext's built-in mapper/make functions.
|
|
"""
|
|
|
|
import json
|
|
from mcp.types import Tool
|
|
from frappe_mcp.client.frappe_api import FrappeClient
|
|
|
|
|
|
def tools() -> list[Tool]:
|
|
return [
|
|
# ── Sales / CRM ──────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_create_quotation_from_opportunity",
|
|
description="Create a Quotation from an Opportunity.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["opportunity_name"],
|
|
"properties": {"opportunity_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_sales_order_from_quotation",
|
|
description="Create a Sales Order from a Quotation.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["quotation_name"],
|
|
"properties": {"quotation_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_sales_invoice_from_sales_order",
|
|
description="Create a Sales Invoice from a Sales Order.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["sales_order_name"],
|
|
"properties": {"sales_order_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_delivery_note_from_sales_order",
|
|
description="Create a Delivery Note from a Sales Order.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["sales_order_name"],
|
|
"properties": {"sales_order_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_payment_entry_for_invoice",
|
|
description="Create a Payment Entry for a Sales Invoice or Purchase Invoice.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["invoice_doctype", "invoice_name"],
|
|
"properties": {
|
|
"invoice_doctype": {"type": "string", "enum": ["Sales Invoice", "Purchase Invoice"]},
|
|
"invoice_name": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
# ── Buying ───────────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_create_purchase_order_from_supplier_quotation",
|
|
description="Create a Purchase Order from a Supplier Quotation.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["supplier_quotation_name"],
|
|
"properties": {"supplier_quotation_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_purchase_receipt_from_purchase_order",
|
|
description="Create a Purchase Receipt from a Purchase Order.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["purchase_order_name"],
|
|
"properties": {"purchase_order_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_purchase_invoice_from_purchase_receipt",
|
|
description="Create a Purchase Invoice from a Purchase Receipt.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["purchase_receipt_name"],
|
|
"properties": {"purchase_receipt_name": {"type": "string"}},
|
|
},
|
|
),
|
|
# ── Stock ────────────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_get_item_stock_balance",
|
|
description="Get current stock balance for an item, optionally at a specific warehouse.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["item_code"],
|
|
"properties": {
|
|
"item_code": {"type": "string"},
|
|
"warehouse": {"type": "string"},
|
|
"posting_date": {"type": "string", "description": "YYYY-MM-DD (defaults to today)"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_transfer_stock",
|
|
description="Transfer stock between warehouses via a Material Transfer Stock Entry.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["item_code", "qty", "from_warehouse", "to_warehouse"],
|
|
"properties": {
|
|
"item_code": {"type": "string"},
|
|
"qty": {"type": "number"},
|
|
"from_warehouse": {"type": "string"},
|
|
"to_warehouse": {"type": "string"},
|
|
"posting_date": {"type": "string"},
|
|
"remarks": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_create_stock_entry",
|
|
description=(
|
|
"Create a Stock Entry of any type: "
|
|
"Material Issue, Material Receipt, Material Transfer, Manufacture, Repack."
|
|
),
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["stock_entry_type", "items"],
|
|
"properties": {
|
|
"stock_entry_type": {
|
|
"type": "string",
|
|
"enum": ["Material Issue", "Material Receipt", "Material Transfer",
|
|
"Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"],
|
|
},
|
|
"items": {
|
|
"type": "array",
|
|
"description": "List of {item_code, qty, s_warehouse, t_warehouse} objects",
|
|
"items": {"type": "object"},
|
|
},
|
|
"posting_date": {"type": "string"},
|
|
"remarks": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
# ── HR ───────────────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_approve_leave_application",
|
|
description="Approve a Leave Application.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["leave_application_name"],
|
|
"properties": {"leave_application_name": {"type": "string"}},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_reject_leave_application",
|
|
description="Reject a Leave Application.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["leave_application_name"],
|
|
"properties": {
|
|
"leave_application_name": {"type": "string"},
|
|
"reason": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_mark_attendance",
|
|
description="Mark attendance for an employee on a specific date.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["employee", "attendance_date", "status"],
|
|
"properties": {
|
|
"employee": {"type": "string"},
|
|
"attendance_date": {"type": "string", "description": "YYYY-MM-DD"},
|
|
"status": {"type": "string", "enum": ["Present", "Absent", "Half Day", "Work From Home", "On Leave"]},
|
|
"shift": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_approve_expense_claim",
|
|
description="Approve an Expense Claim.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["expense_claim_name"],
|
|
"properties": {"expense_claim_name": {"type": "string"}},
|
|
},
|
|
),
|
|
# ── Support ──────────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_create_issue",
|
|
description="Create a new Support Issue.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["subject"],
|
|
"properties": {
|
|
"subject": {"type": "string"},
|
|
"customer": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"priority": {"type": "string", "enum": ["Low", "Medium", "High", "Urgent"], "default": "Medium"},
|
|
"issue_type": {"type": "string"},
|
|
"raised_by": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_close_issue",
|
|
description="Close a Support Issue.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["issue_name"],
|
|
"properties": {
|
|
"issue_name": {"type": "string"},
|
|
"resolution_details": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
# ── Projects ─────────────────────────────────────────────────────────
|
|
Tool(
|
|
name="erpnext_create_task",
|
|
description="Create a Task in a Project.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["subject"],
|
|
"properties": {
|
|
"subject": {"type": "string"},
|
|
"project": {"type": "string"},
|
|
"assigned_to": {"type": "string"},
|
|
"exp_start_date": {"type": "string"},
|
|
"exp_end_date": {"type": "string"},
|
|
"priority": {"type": "string", "enum": ["Low", "Medium", "High", "Urgent"]},
|
|
"description": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_update_task_status",
|
|
description="Update the status of a Task.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["task_name", "status"],
|
|
"properties": {
|
|
"task_name": {"type": "string"},
|
|
"status": {"type": "string", "enum": ["Open", "Working", "Pending Review", "Overdue", "Template", "Cancelled", "Completed"]},
|
|
},
|
|
},
|
|
),
|
|
Tool(
|
|
name="erpnext_log_timesheet_entry",
|
|
description="Log a timesheet entry for an employee on a task or project.",
|
|
inputSchema={
|
|
"type": "object",
|
|
"required": ["employee", "from_time", "to_time"],
|
|
"properties": {
|
|
"employee": {"type": "string"},
|
|
"from_time": {"type": "string", "description": "ISO datetime"},
|
|
"to_time": {"type": "string", "description": "ISO datetime"},
|
|
"project": {"type": "string"},
|
|
"task": {"type": "string"},
|
|
"activity_type": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
},
|
|
},
|
|
),
|
|
]
|
|
|
|
|
|
def handlers() -> dict:
|
|
return {
|
|
"erpnext_create_quotation_from_opportunity": _make_quotation_from_opportunity,
|
|
"erpnext_create_sales_order_from_quotation": _make_so_from_quotation,
|
|
"erpnext_create_sales_invoice_from_sales_order": _make_si_from_so,
|
|
"erpnext_create_delivery_note_from_sales_order": _make_dn_from_so,
|
|
"erpnext_create_payment_entry_for_invoice": _make_payment_entry,
|
|
"erpnext_create_purchase_order_from_supplier_quotation": _make_po_from_sq,
|
|
"erpnext_create_purchase_receipt_from_purchase_order": _make_pr_from_po,
|
|
"erpnext_create_purchase_invoice_from_purchase_receipt": _make_pi_from_pr,
|
|
"erpnext_get_item_stock_balance": _get_item_stock_balance,
|
|
"erpnext_transfer_stock": _transfer_stock,
|
|
"erpnext_create_stock_entry": _create_stock_entry,
|
|
"erpnext_approve_leave_application": _approve_leave,
|
|
"erpnext_reject_leave_application": _reject_leave,
|
|
"erpnext_mark_attendance": _mark_attendance,
|
|
"erpnext_approve_expense_claim": _approve_expense_claim,
|
|
"erpnext_create_issue": _create_issue,
|
|
"erpnext_close_issue": _close_issue,
|
|
"erpnext_create_task": _create_task,
|
|
"erpnext_update_task_status": _update_task_status,
|
|
"erpnext_log_timesheet_entry": _log_timesheet,
|
|
}
|
|
|
|
|
|
async def _make_quotation_from_opportunity(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.crm.doctype.opportunity.opportunity.make_quotation",
|
|
source_name=args["opportunity_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_so_from_quotation(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
|
source_name=args["quotation_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_si_from_so(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice",
|
|
source_name=args["sales_order_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_dn_from_so(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
|
source_name=args["sales_order_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_payment_entry(args: dict) -> str:
|
|
client = FrappeClient()
|
|
method = (
|
|
"erpnext.accounts.doctype.sales_invoice.sales_invoice.get_bank_cash_account"
|
|
if args["invoice_doctype"] == "Sales Invoice"
|
|
else "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_payment_entry"
|
|
)
|
|
result = await client.call_method(
|
|
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
|
|
dt=args["invoice_doctype"],
|
|
dn=args["invoice_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_po_from_sq(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order",
|
|
source_name=args["supplier_quotation_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_pr_from_po(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
|
|
source_name=args["purchase_order_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _make_pi_from_pr(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.buying.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
|
|
source_name=args["purchase_receipt_name"],
|
|
)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _get_item_stock_balance(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.call_method(
|
|
"erpnext.stock.utils.get_stock_balance",
|
|
item_code=args["item_code"],
|
|
warehouse=args.get("warehouse", ""),
|
|
posting_date=args.get("posting_date", ""),
|
|
)
|
|
return json.dumps({"item_code": args["item_code"], "warehouse": args.get("warehouse"), "balance": result}, indent=2)
|
|
|
|
async def _transfer_stock(args: dict) -> str:
|
|
client = FrappeClient()
|
|
payload = {
|
|
"stock_entry_type": "Material Transfer",
|
|
"posting_date": args.get("posting_date", ""),
|
|
"remarks": args.get("remarks", ""),
|
|
"items": [{
|
|
"item_code": args["item_code"],
|
|
"qty": args["qty"],
|
|
"s_warehouse": args["from_warehouse"],
|
|
"t_warehouse": args["to_warehouse"],
|
|
}],
|
|
}
|
|
result = await client.create_doc("Stock Entry", payload)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _create_stock_entry(args: dict) -> str:
|
|
client = FrappeClient()
|
|
payload = {
|
|
"stock_entry_type": args["stock_entry_type"],
|
|
"posting_date": args.get("posting_date", ""),
|
|
"remarks": args.get("remarks", ""),
|
|
"items": args["items"],
|
|
}
|
|
result = await client.create_doc("Stock Entry", payload)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _approve_leave(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.update_doc("Leave Application", args["leave_application_name"], {"status": "Approved"})
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _reject_leave(args: dict) -> str:
|
|
client = FrappeClient()
|
|
updates = {"status": "Rejected"}
|
|
if reason := args.get("reason"):
|
|
updates["reason_for_rejection"] = reason
|
|
result = await client.update_doc("Leave Application", args["leave_application_name"], updates)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _mark_attendance(args: dict) -> str:
|
|
client = FrappeClient()
|
|
payload = {
|
|
"employee": args["employee"],
|
|
"attendance_date": args["attendance_date"],
|
|
"status": args["status"],
|
|
"shift": args.get("shift", ""),
|
|
"docstatus": 1,
|
|
}
|
|
result = await client.create_doc("Attendance", payload)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _approve_expense_claim(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.update_doc("Expense Claim", args["expense_claim_name"], {"approval_status": "Approved"})
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _create_issue(args: dict) -> str:
|
|
client = FrappeClient()
|
|
payload = {
|
|
"subject": args["subject"],
|
|
"customer": args.get("customer", ""),
|
|
"description": args.get("description", ""),
|
|
"priority": args.get("priority", "Medium"),
|
|
"issue_type": args.get("issue_type", ""),
|
|
"raised_by": args.get("raised_by", ""),
|
|
}
|
|
result = await client.create_doc("Issue", payload)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _close_issue(args: dict) -> str:
|
|
client = FrappeClient()
|
|
updates = {"status": "Closed"}
|
|
if res := args.get("resolution_details"):
|
|
updates["resolution_details"] = res
|
|
result = await client.update_doc("Issue", args["issue_name"], updates)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _create_task(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.create_doc("Task", args)
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _update_task_status(args: dict) -> str:
|
|
client = FrappeClient()
|
|
result = await client.update_doc("Task", args["task_name"], {"status": args["status"]})
|
|
return json.dumps(result, indent=2)
|
|
|
|
async def _log_timesheet(args: dict) -> str:
|
|
client = FrappeClient()
|
|
payload = {
|
|
"employee": args["employee"],
|
|
"time_logs": [{
|
|
"from_time": args["from_time"],
|
|
"to_time": args["to_time"],
|
|
"project": args.get("project", ""),
|
|
"task": args.get("task", ""),
|
|
"activity_type": args.get("activity_type", ""),
|
|
"description": args.get("description", ""),
|
|
}],
|
|
}
|
|
result = await client.create_doc("Timesheet", payload)
|
|
return json.dumps(result, indent=2)
|