Add mcp-gateway/erpnext-mcp/src/tools/core.ts
This commit is contained in:
parent
0593eb997c
commit
91f11a0911
1 changed files with 454 additions and 0 deletions
454
mcp-gateway/erpnext-mcp/src/tools/core.ts
Normal file
454
mcp-gateway/erpnext-mcp/src/tools/core.ts
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
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}` }] };
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue