@casys/mcp-erpnext 2.4.0 → 2.4.1

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
@@ -39517,6 +39517,7 @@ var hrTools = [
39517
39517
 
39518
39518
  // src/tools/assignment.ts
39519
39519
  var ASSIGNMENT_METHOD = "frappe.desk.form.assign_to.add";
39520
+ var UNASSIGNMENT_METHOD = "frappe.desk.form.assign_to.remove";
39520
39521
  var MAX_ASSIGNEES_PER_CALL = 50;
39521
39522
  var ASSIGNMENT_INPUT_PROPERTIES = {
39522
39523
  assign_to: {
@@ -39640,13 +39641,31 @@ async function applyAssignment(doctype, name, assignment, ctx, failureContext) {
39640
39641
  }) : [];
39641
39642
  return { notify_user: true, assignees: assignment.assignees, todos };
39642
39643
  }
39643
- async function fetchDocAfterAssignment(doctype, name, ctx, toolName) {
39644
+ async function removeAssignment(doctype, name, assignee, ctx, failureContext) {
39645
+ let nativeResult;
39646
+ try {
39647
+ nativeResult = await ctx.client.callMethod(UNASSIGNMENT_METHOD, {
39648
+ doctype,
39649
+ name,
39650
+ assign_to: assignee
39651
+ });
39652
+ } catch (error2) {
39653
+ const reason = error2 instanceof Error ? error2.message : String(error2);
39654
+ throw new Error(`${failureContext}: ${reason}`, { cause: error2 });
39655
+ }
39656
+ const todos = Array.isArray(nativeResult) ? nativeResult.map((todo) => {
39657
+ const record2 = todo;
39658
+ return { owner: record2.owner, name: record2.name };
39659
+ }) : [];
39660
+ return { removed: assignee, remaining: todos };
39661
+ }
39662
+ async function fetchDocAfterAssignment(doctype, name, ctx, toolName, action = "assignment") {
39644
39663
  try {
39645
39664
  return await ctx.client.get(doctype, name);
39646
39665
  } catch (error2) {
39647
39666
  const reason = error2 instanceof Error ? error2.message : String(error2);
39648
39667
  throw new Error(
39649
- `[${toolName}] ${doctype} ${name} assignment succeeded, but re-fetching the document failed: ${reason}`,
39668
+ `[${toolName}] ${doctype} ${name} ${action} succeeded, but re-fetching the document failed: ${reason}`,
39650
39669
  { cause: error2 }
39651
39670
  );
39652
39671
  }
@@ -42168,6 +42187,66 @@ var operationsTools = [
42168
42187
  assignment: assignmentInfo
42169
42188
  };
42170
42189
  }
42190
+ },
42191
+ // ── Generic Unassign ──────────────────────────────────────────────────────
42192
+ {
42193
+ name: "erpnext_doc_unassign",
42194
+ 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.",
42195
+ category: "operations",
42196
+ inputSchema: {
42197
+ type: "object",
42198
+ properties: {
42199
+ doctype: {
42200
+ type: "string",
42201
+ description: "ERPNext DocType name (e.g. 'Task', 'Issue', 'Opportunity')"
42202
+ },
42203
+ name: {
42204
+ type: "string",
42205
+ description: "Document name/ID (e.g. 'TASK-2026-00001')"
42206
+ },
42207
+ assign_to: {
42208
+ type: "string",
42209
+ description: "User email whose assignment should be removed",
42210
+ minLength: 1
42211
+ }
42212
+ },
42213
+ required: ["doctype", "name", "assign_to"]
42214
+ },
42215
+ handler: async (input, ctx) => {
42216
+ if (!input.doctype) {
42217
+ throw new Error("[erpnext_doc_unassign] 'doctype' is required");
42218
+ }
42219
+ if (!input.name) {
42220
+ throw new Error("[erpnext_doc_unassign] 'name' is required");
42221
+ }
42222
+ if (typeof input.assign_to !== "string" || !input.assign_to.trim()) {
42223
+ throw new Error(
42224
+ "[erpnext_doc_unassign] 'assign_to' must be a non-empty user email"
42225
+ );
42226
+ }
42227
+ const doctype = input.doctype;
42228
+ const name = input.name;
42229
+ const assignee = input.assign_to.trim();
42230
+ const unassignment = await removeAssignment(
42231
+ doctype,
42232
+ name,
42233
+ assignee,
42234
+ ctx,
42235
+ `[erpnext_doc_unassign] ${doctype} ${name} unassignment failed`
42236
+ );
42237
+ const doc = await fetchDocAfterAssignment(
42238
+ doctype,
42239
+ name,
42240
+ ctx,
42241
+ "erpnext_doc_unassign",
42242
+ "unassignment"
42243
+ );
42244
+ return {
42245
+ data: doc,
42246
+ message: `${assignee} unassigned from ${doctype} ${name}`,
42247
+ assignment: unassignment
42248
+ };
42249
+ }
42171
42250
  }
42172
42251
  ];
42173
42252
 
@@ -45265,7 +45344,7 @@ async function main() {
45265
45344
  );
45266
45345
  const server = new McpApp({
45267
45346
  name: "mcp-erpnext",
45268
- version: "2.4.0",
45347
+ version: "2.4.1",
45269
45348
  maxConcurrent: 10,
45270
45349
  backpressureStrategy: "queue",
45271
45350
  validateSchema: true,
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.1",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {