@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 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.1/server`. 3.2.1 is the
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.1/server`.
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 <= this.retries; 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(method, path, body, multipart);
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 === this.retries || !this.isRetryable(err, method)) {
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
- const rawText = await response.text();
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("GET", `/api/method/${method}${query}`);
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("POST", `/api/method/${method}`, args);
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 client.callMethodRaw(
55222
- GETDOCTYPE_METHOD,
55223
- { doctype },
55224
- "GET"
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.1",
56204
+ version: "3.2.2",
56112
56205
  transport: "stateless",
56113
56206
  cache: {
56114
56207
  ttlMs: 36e5,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@casys/mcp-erpnext",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {