@casys/mcp-erpnext 3.2.1 → 3.2.2
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 +113 -20
- package/package.json +1 -1
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.2/server`. 3.2.2 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.2/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,27 +42692,49 @@ 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);
|
|
42723
|
+
try {
|
|
42724
|
+
return await this.sendOnce(
|
|
42725
|
+
method,
|
|
42726
|
+
path,
|
|
42727
|
+
body,
|
|
42728
|
+
multipart,
|
|
42729
|
+
controller,
|
|
42730
|
+
signal
|
|
42731
|
+
);
|
|
42732
|
+
} finally {
|
|
42733
|
+
detach();
|
|
42734
|
+
}
|
|
42735
|
+
}
|
|
42736
|
+
async sendOnce(method, path, body, multipart, controller, callerSignal) {
|
|
42737
|
+
const url2 = `${this.baseUrl}${path}`;
|
|
42683
42738
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
42684
42739
|
let response;
|
|
42685
42740
|
try {
|
|
@@ -42692,6 +42747,7 @@ var FrappeClient = class {
|
|
|
42692
42747
|
} catch (err) {
|
|
42693
42748
|
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,
|
|
@@ -42705,7 +42761,13 @@ var FrappeClient = class {
|
|
|
42705
42761
|
);
|
|
42706
42762
|
}
|
|
42707
42763
|
clearTimeout(timer);
|
|
42708
|
-
|
|
42764
|
+
let rawText;
|
|
42765
|
+
try {
|
|
42766
|
+
rawText = await response.text();
|
|
42767
|
+
} catch (err) {
|
|
42768
|
+
if (callerSignal?.aborted) throw callerAbortError(method, path);
|
|
42769
|
+
throw err;
|
|
42770
|
+
}
|
|
42709
42771
|
const contentType = response.headers.get("content-type") ?? "";
|
|
42710
42772
|
let responseBody = rawText;
|
|
42711
42773
|
if (contentType.includes("application/json") && rawText.length > 0) {
|
|
@@ -43145,8 +43207,11 @@ var FrappeClient = class {
|
|
|
43145
43207
|
* its `message`. Needed for methods that fill `frappe.response` rather than
|
|
43146
43208
|
* returning a value, such as `frappe.desk.form.load.getdoctype` (`docs` at
|
|
43147
43209
|
* the top level, no `message`).
|
|
43210
|
+
*
|
|
43211
|
+
* `options` borne un appel facultatif : `{ retry: false }` coupe les reprises
|
|
43212
|
+
* et `signal` annule la requête en cours.
|
|
43148
43213
|
*/
|
|
43149
|
-
async callMethodRaw(method, args = {}, httpMethod = "POST") {
|
|
43214
|
+
async callMethodRaw(method, args = {}, httpMethod = "POST", options = {}) {
|
|
43150
43215
|
if (httpMethod === "GET") {
|
|
43151
43216
|
const params = new URLSearchParams();
|
|
43152
43217
|
for (const [key, value] of Object.entries(args)) {
|
|
@@ -43157,9 +43222,21 @@ var FrappeClient = class {
|
|
|
43157
43222
|
);
|
|
43158
43223
|
}
|
|
43159
43224
|
const query = params.toString() ? `?${params.toString()}` : "";
|
|
43160
|
-
return await this.request(
|
|
43225
|
+
return await this.request(
|
|
43226
|
+
"GET",
|
|
43227
|
+
`/api/method/${method}${query}`,
|
|
43228
|
+
void 0,
|
|
43229
|
+
false,
|
|
43230
|
+
options
|
|
43231
|
+
);
|
|
43161
43232
|
}
|
|
43162
|
-
return await this.request(
|
|
43233
|
+
return await this.request(
|
|
43234
|
+
"POST",
|
|
43235
|
+
`/api/method/${method}`,
|
|
43236
|
+
args,
|
|
43237
|
+
false,
|
|
43238
|
+
options
|
|
43239
|
+
);
|
|
43163
43240
|
}
|
|
43164
43241
|
};
|
|
43165
43242
|
var _client = null;
|
|
@@ -55074,6 +55151,7 @@ function withUiRefreshRequest(result, toolName, args, now = /* @__PURE__ */ new
|
|
|
55074
55151
|
var DOCTYPE_LAYOUT_TTL_MS = 60 * 60 * 1e3;
|
|
55075
55152
|
var DOCTYPE_LAYOUT_FAILURE_TTL_MS = 60 * 1e3;
|
|
55076
55153
|
var DOCTYPE_LAYOUT_MAX_ENTRIES = 200;
|
|
55154
|
+
var DOCTYPE_LAYOUT_TIMEOUT_MS = 1500;
|
|
55077
55155
|
var DOCTYPE_LAYOUT_SKIPPED = /* @__PURE__ */ new Set([
|
|
55078
55156
|
"Company",
|
|
55079
55157
|
"DocType"
|
|
@@ -55217,12 +55295,25 @@ async function fetchDocTypeLayout(client, doctype, options = {}) {
|
|
|
55217
55295
|
const memo3 = options.memo ?? docTypeLayoutMemo;
|
|
55218
55296
|
const remembered = memo3.get(doctype);
|
|
55219
55297
|
if (remembered !== void 0) return remembered;
|
|
55298
|
+
const timeoutMs = options.timeoutMs ?? DOCTYPE_LAYOUT_TIMEOUT_MS;
|
|
55299
|
+
const controller = new AbortController();
|
|
55300
|
+
let timer;
|
|
55301
|
+
const budget = new Promise((_resolve, reject) => {
|
|
55302
|
+
timer = setTimeout(() => {
|
|
55303
|
+
reject(new Error(`getdoctype exceeded the ${timeoutMs}ms budget`));
|
|
55304
|
+
controller.abort();
|
|
55305
|
+
}, timeoutMs);
|
|
55306
|
+
});
|
|
55220
55307
|
try {
|
|
55221
|
-
const response = await
|
|
55222
|
-
|
|
55223
|
-
|
|
55224
|
-
|
|
55225
|
-
|
|
55308
|
+
const response = await Promise.race([
|
|
55309
|
+
client.callMethodRaw(
|
|
55310
|
+
GETDOCTYPE_METHOD,
|
|
55311
|
+
{ doctype },
|
|
55312
|
+
"GET",
|
|
55313
|
+
{ signal: controller.signal, retry: false }
|
|
55314
|
+
),
|
|
55315
|
+
budget
|
|
55316
|
+
]);
|
|
55226
55317
|
const layout = docTypeLayoutOf(response);
|
|
55227
55318
|
if (!layout) throw new Error("unreadable getdoctype response");
|
|
55228
55319
|
memo3.set(doctype, layout);
|
|
@@ -55233,6 +55324,8 @@ async function fetchDocTypeLayout(client, doctype, options = {}) {
|
|
|
55233
55324
|
);
|
|
55234
55325
|
memo3.set(doctype, null);
|
|
55235
55326
|
return null;
|
|
55327
|
+
} finally {
|
|
55328
|
+
clearTimeout(timer);
|
|
55236
55329
|
}
|
|
55237
55330
|
}
|
|
55238
55331
|
|
|
@@ -56108,7 +56201,7 @@ async function main() {
|
|
|
56108
56201
|
assertNonEmptySelection(toolsClient.selection);
|
|
56109
56202
|
const server = new McpApp({
|
|
56110
56203
|
name: "mcp-erpnext",
|
|
56111
|
-
version: "3.2.
|
|
56204
|
+
version: "3.2.2",
|
|
56112
56205
|
transport: "stateless",
|
|
56113
56206
|
cache: {
|
|
56114
56207
|
ttlMs: 36e5,
|