@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 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.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.1/server`.
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 <= 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);
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
- clearTimeout(timer);
42708
- const rawText = await response.text();
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("GET", `/api/method/${method}${query}`);
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("POST", `/api/method/${method}`, args);
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 client.callMethodRaw(
55222
- GETDOCTYPE_METHOD,
55223
- { doctype },
55224
- "GET"
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.1",
56213
+ version: "3.2.3",
56214
+ instructions: ACTIVE_CONTEXT_INSTRUCTIONS,
56112
56215
  transport: "stateless",
56113
56216
  cache: {
56114
56217
  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.3",
4
4
  "description": "MCP server for ERPNext with interactive UI viewers",
5
5
  "type": "module",
6
6
  "bin": {