Add mcp-gateway/erpnext-mcp/src/tools/frappe.ts

This commit is contained in:
Zac Gaetano 2026-03-31 15:29:34 -04:00
parent f8217393c3
commit 2c7f16e555

View file

@ -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<string, unknown> = {};
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}` }] };
}
}
);
}