@casys/mcp-erpnext 2.3.0 → 2.3.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.
Files changed (3) hide show
  1. package/README.md +67 -26
  2. package/mcp-erpnext.mjs +55 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @casys/mcp-erpnext
2
2
 
3
+ [![JSR](https://jsr.io/badges/@casys/mcp-erpnext)](https://jsr.io/@casys/mcp-erpnext)
4
+ [![npm](https://img.shields.io/npm/v/@casys/mcp-erpnext?logo=npm&color=cb3837)](https://www.npmjs.com/package/@casys/mcp-erpnext)
5
+ [![CI](https://github.com/Casys-AI/mcp-erpnext/actions/workflows/test.yml/badge.svg)](https://github.com/Casys-AI/mcp-erpnext/actions/workflows/test.yml)
6
+ [![MCP](https://img.shields.io/badge/MCP-server-1f6feb?logo=modelcontextprotocol&logoColor=white)](https://modelcontextprotocol.io)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
+
3
9
  MCP server for [ERPNext](https://erpnext.com) / Frappe ERP — **120 tools**
4
10
  across **14 categories**, with **7 interactive UI viewers**.
5
11
 
@@ -9,22 +15,59 @@ Copilot, custom) to your ERPNext instance via the
9
15
 
10
16
  Works with **self-hosted** and **ERPNext Cloud** (frappe.cloud) instances.
11
17
 
12
- ## What's New in v2.1
13
-
14
- - **Cross-viewer navigation** click a row in any list to drill down, click a
15
- button to open related documents in another viewer via `sendMessage`
16
- - **Inline detail panels** — expand any row in doclist/stock viewers to see full
17
- document details + action buttons (Submit, Cancel, Payments)
18
- - **Interactive charts** — click bar/pie/line data points to drill into
19
- underlying documents
20
- - **KPI drill-down** click the big number or sparkline to explore exceptions
21
- or trends
22
- - **Funnel redesign** — trapezoid stages with gradient fills, conversion badges,
23
- click-through navigation
24
- - **Better error messages** Frappe API errors are now surfaced with full
25
- detail instead of generic "Tool execution failed"
26
- - **VS Code Copilot fix** — schema validation issue with `erpnext_doc_list`
27
- filters resolved (#2)
18
+ ## Screenshots
19
+
20
+ Interactive viewers rendered inside an MCP host, driven entirely by tool
21
+ results.
22
+
23
+ <table>
24
+ <tr>
25
+ <td width="50%" align="center">
26
+ <img src="docs/assets/doclist-viewer.png" alt="Document list viewer with chip filters and inline detail" width="100%"><br>
27
+ <sub><b>doclist-viewer</b> — any DocType as a sortable table with chip filters and an inline detail panel</sub>
28
+ </td>
29
+ <td width="50%" align="center">
30
+ <img src="docs/assets/invoice-viewer.png" alt="Invoice viewer with line items and actions" width="100%"><br>
31
+ <sub><b>invoice-viewer</b> invoice with parties, line items, item drill-down and Submit/Cancel/Payments</sub>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td width="50%" align="center">
36
+ <img src="docs/assets/funnel-viewer.png" alt="Sales funnel viewer" width="100%"><br>
37
+ <sub><b>funnel-viewer</b> — Lead → Opportunity → Quotation → Order with conversion rates</sub>
38
+ </td>
39
+ <td width="50%" align="center">
40
+ <img src="docs/assets/kpi-viewer.png" alt="KPI viewer with sparkline" width="100%"><br>
41
+ <sub><b>kpi-viewer</b> — big-number KPI with delta vs last period and a sparkline</sub>
42
+ </td>
43
+ </tr>
44
+ <tr>
45
+ <td width="50%" align="center">
46
+ <img src="docs/assets/chart-viewer.png" alt="Chart viewer" width="100%"><br>
47
+ <sub><b>chart-viewer</b> — universal Recharts renderer (here: stock levels)</sub>
48
+ </td>
49
+ <td width="50%" align="center">
50
+ <img src="docs/assets/stock-viewer.png" alt="Stock balance viewer" width="100%"><br>
51
+ <sub><b>stock-viewer</b> — stock balance with color-coded quantity badges</sub>
52
+ </td>
53
+ </tr>
54
+ <tr>
55
+ <td width="50%" align="center">
56
+ <img src="docs/assets/kanban-viewer.png" alt="Read-write kanban board" width="100%"><br>
57
+ <sub><b>kanban-viewer</b> — read-write board (Task / Opportunity / Issue) with inline edit</sub>
58
+ </td>
59
+ <td width="50%" align="center">
60
+ <img src="docs/assets/profit-loss.png" alt="Profit and loss composed chart" width="100%"><br>
61
+ <sub><b>chart-viewer</b> — composed dual-axis chart (here: profit &amp; loss)</sub>
62
+ </td>
63
+ </tr>
64
+ </table>
65
+
66
+ ## What's New
67
+
68
+ See the [CHANGELOG](CHANGELOG.md) for the full release history, or the
69
+ [latest release](https://github.com/Casys-AI/mcp-erpnext/releases/latest) for
70
+ the current version's highlights.
28
71
 
29
72
  ## Quick Start
30
73
 
@@ -293,19 +336,17 @@ cd src/ui && npm run dev:kanban
293
336
 
294
337
  ## Release Flow
295
338
 
296
- Release is intentionally GitHub-driven:
339
+ Releases are manual and explicit:
297
340
 
298
- 1. Run `deno task release:check` locally before publishing work.
299
- 2. Push or merge to `main`.
300
- 3. Release Please opens or updates the release PR with the semver bump,
301
- `CHANGELOG.md`, `deno.json`, and `server.ts`.
302
- 4. Merge the Release Please PR.
303
- 5. `.github/workflows/release-please.yml` creates the GitHub release, then calls
304
- `.github/workflows/publish.yml` to publish the released version to JSR and
341
+ 1. Update `deno.json`, `server.ts`, and `CHANGELOG.md`.
342
+ 2. Run `deno task release:check` locally.
343
+ 3. Commit and push the release commit to `main`.
344
+ 4. Create the GitHub release/tag, for example `v2.3.0`.
345
+ 5. Run the `Publish` workflow manually to publish the same version to JSR and
305
346
  npm.
306
347
 
307
- Do not bump versions manually unless bypassing Release Please for an emergency
308
- release.
348
+ The package name stays `@casys/mcp-erpnext`; releases only bump the package
349
+ version.
309
350
 
310
351
  ## License
311
352
 
package/mcp-erpnext.mjs CHANGED
@@ -22327,6 +22327,21 @@ var HonoRequest = class {
22327
22327
  arrayBuffer() {
22328
22328
  return this.#cachedBody("arrayBuffer");
22329
22329
  }
22330
+ /**
22331
+ * `.bytes()` parses the request body as a `Uint8Array`.
22332
+ *
22333
+ * @see {@link https://hono.dev/docs/api/request#bytes}
22334
+ *
22335
+ * @example
22336
+ * ```ts
22337
+ * app.post('/entry', async (c) => {
22338
+ * const body = await c.req.bytes()
22339
+ * })
22340
+ * ```
22341
+ */
22342
+ bytes() {
22343
+ return this.#cachedBody("arrayBuffer").then((buffer) => new Uint8Array(buffer));
22344
+ }
22330
22345
  /**
22331
22346
  * Parses the request body as a `Blob`.
22332
22347
  * @example
@@ -23025,7 +23040,7 @@ var Hono = class _Hono {
23025
23040
  handler = async (c, next) => (await compose([], app.errorHandler)(c, () => r.handler(c, next))).res;
23026
23041
  handler[COMPOSED_HANDLER] = r.handler;
23027
23042
  }
23028
- subApp.#addRoute(r.method, r.path, handler);
23043
+ subApp.#addRoute(r.method, r.path, handler, r.basePath);
23029
23044
  });
23030
23045
  return this;
23031
23046
  }
@@ -23149,7 +23164,7 @@ var Hono = class _Hono {
23149
23164
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
23150
23165
  return (request) => {
23151
23166
  const url = new URL(request.url);
23152
- url.pathname = url.pathname.slice(pathPrefixLength) || "/";
23167
+ url.pathname = this.getPath(request).slice(pathPrefixLength) || "/";
23153
23168
  return new Request(url, request);
23154
23169
  };
23155
23170
  })();
@@ -23163,10 +23178,15 @@ var Hono = class _Hono {
23163
23178
  this.#addRoute(METHOD_NAME_ALL, mergePath(path, "*"), handler);
23164
23179
  return this;
23165
23180
  }
23166
- #addRoute(method, path, handler) {
23181
+ #addRoute(method, path, handler, baseRoutePath) {
23167
23182
  method = method.toUpperCase();
23168
23183
  path = mergePath(this._basePath, path);
23169
- const r = { basePath: this._basePath, path, method, handler };
23184
+ const r = {
23185
+ basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
23186
+ path,
23187
+ method,
23188
+ handler
23189
+ };
23170
23190
  this.router.add(method, path, [handler, r]);
23171
23191
  this.routes.push(r);
23172
23192
  }
@@ -26468,12 +26488,16 @@ var LoaderState = class {
26468
26488
  }
26469
26489
  for (const [key, value] of Object.entries(source)) {
26470
26490
  if (Object.hasOwn(destination, key)) continue;
26471
- Object.defineProperty(destination, key, {
26472
- value,
26473
- writable: true,
26474
- enumerable: true,
26475
- configurable: true
26476
- });
26491
+ if (key === "__proto__") {
26492
+ Object.defineProperty(destination, key, {
26493
+ value,
26494
+ writable: true,
26495
+ enumerable: true,
26496
+ configurable: true
26497
+ });
26498
+ } else {
26499
+ destination[key] = value;
26500
+ }
26477
26501
  overridableKeys.add(key);
26478
26502
  }
26479
26503
  }
@@ -26507,12 +26531,16 @@ var LoaderState = class {
26507
26531
  this.#scanner.position = startPos || this.#scanner.position;
26508
26532
  throw this.#createError("Cannot store mapping pair: duplicated key");
26509
26533
  }
26510
- Object.defineProperty(result, keyNode, {
26511
- value: valueNode,
26512
- writable: true,
26513
- enumerable: true,
26514
- configurable: true
26515
- });
26534
+ if (keyNode === "__proto__") {
26535
+ Object.defineProperty(result, keyNode, {
26536
+ value: valueNode,
26537
+ writable: true,
26538
+ enumerable: true,
26539
+ configurable: true
26540
+ });
26541
+ } else {
26542
+ result[keyNode] = valueNode;
26543
+ }
26516
26544
  overridableKeys.delete(keyNode);
26517
26545
  }
26518
26546
  return result;
@@ -35994,18 +36022,26 @@ var analyticsTools = [
35994
36022
  if (input.warehouse) {
35995
36023
  filters.push(["warehouse", "=", input.warehouse]);
35996
36024
  }
36025
+ let allowedItems = null;
35997
36026
  if (input.item_group) {
35998
- filters.push(["item_group", "=", input.item_group]);
36027
+ const groupItems = await ctx.client.list("Item", {
36028
+ fields: ["name"],
36029
+ filters: [["item_group", "=", input.item_group]],
36030
+ limit: 1e3
36031
+ });
36032
+ allowedItems = new Set(groupItems.map((i) => i.name));
35999
36033
  }
36000
36034
  const bins = await ctx.client.list("Bin", {
36001
36035
  fields: ["item_code", "warehouse", "actual_qty", "stock_value"],
36002
36036
  filters,
36003
- limit,
36037
+ // widen the fetch when filtering by group in memory, then slice below
36038
+ limit: allowedItems ? 1e3 : limit,
36004
36039
  order_by: "actual_qty desc"
36005
36040
  });
36006
36041
  const byItem = {};
36007
36042
  for (const bin of bins) {
36008
36043
  const item = bin.item_code;
36044
+ if (allowedItems && !allowedItems.has(item)) continue;
36009
36045
  if (!byItem[item]) byItem[item] = { qty: 0, value: 0 };
36010
36046
  byItem[item].qty += Number(bin.actual_qty) || 0;
36011
36047
  byItem[item].value += Number(bin.stock_value) || 0;
@@ -38918,8 +38954,7 @@ async function main() {
38918
38954
  );
38919
38955
  const server = new McpApp({
38920
38956
  name: "mcp-erpnext",
38921
- version: "2.3.0",
38922
- // x-release-please-version
38957
+ version: "2.3.1",
38923
38958
  maxConcurrent: 10,
38924
38959
  backpressureStrategy: "queue",
38925
38960
  validateSchema: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casys/mcp-erpnext",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {