diff --git a/erpnext-mcp/src/tools/domains.ts b/erpnext-mcp/src/tools/domains.ts new file mode 100644 index 0000000..d2f95de --- /dev/null +++ b/erpnext-mcp/src/tools/domains.ts @@ -0,0 +1,533 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ERPNextClient, truncateResponse } from "../services/erpnext-client.js"; +import { z } from "zod"; + +export function registerManufacturingTools(server: McpServer, client: ERPNextClient): void { + + // ─── 51. Get Work Order Details ──────────────────────── + server.registerTool( + "erpnext_get_work_order_details", + { + title: "Get Work Order Details for Stock Entry", + description: `Get work order details needed to create a stock entry (manufacture).`, + inputSchema: z.object({ work_order: z.string(), company: z.string() }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.stock_entry.stock_entry.get_work_order_details", + 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}` }] }; + } + } + ); + + // ─── 52. Make Work Order from SO ─────────────────────── + server.registerTool( + "erpnext_make_work_order_from_so", + { + title: "Create Work Orders from Sales Order", + description: `Create Work Orders for items in a Sales Order that have BOMs.`, + inputSchema: z.object({ + sales_order: z.string(), + items: z.array(z.object({ + item_code: z.string(), + bom: z.string().optional(), + warehouse: z.string().optional(), + qty: z.number().optional(), + })).optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.doctype.sales_order.sales_order.make_work_orders", + { items: JSON.stringify(params.items ?? []), sales_order: params.sales_order } + ); + 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}` }] }; + } + } + ); + + // ─── 53. Get Default BOM ─────────────────────────────── + server.registerTool( + "erpnext_get_default_bom", + { + title: "Get Default BOM for Item", + description: `Get the default Bill of Materials for an item.`, + inputSchema: z.object({ item_code: z.string() }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.get_item_details.get_default_bom", params); + return { content: [{ type: "text", text: `Default BOM for ${params.item_code}: ${result}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); +} + +export function registerHRTools(server: McpServer, client: ERPNextClient): void { + + // ─── 54. Get Employee Org Chart ──────────────────────── + server.registerTool( + "erpnext_get_org_chart", + { + title: "Get Organization Chart", + description: `Get organizational hierarchy chart data.`, + inputSchema: z.object({ + company: z.string(), + method: z.string().default("erpnext.utilities.hierarchy_chart.get_all_nodes"), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod(params.method, { company: params.company }); + 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}` }] }; + } + } + ); + + // ─── 55. Create Employee ─────────────────────────────── + server.registerTool( + "erpnext_create_employee", + { + title: "Create Employee", + description: `Create a new Employee record.`, + inputSchema: z.object({ + first_name: z.string(), + last_name: z.string().optional(), + company: z.string(), + gender: z.string().optional(), + date_of_birth: z.string().optional(), + date_of_joining: z.string().optional(), + designation: z.string().optional(), + department: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Employee", params); + return { content: [{ type: "text", text: `Created Employee: ${doc.name} (${params.first_name} ${params.last_name || ""})` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 56. Create User from Employee ───────────────────── + server.registerTool( + "erpnext_create_user_from_employee", + { + title: "Create User from Employee", + description: `Create a system user account from an Employee record.`, + inputSchema: z.object({ + employee: z.string(), + user: z.string().optional(), + email: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.setup.doctype.employee.employee.create_user", params); + return { content: [{ type: "text", text: `User created: ${JSON.stringify(result)}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 57. Get Department Tree ─────────────────────────── + server.registerTool( + "erpnext_get_department_tree", + { + title: "Get Department Tree", + description: `Get department 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.setup.doctype.department.department.get_children", + { doctype: "Department", ...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}` }] }; + } + } + ); +} + +export function registerCRMTools(server: McpServer, client: ERPNextClient): void { + + // ─── 58. Create Lead ─────────────────────────────────── + server.registerTool( + "erpnext_create_lead", + { + title: "Create Lead", + description: `Create a new Lead record.`, + inputSchema: z.object({ + lead_name: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + company_name: z.string().optional(), + email_id: z.string().optional(), + mobile_no: z.string().optional(), + source: z.string().optional(), + territory: z.string().optional(), + status: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Lead", params); + return { content: [{ type: "text", text: `Created Lead: ${doc.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 59. Create Opportunity ──────────────────────────── + server.registerTool( + "erpnext_create_opportunity", + { + title: "Create Opportunity", + description: `Create a new Opportunity from a Lead or Customer.`, + inputSchema: z.object({ + opportunity_from: z.enum(["Lead", "Customer"]), + party_name: z.string(), + company: z.string(), + opportunity_type: z.string().optional(), + status: z.string().optional(), + expected_closing: z.string().optional(), + probability: z.number().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Opportunity", params); + return { content: [{ type: "text", text: `Created Opportunity: ${doc.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 60. Get Opportunities by Lead Source ────────────── + server.registerTool( + "erpnext_get_opportunities_by_source", + { + title: "Get Opportunities by Lead Source", + description: `Get opportunities grouped by lead source for analytics.`, + inputSchema: z.object({ + from_date: z.string(), + to_date: z.string(), + company: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source", + 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}` }] }; + } + } + ); +} + +export function registerProjectTools(server: McpServer, client: ERPNextClient): void { + + // ─── 61. Create Task ─────────────────────────────────── + server.registerTool( + "erpnext_create_task", + { + title: "Create Task", + description: `Create a new Task, optionally linked to a Project.`, + inputSchema: z.object({ + subject: z.string(), + project: z.string().optional(), + status: z.string().default("Open"), + priority: z.string().optional(), + description: z.string().optional(), + exp_start_date: z.string().optional(), + exp_end_date: z.string().optional(), + assigned_to: z.string().optional(), + parent_task: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const data: Record = { ...params }; + if (params.assigned_to) { + delete data.assigned_to; + } + const doc = await client.createDocument("Task", data); + if (params.assigned_to) { + await client.callMethod("frappe.desk.form.assign_to.add", { + doctype: "Task", + name: doc.name, + assign_to: [params.assigned_to], + }); + } + return { content: [{ type: "text", text: `Created Task: ${doc.name} — ${params.subject}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 62. Create Project ──────────────────────────────── + server.registerTool( + "erpnext_create_project", + { + title: "Create Project", + description: `Create a new Project.`, + inputSchema: z.object({ + project_name: z.string(), + company: z.string(), + status: z.string().default("Open"), + expected_start_date: z.string().optional(), + expected_end_date: z.string().optional(), + priority: z.string().optional(), + project_type: z.string().optional(), + is_active: z.string().default("Yes"), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Project", params); + return { content: [{ type: "text", text: `Created Project: ${doc.name}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 63. Get Project Tasks HTML ──────────────────────── + server.registerTool( + "erpnext_get_project_tasks", + { + title: "Get Project Tasks", + description: `Get tasks for a project.`, + inputSchema: z.object({ + project: z.string(), + start: z.number().default(0), + item_status: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.templates.pages.projects.get_task_html", + 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}` }] }; + } + } + ); +} + +export function registerSupportTools(server: McpServer, client: ERPNextClient): void { + + // ─── 64. Set Issue Status ────────────────────────────── + server.registerTool( + "erpnext_set_issue_status", + { + title: "Set Issue Status", + description: `Set the status of a support Issue.`, + inputSchema: z.object({ + name: z.string(), + status: z.string().describe('Status: "Open", "Replied", "Resolved", "Closed"'), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("erpnext.support.doctype.issue.issue.set_status", params); + return { content: [{ type: "text", text: `Issue ${params.name} status set to ${params.status}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 65. Bulk Set Issue Status ───────────────────────── + server.registerTool( + "erpnext_bulk_set_issue_status", + { + title: "Bulk Set Issue Status", + description: `Set the status of multiple Issues at once.`, + inputSchema: z.object({ + names: z.array(z.string()), + status: z.string(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + await client.callMethod("erpnext.support.doctype.issue.issue.set_multiple_status", { + names: JSON.stringify(params.names), + status: params.status, + }); + return { content: [{ type: "text", text: `Updated ${params.names.length} issues to ${params.status}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 66. Make Task from Issue ────────────────────────── + server.registerTool( + "erpnext_make_task_from_issue", + { + title: "Make Task from Issue", + description: `Create a Task from a support Issue.`, + 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.support.doctype.issue.issue.make_task", + { 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}` }] }; + } + } + ); +} + +export function registerSetupTools(server: McpServer, client: ERPNextClient): void { + + // ─── 67. Get Company Tree ────────────────────────────── + server.registerTool( + "erpnext_get_company_tree", + { + title: "Get Company Tree", + description: `Get company hierarchy tree.`, + inputSchema: z.object({ + parent: 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.setup.doctype.company.company.get_children", + { doctype: "Company", ...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}` }] }; + } + } + ); + + // ─── 68. Get Default Company Address ─────────────────── + server.registerTool( + "erpnext_get_default_company_address", + { + title: "Get Default Company Address", + description: `Get the default/primary address for a company.`, + inputSchema: z.object({ name: z.string().describe("Company name") }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.setup.doctype.company.company.get_default_company_address", + 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}` }] }; + } + } + ); + + // ─── 69. Get Terms and Conditions ────────────────────── + server.registerTool( + "erpnext_get_terms_and_conditions", + { + title: "Get Terms and Conditions", + description: `Get rendered terms and conditions template for a document.`, + inputSchema: z.object({ + template_name: z.string(), + doc: z.record(z.unknown()).describe("Document data for template rendering"), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.setup.doctype.terms_and_conditions.terms_and_conditions.get_terms_and_conditions", + params + ); + return { content: [{ type: "text", text: String(result) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 70. Get Holiday List Events ─────────────────────── + server.registerTool( + "erpnext_get_holidays", + { + title: "Get Holiday Events", + description: `Get holidays from a holiday list for a date range.`, + inputSchema: z.object({ + start: z.string().describe("Start date (YYYY-MM-DD)"), + end: z.string().describe("End date (YYYY-MM-DD)"), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.setup.doctype.holiday_list.holiday_list.get_events", + 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}` }] }; + } + } + ); +}