@casys/mcp-erpnext 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/mcp-erpnext.mjs +101 -4
- package/package.json +1 -1
- package/ui-dist/kanban-viewer/index.html +46 -46
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://modelcontextprotocol.io)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
MCP server for [ERPNext](https://erpnext.com) / Frappe ERP — **
|
|
9
|
+
MCP server for [ERPNext](https://erpnext.com) / Frappe ERP — **123 tools**
|
|
10
10
|
across **14 categories**, with **7 interactive UI viewers**.
|
|
11
11
|
|
|
12
12
|
Connect any MCP-compatible AI agent (Claude Desktop, Claude Code, VS Code
|
|
@@ -151,6 +151,21 @@ ERPNEXT_API_SECRET=xxx \
|
|
|
151
151
|
npx -y @casys/mcp-erpnext --http --port=3012
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
### Deno (HTTP mode)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
ERPNEXT_URL=http://localhost:8000 \
|
|
158
|
+
ERPNEXT_API_KEY=xxx \
|
|
159
|
+
ERPNEXT_API_SECRET=xxx \
|
|
160
|
+
deno run -A npm:@casys/mcp-erpnext --http --port=3012
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> **Note:** Versions ≤ 2.3.1 of the npm bundle crashed with
|
|
164
|
+
> `ReferenceError: Deno is not defined` in HTTP mode — fixed in 2.4.0
|
|
165
|
+
> (`@casys/mcp-server` ≥ 0.21.1). If you hit this error, upgrade with
|
|
166
|
+
> `npx -y @casys/mcp-erpnext@latest`, or use the Deno runner above. See
|
|
167
|
+
> [`docs/known-issues.md`](docs/known-issues.md).
|
|
168
|
+
|
|
154
169
|
### Category filtering
|
|
155
170
|
|
|
156
171
|
Load only the categories you need:
|
|
@@ -220,7 +235,7 @@ npm install
|
|
|
220
235
|
node build-all.mjs
|
|
221
236
|
```
|
|
222
237
|
|
|
223
|
-
## Tools (
|
|
238
|
+
## Tools (123)
|
|
224
239
|
|
|
225
240
|
**14 categories** covering the full ERPNext surface. Each `_list` tool returns
|
|
226
241
|
interactive results in the doclist-viewer with row click, inline detail, and
|
|
@@ -238,7 +253,7 @@ cross-viewer navigation.
|
|
|
238
253
|
| **Manufacturing** | 7 | doclist | BOMs, Work Orders, Job Cards |
|
|
239
254
|
| **CRM** | 8 | doclist | Leads, Opportunities, Contacts, Campaigns |
|
|
240
255
|
| **Assets** | 8 | doclist | Assets, Movements, Maintenance, Categories |
|
|
241
|
-
| **Operations** |
|
|
256
|
+
| **Operations** | 9 | doclist | Generic CRUD + native assignment for **any** DocType (`erpnext_doc_*`) |
|
|
242
257
|
| **Kanban** | 2 | kanban | Task/Opportunity/Issue boards with drag-and-drop |
|
|
243
258
|
| **Analytics** | 17 | chart / kpi / funnel | 12 chart types, 5 KPIs, sales funnel |
|
|
244
259
|
| **Setup** | 3 | — | Company creation, assignable user listing |
|
package/mcp-erpnext.mjs
CHANGED
|
@@ -3775,6 +3775,7 @@ var require_fast_uri = __commonJS({
|
|
|
3775
3775
|
return uriTokens.join("");
|
|
3776
3776
|
}
|
|
3777
3777
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
3778
|
+
var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
|
|
3778
3779
|
function getParseError(parsed, matches) {
|
|
3779
3780
|
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
3780
3781
|
return 'URI path must start with "/" when authority is present.';
|
|
@@ -3804,6 +3805,11 @@ var require_fast_uri = __commonJS({
|
|
|
3804
3805
|
uri = "//" + uri;
|
|
3805
3806
|
}
|
|
3806
3807
|
}
|
|
3808
|
+
const authorityMatch = uri.match(AUTHORITY_PREFIX);
|
|
3809
|
+
if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
|
|
3810
|
+
parsed.error = "URI authority must not contain a literal backslash.";
|
|
3811
|
+
malformedAuthorityOrPort = true;
|
|
3812
|
+
}
|
|
3807
3813
|
const matches = uri.match(URI_PARSE);
|
|
3808
3814
|
if (matches) {
|
|
3809
3815
|
parsed.scheme = matches[1];
|
|
@@ -29680,6 +29686,12 @@ var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) =
|
|
|
29680
29686
|
return {};
|
|
29681
29687
|
};
|
|
29682
29688
|
async function parseFormData(request, options) {
|
|
29689
|
+
if (!isRawRequest(request) && request.bodyCache.formData) {
|
|
29690
|
+
return convertFormDataToBodyData(
|
|
29691
|
+
await request.bodyCache.formData,
|
|
29692
|
+
options
|
|
29693
|
+
);
|
|
29694
|
+
}
|
|
29683
29695
|
const headers = isRawRequest(request) ? request.headers : request.raw.headers;
|
|
29684
29696
|
const arrayBuffer = await request.arrayBuffer();
|
|
29685
29697
|
const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || "");
|
|
@@ -39517,6 +39529,7 @@ var hrTools = [
|
|
|
39517
39529
|
|
|
39518
39530
|
// src/tools/assignment.ts
|
|
39519
39531
|
var ASSIGNMENT_METHOD = "frappe.desk.form.assign_to.add";
|
|
39532
|
+
var UNASSIGNMENT_METHOD = "frappe.desk.form.assign_to.remove";
|
|
39520
39533
|
var MAX_ASSIGNEES_PER_CALL = 50;
|
|
39521
39534
|
var ASSIGNMENT_INPUT_PROPERTIES = {
|
|
39522
39535
|
assign_to: {
|
|
@@ -39640,13 +39653,31 @@ async function applyAssignment(doctype, name, assignment, ctx, failureContext) {
|
|
|
39640
39653
|
}) : [];
|
|
39641
39654
|
return { notify_user: true, assignees: assignment.assignees, todos };
|
|
39642
39655
|
}
|
|
39643
|
-
async function
|
|
39656
|
+
async function removeAssignment(doctype, name, assignee, ctx, failureContext) {
|
|
39657
|
+
let nativeResult;
|
|
39658
|
+
try {
|
|
39659
|
+
nativeResult = await ctx.client.callMethod(UNASSIGNMENT_METHOD, {
|
|
39660
|
+
doctype,
|
|
39661
|
+
name,
|
|
39662
|
+
assign_to: assignee
|
|
39663
|
+
});
|
|
39664
|
+
} catch (error2) {
|
|
39665
|
+
const reason = error2 instanceof Error ? error2.message : String(error2);
|
|
39666
|
+
throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
|
|
39667
|
+
}
|
|
39668
|
+
const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
|
|
39669
|
+
const record2 = todo;
|
|
39670
|
+
return { owner: record2.owner, name: record2.name };
|
|
39671
|
+
}) : [];
|
|
39672
|
+
return { removed: assignee, remaining: todos };
|
|
39673
|
+
}
|
|
39674
|
+
async function fetchDocAfterAssignment(doctype, name, ctx, toolName, action = "assignment") {
|
|
39644
39675
|
try {
|
|
39645
39676
|
return await ctx.client.get(doctype, name);
|
|
39646
39677
|
} catch (error2) {
|
|
39647
39678
|
const reason = error2 instanceof Error ? error2.message : String(error2);
|
|
39648
39679
|
throw new Error(
|
|
39649
|
-
`[${toolName}] ${doctype} ${name}
|
|
39680
|
+
`[${toolName}] ${doctype} ${name} ${action} succeeded, but re-fetching the document failed: ${reason}`,
|
|
39650
39681
|
{ cause: error2 }
|
|
39651
39682
|
);
|
|
39652
39683
|
}
|
|
@@ -42168,6 +42199,66 @@ var operationsTools = [
|
|
|
42168
42199
|
assignment: assignmentInfo
|
|
42169
42200
|
};
|
|
42170
42201
|
}
|
|
42202
|
+
},
|
|
42203
|
+
// ── Generic Unassign ──────────────────────────────────────────────────────
|
|
42204
|
+
{
|
|
42205
|
+
name: "erpnext_doc_unassign",
|
|
42206
|
+
description: "Remove one user's assignment from any ERPNext document through Frappe's native workflow (closes the user's ToDo and resyncs _assign). Works on any DocType. Pass one user per call. Idempotent: removing a user who is not assigned is a no-op on the Frappe side.",
|
|
42207
|
+
category: "operations",
|
|
42208
|
+
inputSchema: {
|
|
42209
|
+
type: "object",
|
|
42210
|
+
properties: {
|
|
42211
|
+
doctype: {
|
|
42212
|
+
type: "string",
|
|
42213
|
+
description: "ERPNext DocType name (e.g. 'Task', 'Issue', 'Opportunity')"
|
|
42214
|
+
},
|
|
42215
|
+
name: {
|
|
42216
|
+
type: "string",
|
|
42217
|
+
description: "Document name/ID (e.g. 'TASK-2026-00001')"
|
|
42218
|
+
},
|
|
42219
|
+
assign_to: {
|
|
42220
|
+
type: "string",
|
|
42221
|
+
description: "User email whose assignment should be removed",
|
|
42222
|
+
minLength: 1
|
|
42223
|
+
}
|
|
42224
|
+
},
|
|
42225
|
+
required: ["doctype", "name", "assign_to"]
|
|
42226
|
+
},
|
|
42227
|
+
handler: async (input, ctx) => {
|
|
42228
|
+
if (!input.doctype) {
|
|
42229
|
+
throw new Error("[erpnext_doc_unassign] 'doctype' is required");
|
|
42230
|
+
}
|
|
42231
|
+
if (!input.name) {
|
|
42232
|
+
throw new Error("[erpnext_doc_unassign] 'name' is required");
|
|
42233
|
+
}
|
|
42234
|
+
if (typeof input.assign_to !== "string" || !input.assign_to.trim()) {
|
|
42235
|
+
throw new Error(
|
|
42236
|
+
"[erpnext_doc_unassign] 'assign_to' must be a non-empty user email"
|
|
42237
|
+
);
|
|
42238
|
+
}
|
|
42239
|
+
const doctype = input.doctype;
|
|
42240
|
+
const name = input.name;
|
|
42241
|
+
const assignee = input.assign_to.trim();
|
|
42242
|
+
const unassignment = await removeAssignment(
|
|
42243
|
+
doctype,
|
|
42244
|
+
name,
|
|
42245
|
+
assignee,
|
|
42246
|
+
ctx,
|
|
42247
|
+
`[erpnext_doc_unassign] ${doctype} ${name} unassignment failed`
|
|
42248
|
+
);
|
|
42249
|
+
const doc = await fetchDocAfterAssignment(
|
|
42250
|
+
doctype,
|
|
42251
|
+
name,
|
|
42252
|
+
ctx,
|
|
42253
|
+
"erpnext_doc_unassign",
|
|
42254
|
+
"unassignment"
|
|
42255
|
+
);
|
|
42256
|
+
return {
|
|
42257
|
+
data: doc,
|
|
42258
|
+
message: `${assignee} unassigned from ${doctype} ${name}`,
|
|
42259
|
+
assignment: unassignment
|
|
42260
|
+
};
|
|
42261
|
+
}
|
|
42171
42262
|
}
|
|
42172
42263
|
];
|
|
42173
42264
|
|
|
@@ -45259,13 +45350,13 @@ async function main() {
|
|
|
45259
45350
|
const portArg = args.find((arg) => arg.startsWith("--port="));
|
|
45260
45351
|
const httpPort = portArg ? parseInt(portArg.split("=")[1], 10) : DEFAULT_HTTP_PORT;
|
|
45261
45352
|
const hostnameArg = args.find((arg) => arg.startsWith("--hostname="));
|
|
45262
|
-
const hostname = hostnameArg ? hostnameArg.split("=")[1] : "
|
|
45353
|
+
const hostname = hostnameArg ? hostnameArg.split("=")[1] : "127.0.0.1";
|
|
45263
45354
|
const toolsClient = new ErpNextToolsClient(
|
|
45264
45355
|
categories ? { categories } : void 0
|
|
45265
45356
|
);
|
|
45266
45357
|
const server = new McpApp({
|
|
45267
45358
|
name: "mcp-erpnext",
|
|
45268
|
-
version: "2.4.
|
|
45359
|
+
version: "2.4.2",
|
|
45269
45360
|
maxConcurrent: 10,
|
|
45270
45361
|
backpressureStrategy: "queue",
|
|
45271
45362
|
validateSchema: true,
|
|
@@ -45311,6 +45402,12 @@ async function main() {
|
|
|
45311
45402
|
`[mcp-erpnext] Initialized \u2014 ${toolsClient.count} tools${categories ? ` (categories: ${categories.join(", ")})` : ""}`
|
|
45312
45403
|
);
|
|
45313
45404
|
if (httpFlag) {
|
|
45405
|
+
const isLoopback = hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
|
|
45406
|
+
if (!isLoopback) {
|
|
45407
|
+
console.error(
|
|
45408
|
+
`[mcp-erpnext] WARNING: binding to ${hostname} exposes the HTTP server to the network. Every tool acts with the server's ERPNext API key, so restrict access (firewall, private network, or an authenticating reverse proxy).`
|
|
45409
|
+
);
|
|
45410
|
+
}
|
|
45314
45411
|
await server.startHttp({
|
|
45315
45412
|
port: httpPort,
|
|
45316
45413
|
hostname,
|