diff --git a/erpnext-mcp/src/tools/frappe.ts b/erpnext-mcp/src/tools/frappe.ts new file mode 100644 index 0000000..f72a3fd --- /dev/null +++ b/erpnext-mcp/src/tools/frappe.ts @@ -0,0 +1,615 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ERPNextClient, truncateResponse } from "../services/erpnext-client.js"; +import { z } from "zod"; + +export function registerFrappeTools(server: McpServer, client: ERPNextClient): void { + + // ─── 71. Get Value ───────────────────────────────────── + server.registerTool( + "erpnext_get_value", + { + title: "Get Field Value", + description: `Get a specific field value from a document without fetching the entire document. + +Args: + - doctype: DocType name + - fieldname: Field to retrieve (or comma-separated list) + - filters: Filters to identify the document (e.g. {"name": "CUST-00001"})`, + inputSchema: z.object({ + doctype: z.string(), + fieldname: z.string().describe('Field name or comma-separated list (e.g. "customer_name" or "customer_name,territory")'), + filters: z.record(z.unknown()), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.client.get_value", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 72. Set Value ───────────────────────────────────── + server.registerTool( + "erpnext_set_value", + { + title: "Set Field Value", + description: `Set a specific field value on a document. + +Args: + - doctype: DocType name + - name: Document name + - fieldname: Field to set + - value: Value to set`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + fieldname: z.string(), + value: z.union([z.string(), z.number(), z.boolean(), z.null()]), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.client.set_value", params); + return { content: [{ type: "text", text: `Set ${params.fieldname}=${params.value} on ${params.doctype} ${params.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 73. Assign To ───────────────────────────────────── + server.registerTool( + "erpnext_assign_to", + { + title: "Assign Document To User", + description: `Assign a document to one or more users.`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + assign_to: z.array(z.string()).describe("Array of user email addresses"), + description: z.string().optional(), + priority: z.enum(["Low", "Medium", "High"]).optional(), + date: z.string().optional().describe("Due date"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.desk.form.assign_to.add", params); + return { content: [{ type: "text", text: `Assigned ${params.doctype} ${params.name} to ${params.assign_to.join(", ")}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 74. Remove Assignment ───────────────────────────── + server.registerTool( + "erpnext_remove_assignment", + { + title: "Remove Document Assignment", + description: `Remove a user assignment from a document.`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + assign_to: z.string().describe("User email to unassign"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("frappe.desk.form.assign_to.remove", params); + return { content: [{ type: "text", text: `Removed assignment of ${params.assign_to} from ${params.doctype} ${params.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 75. Get Assignments ─────────────────────────────── + server.registerTool( + "erpnext_get_assignments", + { + title: "Get Document Assignments", + description: `Get all user assignments for a document.`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.desk.form.assign_to.get", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 76. Add Comment ─────────────────────────────────── + server.registerTool( + "erpnext_add_comment", + { + title: "Add Comment to Document", + description: `Add a comment to any document.`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + content: z.string().describe("Comment text (supports HTML)"), + comment_type: z.enum(["Comment", "Like", "Info", "Label", "Workflow"]).default("Comment"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.createDocument("Comment", { + comment_type: params.comment_type, + reference_doctype: params.doctype, + reference_name: params.name, + content: params.content, + }); + return { content: [{ type: "text", text: `Comment added to ${params.doctype} ${params.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 77. Add Tag ─────────────────────────────────────── + server.registerTool( + "erpnext_add_tag", + { + title: "Add Tag to Document", + description: `Add a tag to a document for organization and filtering.`, + inputSchema: z.object({ + tag: z.string(), + dt: z.string().describe("DocType"), + dn: z.string().describe("Document name"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("frappe.desk.doctype.tag.tag.add_tag", params); + return { content: [{ type: "text", text: `Tag "${params.tag}" added to ${params.dt} ${params.dn}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 78. Remove Tag ──────────────────────────────────── + server.registerTool( + "erpnext_remove_tag", + { + title: "Remove Tag from Document", + description: `Remove a tag from a document.`, + inputSchema: z.object({ + tag: z.string(), + dt: z.string(), + dn: z.string(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("frappe.desk.doctype.tag.tag.remove_tag", params); + return { content: [{ type: "text", text: `Tag "${params.tag}" removed from ${params.dt} ${params.dn}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 79. Get Logged In User ──────────────────────────── + server.registerTool( + "erpnext_get_logged_in_user", + { + title: "Get Current User", + description: `Get the currently authenticated user.`, + inputSchema: {}, + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async () => { + try { + const result = await client.callGetMethod("frappe.auth.get_logged_user"); + return { content: [{ type: "text", text: `Logged in as: ${result}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 80. Get System Settings ─────────────────────────── + server.registerTool( + "erpnext_get_system_settings", + { + title: "Get System Settings", + description: `Get system settings values.`, + inputSchema: z.object({ + keys: z.array(z.string()).describe('Setting keys (e.g. ["setup_complete", "default_currency"])'), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const results: Record = {}; + for (const key of params.keys) { + const result = await client.callMethod("frappe.client.get_value", { + doctype: "System Settings", + fieldname: key, + filters: { name: "System Settings" }, + }); + results[key] = result; + } + return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 81. Rename Document ─────────────────────────────── + server.registerTool( + "erpnext_rename_document", + { + title: "Rename Document", + description: `Rename a document (change its name/ID).`, + inputSchema: z.object({ + doctype: z.string(), + old: z.string().describe("Current name"), + new_name: z.string().describe("New name"), + merge: z.boolean().default(false).describe("Merge with existing if name exists"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.client.rename_doc", { + doctype: params.doctype, + old: params.old, + new: params.new_name, + merge: params.merge, + }); + return { content: [{ type: "text", text: `Renamed ${params.doctype} from "${params.old}" to "${params.new_name}"` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 82. Get Activity Log ────────────────────────────── + server.registerTool( + "erpnext_get_activity_log", + { + title: "Get Activity Log", + description: `Get recent activity log entries.`, + inputSchema: z.object({ + limit: z.number().default(20), + user: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.listDocuments("Activity Log", { + fields: ["name", "user", "subject", "creation", "reference_doctype", "reference_name"], + limit_page_length: params.limit, + order_by: "creation desc", + ...(params.user ? { filters: [["Activity Log", "user", "=", params.user]] } : {}), + }); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result.items, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 83. Get Notifications ───────────────────────────── + server.registerTool( + "erpnext_get_notifications", + { + title: "Get User Notifications", + description: `Get recent notifications for the current user.`, + inputSchema: z.object({ + limit: z.number().default(20), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.listDocuments("Notification Log", { + fields: ["name", "subject", "type", "read", "creation", "document_type", "document_name"], + limit_page_length: params.limit, + order_by: "creation desc", + }); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result.items, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 84. Get ToDo List ───────────────────────────────── + server.registerTool( + "erpnext_get_todos", + { + title: "Get ToDo List", + description: `Get ToDo items for the current user.`, + inputSchema: z.object({ + status: z.enum(["Open", "Closed", "Cancelled"]).default("Open"), + limit: z.number().default(20), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.listDocuments("ToDo", { + fields: ["name", "description", "status", "date", "priority", "reference_type", "reference_name", "allocated_to"], + filters: [["ToDo", "status", "=", params.status]], + limit_page_length: params.limit, + order_by: "date asc", + }); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result.items, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 85-90. Leaderboard tools ────────────────────────── + const leaderboardTools = [ + { name: "customers", method: "erpnext.startup.leaderboard.get_all_customers", title: "Top Customers" }, + { name: "suppliers", method: "erpnext.startup.leaderboard.get_all_suppliers", title: "Top Suppliers" }, + { name: "items", method: "erpnext.startup.leaderboard.get_all_items", title: "Top Items" }, + { name: "sales_partners", method: "erpnext.startup.leaderboard.get_all_sales_partner", title: "Top Sales Partners" }, + { name: "sales_persons", method: "erpnext.startup.leaderboard.get_all_sales_person", title: "Top Sales Persons" }, + ]; + + for (const lb of leaderboardTools) { + server.registerTool( + `erpnext_leaderboard_${lb.name}`, + { + title: lb.title, + description: `Get ${lb.title.toLowerCase()} leaderboard data for a date range and company.`, + inputSchema: z.object({ + date_range: z.string().describe('Date range as JSON, e.g. ["2024-01-01","2024-12-31"]'), + company: z.string(), + field: z.string().default("grand_total").describe("Field to aggregate"), + limit: z.number().default(20), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod(lb.method, params); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + } + + // ─── 91. Get Linked Documents ────────────────────────── + server.registerTool( + "erpnext_get_linked_documents", + { + title: "Get Linked Documents", + description: `Get documents linked to a specific document (e.g. get all Sales Invoices linked to a Sales Order).`, + inputSchema: z.object({ + doctype: z.string(), + name: z.string(), + link_doctype: z.string().describe("DocType to search for links in"), + link_fieldname: z.string().optional().describe("Fieldname containing the link (auto-detected if not provided)"), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const fieldname = params.link_fieldname || params.doctype.toLowerCase().replace(/ /g, "_"); + const result = await client.listDocuments(params.link_doctype, { + fields: ["name", "status", "creation", "modified"], + filters: [[params.link_doctype, fieldname, "=", params.name]], + limit_page_length: 50, + }); + return { content: [{ type: "text", text: `Found ${result.total} linked ${params.link_doctype}:\n${JSON.stringify(result.items, null, 2)}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 92. Get Print Formats ───────────────────────────── + server.registerTool( + "erpnext_get_print_formats", + { + title: "Get Print Formats", + description: `List available print formats for a DocType.`, + inputSchema: z.object({ doctype: z.string() }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.listDocuments("Print Format", { + fields: ["name", "doc_type", "standard", "disabled"], + filters: [["Print Format", "doc_type", "=", params.doctype]], + limit_page_length: 50, + }); + return { content: [{ type: "text", text: JSON.stringify(result.items, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 93. Attach File to Document ─────────────────────── + server.registerTool( + "erpnext_attach_file", + { + title: "Attach File to Document", + description: `Attach a file (by URL) to a document.`, + inputSchema: z.object({ + doctype: z.string(), + docname: z.string(), + filename: z.string(), + fileurl: z.string().describe("Public URL of the file to attach"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("frappe.client.attach_file", { + doctype: params.doctype, + docname: params.docname, + filename: params.filename, + fileurl: params.fileurl, + }); + return { content: [{ type: "text", text: `File attached: ${JSON.stringify(result)}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 94-100+. Quick access convenience tools ────────── + server.registerTool( + "erpnext_get_email_digest", + { + title: "Get Email Digest", + description: `Get email digest content.`, + inputSchema: z.object({ name: z.string().describe("Email Digest name") }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.setup.doctype.email_digest.email_digest.get_digest_msg", + params + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + server.registerTool( + "erpnext_get_warehouse_tree", + { + title: "Get Warehouse Tree", + description: `Get warehouse hierarchy tree.`, + inputSchema: z.object({ + parent: z.string().optional(), + company: z.string().optional(), + is_root: z.boolean().default(false), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.warehouse.warehouse.get_children", + { doctype: "Warehouse", ...params } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + server.registerTool( + "erpnext_make_delivery_note_from_dn_return", + { + title: "Make Sales Return from Delivery Note", + description: `Create a sales return (credit note) from a Delivery Note.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_return", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + server.registerTool( + "erpnext_make_purchase_return", + { + title: "Make Purchase Return from Receipt", + description: `Create a purchase return from a Purchase Receipt.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_return", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + server.registerTool( + "erpnext_make_stock_entry_from_mr", + { + title: "Make Stock Entry from Material Request", + description: `Create a Stock Entry (Material Transfer) from a Material Request.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.material_request.material_request.make_stock_entry", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + server.registerTool( + "erpnext_update_material_request_status", + { + title: "Update Material Request Status", + description: `Update the status of a Material Request.`, + inputSchema: z.object({ + name: z.string(), + status: z.string().describe('"Stopped", "Cancelled", etc.'), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod( + "erpnext.stock.doctype.material_request.material_request.update_status", + params + ); + return { content: [{ type: "text", text: `Material Request ${params.name} status set to ${params.status}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); +}