@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 +5 -5
- package/mcp-erpnext.mjs +152 -73
- package/package.json +1 -1
- package/ui-dist/chart-viewer/index.html +55 -55
- package/ui-dist/doc-viewer/index.html +40 -40
- package/ui-dist/doclist-viewer/index.html +41 -41
- package/ui-dist/funnel-viewer/index.html +40 -40
- package/ui-dist/invoice-viewer/index.html +40 -40
- package/ui-dist/kanban-viewer/index.html +41 -41
- package/ui-dist/kpi-viewer/index.html +41 -41
- package/ui-dist/stock-viewer/index.html +40 -40
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.
|
|
14
|
-
> use `deno run -A jsr:@casys/mcp-erpnext@3.1.0-beta.
|
|
15
|
-
> alias is `@next`.
|
|
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
|
|
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
|
|
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: "
|
|
56456
|
-
label: "
|
|
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: "
|
|
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
|
-
|
|
56669
|
-
|
|
56670
|
-
|
|
56671
|
-
|
|
56672
|
-
|
|
56673
|
-
|
|
56674
|
-
|
|
56675
|
-
|
|
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:
|
|
56715
|
+
message: `Show customer ${customer}`,
|
|
56693
56716
|
tool: "erpnext_customer_get",
|
|
56694
|
-
args: { name:
|
|
56717
|
+
args: { name: customer },
|
|
56695
56718
|
kind: "record"
|
|
56696
|
-
}
|
|
56697
|
-
|
|
56698
|
-
|
|
56699
|
-
|
|
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:
|
|
56726
|
+
message: `Show supplier ${supplier}`,
|
|
56723
56727
|
tool: "erpnext_supplier_get",
|
|
56724
|
-
args: { name:
|
|
56728
|
+
args: { name: supplier },
|
|
56725
56729
|
kind: "record"
|
|
56726
|
-
}
|
|
56727
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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.
|
|
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
|
|
57920
|
-
return {
|
|
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}`);
|