@casys/mcp-erpnext 3.1.0-beta.2 → 3.1.0-beta.4

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
@@ -10,9 +10,9 @@ English | [繁體中文](README.zh-TW.md)
10
10
 
11
11
  > [!IMPORTANT]
12
12
  > **Installing the 3.1 beta:** pin the prerelease while this UX is being
13
- > validated. For npm/Node use `npx -y @casys/mcp-erpnext@3.1.0-beta.2`; for Deno
14
- > use `deno run -A jsr:@casys/mcp-erpnext@3.1.0-beta.2/server`. The moving npm
15
- > alias is `@next`. Beta 2 adds the generic document viewer and attachment
13
+ > validated. For npm/Node use `npx -y @casys/mcp-erpnext@3.1.0-beta.3`; for Deno
14
+ > use `deno run -A jsr:@casys/mcp-erpnext@3.1.0-beta.3/server`. The moving npm
15
+ > alias is `@next`. The beta adds the generic document viewer and attachment
16
16
  > workflows, so test it with the MCP host your users actually run before
17
17
  > replacing a stable deployment.
18
18
 
@@ -80,7 +80,7 @@ See the [CHANGELOG](CHANGELOG.md) for the full release history, or the
80
80
  [latest release](https://github.com/Casys-AI/mcp-erpnext/releases/latest) for
81
81
  the current version's highlights.
82
82
 
83
- > **3.1 beta 2 preview:** the beta keeps the 3.0 tool surface and adds a generic
83
+ > **3.1 beta preview:** the beta keeps the 3.0 tool surface and adds a generic
84
84
  > document viewer, child tables, document attachments, in-view navigation, and
85
85
  > active context. Features remain capability-gated by the MCP host. In
86
86
  > particular, downloads require both proxied server tools and the MCP Apps
@@ -235,7 +235,7 @@ tool is never replayed automatically. Overlapping or stale responses are ignored
235
235
  when a newer host payload or mutation has already won. A confirmed document
236
236
  change marks every potentially derived snapshot in the current navigation stack
237
237
  as stale; each marker is removed only when that surface has actually been read
238
- again. Separate MCP Apps are not synchronized automatically in beta 2.
238
+ again. Separate MCP Apps are not synchronized automatically in the 3.1 beta.
239
239
 
240
240
  ### Building UI viewers
241
241
 
package/mcp-erpnext.mjs CHANGED
@@ -37013,6 +37013,8 @@ var bufferToFormData = (arrayBuffer, contentType) => {
37013
37013
  };
37014
37014
 
37015
37015
  // node_modules/hono/dist/utils/body.js
37016
+ var MAX_NESTING_DEPTH = 32;
37017
+ var MAX_NESTED_OBJECTS = 1e4;
37016
37018
  var isRawRequest = (request) => "headers" in request;
37017
37019
  var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) => {
37018
37020
  const { all = false, dot = false } = options;
@@ -37045,6 +37047,7 @@ async function parseFormData(request, options) {
37045
37047
  }
37046
37048
  function convertFormDataToBodyData(formData, options) {
37047
37049
  const form = /* @__PURE__ */ Object.create(null);
37050
+ const nestingState = { count: 0 };
37048
37051
  formData.forEach((value, key) => {
37049
37052
  const shouldParseAllValues = options.all || key.endsWith("[]");
37050
37053
  if (!shouldParseAllValues) {
@@ -37057,7 +37060,7 @@ function convertFormDataToBodyData(formData, options) {
37057
37060
  Object.entries(form).forEach(([key, value]) => {
37058
37061
  const shouldParseDotValues = key.includes(".");
37059
37062
  if (shouldParseDotValues) {
37060
- handleParsingNestedValues(form, key, value);
37063
+ handleParsingNestedValues(form, key, value, nestingState);
37061
37064
  delete form[key];
37062
37065
  }
37063
37066
  });
@@ -37080,23 +37083,32 @@ var handleParsingAllValues = (form, key, value) => {
37080
37083
  }
37081
37084
  }
37082
37085
  };
37083
- var handleParsingNestedValues = (form, key, value) => {
37086
+ var handleParsingNestedValues = (form, key, value, state) => {
37084
37087
  if (/(?:^|\.)__proto__\./.test(key)) {
37085
37088
  return;
37086
37089
  }
37087
37090
  let nestedForm = form;
37088
- const keys = key.split(".");
37091
+ const keys = key.split(".", MAX_NESTING_DEPTH + 2);
37092
+ if (keys.length > MAX_NESTING_DEPTH + 1) {
37093
+ throwNestingLimitExceeded();
37094
+ }
37089
37095
  keys.forEach((key2, index) => {
37090
37096
  if (index === keys.length - 1) {
37091
37097
  nestedForm[key2] = value;
37092
37098
  } else {
37093
37099
  if (!nestedForm[key2] || typeof nestedForm[key2] !== "object" || Array.isArray(nestedForm[key2]) || nestedForm[key2] instanceof File) {
37100
+ if (state.count++ >= MAX_NESTED_OBJECTS) {
37101
+ throwNestingLimitExceeded();
37102
+ }
37094
37103
  nestedForm[key2] = /* @__PURE__ */ Object.create(null);
37095
37104
  }
37096
37105
  nestedForm = nestedForm[key2];
37097
37106
  }
37098
37107
  });
37099
37108
  };
37109
+ var throwNestingLimitExceeded = () => {
37110
+ throw new Error("Nesting limit exceeded");
37111
+ };
37100
37112
 
37101
37113
  // node_modules/hono/dist/utils/url.js
37102
37114
  var splitPath = (path) => {
@@ -37228,6 +37240,10 @@ var _decodeURI = (value) => {
37228
37240
  return tryDecodeURIComponent(value);
37229
37241
  };
37230
37242
  var _getQueryParam = (url2, key, multiple) => {
37243
+ const hashIndex = url2.indexOf("#", 8);
37244
+ if (hashIndex !== -1) {
37245
+ url2 = url2.slice(0, hashIndex);
37246
+ }
37231
37247
  let encoded;
37232
37248
  if (!multiple && key && key.indexOf("%") === -1 && key.indexOf("+") === -1) {
37233
37249
  let keyIndex2 = url2.indexOf("?", 8);
@@ -56452,8 +56468,8 @@ var DOCTYPE_SEND_MESSAGE_HINTS = {
56452
56468
  ],
56453
56469
  "Sales Order": [
56454
56470
  {
56455
- key: "invoice",
56456
- label: "Invoice",
56471
+ key: "invoices",
56472
+ label: "Invoices",
56457
56473
  message: "Show invoices linked to sales order {id}",
56458
56474
  tool: "erpnext_doc_list",
56459
56475
  args: {
@@ -56464,8 +56480,8 @@ var DOCTYPE_SEND_MESSAGE_HINTS = {
56464
56480
  }
56465
56481
  },
56466
56482
  {
56467
- key: "delivery",
56468
- label: "Delivery",
56483
+ key: "deliveries",
56484
+ label: "Delivery notes",
56469
56485
  message: "Show delivery notes for sales order {id}",
56470
56486
  tool: "erpnext_doc_list",
56471
56487
  args: {
@@ -56496,6 +56512,27 @@ var DOCTYPE_SEND_MESSAGE_HINTS = {
56496
56512
  }
56497
56513
  }
56498
56514
  ],
56515
+ "Purchase Invoice": [
56516
+ {
56517
+ key: "payments",
56518
+ label: "Payments",
56519
+ message: "Show payment entries for purchase invoice {id}",
56520
+ tool: "erpnext_doc_list",
56521
+ args: {
56522
+ doctype: "Payment Entry",
56523
+ fields: [
56524
+ "name",
56525
+ "posting_date",
56526
+ "paid_amount",
56527
+ "mode_of_payment",
56528
+ "docstatus"
56529
+ ],
56530
+ filters: [["Payment Entry Reference", "reference_name", "=", "{id}"]],
56531
+ limit: 20
56532
+ },
56533
+ kind: "list"
56534
+ }
56535
+ ],
56499
56536
  "Item": [
56500
56537
  {
56501
56538
  key: "stock",
@@ -56665,68 +56702,69 @@ var INVOICE_ITEM_HINTS = [
56665
56702
  kind: "list"
56666
56703
  }
56667
56704
  ];
56668
- var INVOICE_HINTS = {
56669
- "Sales Invoice": [
56670
- {
56671
- key: "payments",
56672
- label: "Payments",
56673
- message: "Show payment entries for invoice {id}",
56674
- tool: "erpnext_doc_list",
56675
- args: {
56676
- doctype: "Payment Entry",
56677
- fields: [
56678
- "name",
56679
- "posting_date",
56680
- "paid_amount",
56681
- "mode_of_payment",
56682
- "docstatus"
56683
- ],
56684
- filters: [["Payment Entry Reference", "reference_name", "=", "{id}"]],
56685
- limit: 20
56686
- },
56687
- kind: "list"
56688
- },
56689
- {
56705
+ function nonEmptyDocumentString(document, key) {
56706
+ const value = document?.[key];
56707
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
56708
+ }
56709
+ function documentPartyHint(doctype, document) {
56710
+ if (doctype === "Sales Order" || doctype === "Sales Invoice") {
56711
+ const customer = nonEmptyDocumentString(document, "customer");
56712
+ return customer ? {
56690
56713
  key: "customer",
56691
56714
  label: "Customer",
56692
- message: "Show customer {party}",
56715
+ message: `Show customer ${customer}`,
56693
56716
  tool: "erpnext_customer_get",
56694
- args: { name: "{party}" },
56717
+ args: { name: customer },
56695
56718
  kind: "record"
56696
- },
56697
- ...INVOICE_ITEM_HINTS
56698
- ],
56699
- "Purchase Invoice": [
56700
- {
56701
- key: "payments",
56702
- label: "Payments",
56703
- message: "Show payment entries for invoice {id}",
56704
- tool: "erpnext_doc_list",
56705
- args: {
56706
- doctype: "Payment Entry",
56707
- fields: [
56708
- "name",
56709
- "posting_date",
56710
- "paid_amount",
56711
- "mode_of_payment",
56712
- "docstatus"
56713
- ],
56714
- filters: [["Payment Entry Reference", "reference_name", "=", "{id}"]],
56715
- limit: 20
56716
- },
56717
- kind: "list"
56718
- },
56719
- {
56719
+ } : null;
56720
+ }
56721
+ if (doctype === "Purchase Invoice") {
56722
+ const supplier = nonEmptyDocumentString(document, "supplier");
56723
+ return supplier ? {
56720
56724
  key: "supplier",
56721
56725
  label: "Supplier",
56722
- message: "Show supplier {party}",
56726
+ message: `Show supplier ${supplier}`,
56723
56727
  tool: "erpnext_supplier_get",
56724
- args: { name: "{party}" },
56728
+ args: { name: supplier },
56725
56729
  kind: "record"
56726
- },
56727
- ...INVOICE_ITEM_HINTS
56728
- ]
56729
- };
56730
+ } : null;
56731
+ }
56732
+ if (doctype !== "Quotation") return null;
56733
+ const party = nonEmptyDocumentString(document, "party_name");
56734
+ const target = nonEmptyDocumentString(document, "quotation_to");
56735
+ if (!party || target !== "Customer" && target !== "Lead") return null;
56736
+ return target === "Customer" ? {
56737
+ key: "customer",
56738
+ label: "Customer",
56739
+ message: `Show customer ${party}`,
56740
+ tool: "erpnext_customer_get",
56741
+ args: { name: party },
56742
+ kind: "record"
56743
+ } : {
56744
+ key: "lead",
56745
+ label: "Lead",
56746
+ message: `Show lead ${party}`,
56747
+ tool: "erpnext_lead_get",
56748
+ args: { name: party },
56749
+ kind: "record"
56750
+ };
56751
+ }
56752
+ function documentNavigationHints(doctype, document) {
56753
+ const base = DOCTYPE_SEND_MESSAGE_HINTS[doctype] ?? [];
56754
+ const party = documentPartyHint(doctype, document);
56755
+ if (!party) return [...base];
56756
+ return doctype === "Sales Order" ? [party, ...base] : [...base, party];
56757
+ }
56758
+ var INVOICE_LINE_ITEM_DOCTYPES = /* @__PURE__ */ new Set([
56759
+ "Sales Order",
56760
+ "Sales Invoice",
56761
+ "Purchase Invoice",
56762
+ "Quotation"
56763
+ ]);
56764
+ function invoiceNavigationHints(doctype, document) {
56765
+ const documentHints = documentNavigationHints(doctype, document);
56766
+ return INVOICE_LINE_ITEM_DOCTYPES.has(doctype) ? [...documentHints, ...INVOICE_ITEM_HINTS] : documentHints;
56767
+ }
56730
56768
  var STOCK_HINTS = [
56731
56769
  {
56732
56770
  key: "item",
@@ -57174,6 +57212,9 @@ function resultDoctype(result) {
57174
57212
  if (isRecord7(data) && typeof data.doctype === "string") return data.doctype;
57175
57213
  return void 0;
57176
57214
  }
57215
+ function resultDocument(result) {
57216
+ return isRecord7(result.data) ? result.data : void 0;
57217
+ }
57177
57218
  function isInvoiceViewer(result) {
57178
57219
  const uri = result._meta?.ui?.resourceUri;
57179
57220
  return uri === "ui://mcp-erpnext/invoice-viewer";
@@ -57409,8 +57450,8 @@ function withUiRefreshRequest(result, toolName, args, now = /* @__PURE__ */ new
57409
57450
  }
57410
57451
  }
57411
57452
  if (isDoclistResult(enriched) && isDoclistViewer(enriched) && !enriched._sendMessageHints) {
57412
- const hints = DOCTYPE_SEND_MESSAGE_HINTS[enriched.doctype];
57413
- if (hints) {
57453
+ const hints = documentNavigationHints(enriched.doctype);
57454
+ if (hints.length > 0) {
57414
57455
  enriched._sendMessageHints = hints;
57415
57456
  }
57416
57457
  }
@@ -57450,13 +57491,13 @@ function withUiRefreshRequest(result, toolName, args, now = /* @__PURE__ */ new
57450
57491
  if (!enriched._sendMessageHints) {
57451
57492
  const doctype = resultDoctype(enriched);
57452
57493
  if (isInvoiceViewer(enriched) && doctype) {
57453
- const hints = INVOICE_HINTS[doctype];
57454
- if (hints) enriched._sendMessageHints = hints;
57494
+ const hints = invoiceNavigationHints(doctype, resultDocument(enriched));
57495
+ if (hints.length > 0) enriched._sendMessageHints = hints;
57455
57496
  } else if (isStockViewer(enriched)) {
57456
57497
  enriched._sendMessageHints = STOCK_HINTS;
57457
57498
  } else if ((isKanbanViewer(enriched) || isDocViewer(enriched)) && doctype) {
57458
- const hints = DOCTYPE_SEND_MESSAGE_HINTS[doctype];
57459
- if (hints) enriched._sendMessageHints = hints;
57499
+ const hints = documentNavigationHints(doctype, resultDocument(enriched));
57500
+ if (hints.length > 0) enriched._sendMessageHints = hints;
57460
57501
  }
57461
57502
  }
57462
57503
  return filterNavJumpsByAvailableTools(enriched, availableToolNames);
@@ -57640,6 +57681,17 @@ var UI_VIEWERS = [
57640
57681
  "funnel-viewer",
57641
57682
  "kanban-viewer"
57642
57683
  ];
57684
+ var VIEWER_RESOURCE_META = {
57685
+ ui: {
57686
+ csp: {
57687
+ resourceDomains: ["blob:"],
57688
+ frameDomains: ["blob:"]
57689
+ }
57690
+ }
57691
+ };
57692
+ function withViewerResourceMeta(resource) {
57693
+ return { ...resource, _meta: VIEWER_RESOURCE_META };
57694
+ }
57643
57695
 
57644
57696
  // src/ui/viewer-resource-paths.ts
57645
57697
  function fileUrlToPath2(url2) {
@@ -57652,9 +57704,19 @@ function fileUrlToPath2(url2) {
57652
57704
  }
57653
57705
  return decodedPath;
57654
57706
  }
57707
+ function isRemoteUrl(url2) {
57708
+ return url2.protocol === "http:" || url2.protocol === "https:";
57709
+ }
57655
57710
  function resolveViewerDistPath2(moduleUrl, viewerName, exists) {
57711
+ const sourceDistUrl = new URL(
57712
+ `./src/ui/dist/${viewerName}/index.html`,
57713
+ moduleUrl
57714
+ );
57715
+ if (isRemoteUrl(sourceDistUrl)) {
57716
+ return sourceDistUrl.href;
57717
+ }
57656
57718
  const candidates = [
57657
- fileUrlToPath2(new URL(`./src/ui/dist/${viewerName}/index.html`, moduleUrl)),
57719
+ fileUrlToPath2(sourceDistUrl),
57658
57720
  fileUrlToPath2(new URL(`./ui-dist/${viewerName}/index.html`, moduleUrl))
57659
57721
  ];
57660
57722
  for (const candidate of candidates) {
@@ -57664,6 +57726,19 @@ function resolveViewerDistPath2(moduleUrl, viewerName, exists) {
57664
57726
  }
57665
57727
  return null;
57666
57728
  }
57729
+ async function readViewerDist(location, readLocal, fetchRemote = fetch) {
57730
+ const url2 = new URL(location, "file:///");
57731
+ if (!isRemoteUrl(url2)) {
57732
+ return await readLocal(location);
57733
+ }
57734
+ const response = await fetchRemote(location);
57735
+ if (!response.ok) {
57736
+ throw new Error(
57737
+ `Viewer resource fetch failed (${response.status} ${response.statusText})`
57738
+ );
57739
+ }
57740
+ return await response.text();
57741
+ }
57667
57742
 
57668
57743
  // src/auth/composite-provider.ts
57669
57744
  var CompositeAuthProvider = class extends AuthProvider {
@@ -57878,7 +57953,7 @@ async function main() {
57878
57953
  );
57879
57954
  const server = new McpApp({
57880
57955
  name: "mcp-erpnext",
57881
- version: "3.1.0-beta.2",
57956
+ version: "3.1.0-beta.4",
57882
57957
  transport: "stateless",
57883
57958
  cache: {
57884
57959
  ttlMs: 36e5,
@@ -57909,15 +57984,19 @@ async function main() {
57909
57984
  const humanName = viewerName.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
57910
57985
  if (distPath) {
57911
57986
  server.registerResource(
57912
- {
57987
+ withViewerResourceMeta({
57913
57988
  uri: resourceUri,
57914
57989
  name: `ERPNext ${humanName}`,
57915
57990
  description: `ERPNext UI: ${viewerName}`,
57916
57991
  mimeType: MCP_APP_MIME_TYPE
57917
- },
57992
+ }),
57918
57993
  async () => {
57919
- const html = await readTextFile6(distPath);
57920
- return { uri: resourceUri, mimeType: MCP_APP_MIME_TYPE, text: html };
57994
+ const html = await readViewerDist(distPath, readTextFile6);
57995
+ return withViewerResourceMeta({
57996
+ uri: resourceUri,
57997
+ mimeType: MCP_APP_MIME_TYPE,
57998
+ text: html
57999
+ });
57921
58000
  }
57922
58001
  );
57923
58002
  console.error(`[mcp-erpnext] Registered UI resource: ${resourceUri}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casys/mcp-erpnext",
3
- "version": "3.1.0-beta.2",
3
+ "version": "3.1.0-beta.4",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {