@casys/mcp-erpnext 2.4.1 → 2.5.0

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.
Files changed (3) hide show
  1. package/README.md +42 -29
  2. package/mcp-erpnext.mjs +903 -351
  3. package/package.json +1 -1
package/mcp-erpnext.mjs CHANGED
@@ -3775,6 +3775,7 @@ var require_fast_uri = __commonJS({
3775
3775
  return uriTokens.join("");
3776
3776
  }
3777
3777
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3778
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3778
3779
  function getParseError(parsed, matches) {
3779
3780
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3780
3781
  return 'URI path must start with "/" when authority is present.';
@@ -3804,6 +3805,11 @@ var require_fast_uri = __commonJS({
3804
3805
  uri = "//" + uri;
3805
3806
  }
3806
3807
  }
3808
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
3809
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
3810
+ parsed.error = "URI authority must not contain a literal backslash.";
3811
+ malformedAuthorityOrPort = true;
3812
+ }
3807
3813
  const matches = uri.match(URI_PARSE);
3808
3814
  if (matches) {
3809
3815
  parsed.scheme = matches[1];
@@ -29680,6 +29686,12 @@ var parseBody = async (request, options = /* @__PURE__ */ Object.create(null)) =
29680
29686
  return {};
29681
29687
  };
29682
29688
  async function parseFormData(request, options) {
29689
+ if (!isRawRequest(request) && request.bodyCache.formData) {
29690
+ return convertFormDataToBodyData(
29691
+ await request.bodyCache.formData,
29692
+ options
29693
+ );
29694
+ }
29683
29695
  const headers = isRawRequest(request) ? request.headers : request.raw.headers;
29684
29696
  const arrayBuffer = await request.arrayBuffer();
29685
29697
  const formDataPromise = bufferToFormData(arrayBuffer, headers.get("Content-Type") || "");
@@ -37377,6 +37389,516 @@ var KANBAN_META = viewer("kanban-viewer");
37377
37389
  var KPI_META = viewer("kpi-viewer");
37378
37390
  var FUNNEL_META = viewer("funnel-viewer");
37379
37391
 
37392
+ // src/runtime.ts
37393
+ var isDeno2 = typeof globalThis.Deno?.version?.deno === "string";
37394
+ var impl2 = isDeno2 ? await Promise.resolve().then(() => (init_runtime_deno2(), runtime_deno_exports2)) : await Promise.resolve().then(() => (init_runtime_node2(), runtime_node_exports2));
37395
+ var env6 = impl2.env;
37396
+ var readTextFile6 = impl2.readTextFile;
37397
+ var statSync3 = impl2.statSync;
37398
+ var readDirSync3 = impl2.readDirSync;
37399
+ var getArgs3 = impl2.getArgs;
37400
+ var exit3 = impl2.exit;
37401
+ var onSignal3 = impl2.onSignal;
37402
+
37403
+ // src/cache/memory.ts
37404
+ var MemoryCache = class {
37405
+ store = /* @__PURE__ */ new Map();
37406
+ get(key) {
37407
+ const entry = this.store.get(key);
37408
+ if (!entry) return void 0;
37409
+ if (Date.now() >= entry.expiresAt) {
37410
+ this.store.delete(key);
37411
+ return void 0;
37412
+ }
37413
+ return structuredClone(entry.value);
37414
+ }
37415
+ set(key, value, ttlMs) {
37416
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
37417
+ }
37418
+ delete(key) {
37419
+ this.store.delete(key);
37420
+ }
37421
+ deleteByPrefix(prefix) {
37422
+ for (const key of this.store.keys()) {
37423
+ if (key.startsWith(prefix)) this.store.delete(key);
37424
+ }
37425
+ }
37426
+ clear() {
37427
+ this.store.clear();
37428
+ }
37429
+ };
37430
+
37431
+ // src/cache/noop.ts
37432
+ var NoopCache = class {
37433
+ get(_key) {
37434
+ return void 0;
37435
+ }
37436
+ set(_key, _value, _ttlMs) {
37437
+ }
37438
+ delete(_key) {
37439
+ }
37440
+ deleteByPrefix(_prefix) {
37441
+ }
37442
+ clear() {
37443
+ }
37444
+ };
37445
+
37446
+ // src/cache/cache.ts
37447
+ var DEFAULT_CACHE_TTL_MS = 15e3;
37448
+ var _cache = null;
37449
+ function getCacheTtlMs() {
37450
+ const raw2 = env6("MCP_CACHE_TTL_MS");
37451
+ if (raw2 === void 0) return DEFAULT_CACHE_TTL_MS;
37452
+ const parsed = Number(raw2);
37453
+ if (!Number.isFinite(parsed) || parsed < 0) {
37454
+ console.error(
37455
+ `[mcp-erpnext] Invalid MCP_CACHE_TTL_MS=${JSON.stringify(raw2)} \u2014 must be a non-negative number. Falling back to default (${DEFAULT_CACHE_TTL_MS}ms).`
37456
+ );
37457
+ return DEFAULT_CACHE_TTL_MS;
37458
+ }
37459
+ return parsed;
37460
+ }
37461
+ function getCache() {
37462
+ if (_cache) return _cache;
37463
+ const enabled = env6("MCP_CACHE_ENABLED") !== "false";
37464
+ _cache = enabled ? new MemoryCache() : new NoopCache();
37465
+ return _cache;
37466
+ }
37467
+
37468
+ // src/api/frappe-client.ts
37469
+ function stableStringify(value) {
37470
+ if (Array.isArray(value)) {
37471
+ return `[${value.map(stableStringify).join(",")}]`;
37472
+ }
37473
+ if (value !== null && typeof value === "object") {
37474
+ const keys = Object.keys(value).sort();
37475
+ return `{${keys.map(
37476
+ (k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`
37477
+ ).join(",")}}`;
37478
+ }
37479
+ return JSON.stringify(value);
37480
+ }
37481
+ var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
37482
+ var DEFAULT_RETRY_METHODS = ["GET"];
37483
+ var FrappeAPIError = class extends Error {
37484
+ /**
37485
+ * @param message - Human-readable error description
37486
+ * @param status - HTTP status code (0 for network errors, 408 for timeouts)
37487
+ * @param body - Raw response body (parsed JSON object or plain text string)
37488
+ * @param retryAfterMs - When the server sent a `Retry-After` header on a
37489
+ * retryable status (typically 429), the parsed delay
37490
+ * in ms. Used by the retry loop; absent otherwise.
37491
+ */
37492
+ constructor(message2, status, body, retryAfterMs) {
37493
+ super(`[FrappeClient] ${message2} (HTTP ${status})`);
37494
+ this.status = status;
37495
+ this.body = body;
37496
+ this.retryAfterMs = retryAfterMs;
37497
+ this.name = "FrappeAPIError";
37498
+ }
37499
+ };
37500
+ function parseRetryAfter(raw2) {
37501
+ if (!raw2) return void 0;
37502
+ const trimmed = raw2.trim();
37503
+ const seconds = Number(trimmed);
37504
+ if (Number.isFinite(seconds) && seconds >= 0) {
37505
+ return Math.round(seconds * 1e3);
37506
+ }
37507
+ const date3 = Date.parse(trimmed);
37508
+ if (Number.isFinite(date3)) {
37509
+ return Math.max(0, date3 - Date.now());
37510
+ }
37511
+ return void 0;
37512
+ }
37513
+ function extractServerMessages(raw2) {
37514
+ if (!raw2 || typeof raw2 !== "string") return void 0;
37515
+ try {
37516
+ const outer = JSON.parse(raw2);
37517
+ if (!Array.isArray(outer)) return void 0;
37518
+ const msgs = [];
37519
+ for (const item of outer) {
37520
+ try {
37521
+ const inner = JSON.parse(item);
37522
+ if (typeof inner === "object" && inner?.message) {
37523
+ msgs.push(inner.message);
37524
+ } else if (typeof inner === "string") {
37525
+ msgs.push(inner);
37526
+ }
37527
+ } catch {
37528
+ if (typeof item === "string") msgs.push(item);
37529
+ }
37530
+ }
37531
+ return msgs.length > 0 ? msgs.join("; ") : void 0;
37532
+ } catch {
37533
+ return void 0;
37534
+ }
37535
+ }
37536
+ var FrappeClient = class {
37537
+ baseUrl;
37538
+ authHeader;
37539
+ timeoutMs;
37540
+ retries;
37541
+ retryStatuses;
37542
+ retryBackoffMs;
37543
+ retryMethods;
37544
+ cache;
37545
+ constructor(config2) {
37546
+ this.baseUrl = config2.baseUrl.replace(/\/$/, "");
37547
+ this.authHeader = `token ${config2.apiKey}:${config2.apiSecret}`;
37548
+ this.timeoutMs = config2.timeoutMs ?? 3e4;
37549
+ this.retries = config2.retries ?? 3;
37550
+ this.retryStatuses = config2.retryStatuses ?? DEFAULT_RETRY_STATUSES;
37551
+ this.retryBackoffMs = config2.retryBackoffMs ?? 200;
37552
+ this.retryMethods = config2.retryMethods ?? DEFAULT_RETRY_METHODS;
37553
+ this.cache = config2.cache ?? new MemoryCache();
37554
+ }
37555
+ // ── Private HTTP helpers ────────────────────────────────────────────────────
37556
+ buildHeaders() {
37557
+ return {
37558
+ "Authorization": this.authHeader,
37559
+ "Accept": "application/json",
37560
+ "Content-Type": "application/json"
37561
+ };
37562
+ }
37563
+ /**
37564
+ * Decide whether an error is worth retrying.
37565
+ * Network errors (status 0) and timeouts (status 408) are always retryable;
37566
+ * other statuses are checked against `retryStatuses`.
37567
+ */
37568
+ isRetryable(err, method) {
37569
+ if (!this.retryMethods.includes(method)) return false;
37570
+ if (!(err instanceof FrappeAPIError)) return false;
37571
+ if (err.status === 0) return true;
37572
+ return this.retryStatuses.includes(err.status);
37573
+ }
37574
+ /** Compute the backoff delay for a given attempt (0-indexed). */
37575
+ computeBackoff(attempt, err) {
37576
+ if (err instanceof FrappeAPIError && err.retryAfterMs !== void 0) {
37577
+ return err.retryAfterMs;
37578
+ }
37579
+ return this.retryBackoffMs * 2 ** attempt;
37580
+ }
37581
+ async request(method, path, body) {
37582
+ let lastError;
37583
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
37584
+ try {
37585
+ return await this.requestOnce(method, path, body);
37586
+ } catch (err) {
37587
+ lastError = err;
37588
+ if (attempt === this.retries || !this.isRetryable(err, method)) {
37589
+ throw err;
37590
+ }
37591
+ const delay = this.computeBackoff(attempt, err);
37592
+ if (delay > 0) {
37593
+ await new Promise((resolve) => setTimeout(resolve, delay));
37594
+ }
37595
+ }
37596
+ }
37597
+ throw lastError;
37598
+ }
37599
+ async requestOnce(method, path, body) {
37600
+ const url = `${this.baseUrl}${path}`;
37601
+ const controller = new AbortController();
37602
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
37603
+ let response;
37604
+ try {
37605
+ response = await fetch(url, {
37606
+ method,
37607
+ headers: this.buildHeaders(),
37608
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
37609
+ signal: controller.signal
37610
+ });
37611
+ } catch (err) {
37612
+ clearTimeout(timer);
37613
+ if (err instanceof Error && err.name === "AbortError") {
37614
+ throw new FrappeAPIError(
37615
+ `Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
37616
+ 408,
37617
+ null
37618
+ );
37619
+ }
37620
+ throw new FrappeAPIError(
37621
+ `Network error on ${method} ${path}: ${err.message}`,
37622
+ 0,
37623
+ null
37624
+ );
37625
+ }
37626
+ clearTimeout(timer);
37627
+ const rawText = await response.text();
37628
+ const contentType = response.headers.get("content-type") ?? "";
37629
+ let responseBody = rawText;
37630
+ if (contentType.includes("application/json") && rawText.length > 0) {
37631
+ try {
37632
+ responseBody = JSON.parse(rawText);
37633
+ } catch {
37634
+ responseBody = rawText;
37635
+ }
37636
+ }
37637
+ if (!response.ok) {
37638
+ let msg = response.statusText;
37639
+ if (typeof responseBody === "object" && responseBody !== null) {
37640
+ const rb = responseBody;
37641
+ const excType = rb.exc_type;
37642
+ const baseMsg = rb.message ?? excType ?? response.statusText;
37643
+ const serverDetails = extractServerMessages(rb._server_messages);
37644
+ msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
37645
+ } else if (typeof responseBody === "string" && responseBody.length > 0) {
37646
+ msg = responseBody.slice(0, 200);
37647
+ }
37648
+ const retryAfterMs = parseRetryAfter(
37649
+ response.headers.get("retry-after")
37650
+ );
37651
+ throw new FrappeAPIError(
37652
+ `${method} ${path} failed: ${msg}`,
37653
+ response.status,
37654
+ responseBody,
37655
+ retryAfterMs
37656
+ );
37657
+ }
37658
+ return responseBody;
37659
+ }
37660
+ // ── Resource CRUD ───────────────────────────────────────────────────────────
37661
+ /**
37662
+ * List documents of a DocType.
37663
+ * Frappe list API: GET /api/resource/{doctype}?fields=...&filters=...
37664
+ *
37665
+ * Pass `{ skipCache: true }` to force a fresh read — e.g. for aggregate/KPI
37666
+ * tools that read across doctypes other than the one a preceding mutation
37667
+ * invalidated (see `invalidate()` below for why that gap exists). The fresh
37668
+ * result still refreshes the cache for subsequent normal reads.
37669
+ */
37670
+ async list(doctype, options = {}, opts = {}) {
37671
+ const cacheKey = `list:${doctype}:${stableStringify(options)}`;
37672
+ if (!opts.skipCache) {
37673
+ const cached2 = this.cache.get(cacheKey);
37674
+ if (cached2 !== void 0) return cached2;
37675
+ }
37676
+ const params = new URLSearchParams();
37677
+ if (options.fields && options.fields.length > 0) {
37678
+ params.set("fields", JSON.stringify(options.fields));
37679
+ }
37680
+ if (options.filters && options.filters.length > 0) {
37681
+ params.set("filters", JSON.stringify(options.filters));
37682
+ }
37683
+ if (options.order_by) {
37684
+ params.set("order_by", options.order_by);
37685
+ }
37686
+ if (options.limit !== void 0) {
37687
+ params.set("limit", String(options.limit));
37688
+ }
37689
+ if (options.limit_start !== void 0) {
37690
+ params.set("limit_start", String(options.limit_start));
37691
+ }
37692
+ params.set("as_dict", "1");
37693
+ const query = params.toString() ? `?${params.toString()}` : "";
37694
+ const res = await this.request(
37695
+ "GET",
37696
+ `/api/resource/${encodeURIComponent(doctype)}${query}`
37697
+ );
37698
+ const docs = res.data ?? [];
37699
+ this.cache.set(cacheKey, docs, getCacheTtlMs());
37700
+ return docs;
37701
+ }
37702
+ /**
37703
+ * Get a single document by name.
37704
+ * GET /api/resource/{doctype}/{name}
37705
+ *
37706
+ * Pass `{ skipCache: true }` to force a fresh read — required before any
37707
+ * operation relying on the doc's `modified` timestamp for optimistic
37708
+ * locking (see erpnext_doc_submit/erpnext_doc_cancel in operations.ts).
37709
+ * The fresh result still refreshes the cache for subsequent normal reads.
37710
+ */
37711
+ async get(doctype, name, opts = {}) {
37712
+ const cacheKey = `get:${doctype}:${name}`;
37713
+ if (!opts.skipCache) {
37714
+ const cached2 = this.cache.get(cacheKey);
37715
+ if (cached2 !== void 0) return cached2;
37716
+ }
37717
+ const res = await this.request(
37718
+ "GET",
37719
+ `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
37720
+ );
37721
+ this.cache.set(cacheKey, res.data, getCacheTtlMs());
37722
+ return res.data;
37723
+ }
37724
+ /**
37725
+ * Clear cached entries for a doctype (list results, resolveLink's
37726
+ * negative-match cache) and, if `name` is given, the cached single-document
37727
+ * read too. Called automatically after create/update/delete; call explicitly
37728
+ * after any mutation that bypasses those methods (e.g.
37729
+ * frappe.client.submit/cancel via callMethod).
37730
+ *
37731
+ * Known limitation: this only clears the mutated doctype. Frappe mutations
37732
+ * commonly cascade — submitting a Sales Order also writes Bin/GL
37733
+ * Entry/Sales Invoice rows — and those doctypes' cached `list()` results
37734
+ * aren't invalidated here. Aggregate/KPI tools that read across doctypes
37735
+ * can therefore serve up-to-TTL-stale numbers right after a mutation; pass
37736
+ * `{ skipCache: true }` to `list()` in those tools if that staleness isn't
37737
+ * acceptable for a given call site.
37738
+ */
37739
+ invalidate(doctype, name) {
37740
+ this.cache.deleteByPrefix(`list:${doctype}:`);
37741
+ this.cache.deleteByPrefix(`resolve:miss:${doctype}:`);
37742
+ if (name) this.cache.delete(`get:${doctype}:${name}`);
37743
+ }
37744
+ /**
37745
+ * Create a new document.
37746
+ * POST /api/resource/{doctype}
37747
+ */
37748
+ async create(doctype, data) {
37749
+ const res = await this.request(
37750
+ "POST",
37751
+ `/api/resource/${encodeURIComponent(doctype)}`,
37752
+ { data: { ...data, doctype } }
37753
+ );
37754
+ this.invalidate(doctype, res.data.name);
37755
+ return res.data;
37756
+ }
37757
+ /**
37758
+ * Update an existing document (partial update).
37759
+ * PUT /api/resource/{doctype}/{name}
37760
+ */
37761
+ async update(doctype, name, data) {
37762
+ const res = await this.request(
37763
+ "PUT",
37764
+ `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`,
37765
+ { data }
37766
+ );
37767
+ this.invalidate(doctype, name);
37768
+ return res.data;
37769
+ }
37770
+ /**
37771
+ * Delete a document.
37772
+ * DELETE /api/resource/{doctype}/{name}
37773
+ */
37774
+ async delete(doctype, name) {
37775
+ await this.request(
37776
+ "DELETE",
37777
+ `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
37778
+ );
37779
+ this.invalidate(doctype, name);
37780
+ }
37781
+ /**
37782
+ * Call a whitelisted Frappe method.
37783
+ * POST /api/method/{method}
37784
+ */
37785
+ async callMethod(method, args = {}) {
37786
+ const res = await this.request(
37787
+ "POST",
37788
+ `/api/method/${method}`,
37789
+ args
37790
+ );
37791
+ return res.message;
37792
+ }
37793
+ };
37794
+ var _client = null;
37795
+ function getFrappeClient() {
37796
+ if (_client) return _client;
37797
+ const url = env6("ERPNEXT_URL");
37798
+ const apiKey = env6("ERPNEXT_API_KEY");
37799
+ const apiSecret = env6("ERPNEXT_API_SECRET");
37800
+ if (!url) {
37801
+ throw new Error(
37802
+ "[lib/erpnext] ERPNEXT_URL is required. Set it to your ERPNext instance URL, e.g. http://localhost:8000"
37803
+ );
37804
+ }
37805
+ if (!apiKey || !apiSecret) {
37806
+ throw new Error(
37807
+ "[lib/erpnext] ERPNEXT_API_KEY and ERPNEXT_API_SECRET are required. Generate them in ERPNext: User Settings \u2192 API Access."
37808
+ );
37809
+ }
37810
+ _client = new FrappeClient({
37811
+ baseUrl: url,
37812
+ apiKey,
37813
+ apiSecret,
37814
+ cache: getCache()
37815
+ });
37816
+ return _client;
37817
+ }
37818
+
37819
+ // src/api/resolve.ts
37820
+ var NEGATIVE_CACHE_TTL_MS = 15e3;
37821
+ var EXACT_MATCH_PROBE_LIMIT = 2;
37822
+ var PARTIAL_MATCH_PROBE_LIMIT = 5;
37823
+ async function resolveUnique(client, doctype, identifier, searchField, op, value, probeLimit, ambiguityHint) {
37824
+ const rows = await client.list(doctype, {
37825
+ filters: [[searchField, op, value]],
37826
+ fields: ["name", searchField],
37827
+ limit: probeLimit
37828
+ });
37829
+ if (rows.length === 0) return void 0;
37830
+ if (rows.length === 1) return rows[0].name;
37831
+ const candidates = rows.map((r) => `${r.name} (${r[searchField]})`).join(
37832
+ ", "
37833
+ );
37834
+ const suffix = rows.length === probeLimit ? ", and possibly more" : "";
37835
+ throw new Error(
37836
+ `[resolveLink] Ambiguous ${doctype} identifier "${identifier}": did you mean ${candidates}${suffix}? ${ambiguityHint}`
37837
+ );
37838
+ }
37839
+ async function resolveLink(client, doctype, identifier, searchField, options = {}) {
37840
+ const { allowPartialMatch = true } = options;
37841
+ const cache2 = getCache();
37842
+ const missKey = `resolve:miss:${doctype}:${identifier}`;
37843
+ if (cache2.get(missKey) === void 0) {
37844
+ try {
37845
+ await client.get(doctype, identifier);
37846
+ return identifier;
37847
+ } catch (e) {
37848
+ if (!(e instanceof FrappeAPIError) || e.status !== 404) throw e;
37849
+ cache2.set(missKey, true, NEGATIVE_CACHE_TTL_MS);
37850
+ }
37851
+ }
37852
+ const exact = await resolveUnique(
37853
+ client,
37854
+ doctype,
37855
+ identifier,
37856
+ searchField,
37857
+ "=",
37858
+ identifier,
37859
+ EXACT_MATCH_PROBE_LIMIT,
37860
+ "Please pass the record's ID directly."
37861
+ );
37862
+ if (exact !== void 0) return exact;
37863
+ if (allowPartialMatch) {
37864
+ const partial2 = await resolveUnique(
37865
+ client,
37866
+ doctype,
37867
+ identifier,
37868
+ searchField,
37869
+ "like",
37870
+ `%${identifier}%`,
37871
+ PARTIAL_MATCH_PROBE_LIMIT,
37872
+ "Please pass an exact value."
37873
+ );
37874
+ if (partial2 !== void 0) return partial2;
37875
+ }
37876
+ throw new Error(`[resolveLink] No ${doctype} found matching "${identifier}"`);
37877
+ }
37878
+ function resolveEmployee(client, identifier) {
37879
+ return resolveLink(client, "Employee", identifier, "employee_name");
37880
+ }
37881
+ function resolveCustomer(client, identifier) {
37882
+ return resolveLink(client, "Customer", identifier, "customer_name");
37883
+ }
37884
+ function resolveSupplier(client, identifier) {
37885
+ return resolveLink(client, "Supplier", identifier, "supplier_name");
37886
+ }
37887
+ function resolveItem(client, identifier) {
37888
+ return resolveLink(client, "Item", identifier, "item_name");
37889
+ }
37890
+ var DYNAMIC_LINK_SEARCH_FIELDS = {
37891
+ Customer: "customer_name",
37892
+ Supplier: "supplier_name",
37893
+ Employee: "employee_name",
37894
+ Lead: "lead_name"
37895
+ };
37896
+ async function resolveDynamicLink(client, targetDoctype, identifier, options = {}) {
37897
+ const searchField = DYNAMIC_LINK_SEARCH_FIELDS[targetDoctype];
37898
+ if (!searchField) return identifier;
37899
+ return resolveLink(client, targetDoctype, identifier, searchField, options);
37900
+ }
37901
+
37380
37902
  // src/tools/sales.ts
37381
37903
  function mapLineItems(items, options) {
37382
37904
  if (!Array.isArray(items) || items.length === 0) {
@@ -37580,7 +38102,10 @@ var salesTools = [
37580
38102
  type: "object",
37581
38103
  properties: {
37582
38104
  limit: { type: "number", description: "Max results (default 20)" },
37583
- customer: { type: "string", description: "Filter by customer" },
38105
+ customer: {
38106
+ type: "string",
38107
+ description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
38108
+ },
37584
38109
  status: {
37585
38110
  type: "string",
37586
38111
  description: "Filter by status (Draft, To Deliver and Bill, Completed, Cancelled, etc.)"
@@ -37596,7 +38121,11 @@ var salesTools = [
37596
38121
  const limit = input.limit ?? 20;
37597
38122
  const filters = [];
37598
38123
  if (input.customer) {
37599
- filters.push(["customer", "=", input.customer]);
38124
+ filters.push([
38125
+ "customer",
38126
+ "=",
38127
+ await resolveCustomer(ctx.client, input.customer)
38128
+ ]);
37600
38129
  }
37601
38130
  if (input.status) {
37602
38131
  filters.push(["status", "=", input.status]);
@@ -37860,7 +38389,10 @@ var salesTools = [
37860
38389
  type: "object",
37861
38390
  properties: {
37862
38391
  limit: { type: "number", description: "Max results (default 20)" },
37863
- customer: { type: "string", description: "Filter by customer" },
38392
+ customer: {
38393
+ type: "string",
38394
+ description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
38395
+ },
37864
38396
  status: {
37865
38397
  type: "string",
37866
38398
  description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
@@ -37876,7 +38408,11 @@ var salesTools = [
37876
38408
  const limit = input.limit ?? 20;
37877
38409
  const filters = [];
37878
38410
  if (input.customer) {
37879
- filters.push(["customer", "=", input.customer]);
38411
+ filters.push([
38412
+ "customer",
38413
+ "=",
38414
+ await resolveCustomer(ctx.client, input.customer)
38415
+ ]);
37880
38416
  }
37881
38417
  if (input.status) {
37882
38418
  filters.push(["status", "=", input.status]);
@@ -38064,22 +38600,54 @@ var salesTools = [
38064
38600
  type: "object",
38065
38601
  properties: {
38066
38602
  limit: { type: "number", description: "Max results (default 20)" },
38067
- party_name: { type: "string", description: "Filter by party name" },
38603
+ quotation_to: {
38604
+ type: "string",
38605
+ description: "Party type: Customer or Lead. Required when 'party_name' is set, so the party name/ID can be resolved against the right doctype.",
38606
+ enum: ["Customer", "Lead"]
38607
+ },
38608
+ party_name: {
38609
+ type: "string",
38610
+ description: "Filter by party \u2014 ID or name (e.g. customer/lead name). Requires 'quotation_to'."
38611
+ },
38068
38612
  status: {
38069
38613
  type: "string",
38070
38614
  description: "Filter by status (Draft, Open, Replied, Ordered, Lost, Cancelled)"
38071
- }
38615
+ },
38616
+ date_from: {
38617
+ type: "string",
38618
+ description: "Start date filter YYYY-MM-DD"
38619
+ },
38620
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
38072
38621
  }
38073
38622
  },
38074
38623
  handler: async (input, ctx) => {
38075
38624
  const limit = input.limit ?? 20;
38076
38625
  const filters = [];
38077
38626
  if (input.party_name) {
38078
- filters.push(["party_name", "=", input.party_name]);
38627
+ if (!input.quotation_to) {
38628
+ throw new Error(
38629
+ "[erpnext_quotation_list] 'quotation_to' is required when filtering by 'party_name'"
38630
+ );
38631
+ }
38632
+ filters.push([
38633
+ "party_name",
38634
+ "=",
38635
+ await resolveDynamicLink(
38636
+ ctx.client,
38637
+ input.quotation_to,
38638
+ input.party_name
38639
+ )
38640
+ ]);
38079
38641
  }
38080
38642
  if (input.status) {
38081
38643
  filters.push(["status", "=", input.status]);
38082
38644
  }
38645
+ if (input.date_from) {
38646
+ filters.push(["transaction_date", ">=", input.date_from]);
38647
+ }
38648
+ if (input.date_to) {
38649
+ filters.push(["transaction_date", "<=", input.date_to]);
38650
+ }
38083
38651
  const docs = await ctx.client.list("Quotation", {
38084
38652
  fields: [
38085
38653
  "name",
@@ -38139,7 +38707,7 @@ var salesTools = [
38139
38707
  },
38140
38708
  party_name: {
38141
38709
  type: "string",
38142
- description: "Customer or Lead name"
38710
+ description: "Customer or Lead \u2014 ID or name"
38143
38711
  },
38144
38712
  items: {
38145
38713
  type: "array",
@@ -38192,7 +38760,12 @@ var salesTools = [
38192
38760
  });
38193
38761
  const data = {
38194
38762
  quotation_to: input.quotation_to,
38195
- party_name: input.party_name,
38763
+ party_name: await resolveDynamicLink(
38764
+ ctx.client,
38765
+ input.quotation_to,
38766
+ input.party_name,
38767
+ { allowPartialMatch: false }
38768
+ ),
38196
38769
  items
38197
38770
  };
38198
38771
  if (input.transaction_date) {
@@ -38406,7 +38979,10 @@ var inventoryTools = [
38406
38979
  type: "object",
38407
38980
  properties: {
38408
38981
  limit: { type: "number", description: "Max results (default 50)" },
38409
- item_code: { type: "string", description: "Filter by item code" },
38982
+ item_code: {
38983
+ type: "string",
38984
+ description: "Filter by item code or name (e.g. 'ITEM-001' or 'Widget A')"
38985
+ },
38410
38986
  warehouse: { type: "string", description: "Filter by warehouse" }
38411
38987
  }
38412
38988
  },
@@ -38414,7 +38990,11 @@ var inventoryTools = [
38414
38990
  const limit = input.limit ?? 50;
38415
38991
  const filters = [];
38416
38992
  if (input.item_code) {
38417
- filters.push(["item_code", "=", input.item_code]);
38993
+ filters.push([
38994
+ "item_code",
38995
+ "=",
38996
+ await resolveItem(ctx.client, input.item_code)
38997
+ ]);
38418
38998
  }
38419
38999
  if (input.warehouse) {
38420
39000
  filters.push(["warehouse", "=", input.warehouse]);
@@ -38795,9 +39375,13 @@ var accountingTools = [
38795
39375
  },
38796
39376
  party_type: {
38797
39377
  type: "string",
38798
- description: "Filter by party type (Customer, Supplier, Employee)"
39378
+ description: "Filter by party type (Customer, Supplier, Employee). Required when 'party' is set, so the party name/ID can be resolved against the right doctype.",
39379
+ enum: ["Customer", "Supplier", "Employee"]
39380
+ },
39381
+ party: {
39382
+ type: "string",
39383
+ description: "Filter by party \u2014 ID or name (e.g. 'CUST-00001' or 'Acme Corp'). Requires 'party_type'."
38799
39384
  },
38800
- party: { type: "string", description: "Filter by party name" },
38801
39385
  date_from: {
38802
39386
  type: "string",
38803
39387
  description: "Start date filter YYYY-MM-DD"
@@ -38814,7 +39398,20 @@ var accountingTools = [
38814
39398
  filters.push(["party_type", "=", input.party_type]);
38815
39399
  }
38816
39400
  if (input.party) {
38817
- filters.push(["party", "=", input.party]);
39401
+ if (!input.party_type) {
39402
+ throw new Error(
39403
+ "[erpnext_payment_entry_list] 'party_type' is required when filtering by 'party'"
39404
+ );
39405
+ }
39406
+ filters.push([
39407
+ "party",
39408
+ "=",
39409
+ await resolveDynamicLink(
39410
+ ctx.client,
39411
+ input.party_type,
39412
+ input.party
39413
+ )
39414
+ ]);
38818
39415
  }
38819
39416
  if (input.date_from) {
38820
39417
  filters.push(["posting_date", ">=", input.date_from]);
@@ -39018,7 +39615,10 @@ var hrTools = [
39018
39615
  type: "object",
39019
39616
  properties: {
39020
39617
  limit: { type: "number", description: "Max results (default 20)" },
39021
- employee: { type: "string", description: "Filter by employee ID" },
39618
+ employee: {
39619
+ type: "string",
39620
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39621
+ },
39022
39622
  status: {
39023
39623
  type: "string",
39024
39624
  description: "Filter by status (Present, Absent, Half Day, On Leave)",
@@ -39035,7 +39635,11 @@ var hrTools = [
39035
39635
  const limit = input.limit ?? 20;
39036
39636
  const filters = [];
39037
39637
  if (input.employee) {
39038
- filters.push(["employee", "=", input.employee]);
39638
+ filters.push([
39639
+ "employee",
39640
+ "=",
39641
+ await resolveEmployee(ctx.client, input.employee)
39642
+ ]);
39039
39643
  }
39040
39644
  if (input.status) {
39041
39645
  filters.push(["status", "=", input.status]);
@@ -39077,7 +39681,10 @@ var hrTools = [
39077
39681
  type: "object",
39078
39682
  properties: {
39079
39683
  limit: { type: "number", description: "Max results (default 20)" },
39080
- employee: { type: "string", description: "Filter by employee ID" },
39684
+ employee: {
39685
+ type: "string",
39686
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39687
+ },
39081
39688
  status: {
39082
39689
  type: "string",
39083
39690
  description: "Filter by status (Open, Approved, Rejected, Cancelled)",
@@ -39086,14 +39693,23 @@ var hrTools = [
39086
39693
  leave_type: {
39087
39694
  type: "string",
39088
39695
  description: "Filter by leave type (e.g. Sick Leave)"
39089
- }
39696
+ },
39697
+ date_from: {
39698
+ type: "string",
39699
+ description: "Start date filter YYYY-MM-DD"
39700
+ },
39701
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
39090
39702
  }
39091
39703
  },
39092
39704
  handler: async (input, ctx) => {
39093
39705
  const limit = input.limit ?? 20;
39094
39706
  const filters = [];
39095
39707
  if (input.employee) {
39096
- filters.push(["employee", "=", input.employee]);
39708
+ filters.push([
39709
+ "employee",
39710
+ "=",
39711
+ await resolveEmployee(ctx.client, input.employee)
39712
+ ]);
39097
39713
  }
39098
39714
  if (input.status) {
39099
39715
  filters.push(["status", "=", input.status]);
@@ -39101,6 +39717,12 @@ var hrTools = [
39101
39717
  if (input.leave_type) {
39102
39718
  filters.push(["leave_type", "=", input.leave_type]);
39103
39719
  }
39720
+ if (input.date_from) {
39721
+ filters.push(["from_date", ">=", input.date_from]);
39722
+ }
39723
+ if (input.date_to) {
39724
+ filters.push(["to_date", "<=", input.date_to]);
39725
+ }
39104
39726
  const docs = await ctx.client.list("Leave Application", {
39105
39727
  fields: [
39106
39728
  "name",
@@ -39215,7 +39837,10 @@ var hrTools = [
39215
39837
  type: "object",
39216
39838
  properties: {
39217
39839
  limit: { type: "number", description: "Max results (default 20)" },
39218
- employee: { type: "string", description: "Filter by employee ID" },
39840
+ employee: {
39841
+ type: "string",
39842
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39843
+ },
39219
39844
  status: {
39220
39845
  type: "string",
39221
39846
  description: "Filter by status (Draft, Submitted, Cancelled)",
@@ -39235,7 +39860,11 @@ var hrTools = [
39235
39860
  const limit = input.limit ?? 20;
39236
39861
  const filters = [];
39237
39862
  if (input.employee) {
39238
- filters.push(["employee", "=", input.employee]);
39863
+ filters.push([
39864
+ "employee",
39865
+ "=",
39866
+ await resolveEmployee(ctx.client, input.employee)
39867
+ ]);
39239
39868
  }
39240
39869
  if (input.status) {
39241
39870
  filters.push(["status", "=", input.status]);
@@ -39309,7 +39938,12 @@ var hrTools = [
39309
39938
  type: "string",
39310
39939
  description: "Filter by status (Draft, Submitted, Cancelled)",
39311
39940
  enum: ["Draft", "Submitted", "Cancelled"]
39312
- }
39941
+ },
39942
+ date_from: {
39943
+ type: "string",
39944
+ description: "Start date filter YYYY-MM-DD"
39945
+ },
39946
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
39313
39947
  }
39314
39948
  },
39315
39949
  handler: async (input, ctx) => {
@@ -39321,6 +39955,12 @@ var hrTools = [
39321
39955
  if (input.status) {
39322
39956
  filters.push(["status", "=", input.status]);
39323
39957
  }
39958
+ if (input.date_from) {
39959
+ filters.push(["posting_date", ">=", input.date_from]);
39960
+ }
39961
+ if (input.date_to) {
39962
+ filters.push(["posting_date", "<=", input.date_to]);
39963
+ }
39324
39964
  const docs = await ctx.client.list("Payroll Entry", {
39325
39965
  fields: [
39326
39966
  "name",
@@ -39352,7 +39992,10 @@ var hrTools = [
39352
39992
  type: "object",
39353
39993
  properties: {
39354
39994
  limit: { type: "number", description: "Max results (default 20)" },
39355
- employee: { type: "string", description: "Filter by employee ID" },
39995
+ employee: {
39996
+ type: "string",
39997
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39998
+ },
39356
39999
  status: {
39357
40000
  type: "string",
39358
40001
  description: "Filter by status (Draft, Submitted, Cancelled)",
@@ -39362,14 +40005,23 @@ var hrTools = [
39362
40005
  type: "string",
39363
40006
  description: "Filter by approval status (Pending, Approved, Rejected)",
39364
40007
  enum: ["Pending", "Approved", "Rejected"]
39365
- }
40008
+ },
40009
+ date_from: {
40010
+ type: "string",
40011
+ description: "Start date filter YYYY-MM-DD"
40012
+ },
40013
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
39366
40014
  }
39367
40015
  },
39368
40016
  handler: async (input, ctx) => {
39369
40017
  const limit = input.limit ?? 20;
39370
40018
  const filters = [];
39371
40019
  if (input.employee) {
39372
- filters.push(["employee", "=", input.employee]);
40020
+ filters.push([
40021
+ "employee",
40022
+ "=",
40023
+ await resolveEmployee(ctx.client, input.employee)
40024
+ ]);
39373
40025
  }
39374
40026
  if (input.status) {
39375
40027
  filters.push(["status", "=", input.status]);
@@ -39377,6 +40029,12 @@ var hrTools = [
39377
40029
  if (input.approval_status) {
39378
40030
  filters.push(["approval_status", "=", input.approval_status]);
39379
40031
  }
40032
+ if (input.date_from) {
40033
+ filters.push(["posting_date", ">=", input.date_from]);
40034
+ }
40035
+ if (input.date_to) {
40036
+ filters.push(["posting_date", "<=", input.date_to]);
40037
+ }
39380
40038
  const docs = await ctx.client.list("Expense Claim", {
39381
40039
  fields: [
39382
40040
  "name",
@@ -39478,7 +40136,7 @@ var hrTools = [
39478
40136
  properties: {
39479
40137
  employee: {
39480
40138
  type: "string",
39481
- description: "Employee ID (e.g. HR-EMP-00001)"
40139
+ description: "Employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39482
40140
  }
39483
40141
  },
39484
40142
  required: ["employee"]
@@ -39488,7 +40146,11 @@ var hrTools = [
39488
40146
  throw new Error("[erpnext_leave_balance] 'employee' is required");
39489
40147
  }
39490
40148
  const filters = [
39491
- ["employee", "=", input.employee],
40149
+ [
40150
+ "employee",
40151
+ "=",
40152
+ await resolveEmployee(ctx.client, input.employee)
40153
+ ],
39492
40154
  ["docstatus", "=", 1]
39493
40155
  ];
39494
40156
  const docs = await ctx.client.list("Leave Allocation", {
@@ -39689,7 +40351,12 @@ var projectTools = [
39689
40351
  description: "Filter by status (Open, Completed, Cancelled)",
39690
40352
  enum: ["Open", "Completed", "Cancelled"]
39691
40353
  },
39692
- company: { type: "string", description: "Filter by company" }
40354
+ company: { type: "string", description: "Filter by company" },
40355
+ date_from: {
40356
+ type: "string",
40357
+ description: "Start date filter YYYY-MM-DD"
40358
+ },
40359
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
39693
40360
  }
39694
40361
  },
39695
40362
  handler: async (input, ctx) => {
@@ -39701,6 +40368,12 @@ var projectTools = [
39701
40368
  if (input.company) {
39702
40369
  filters.push(["company", "=", input.company]);
39703
40370
  }
40371
+ if (input.date_from) {
40372
+ filters.push(["expected_start_date", ">=", input.date_from]);
40373
+ }
40374
+ if (input.date_to) {
40375
+ filters.push(["expected_end_date", "<=", input.date_to]);
40376
+ }
39704
40377
  const docs = await ctx.client.list("Project", {
39705
40378
  fields: [
39706
40379
  "name",
@@ -39763,7 +40436,12 @@ var projectTools = [
39763
40436
  type: "string",
39764
40437
  description: "Filter by priority (Low, Medium, High, Urgent)",
39765
40438
  enum: ["Low", "Medium", "High", "Urgent"]
39766
- }
40439
+ },
40440
+ date_from: {
40441
+ type: "string",
40442
+ description: "Start date filter YYYY-MM-DD"
40443
+ },
40444
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
39767
40445
  }
39768
40446
  },
39769
40447
  handler: async (input, ctx) => {
@@ -39778,6 +40456,12 @@ var projectTools = [
39778
40456
  if (input.priority) {
39779
40457
  filters.push(["priority", "=", input.priority]);
39780
40458
  }
40459
+ if (input.date_from) {
40460
+ filters.push(["exp_start_date", ">=", input.date_from]);
40461
+ }
40462
+ if (input.date_to) {
40463
+ filters.push(["exp_end_date", "<=", input.date_to]);
40464
+ }
39781
40465
  const docs = await ctx.client.list("Task", {
39782
40466
  fields: [
39783
40467
  "name",
@@ -40020,19 +40704,31 @@ var projectTools = [
40020
40704
  type: "object",
40021
40705
  properties: {
40022
40706
  limit: { type: "number", description: "Max results (default 20)" },
40023
- employee: { type: "string", description: "Filter by employee ID" },
40707
+ employee: {
40708
+ type: "string",
40709
+ description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
40710
+ },
40024
40711
  project: { type: "string", description: "Filter by project name" },
40025
40712
  status: {
40026
40713
  type: "string",
40027
40714
  description: "Filter by status (Draft, Submitted, Cancelled)"
40028
- }
40715
+ },
40716
+ date_from: {
40717
+ type: "string",
40718
+ description: "Start date filter YYYY-MM-DD"
40719
+ },
40720
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
40029
40721
  }
40030
40722
  },
40031
40723
  handler: async (input, ctx) => {
40032
40724
  const limit = input.limit ?? 20;
40033
40725
  const filters = [];
40034
40726
  if (input.employee) {
40035
- filters.push(["employee", "=", input.employee]);
40727
+ filters.push([
40728
+ "employee",
40729
+ "=",
40730
+ await resolveEmployee(ctx.client, input.employee)
40731
+ ]);
40036
40732
  }
40037
40733
  if (input.project) {
40038
40734
  filters.push(["project", "=", input.project]);
@@ -40040,6 +40736,12 @@ var projectTools = [
40040
40736
  if (input.status) {
40041
40737
  filters.push(["status", "=", input.status]);
40042
40738
  }
40739
+ if (input.date_from) {
40740
+ filters.push(["start_date", ">=", input.date_from]);
40741
+ }
40742
+ if (input.date_to) {
40743
+ filters.push(["end_date", "<=", input.date_to]);
40744
+ }
40043
40745
  const docs = await ctx.client.list("Timesheet", {
40044
40746
  fields: [
40045
40747
  "name",
@@ -40282,7 +40984,10 @@ var purchasingTools = [
40282
40984
  type: "object",
40283
40985
  properties: {
40284
40986
  limit: { type: "number", description: "Max results (default 20)" },
40285
- supplier: { type: "string", description: "Filter by supplier" },
40987
+ supplier: {
40988
+ type: "string",
40989
+ description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
40990
+ },
40286
40991
  status: {
40287
40992
  type: "string",
40288
40993
  description: "Filter by status (Draft, To Receive and Bill, To Bill, Completed, Cancelled, etc.)"
@@ -40298,7 +41003,11 @@ var purchasingTools = [
40298
41003
  const limit = input.limit ?? 20;
40299
41004
  const filters = [];
40300
41005
  if (input.supplier) {
40301
- filters.push(["supplier", "=", input.supplier]);
41006
+ filters.push([
41007
+ "supplier",
41008
+ "=",
41009
+ await resolveSupplier(ctx.client, input.supplier)
41010
+ ]);
40302
41011
  }
40303
41012
  if (input.status) {
40304
41013
  filters.push(["status", "=", input.status]);
@@ -40430,7 +41139,10 @@ var purchasingTools = [
40430
41139
  type: "object",
40431
41140
  properties: {
40432
41141
  limit: { type: "number", description: "Max results (default 20)" },
40433
- supplier: { type: "string", description: "Filter by supplier" },
41142
+ supplier: {
41143
+ type: "string",
41144
+ description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
41145
+ },
40434
41146
  status: {
40435
41147
  type: "string",
40436
41148
  description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
@@ -40446,7 +41158,11 @@ var purchasingTools = [
40446
41158
  const limit = input.limit ?? 20;
40447
41159
  const filters = [];
40448
41160
  if (input.supplier) {
40449
- filters.push(["supplier", "=", input.supplier]);
41161
+ filters.push([
41162
+ "supplier",
41163
+ "=",
41164
+ await resolveSupplier(ctx.client, input.supplier)
41165
+ ]);
40450
41166
  }
40451
41167
  if (input.status) {
40452
41168
  filters.push(["status", "=", input.status]);
@@ -40516,7 +41232,10 @@ var purchasingTools = [
40516
41232
  type: "object",
40517
41233
  properties: {
40518
41234
  limit: { type: "number", description: "Max results (default 20)" },
40519
- supplier: { type: "string", description: "Filter by supplier" },
41235
+ supplier: {
41236
+ type: "string",
41237
+ description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
41238
+ },
40520
41239
  status: {
40521
41240
  type: "string",
40522
41241
  description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
@@ -40532,7 +41251,11 @@ var purchasingTools = [
40532
41251
  const limit = input.limit ?? 20;
40533
41252
  const filters = [];
40534
41253
  if (input.supplier) {
40535
- filters.push(["supplier", "=", input.supplier]);
41254
+ filters.push([
41255
+ "supplier",
41256
+ "=",
41257
+ await resolveSupplier(ctx.client, input.supplier)
41258
+ ]);
40536
41259
  }
40537
41260
  if (input.status) {
40538
41261
  filters.push(["status", "=", input.status]);
@@ -40601,22 +41324,40 @@ var purchasingTools = [
40601
41324
  type: "object",
40602
41325
  properties: {
40603
41326
  limit: { type: "number", description: "Max results (default 20)" },
40604
- supplier: { type: "string", description: "Filter by supplier" },
41327
+ supplier: {
41328
+ type: "string",
41329
+ description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
41330
+ },
40605
41331
  status: {
40606
41332
  type: "string",
40607
41333
  description: "Filter by status (Draft, Submitted, Ordered, Lost, Cancelled)"
40608
- }
41334
+ },
41335
+ date_from: {
41336
+ type: "string",
41337
+ description: "Start date filter YYYY-MM-DD"
41338
+ },
41339
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
40609
41340
  }
40610
41341
  },
40611
41342
  handler: async (input, ctx) => {
40612
41343
  const limit = input.limit ?? 20;
40613
41344
  const filters = [];
40614
41345
  if (input.supplier) {
40615
- filters.push(["supplier", "=", input.supplier]);
41346
+ filters.push([
41347
+ "supplier",
41348
+ "=",
41349
+ await resolveSupplier(ctx.client, input.supplier)
41350
+ ]);
40616
41351
  }
40617
41352
  if (input.status) {
40618
41353
  filters.push(["status", "=", input.status]);
40619
41354
  }
41355
+ if (input.date_from) {
41356
+ filters.push(["transaction_date", ">=", input.date_from]);
41357
+ }
41358
+ if (input.date_to) {
41359
+ filters.push(["transaction_date", "<=", input.date_to]);
41360
+ }
40620
41361
  const docs = await ctx.client.list("Supplier Quotation", {
40621
41362
  fields: [
40622
41363
  "name",
@@ -40652,7 +41393,10 @@ var deliveryTools = [
40652
41393
  type: "object",
40653
41394
  properties: {
40654
41395
  limit: { type: "number", description: "Max results (default 20)" },
40655
- customer: { type: "string", description: "Filter by customer" },
41396
+ customer: {
41397
+ type: "string",
41398
+ description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
41399
+ },
40656
41400
  status: {
40657
41401
  type: "string",
40658
41402
  description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
@@ -40668,7 +41412,11 @@ var deliveryTools = [
40668
41412
  const limit = input.limit ?? 20;
40669
41413
  const filters = [];
40670
41414
  if (input.customer) {
40671
- filters.push(["customer", "=", input.customer]);
41415
+ filters.push([
41416
+ "customer",
41417
+ "=",
41418
+ await resolveCustomer(ctx.client, input.customer)
41419
+ ]);
40672
41420
  }
40673
41421
  if (input.status) {
40674
41422
  filters.push(["status", "=", input.status]);
@@ -40887,7 +41635,7 @@ var manufacturingTools = [
40887
41635
  limit: { type: "number", description: "Max results (default 20)" },
40888
41636
  item: {
40889
41637
  type: "string",
40890
- description: "Filter by finished goods item code"
41638
+ description: "Filter by finished goods item code or name (e.g. 'ITEM-001' or 'Widget A')"
40891
41639
  },
40892
41640
  is_active: {
40893
41641
  type: "boolean",
@@ -40903,7 +41651,11 @@ var manufacturingTools = [
40903
41651
  const limit = input.limit ?? 20;
40904
41652
  const filters = [];
40905
41653
  if (input.item) {
40906
- filters.push(["item", "=", input.item]);
41654
+ filters.push([
41655
+ "item",
41656
+ "=",
41657
+ await resolveItem(ctx.client, input.item)
41658
+ ]);
40907
41659
  }
40908
41660
  if (input.is_active !== void 0) {
40909
41661
  filters.push(["is_active", "=", input.is_active ? 1 : 0]);
@@ -40971,7 +41723,7 @@ var manufacturingTools = [
40971
41723
  limit: { type: "number", description: "Max results (default 20)" },
40972
41724
  production_item: {
40973
41725
  type: "string",
40974
- description: "Filter by item being produced"
41726
+ description: "Filter by item being produced \u2014 item code or name (e.g. 'ITEM-001' or 'Widget A')"
40975
41727
  },
40976
41728
  status: {
40977
41729
  type: "string",
@@ -40991,7 +41743,11 @@ var manufacturingTools = [
40991
41743
  const limit = input.limit ?? 20;
40992
41744
  const filters = [];
40993
41745
  if (input.production_item) {
40994
- filters.push(["production_item", "=", input.production_item]);
41746
+ filters.push([
41747
+ "production_item",
41748
+ "=",
41749
+ await resolveItem(ctx.client, input.production_item)
41750
+ ]);
40995
41751
  }
40996
41752
  if (input.status) {
40997
41753
  filters.push(["status", "=", input.status]);
@@ -41315,9 +42071,14 @@ var crmTools = [
41315
42071
  type: "string",
41316
42072
  description: "Filter by assigned sales rep (user)"
41317
42073
  },
42074
+ opportunity_from: {
42075
+ type: "string",
42076
+ description: "Party type: Customer or Lead. Required when 'party_name' is set, so the party name/ID can be resolved against the right doctype.",
42077
+ enum: ["Customer", "Lead"]
42078
+ },
41318
42079
  party_name: {
41319
42080
  type: "string",
41320
- description: "Filter by customer or lead name"
42081
+ description: "Filter by party \u2014 ID or name (e.g. customer/lead name). Requires 'opportunity_from'."
41321
42082
  }
41322
42083
  }
41323
42084
  },
@@ -41335,7 +42096,20 @@ var crmTools = [
41335
42096
  ]);
41336
42097
  }
41337
42098
  if (input.party_name) {
41338
- filters.push(["party_name", "=", input.party_name]);
42099
+ if (!input.opportunity_from) {
42100
+ throw new Error(
42101
+ "[erpnext_opportunity_list] 'opportunity_from' is required when filtering by 'party_name'"
42102
+ );
42103
+ }
42104
+ filters.push([
42105
+ "party_name",
42106
+ "=",
42107
+ await resolveDynamicLink(
42108
+ ctx.client,
42109
+ input.opportunity_from,
42110
+ input.party_name
42111
+ )
42112
+ ]);
41339
42113
  }
41340
42114
  const docs = await ctx.client.list("Opportunity", {
41341
42115
  fields: [
@@ -41463,7 +42237,12 @@ var crmTools = [
41463
42237
  campaign_type: {
41464
42238
  type: "string",
41465
42239
  description: "Filter by campaign type"
41466
- }
42240
+ },
42241
+ date_from: {
42242
+ type: "string",
42243
+ description: "Start date filter YYYY-MM-DD"
42244
+ },
42245
+ date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
41467
42246
  }
41468
42247
  },
41469
42248
  handler: async (input, ctx) => {
@@ -41472,6 +42251,12 @@ var crmTools = [
41472
42251
  if (input.campaign_type) {
41473
42252
  filters.push(["campaign_type", "=", input.campaign_type]);
41474
42253
  }
42254
+ if (input.date_from) {
42255
+ filters.push(["start_date", ">=", input.date_from]);
42256
+ }
42257
+ if (input.date_to) {
42258
+ filters.push(["end_date", "<=", input.date_to]);
42259
+ }
41475
42260
  const docs = await ctx.client.list("Campaign", {
41476
42261
  fields: [
41477
42262
  "name",
@@ -41519,7 +42304,15 @@ var assetsTools = [
41519
42304
  location: { type: "string", description: "Filter by location" },
41520
42305
  custodian: {
41521
42306
  type: "string",
41522
- description: "Filter by custodian (employee)"
42307
+ description: "Filter by custodian \u2014 employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
42308
+ },
42309
+ date_from: {
42310
+ type: "string",
42311
+ description: "Purchase date start filter YYYY-MM-DD"
42312
+ },
42313
+ date_to: {
42314
+ type: "string",
42315
+ description: "Purchase date end filter YYYY-MM-DD"
41523
42316
  }
41524
42317
  }
41525
42318
  },
@@ -41536,7 +42329,17 @@ var assetsTools = [
41536
42329
  filters.push(["location", "=", input.location]);
41537
42330
  }
41538
42331
  if (input.custodian) {
41539
- filters.push(["custodian", "=", input.custodian]);
42332
+ filters.push([
42333
+ "custodian",
42334
+ "=",
42335
+ await resolveEmployee(ctx.client, input.custodian)
42336
+ ]);
42337
+ }
42338
+ if (input.date_from) {
42339
+ filters.push(["purchase_date", ">=", input.date_from]);
42340
+ }
42341
+ if (input.date_to) {
42342
+ filters.push(["purchase_date", "<=", input.date_to]);
41540
42343
  }
41541
42344
  const docs = await ctx.client.list("Asset", {
41542
42345
  fields: [
@@ -41990,11 +42793,13 @@ var operationsTools = [
41990
42793
  }
41991
42794
  const doc = await ctx.client.get(
41992
42795
  input.doctype,
41993
- input.name
42796
+ input.name,
42797
+ { skipCache: true }
41994
42798
  );
41995
42799
  const result = await ctx.client.callMethod("frappe.client.submit", {
41996
42800
  doc: { ...doc, doctype: input.doctype }
41997
42801
  });
42802
+ ctx.client.invalidate(input.doctype, input.name);
41998
42803
  return {
41999
42804
  data: result,
42000
42805
  message: `${input.doctype} ${input.name} submitted successfully`,
@@ -42034,6 +42839,7 @@ var operationsTools = [
42034
42839
  doctype: input.doctype,
42035
42840
  name: input.name
42036
42841
  });
42842
+ ctx.client.invalidate(input.doctype, input.name);
42037
42843
  return {
42038
42844
  data: result,
42039
42845
  message: `${input.doctype} ${input.name} cancelled successfully`,
@@ -44728,302 +45534,8 @@ var allTools = [
44728
45534
  function getToolsByCategory(category) {
44729
45535
  return toolsByCategory[category] ?? [];
44730
45536
  }
44731
-
44732
- // src/runtime.ts
44733
- var isDeno2 = typeof globalThis.Deno?.version?.deno === "string";
44734
- var impl2 = isDeno2 ? await Promise.resolve().then(() => (init_runtime_deno2(), runtime_deno_exports2)) : await Promise.resolve().then(() => (init_runtime_node2(), runtime_node_exports2));
44735
- var env6 = impl2.env;
44736
- var readTextFile6 = impl2.readTextFile;
44737
- var statSync3 = impl2.statSync;
44738
- var readDirSync3 = impl2.readDirSync;
44739
- var getArgs3 = impl2.getArgs;
44740
- var exit3 = impl2.exit;
44741
- var onSignal3 = impl2.onSignal;
44742
-
44743
- // src/api/frappe-client.ts
44744
- var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
44745
- var DEFAULT_RETRY_METHODS = ["GET"];
44746
- var FrappeAPIError = class extends Error {
44747
- /**
44748
- * @param message - Human-readable error description
44749
- * @param status - HTTP status code (0 for network errors, 408 for timeouts)
44750
- * @param body - Raw response body (parsed JSON object or plain text string)
44751
- * @param retryAfterMs - When the server sent a `Retry-After` header on a
44752
- * retryable status (typically 429), the parsed delay
44753
- * in ms. Used by the retry loop; absent otherwise.
44754
- */
44755
- constructor(message2, status, body, retryAfterMs) {
44756
- super(`[FrappeClient] ${message2} (HTTP ${status})`);
44757
- this.status = status;
44758
- this.body = body;
44759
- this.retryAfterMs = retryAfterMs;
44760
- this.name = "FrappeAPIError";
44761
- }
44762
- };
44763
- function parseRetryAfter(raw2) {
44764
- if (!raw2) return void 0;
44765
- const trimmed = raw2.trim();
44766
- const seconds = Number(trimmed);
44767
- if (Number.isFinite(seconds) && seconds >= 0) {
44768
- return Math.round(seconds * 1e3);
44769
- }
44770
- const date3 = Date.parse(trimmed);
44771
- if (Number.isFinite(date3)) {
44772
- return Math.max(0, date3 - Date.now());
44773
- }
44774
- return void 0;
44775
- }
44776
- function extractServerMessages(raw2) {
44777
- if (!raw2 || typeof raw2 !== "string") return void 0;
44778
- try {
44779
- const outer = JSON.parse(raw2);
44780
- if (!Array.isArray(outer)) return void 0;
44781
- const msgs = [];
44782
- for (const item of outer) {
44783
- try {
44784
- const inner = JSON.parse(item);
44785
- if (typeof inner === "object" && inner?.message) {
44786
- msgs.push(inner.message);
44787
- } else if (typeof inner === "string") {
44788
- msgs.push(inner);
44789
- }
44790
- } catch {
44791
- if (typeof item === "string") msgs.push(item);
44792
- }
44793
- }
44794
- return msgs.length > 0 ? msgs.join("; ") : void 0;
44795
- } catch {
44796
- return void 0;
44797
- }
44798
- }
44799
- var FrappeClient = class {
44800
- baseUrl;
44801
- authHeader;
44802
- timeoutMs;
44803
- retries;
44804
- retryStatuses;
44805
- retryBackoffMs;
44806
- retryMethods;
44807
- constructor(config2) {
44808
- this.baseUrl = config2.baseUrl.replace(/\/$/, "");
44809
- this.authHeader = `token ${config2.apiKey}:${config2.apiSecret}`;
44810
- this.timeoutMs = config2.timeoutMs ?? 3e4;
44811
- this.retries = config2.retries ?? 3;
44812
- this.retryStatuses = config2.retryStatuses ?? DEFAULT_RETRY_STATUSES;
44813
- this.retryBackoffMs = config2.retryBackoffMs ?? 200;
44814
- this.retryMethods = config2.retryMethods ?? DEFAULT_RETRY_METHODS;
44815
- }
44816
- // ── Private HTTP helpers ────────────────────────────────────────────────────
44817
- buildHeaders() {
44818
- return {
44819
- "Authorization": this.authHeader,
44820
- "Accept": "application/json",
44821
- "Content-Type": "application/json"
44822
- };
44823
- }
44824
- /**
44825
- * Decide whether an error is worth retrying.
44826
- * Network errors (status 0) and timeouts (status 408) are always retryable;
44827
- * other statuses are checked against `retryStatuses`.
44828
- */
44829
- isRetryable(err, method) {
44830
- if (!this.retryMethods.includes(method)) return false;
44831
- if (!(err instanceof FrappeAPIError)) return false;
44832
- if (err.status === 0) return true;
44833
- return this.retryStatuses.includes(err.status);
44834
- }
44835
- /** Compute the backoff delay for a given attempt (0-indexed). */
44836
- computeBackoff(attempt, err) {
44837
- if (err instanceof FrappeAPIError && err.retryAfterMs !== void 0) {
44838
- return err.retryAfterMs;
44839
- }
44840
- return this.retryBackoffMs * 2 ** attempt;
44841
- }
44842
- async request(method, path, body) {
44843
- let lastError;
44844
- for (let attempt = 0; attempt <= this.retries; attempt++) {
44845
- try {
44846
- return await this.requestOnce(method, path, body);
44847
- } catch (err) {
44848
- lastError = err;
44849
- if (attempt === this.retries || !this.isRetryable(err, method)) {
44850
- throw err;
44851
- }
44852
- const delay = this.computeBackoff(attempt, err);
44853
- if (delay > 0) {
44854
- await new Promise((resolve) => setTimeout(resolve, delay));
44855
- }
44856
- }
44857
- }
44858
- throw lastError;
44859
- }
44860
- async requestOnce(method, path, body) {
44861
- const url = `${this.baseUrl}${path}`;
44862
- const controller = new AbortController();
44863
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
44864
- let response;
44865
- try {
44866
- response = await fetch(url, {
44867
- method,
44868
- headers: this.buildHeaders(),
44869
- body: body !== void 0 ? JSON.stringify(body) : void 0,
44870
- signal: controller.signal
44871
- });
44872
- } catch (err) {
44873
- clearTimeout(timer);
44874
- if (err instanceof Error && err.name === "AbortError") {
44875
- throw new FrappeAPIError(
44876
- `Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
44877
- 408,
44878
- null
44879
- );
44880
- }
44881
- throw new FrappeAPIError(
44882
- `Network error on ${method} ${path}: ${err.message}`,
44883
- 0,
44884
- null
44885
- );
44886
- }
44887
- clearTimeout(timer);
44888
- const rawText = await response.text();
44889
- const contentType = response.headers.get("content-type") ?? "";
44890
- let responseBody = rawText;
44891
- if (contentType.includes("application/json") && rawText.length > 0) {
44892
- try {
44893
- responseBody = JSON.parse(rawText);
44894
- } catch {
44895
- responseBody = rawText;
44896
- }
44897
- }
44898
- if (!response.ok) {
44899
- let msg = response.statusText;
44900
- if (typeof responseBody === "object" && responseBody !== null) {
44901
- const rb = responseBody;
44902
- const excType = rb.exc_type;
44903
- const baseMsg = rb.message ?? excType ?? response.statusText;
44904
- const serverDetails = extractServerMessages(rb._server_messages);
44905
- msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
44906
- } else if (typeof responseBody === "string" && responseBody.length > 0) {
44907
- msg = responseBody.slice(0, 200);
44908
- }
44909
- const retryAfterMs = parseRetryAfter(
44910
- response.headers.get("retry-after")
44911
- );
44912
- throw new FrappeAPIError(
44913
- `${method} ${path} failed: ${msg}`,
44914
- response.status,
44915
- responseBody,
44916
- retryAfterMs
44917
- );
44918
- }
44919
- return responseBody;
44920
- }
44921
- // ── Resource CRUD ───────────────────────────────────────────────────────────
44922
- /**
44923
- * List documents of a DocType.
44924
- * Frappe list API: GET /api/resource/{doctype}?fields=...&filters=...
44925
- */
44926
- async list(doctype, options = {}) {
44927
- const params = new URLSearchParams();
44928
- if (options.fields && options.fields.length > 0) {
44929
- params.set("fields", JSON.stringify(options.fields));
44930
- }
44931
- if (options.filters && options.filters.length > 0) {
44932
- params.set("filters", JSON.stringify(options.filters));
44933
- }
44934
- if (options.order_by) {
44935
- params.set("order_by", options.order_by);
44936
- }
44937
- if (options.limit !== void 0) {
44938
- params.set("limit", String(options.limit));
44939
- }
44940
- if (options.limit_start !== void 0) {
44941
- params.set("limit_start", String(options.limit_start));
44942
- }
44943
- params.set("as_dict", "1");
44944
- const query = params.toString() ? `?${params.toString()}` : "";
44945
- const res = await this.request(
44946
- "GET",
44947
- `/api/resource/${encodeURIComponent(doctype)}${query}`
44948
- );
44949
- return res.data ?? [];
44950
- }
44951
- /**
44952
- * Get a single document by name.
44953
- * GET /api/resource/{doctype}/{name}
44954
- */
44955
- async get(doctype, name) {
44956
- const res = await this.request(
44957
- "GET",
44958
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
44959
- );
44960
- return res.data;
44961
- }
44962
- /**
44963
- * Create a new document.
44964
- * POST /api/resource/{doctype}
44965
- */
44966
- async create(doctype, data) {
44967
- const res = await this.request(
44968
- "POST",
44969
- `/api/resource/${encodeURIComponent(doctype)}`,
44970
- { data: { ...data, doctype } }
44971
- );
44972
- return res.data;
44973
- }
44974
- /**
44975
- * Update an existing document (partial update).
44976
- * PUT /api/resource/{doctype}/{name}
44977
- */
44978
- async update(doctype, name, data) {
44979
- const res = await this.request(
44980
- "PUT",
44981
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`,
44982
- { data }
44983
- );
44984
- return res.data;
44985
- }
44986
- /**
44987
- * Delete a document.
44988
- * DELETE /api/resource/{doctype}/{name}
44989
- */
44990
- async delete(doctype, name) {
44991
- await this.request(
44992
- "DELETE",
44993
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
44994
- );
44995
- }
44996
- /**
44997
- * Call a whitelisted Frappe method.
44998
- * POST /api/method/{method}
44999
- */
45000
- async callMethod(method, args = {}) {
45001
- const res = await this.request(
45002
- "POST",
45003
- `/api/method/${method}`,
45004
- args
45005
- );
45006
- return res.message;
45007
- }
45008
- };
45009
- var _client = null;
45010
- function getFrappeClient() {
45011
- if (_client) return _client;
45012
- const url = env6("ERPNEXT_URL");
45013
- const apiKey = env6("ERPNEXT_API_KEY");
45014
- const apiSecret = env6("ERPNEXT_API_SECRET");
45015
- if (!url) {
45016
- throw new Error(
45017
- "[lib/erpnext] ERPNEXT_URL is required. Set it to your ERPNext instance URL, e.g. http://localhost:8000"
45018
- );
45019
- }
45020
- if (!apiKey || !apiSecret) {
45021
- throw new Error(
45022
- "[lib/erpnext] ERPNEXT_API_KEY and ERPNEXT_API_SECRET are required. Generate them in ERPNext: User Settings \u2192 API Access."
45023
- );
45024
- }
45025
- _client = new FrappeClient({ baseUrl: url, apiKey, apiSecret });
45026
- return _client;
45537
+ function getToolByName(name) {
45538
+ return allTools.find((t) => t.name === name);
45027
45539
  }
45028
45540
 
45029
45541
  // src/tools/ui-refresh.ts
@@ -45320,6 +45832,37 @@ function resolveViewerDistPath2(moduleUrl, viewerName, exists) {
45320
45832
  return null;
45321
45833
  }
45322
45834
 
45835
+ // src/cache/warm.ts
45836
+ async function warmCache() {
45837
+ const configured = env6("MCP_CACHE_WARM_TOOLS");
45838
+ if (!configured?.trim()) return;
45839
+ const client = getFrappeClient();
45840
+ const names = configured.split(",").map((s) => s.trim()).filter(Boolean);
45841
+ for (const name of names) {
45842
+ const tool = getToolByName(name);
45843
+ if (!tool) {
45844
+ console.error(
45845
+ `[mcp-erpnext] Cache warm: unknown tool "${name}", skipping`
45846
+ );
45847
+ continue;
45848
+ }
45849
+ if (!tool.annotations?.readOnlyHint) {
45850
+ console.error(
45851
+ `[mcp-erpnext] Cache warm: "${name}" is not read-only, refusing to call it, skipping`
45852
+ );
45853
+ continue;
45854
+ }
45855
+ try {
45856
+ await tool.handler({}, { client });
45857
+ } catch (err) {
45858
+ console.error(
45859
+ `[mcp-erpnext] Cache warm: "${name}" failed (non-fatal):`,
45860
+ err
45861
+ );
45862
+ }
45863
+ }
45864
+ }
45865
+
45323
45866
  // server.ts
45324
45867
  var DEFAULT_HTTP_PORT = 3012;
45325
45868
  async function main() {
@@ -45338,13 +45881,13 @@ async function main() {
45338
45881
  const portArg = args.find((arg) => arg.startsWith("--port="));
45339
45882
  const httpPort = portArg ? parseInt(portArg.split("=")[1], 10) : DEFAULT_HTTP_PORT;
45340
45883
  const hostnameArg = args.find((arg) => arg.startsWith("--hostname="));
45341
- const hostname = hostnameArg ? hostnameArg.split("=")[1] : "0.0.0.0";
45884
+ const hostname = hostnameArg ? hostnameArg.split("=")[1] : "127.0.0.1";
45342
45885
  const toolsClient = new ErpNextToolsClient(
45343
45886
  categories ? { categories } : void 0
45344
45887
  );
45345
45888
  const server = new McpApp({
45346
45889
  name: "mcp-erpnext",
45347
- version: "2.4.1",
45890
+ version: "2.5.0",
45348
45891
  maxConcurrent: 10,
45349
45892
  backpressureStrategy: "queue",
45350
45893
  validateSchema: true,
@@ -45389,7 +45932,16 @@ async function main() {
45389
45932
  console.error(
45390
45933
  `[mcp-erpnext] Initialized \u2014 ${toolsClient.count} tools${categories ? ` (categories: ${categories.join(", ")})` : ""}`
45391
45934
  );
45935
+ warmCache().catch((err) => {
45936
+ console.error("[mcp-erpnext] Cache warm failed (non-fatal):", err);
45937
+ });
45392
45938
  if (httpFlag) {
45939
+ const isLoopback = hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
45940
+ if (!isLoopback) {
45941
+ console.error(
45942
+ `[mcp-erpnext] WARNING: binding to ${hostname} exposes the HTTP server to the network. Every tool acts with the server's ERPNext API key, so restrict access (firewall, private network, or an authenticating reverse proxy).`
45943
+ );
45944
+ }
45393
45945
  await server.startHttp({
45394
45946
  port: httpPort,
45395
45947
  hostname,