@casys/mcp-erpnext 3.2.1 → 3.2.3
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 +2 -2
- package/mcp-erpnext.mjs +125 -22
- package/package.json +1 -1
- package/ui-dist/buy-evidence-viewer/index.html +2 -2
- package/ui-dist/chart-viewer/index.html +48 -48
- package/ui-dist/doc-viewer/index.html +28 -28
- package/ui-dist/doclist-viewer/index.html +29 -29
- package/ui-dist/funnel-viewer/index.html +28 -28
- package/ui-dist/invoice-viewer/index.html +28 -28
- package/ui-dist/kanban-viewer/index.html +12 -12
- package/ui-dist/kpi-viewer/index.html +28 -28
- package/ui-dist/stock-viewer/index.html +27 -27
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ English | [繁體中文](README.zh-TW.md)
|
|
|
11
11
|
|
|
12
12
|
> [!IMPORTANT]
|
|
13
13
|
> **Installing 3.2:** for npm/Node use `npx -y @casys/mcp-erpnext@latest`; for
|
|
14
|
-
> Deno use `deno run -A jsr:@casys/mcp-erpnext@3.2.
|
|
14
|
+
> Deno use `deno run -A jsr:@casys/mcp-erpnext@3.2.3/server`. 3.2.3 is the
|
|
15
15
|
> stable release and carries npm's `latest` tag; prereleases, when published,
|
|
16
16
|
> use the `next` tag. 3.2 adds the generic document viewer and attachment
|
|
17
17
|
> workflows, so test it with the MCP host your users actually run before
|
|
@@ -96,7 +96,7 @@ stable 3.0.5 and 3.1.0 releases. Features remain capability-gated by the MCP
|
|
|
96
96
|
host; Buy evidence does not purchase or qualify a live ERP.
|
|
97
97
|
|
|
98
98
|
For stable npm installations use `@casys/mcp-erpnext@latest`; for Deno use
|
|
99
|
-
`jsr:@casys/mcp-erpnext@3.2.
|
|
99
|
+
`jsr:@casys/mcp-erpnext@3.2.3/server`.
|
|
100
100
|
|
|
101
101
|
See the [CHANGELOG](CHANGELOG.md) for the full release history, or the
|
|
102
102
|
[latest release](https://github.com/Casys-AI/mcp-erpnext/releases/latest) for
|
package/mcp-erpnext.mjs
CHANGED
|
@@ -42376,6 +42376,31 @@ function stableStringify(value) {
|
|
|
42376
42376
|
}
|
|
42377
42377
|
return JSON.stringify(value);
|
|
42378
42378
|
}
|
|
42379
|
+
var CALLER_ABORT_STATUS = 499;
|
|
42380
|
+
function relayAbort(from, to) {
|
|
42381
|
+
if (!from) return () => {
|
|
42382
|
+
};
|
|
42383
|
+
if (from.aborted) {
|
|
42384
|
+
to.abort();
|
|
42385
|
+
return () => {
|
|
42386
|
+
};
|
|
42387
|
+
}
|
|
42388
|
+
const onAbort = () => to.abort();
|
|
42389
|
+
from.addEventListener("abort", onAbort, { once: true });
|
|
42390
|
+
return () => from.removeEventListener("abort", onAbort);
|
|
42391
|
+
}
|
|
42392
|
+
function sleep2(ms, signal) {
|
|
42393
|
+
return new Promise((resolve) => {
|
|
42394
|
+
if (signal?.aborted) return resolve();
|
|
42395
|
+
const done = () => {
|
|
42396
|
+
clearTimeout(timer);
|
|
42397
|
+
signal?.removeEventListener("abort", done);
|
|
42398
|
+
resolve();
|
|
42399
|
+
};
|
|
42400
|
+
const timer = setTimeout(done, ms);
|
|
42401
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
42402
|
+
});
|
|
42403
|
+
}
|
|
42379
42404
|
var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
|
|
42380
42405
|
var DEFAULT_RETRY_METHODS = ["GET"];
|
|
42381
42406
|
var DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
@@ -42536,7 +42561,8 @@ function normalizeFrappeSiteUrl(baseUrl) {
|
|
|
42536
42561
|
var FrappeAPIError = class extends Error {
|
|
42537
42562
|
/**
|
|
42538
42563
|
* @param message - Human-readable error description
|
|
42539
|
-
* @param status - HTTP status code (0 for network errors, 408 for timeouts
|
|
42564
|
+
* @param status - HTTP status code (0 for network errors, 408 for timeouts,
|
|
42565
|
+
* 499 when the caller aborted through its `signal`)
|
|
42540
42566
|
* @param body - Raw response body (parsed JSON object or plain text string)
|
|
42541
42567
|
* @param retryAfterMs - When the server sent a `Retry-After` header on a
|
|
42542
42568
|
* retryable status (typically 429), the parsed delay
|
|
@@ -42550,6 +42576,13 @@ var FrappeAPIError = class extends Error {
|
|
|
42550
42576
|
this.name = "FrappeAPIError";
|
|
42551
42577
|
}
|
|
42552
42578
|
};
|
|
42579
|
+
function callerAbortError(method, path) {
|
|
42580
|
+
return new FrappeAPIError(
|
|
42581
|
+
`Request aborted by caller: ${method} ${path}`,
|
|
42582
|
+
CALLER_ABORT_STATUS,
|
|
42583
|
+
null
|
|
42584
|
+
);
|
|
42585
|
+
}
|
|
42553
42586
|
function parseRetryAfter(raw2) {
|
|
42554
42587
|
if (!raw2) return void 0;
|
|
42555
42588
|
const trimmed = raw2.trim();
|
|
@@ -42659,28 +42692,51 @@ var FrappeClient = class {
|
|
|
42659
42692
|
}
|
|
42660
42693
|
return this.retryBackoffMs * 2 ** attempt;
|
|
42661
42694
|
}
|
|
42662
|
-
async request(method, path, body, multipart = false) {
|
|
42695
|
+
async request(method, path, body, multipart = false, options = {}) {
|
|
42696
|
+
const { signal } = options;
|
|
42697
|
+
const retries = options.retry === false ? 0 : this.retries;
|
|
42663
42698
|
let lastError;
|
|
42664
|
-
for (let attempt = 0; attempt <=
|
|
42699
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
42700
|
+
if (signal?.aborted) throw callerAbortError(method, path);
|
|
42665
42701
|
try {
|
|
42666
|
-
return await this.requestOnce(
|
|
42702
|
+
return await this.requestOnce(
|
|
42703
|
+
method,
|
|
42704
|
+
path,
|
|
42705
|
+
body,
|
|
42706
|
+
multipart,
|
|
42707
|
+
signal
|
|
42708
|
+
);
|
|
42667
42709
|
} catch (err) {
|
|
42668
42710
|
lastError = err;
|
|
42669
|
-
if (attempt ===
|
|
42711
|
+
if (attempt === retries || signal?.aborted || !this.isRetryable(err, method)) {
|
|
42670
42712
|
throw err;
|
|
42671
42713
|
}
|
|
42672
42714
|
const delay = this.computeBackoff(attempt, err);
|
|
42673
|
-
if (delay > 0)
|
|
42674
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
42675
|
-
}
|
|
42715
|
+
if (delay > 0) await sleep2(delay, signal);
|
|
42676
42716
|
}
|
|
42677
42717
|
}
|
|
42678
42718
|
throw lastError;
|
|
42679
42719
|
}
|
|
42680
|
-
async requestOnce(method, path, body, multipart = false) {
|
|
42681
|
-
const url2 = `${this.baseUrl}${path}`;
|
|
42720
|
+
async requestOnce(method, path, body, multipart = false, signal) {
|
|
42682
42721
|
const controller = new AbortController();
|
|
42722
|
+
const detach = relayAbort(signal, controller);
|
|
42683
42723
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
42724
|
+
try {
|
|
42725
|
+
return await this.sendOnce(
|
|
42726
|
+
method,
|
|
42727
|
+
path,
|
|
42728
|
+
body,
|
|
42729
|
+
multipart,
|
|
42730
|
+
controller,
|
|
42731
|
+
signal
|
|
42732
|
+
);
|
|
42733
|
+
} finally {
|
|
42734
|
+
clearTimeout(timer);
|
|
42735
|
+
detach();
|
|
42736
|
+
}
|
|
42737
|
+
}
|
|
42738
|
+
async sendOnce(method, path, body, multipart, controller, callerSignal) {
|
|
42739
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
42684
42740
|
let response;
|
|
42685
42741
|
try {
|
|
42686
42742
|
response = await fetch(url2, {
|
|
@@ -42690,8 +42746,8 @@ var FrappeClient = class {
|
|
|
42690
42746
|
signal: controller.signal
|
|
42691
42747
|
});
|
|
42692
42748
|
} catch (err) {
|
|
42693
|
-
clearTimeout(timer);
|
|
42694
42749
|
if (err instanceof Error && err.name === "AbortError") {
|
|
42750
|
+
if (callerSignal?.aborted) throw callerAbortError(method, path);
|
|
42695
42751
|
throw new FrappeAPIError(
|
|
42696
42752
|
`Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
|
|
42697
42753
|
408,
|
|
@@ -42704,8 +42760,20 @@ var FrappeClient = class {
|
|
|
42704
42760
|
null
|
|
42705
42761
|
);
|
|
42706
42762
|
}
|
|
42707
|
-
|
|
42708
|
-
|
|
42763
|
+
let rawText;
|
|
42764
|
+
try {
|
|
42765
|
+
rawText = await response.text();
|
|
42766
|
+
} catch (err) {
|
|
42767
|
+
if (callerSignal?.aborted) throw callerAbortError(method, path);
|
|
42768
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
42769
|
+
throw new FrappeAPIError(
|
|
42770
|
+
`Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
|
|
42771
|
+
408,
|
|
42772
|
+
null
|
|
42773
|
+
);
|
|
42774
|
+
}
|
|
42775
|
+
throw err;
|
|
42776
|
+
}
|
|
42709
42777
|
const contentType = response.headers.get("content-type") ?? "";
|
|
42710
42778
|
let responseBody = rawText;
|
|
42711
42779
|
if (contentType.includes("application/json") && rawText.length > 0) {
|
|
@@ -43145,8 +43213,11 @@ var FrappeClient = class {
|
|
|
43145
43213
|
* its `message`. Needed for methods that fill `frappe.response` rather than
|
|
43146
43214
|
* returning a value, such as `frappe.desk.form.load.getdoctype` (`docs` at
|
|
43147
43215
|
* the top level, no `message`).
|
|
43216
|
+
*
|
|
43217
|
+
* `options` borne un appel facultatif : `{ retry: false }` coupe les reprises
|
|
43218
|
+
* et `signal` annule la requête en cours.
|
|
43148
43219
|
*/
|
|
43149
|
-
async callMethodRaw(method, args = {}, httpMethod = "POST") {
|
|
43220
|
+
async callMethodRaw(method, args = {}, httpMethod = "POST", options = {}) {
|
|
43150
43221
|
if (httpMethod === "GET") {
|
|
43151
43222
|
const params = new URLSearchParams();
|
|
43152
43223
|
for (const [key, value] of Object.entries(args)) {
|
|
@@ -43157,9 +43228,21 @@ var FrappeClient = class {
|
|
|
43157
43228
|
);
|
|
43158
43229
|
}
|
|
43159
43230
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
43160
|
-
return await this.request(
|
|
43231
|
+
return await this.request(
|
|
43232
|
+
"GET",
|
|
43233
|
+
`/api/method/${method}${query}`,
|
|
43234
|
+
void 0,
|
|
43235
|
+
false,
|
|
43236
|
+
options
|
|
43237
|
+
);
|
|
43161
43238
|
}
|
|
43162
|
-
return await this.request(
|
|
43239
|
+
return await this.request(
|
|
43240
|
+
"POST",
|
|
43241
|
+
`/api/method/${method}`,
|
|
43242
|
+
args,
|
|
43243
|
+
false,
|
|
43244
|
+
options
|
|
43245
|
+
);
|
|
43163
43246
|
}
|
|
43164
43247
|
};
|
|
43165
43248
|
var _client = null;
|
|
@@ -55074,6 +55157,7 @@ function withUiRefreshRequest(result, toolName, args, now = /* @__PURE__ */ new
|
|
|
55074
55157
|
var DOCTYPE_LAYOUT_TTL_MS = 60 * 60 * 1e3;
|
|
55075
55158
|
var DOCTYPE_LAYOUT_FAILURE_TTL_MS = 60 * 1e3;
|
|
55076
55159
|
var DOCTYPE_LAYOUT_MAX_ENTRIES = 200;
|
|
55160
|
+
var DOCTYPE_LAYOUT_TIMEOUT_MS = 1500;
|
|
55077
55161
|
var DOCTYPE_LAYOUT_SKIPPED = /* @__PURE__ */ new Set([
|
|
55078
55162
|
"Company",
|
|
55079
55163
|
"DocType"
|
|
@@ -55217,12 +55301,25 @@ async function fetchDocTypeLayout(client, doctype, options = {}) {
|
|
|
55217
55301
|
const memo3 = options.memo ?? docTypeLayoutMemo;
|
|
55218
55302
|
const remembered = memo3.get(doctype);
|
|
55219
55303
|
if (remembered !== void 0) return remembered;
|
|
55304
|
+
const timeoutMs = options.timeoutMs ?? DOCTYPE_LAYOUT_TIMEOUT_MS;
|
|
55305
|
+
const controller = new AbortController();
|
|
55306
|
+
let timer;
|
|
55307
|
+
const budget = new Promise((_resolve, reject) => {
|
|
55308
|
+
timer = setTimeout(() => {
|
|
55309
|
+
reject(new Error(`getdoctype exceeded the ${timeoutMs}ms budget`));
|
|
55310
|
+
controller.abort();
|
|
55311
|
+
}, timeoutMs);
|
|
55312
|
+
});
|
|
55220
55313
|
try {
|
|
55221
|
-
const response = await
|
|
55222
|
-
|
|
55223
|
-
|
|
55224
|
-
|
|
55225
|
-
|
|
55314
|
+
const response = await Promise.race([
|
|
55315
|
+
client.callMethodRaw(
|
|
55316
|
+
GETDOCTYPE_METHOD,
|
|
55317
|
+
{ doctype },
|
|
55318
|
+
"GET",
|
|
55319
|
+
{ signal: controller.signal, retry: false }
|
|
55320
|
+
),
|
|
55321
|
+
budget
|
|
55322
|
+
]);
|
|
55226
55323
|
const layout = docTypeLayoutOf(response);
|
|
55227
55324
|
if (!layout) throw new Error("unreadable getdoctype response");
|
|
55228
55325
|
memo3.set(doctype, layout);
|
|
@@ -55233,6 +55330,8 @@ async function fetchDocTypeLayout(client, doctype, options = {}) {
|
|
|
55233
55330
|
);
|
|
55234
55331
|
memo3.set(doctype, null);
|
|
55235
55332
|
return null;
|
|
55333
|
+
} finally {
|
|
55334
|
+
clearTimeout(timer);
|
|
55236
55335
|
}
|
|
55237
55336
|
}
|
|
55238
55337
|
|
|
@@ -56047,6 +56146,9 @@ function loadMrtrConfig() {
|
|
|
56047
56146
|
return { signingKey };
|
|
56048
56147
|
}
|
|
56049
56148
|
|
|
56149
|
+
// src/server-instructions.ts
|
|
56150
|
+
var ACTIVE_CONTEXT_INSTRUCTIONS = 'User selections in ERPNext viewers are shared as model context on compatible hosts with schema "casys.erpnext/active-context", version 2, and an items array of {id, view, label, value?}. This context identifies the documents and values the user is referring to. Document selections have IDs of the form record:<doctype>:<name>. Their <name> component is accepted as the name argument by the matching *_get tool; erpnext_doc_get accepts <doctype> and <name> as separate arguments. Child-row selections add :row:<table>:<row> to the document ID.';
|
|
56151
|
+
|
|
56050
56152
|
// server.ts
|
|
56051
56153
|
var DEFAULT_HTTP_PORT = 3012;
|
|
56052
56154
|
function registerBuyViewAppManifest(server) {
|
|
@@ -56108,7 +56210,8 @@ async function main() {
|
|
|
56108
56210
|
assertNonEmptySelection(toolsClient.selection);
|
|
56109
56211
|
const server = new McpApp({
|
|
56110
56212
|
name: "mcp-erpnext",
|
|
56111
|
-
version: "3.2.
|
|
56213
|
+
version: "3.2.3",
|
|
56214
|
+
instructions: ACTIVE_CONTEXT_INSTRUCTIONS,
|
|
56112
56215
|
transport: "stateless",
|
|
56113
56216
|
cache: {
|
|
56114
56217
|
ttlMs: 36e5,
|