diff --git a/mcp-gateway/erpnext-mcp/src/tools/core.ts b/mcp-gateway/erpnext-mcp/src/tools/core.ts deleted file mode 100644 index b762255..0000000 --- a/mcp-gateway/erpnext-mcp/src/tools/core.ts +++ /dev/null @@ -1,454 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { ERPNextClient, truncateResponse, formatDocumentMarkdown, formatListMarkdown } from "../services/erpnext-client.js"; -import { - ListDocumentsSchema, - GetDocumentSchema, - CreateDocumentSchema, - UpdateDocumentSchema, - DeleteDocumentSchema, - SubmitDocumentSchema, - CancelDocumentSchema, - SearchLinkSchema, - GetCountSchema, - CallMethodSchema, - GetDocTypeMetaSchema, - RunReportSchema, - BulkUpdateSchema, - WorkflowActionSchema, -} from "../schemas/common.js"; - -export function registerCoreTools(server: McpServer, client: ERPNextClient): void { - - // ─── 1. List Documents ───────────────────────────────── - server.registerTool( - "erpnext_list_documents", - { - title: "List ERPNext Documents", - description: `List documents of any DocType with filtering, sorting, pagination and field selection. - -Args: - - doctype: DocType name (e.g. "Sales Order", "Customer", "Item") - - fields: Array of field names to return (default: ["name"]) - - filters: Array of [field, operator, value] tuples (e.g. [["status","=","Open"]]) - - order_by: Sort string (e.g. "modified desc") - - group_by: Group results by field - - limit: Max results (1-100, default 20) - - offset: Skip N results for pagination - -Returns: Paginated list with total count, items array, and pagination metadata. - -Examples: - - List open sales orders: doctype="Sales Order", filters=[["status","=","To Deliver and Bill"]] - - List customers: doctype="Customer", fields=["name","customer_name","territory"] - - List items by group: doctype="Item", filters=[["item_group","=","Products"]]`, - inputSchema: ListDocumentsSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const result = await client.listDocuments(params.doctype, { - fields: params.fields, - filters: params.filters?.map(([f, op, val]) => [params.doctype, f, op, val as string]), - order_by: params.order_by, - group_by: params.group_by, - limit_start: params.offset, - limit_page_length: params.limit, - }); - - const text = formatListMarkdown(result.items, params.doctype, result); - return { content: [{ type: "text", text: truncateResponse(text) }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 2. Get Document ─────────────────────────────────── - server.registerTool( - "erpnext_get_document", - { - title: "Get ERPNext Document", - description: `Retrieve a single document by DocType and name with all or selected fields. - -Args: - - doctype: DocType name - - name: Document name/ID - - fields: Optional array of specific fields to return - -Returns: Full document data including child tables.`, - inputSchema: GetDocumentSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const doc = await client.getDocument(params.doctype, params.name, params.fields); - const text = formatDocumentMarkdown(doc, params.doctype); - return { content: [{ type: "text", text: truncateResponse(text) }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 3. Create Document ──────────────────────────────── - server.registerTool( - "erpnext_create_document", - { - title: "Create ERPNext Document", - description: `Create a new document of any DocType. - -Args: - - doctype: DocType name - - data: Object with field values. For child tables, pass array of objects under the table fieldname. - -Returns: Created document with assigned name. - -Examples: - - Create customer: doctype="Customer", data={"customer_name":"Acme Corp","customer_type":"Company","customer_group":"Commercial","territory":"All Territories"} - - Create item: doctype="Item", data={"item_code":"ITEM-001","item_name":"Widget","item_group":"Products","stock_uom":"Nos"}`, - inputSchema: CreateDocumentSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async (params) => { - try { - const doc = await client.createDocument(params.doctype, params.data); - return { content: [{ type: "text", text: `Created ${params.doctype}: ${doc.name}\n\n${formatDocumentMarkdown(doc, params.doctype)}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 4. Update Document ──────────────────────────────── - server.registerTool( - "erpnext_update_document", - { - title: "Update ERPNext Document", - description: `Update an existing document. Only pass the fields you want to change (PATCH semantics). - -Args: - - doctype: DocType name - - name: Document name/ID - - data: Object with fields to update - -Returns: Updated document.`, - inputSchema: UpdateDocumentSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const doc = await client.updateDocument(params.doctype, params.name, params.data); - return { content: [{ type: "text", text: `Updated ${params.doctype}: ${params.name}\n\n${formatDocumentMarkdown(doc, params.doctype)}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 5. Delete Document ──────────────────────────────── - server.registerTool( - "erpnext_delete_document", - { - title: "Delete ERPNext Document", - description: `Permanently delete a document. This cannot be undone. - -Args: - - doctype: DocType name - - name: Document name/ID`, - inputSchema: DeleteDocumentSchema, - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, - }, - async (params) => { - try { - await client.deleteDocument(params.doctype, params.name); - return { content: [{ type: "text", text: `Deleted ${params.doctype}: ${params.name}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 6. Submit Document ──────────────────────────────── - server.registerTool( - "erpnext_submit_document", - { - title: "Submit ERPNext Document", - description: `Submit a submittable document (sets docstatus=1). Used for Sales Orders, Purchase Orders, Invoices, Journal Entries, etc. Submitted documents are locked from editing. - -Args: - - doctype: DocType name - - name: Document name/ID`, - inputSchema: SubmitDocumentSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const doc = await client.submitDocument(params.doctype, params.name); - return { content: [{ type: "text", text: `Submitted ${params.doctype}: ${params.name}\n\n${formatDocumentMarkdown(doc, params.doctype)}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 7. Cancel Document ──────────────────────────────── - server.registerTool( - "erpnext_cancel_document", - { - title: "Cancel ERPNext Document", - description: `Cancel a submitted document (sets docstatus=2). Creates reversal entries where applicable (e.g. GL entries for invoices). - -Args: - - doctype: DocType name - - name: Document name/ID`, - inputSchema: CancelDocumentSchema, - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const doc = await client.cancelDocument(params.doctype, params.name); - return { content: [{ type: "text", text: `Cancelled ${params.doctype}: ${params.name}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 8. Search Link ──────────────────────────────────── - server.registerTool( - "erpnext_search_link", - { - title: "Search ERPNext Link", - description: `Search for documents by text — the same search used in Link fields across ERPNext. Returns name + description pairs. - -Args: - - doctype: DocType to search in - - txt: Search text - - filters: Optional additional filters - - page_length: Max results (default 20) - -Examples: - - Find customers matching "acme": doctype="Customer", txt="acme" - - Find items matching "widget": doctype="Item", txt="widget"`, - inputSchema: SearchLinkSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const results = await client.searchLink( - params.doctype, params.txt, params.filters, params.page_length - ); - const text = results.length - ? results.map((r) => `- **${r.value}** — ${r.description || ""}`).join("\n") - : `No ${params.doctype} found matching "${params.txt}"`; - return { content: [{ type: "text", text }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 9. Get Count ────────────────────────────────────── - server.registerTool( - "erpnext_get_count", - { - title: "Count ERPNext Documents", - description: `Get the total count of documents matching optional filters. - -Args: - - doctype: DocType name - - filters: Optional [field, operator, value] tuples - -Examples: - - Count open sales orders: doctype="Sales Order", filters=[["status","=","To Deliver and Bill"]] - - Count all customers: doctype="Customer"`, - inputSchema: GetCountSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const count = await client.getCount( - params.doctype, - params.filters?.map(([f, op, val]) => [params.doctype, f, op, val as string]) - ); - return { content: [{ type: "text", text: `Count of ${params.doctype}: ${count}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 10. Call Method ─────────────────────────────────── - server.registerTool( - "erpnext_call_method", - { - title: "Call ERPNext Method", - description: `Call any whitelisted ERPNext/Frappe method via the /api/method/ endpoint. This is the escape hatch for any operation not covered by the other tools. - -Args: - - method: Dotted method path (e.g. "erpnext.stock.utils.get_stock_balance") - - args: Method arguments as key-value object - -Common methods: - - frappe.client.get_value: Get a field value from a document - - frappe.client.set_value: Set a field value on a document - - frappe.client.get_list: Alternative list with more options - - erpnext.stock.utils.get_stock_balance: Get stock balance for item/warehouse - - erpnext.setup.utils.get_exchange_rate: Get currency exchange rate - - erpnext.selling.page.point_of_sale.point_of_sale.get_items: POS items - - erpnext.stock.get_item_details.get_item_details: Full item details for transactions`, - inputSchema: CallMethodSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async (params) => { - try { - const result = await client.callMethod(params.method, params.args); - const text = typeof result === "string" ? result : JSON.stringify(result, null, 2); - return { content: [{ type: "text", text: truncateResponse(text) }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 11. Get DocType Schema ──────────────────────────── - server.registerTool( - "erpnext_get_doctype_meta", - { - title: "Get DocType Schema/Meta", - description: `Get the field definitions and schema for any DocType. Useful to discover available fields before listing or creating documents. - -Args: - - doctype: DocType name - -Returns: List of fields with fieldname, fieldtype, label, options, reqd (required), read_only, hidden.`, - inputSchema: GetDocTypeMetaSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const meta = await client.getDocTypeMeta(params.doctype); - const text = JSON.stringify(meta, null, 2); - return { content: [{ type: "text", text: truncateResponse(text) }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 12. Run Report ──────────────────────────────────── - server.registerTool( - "erpnext_run_report", - { - title: "Run ERPNext Report", - description: `Run a Script Report or Query Report and return the data. - -Args: - - report_name: Name of the report (e.g. "General Ledger", "Stock Balance", "Accounts Receivable") - - filters: Report filter parameters as key-value object - -Examples: - - General Ledger: report_name="General Ledger", filters={"company":"My Company","from_date":"2024-01-01","to_date":"2024-12-31"} - - Stock Balance: report_name="Stock Balance", filters={"company":"My Company"} - - Accounts Receivable: report_name="Accounts Receivable", filters={"company":"My Company"}`, - inputSchema: RunReportSchema, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const result = await client.runReport(params.report_name, params.filters); - const text = JSON.stringify(result, null, 2); - return { content: [{ type: "text", text: truncateResponse(text) }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 13. List DocTypes ───────────────────────────────── - server.registerTool( - "erpnext_list_doctypes", - { - title: "List Available DocTypes", - description: `List all available DocTypes in the ERPNext instance. Useful to discover what entities exist. - -Returns: Array of DocType names sorted alphabetically.`, - inputSchema: {}, - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async () => { - try { - const doctypes = await client.getDocTypeList(); - return { content: [{ type: "text", text: `Available DocTypes (${doctypes.length}):\n\n${doctypes.join("\n")}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 14. Bulk Update ─────────────────────────────────── - server.registerTool( - "erpnext_bulk_update", - { - title: "Bulk Update ERPNext Documents", - description: `Update multiple documents of the same DocType with the same field values. - -Args: - - doctype: DocType name - - names: Array of document names to update - - data: Field values to set on all documents - -Example: Set status on multiple issues: doctype="Issue", names=["ISS-001","ISS-002"], data={"status":"Closed"}`, - inputSchema: BulkUpdateSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async (params) => { - try { - const results: string[] = []; - const errors: string[] = []; - for (const name of params.names) { - try { - await client.updateDocument(params.doctype, name, params.data); - results.push(name); - } catch (error) { - errors.push(`${name}: ${(error as Error).message}`); - } - } - let text = `Updated ${results.length}/${params.names.length} ${params.doctype} documents.`; - if (errors.length) { - text += `\n\nErrors:\n${errors.join("\n")}`; - } - return { content: [{ type: "text", text }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); - - // ─── 15. Workflow Action ─────────────────────────────── - server.registerTool( - "erpnext_workflow_action", - { - title: "Apply Workflow Action", - description: `Apply a workflow action to a document that has a workflow configured. - -Args: - - doctype: DocType name - - name: Document name - - action: Workflow action name (e.g. "Approve", "Reject", "Submit")`, - inputSchema: WorkflowActionSchema, - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async (params) => { - try { - const result = await client.callMethod("frappe.model.workflow.apply_workflow", { - doc: { doctype: params.doctype, name: params.name }, - action: params.action, - }); - return { content: [{ type: "text", text: `Applied workflow action "${params.action}" on ${params.doctype}: ${params.name}` }] }; - } catch (error) { - return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; - } - } - ); -}