@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 CHANGED
@@ -6,7 +6,7 @@
6
6
  [![MCP](https://img.shields.io/badge/MCP-server-1f6feb?logo=modelcontextprotocol&logoColor=white)](https://modelcontextprotocol.io)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
 
9
- MCP server for [ERPNext](https://erpnext.com) / Frappe ERP — **122 tools**
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 (122)
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** | 8 | doclist | Generic CRUD + native assignment for **any** DocType (`erpnext_doc_*`) |
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 fetchDocAfterAssignment(doctype, name, ctx, toolName) {
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} assignment succeeded, but re-fetching the document failed: ${reason}`,
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] : "0.0.0.0";
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.0",
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casys/mcp-erpnext",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {