@casys/mcp-erpnext 2.4.2 → 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 +1270 -736
  3. package/package.json +1 -1
package/mcp-erpnext.mjs CHANGED
@@ -37389,87 +37389,597 @@ var KANBAN_META = viewer("kanban-viewer");
37389
37389
  var KPI_META = viewer("kpi-viewer");
37390
37390
  var FUNNEL_META = viewer("funnel-viewer");
37391
37391
 
37392
- // src/tools/sales.ts
37393
- function mapLineItems(items, options) {
37394
- if (!Array.isArray(items) || items.length === 0) {
37395
- throw new Error(
37396
- `[${options.toolName}] 'items' must be a non-empty array`
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).`
37397
37456
  );
37457
+ return DEFAULT_CACHE_TTL_MS;
37398
37458
  }
37399
- return items.map((item) => {
37400
- if (!item.item_code || item.qty == null || item.rate == null) {
37401
- throw new Error(
37402
- `[${options.toolName}] Each item must have item_code, qty, and rate`
37403
- );
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
+ }
37404
37530
  }
37405
- const mapped = {
37406
- item_code: item.item_code,
37407
- qty: item.qty,
37408
- rate: item.rate
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"
37409
37561
  };
37410
- if (options.defaultDeliveryDate !== void 0) {
37411
- mapped.delivery_date = options.defaultDeliveryDate;
37412
- }
37413
- if (options.includeWarehouse !== false && item.warehouse) {
37414
- mapped.warehouse = item.warehouse;
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;
37415
37578
  }
37416
- return mapped;
37417
- });
37418
- }
37419
- var salesTools = [
37420
- // ── Customers ─────────────────────────────────────────────────────────────
37421
- {
37422
- name: "erpnext_customer_list",
37423
- annotations: { readOnlyHint: true },
37424
- _meta: DOCLIST_META,
37425
- description: "List ERPNext customers. Returns active customers by default. Fields: name, customer_name, customer_group, territory, email_id, disabled.",
37426
- category: "sales",
37427
- inputSchema: {
37428
- type: "object",
37429
- properties: {
37430
- limit: { type: "number", description: "Max results (default 20)" },
37431
- customer_group: {
37432
- type: "string",
37433
- description: "Filter by customer group"
37434
- },
37435
- territory: { type: "string", description: "Filter by territory" },
37436
- include_disabled: {
37437
- type: "boolean",
37438
- description: "Include disabled customers (default false)"
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));
37439
37594
  }
37440
37595
  }
37441
- },
37442
- handler: async (input, ctx) => {
37443
- const limit = input.limit ?? 20;
37444
- const filters = [];
37445
- if (!input.include_disabled) {
37446
- filters.push(["disabled", "=", 0]);
37447
- }
37448
- if (input.customer_group) {
37449
- filters.push(["customer_group", "=", input.customer_group]);
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
+ );
37450
37619
  }
37451
- if (input.territory) {
37452
- filters.push(["territory", "=", input.territory]);
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;
37453
37635
  }
37454
- const docs = await ctx.client.list("Customer", {
37455
- fields: [
37456
- "name",
37457
- "customer_name",
37458
- "customer_group",
37459
- "territory",
37460
- "email_id",
37461
- "disabled"
37462
- ],
37463
- filters,
37464
- limit,
37465
- order_by: "modified desc"
37466
- });
37467
- return {
37468
- doctype: "Customer",
37469
- count: docs.length,
37470
- data: docs,
37471
- _meta: DOCLIST_META
37472
- };
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
+
37902
+ // src/tools/sales.ts
37903
+ function mapLineItems(items, options) {
37904
+ if (!Array.isArray(items) || items.length === 0) {
37905
+ throw new Error(
37906
+ `[${options.toolName}] 'items' must be a non-empty array`
37907
+ );
37908
+ }
37909
+ return items.map((item) => {
37910
+ if (!item.item_code || item.qty == null || item.rate == null) {
37911
+ throw new Error(
37912
+ `[${options.toolName}] Each item must have item_code, qty, and rate`
37913
+ );
37914
+ }
37915
+ const mapped = {
37916
+ item_code: item.item_code,
37917
+ qty: item.qty,
37918
+ rate: item.rate
37919
+ };
37920
+ if (options.defaultDeliveryDate !== void 0) {
37921
+ mapped.delivery_date = options.defaultDeliveryDate;
37922
+ }
37923
+ if (options.includeWarehouse !== false && item.warehouse) {
37924
+ mapped.warehouse = item.warehouse;
37925
+ }
37926
+ return mapped;
37927
+ });
37928
+ }
37929
+ var salesTools = [
37930
+ // ── Customers ─────────────────────────────────────────────────────────────
37931
+ {
37932
+ name: "erpnext_customer_list",
37933
+ annotations: { readOnlyHint: true },
37934
+ _meta: DOCLIST_META,
37935
+ description: "List ERPNext customers. Returns active customers by default. Fields: name, customer_name, customer_group, territory, email_id, disabled.",
37936
+ category: "sales",
37937
+ inputSchema: {
37938
+ type: "object",
37939
+ properties: {
37940
+ limit: { type: "number", description: "Max results (default 20)" },
37941
+ customer_group: {
37942
+ type: "string",
37943
+ description: "Filter by customer group"
37944
+ },
37945
+ territory: { type: "string", description: "Filter by territory" },
37946
+ include_disabled: {
37947
+ type: "boolean",
37948
+ description: "Include disabled customers (default false)"
37949
+ }
37950
+ }
37951
+ },
37952
+ handler: async (input, ctx) => {
37953
+ const limit = input.limit ?? 20;
37954
+ const filters = [];
37955
+ if (!input.include_disabled) {
37956
+ filters.push(["disabled", "=", 0]);
37957
+ }
37958
+ if (input.customer_group) {
37959
+ filters.push(["customer_group", "=", input.customer_group]);
37960
+ }
37961
+ if (input.territory) {
37962
+ filters.push(["territory", "=", input.territory]);
37963
+ }
37964
+ const docs = await ctx.client.list("Customer", {
37965
+ fields: [
37966
+ "name",
37967
+ "customer_name",
37968
+ "customer_group",
37969
+ "territory",
37970
+ "email_id",
37971
+ "disabled"
37972
+ ],
37973
+ filters,
37974
+ limit,
37975
+ order_by: "modified desc"
37976
+ });
37977
+ return {
37978
+ doctype: "Customer",
37979
+ count: docs.length,
37980
+ data: docs,
37981
+ _meta: DOCLIST_META
37982
+ };
37473
37983
  }
37474
37984
  },
37475
37985
  {
@@ -37592,7 +38102,10 @@ var salesTools = [
37592
38102
  type: "object",
37593
38103
  properties: {
37594
38104
  limit: { type: "number", description: "Max results (default 20)" },
37595
- 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
+ },
37596
38109
  status: {
37597
38110
  type: "string",
37598
38111
  description: "Filter by status (Draft, To Deliver and Bill, Completed, Cancelled, etc.)"
@@ -37608,7 +38121,11 @@ var salesTools = [
37608
38121
  const limit = input.limit ?? 20;
37609
38122
  const filters = [];
37610
38123
  if (input.customer) {
37611
- filters.push(["customer", "=", input.customer]);
38124
+ filters.push([
38125
+ "customer",
38126
+ "=",
38127
+ await resolveCustomer(ctx.client, input.customer)
38128
+ ]);
37612
38129
  }
37613
38130
  if (input.status) {
37614
38131
  filters.push(["status", "=", input.status]);
@@ -37872,7 +38389,10 @@ var salesTools = [
37872
38389
  type: "object",
37873
38390
  properties: {
37874
38391
  limit: { type: "number", description: "Max results (default 20)" },
37875
- 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
+ },
37876
38396
  status: {
37877
38397
  type: "string",
37878
38398
  description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
@@ -37888,7 +38408,11 @@ var salesTools = [
37888
38408
  const limit = input.limit ?? 20;
37889
38409
  const filters = [];
37890
38410
  if (input.customer) {
37891
- filters.push(["customer", "=", input.customer]);
38411
+ filters.push([
38412
+ "customer",
38413
+ "=",
38414
+ await resolveCustomer(ctx.client, input.customer)
38415
+ ]);
37892
38416
  }
37893
38417
  if (input.status) {
37894
38418
  filters.push(["status", "=", input.status]);
@@ -38076,22 +38600,54 @@ var salesTools = [
38076
38600
  type: "object",
38077
38601
  properties: {
38078
38602
  limit: { type: "number", description: "Max results (default 20)" },
38079
- party_name: { type: "string", description: "Filter by party name" },
38080
- status: {
38603
+ quotation_to: {
38081
38604
  type: "string",
38082
- description: "Filter by status (Draft, Open, Replied, Ordered, Lost, Cancelled)"
38083
- }
38084
- }
38085
- },
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
+ },
38612
+ status: {
38613
+ type: "string",
38614
+ description: "Filter by status (Draft, Open, Replied, Ordered, Lost, Cancelled)"
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" }
38621
+ }
38622
+ },
38086
38623
  handler: async (input, ctx) => {
38087
38624
  const limit = input.limit ?? 20;
38088
38625
  const filters = [];
38089
38626
  if (input.party_name) {
38090
- 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
+ ]);
38091
38641
  }
38092
38642
  if (input.status) {
38093
38643
  filters.push(["status", "=", input.status]);
38094
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
+ }
38095
38651
  const docs = await ctx.client.list("Quotation", {
38096
38652
  fields: [
38097
38653
  "name",
@@ -38151,7 +38707,7 @@ var salesTools = [
38151
38707
  },
38152
38708
  party_name: {
38153
38709
  type: "string",
38154
- description: "Customer or Lead name"
38710
+ description: "Customer or Lead \u2014 ID or name"
38155
38711
  },
38156
38712
  items: {
38157
38713
  type: "array",
@@ -38204,7 +38760,12 @@ var salesTools = [
38204
38760
  });
38205
38761
  const data = {
38206
38762
  quotation_to: input.quotation_to,
38207
- 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
+ ),
38208
38769
  items
38209
38770
  };
38210
38771
  if (input.transaction_date) {
@@ -38418,7 +38979,10 @@ var inventoryTools = [
38418
38979
  type: "object",
38419
38980
  properties: {
38420
38981
  limit: { type: "number", description: "Max results (default 50)" },
38421
- 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
+ },
38422
38986
  warehouse: { type: "string", description: "Filter by warehouse" }
38423
38987
  }
38424
38988
  },
@@ -38426,7 +38990,11 @@ var inventoryTools = [
38426
38990
  const limit = input.limit ?? 50;
38427
38991
  const filters = [];
38428
38992
  if (input.item_code) {
38429
- filters.push(["item_code", "=", input.item_code]);
38993
+ filters.push([
38994
+ "item_code",
38995
+ "=",
38996
+ await resolveItem(ctx.client, input.item_code)
38997
+ ]);
38430
38998
  }
38431
38999
  if (input.warehouse) {
38432
39000
  filters.push(["warehouse", "=", input.warehouse]);
@@ -38807,9 +39375,13 @@ var accountingTools = [
38807
39375
  },
38808
39376
  party_type: {
38809
39377
  type: "string",
38810
- 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'."
38811
39384
  },
38812
- party: { type: "string", description: "Filter by party name" },
38813
39385
  date_from: {
38814
39386
  type: "string",
38815
39387
  description: "Start date filter YYYY-MM-DD"
@@ -38826,7 +39398,20 @@ var accountingTools = [
38826
39398
  filters.push(["party_type", "=", input.party_type]);
38827
39399
  }
38828
39400
  if (input.party) {
38829
- 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
+ ]);
38830
39415
  }
38831
39416
  if (input.date_from) {
38832
39417
  filters.push(["posting_date", ">=", input.date_from]);
@@ -39030,7 +39615,10 @@ var hrTools = [
39030
39615
  type: "object",
39031
39616
  properties: {
39032
39617
  limit: { type: "number", description: "Max results (default 20)" },
39033
- 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
+ },
39034
39622
  status: {
39035
39623
  type: "string",
39036
39624
  description: "Filter by status (Present, Absent, Half Day, On Leave)",
@@ -39047,7 +39635,11 @@ var hrTools = [
39047
39635
  const limit = input.limit ?? 20;
39048
39636
  const filters = [];
39049
39637
  if (input.employee) {
39050
- filters.push(["employee", "=", input.employee]);
39638
+ filters.push([
39639
+ "employee",
39640
+ "=",
39641
+ await resolveEmployee(ctx.client, input.employee)
39642
+ ]);
39051
39643
  }
39052
39644
  if (input.status) {
39053
39645
  filters.push(["status", "=", input.status]);
@@ -39089,7 +39681,10 @@ var hrTools = [
39089
39681
  type: "object",
39090
39682
  properties: {
39091
39683
  limit: { type: "number", description: "Max results (default 20)" },
39092
- 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
+ },
39093
39688
  status: {
39094
39689
  type: "string",
39095
39690
  description: "Filter by status (Open, Approved, Rejected, Cancelled)",
@@ -39098,14 +39693,23 @@ var hrTools = [
39098
39693
  leave_type: {
39099
39694
  type: "string",
39100
39695
  description: "Filter by leave type (e.g. Sick Leave)"
39101
- }
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" }
39102
39702
  }
39103
39703
  },
39104
39704
  handler: async (input, ctx) => {
39105
39705
  const limit = input.limit ?? 20;
39106
39706
  const filters = [];
39107
39707
  if (input.employee) {
39108
- filters.push(["employee", "=", input.employee]);
39708
+ filters.push([
39709
+ "employee",
39710
+ "=",
39711
+ await resolveEmployee(ctx.client, input.employee)
39712
+ ]);
39109
39713
  }
39110
39714
  if (input.status) {
39111
39715
  filters.push(["status", "=", input.status]);
@@ -39113,6 +39717,12 @@ var hrTools = [
39113
39717
  if (input.leave_type) {
39114
39718
  filters.push(["leave_type", "=", input.leave_type]);
39115
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
+ }
39116
39726
  const docs = await ctx.client.list("Leave Application", {
39117
39727
  fields: [
39118
39728
  "name",
@@ -39227,7 +39837,10 @@ var hrTools = [
39227
39837
  type: "object",
39228
39838
  properties: {
39229
39839
  limit: { type: "number", description: "Max results (default 20)" },
39230
- 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
+ },
39231
39844
  status: {
39232
39845
  type: "string",
39233
39846
  description: "Filter by status (Draft, Submitted, Cancelled)",
@@ -39247,7 +39860,11 @@ var hrTools = [
39247
39860
  const limit = input.limit ?? 20;
39248
39861
  const filters = [];
39249
39862
  if (input.employee) {
39250
- filters.push(["employee", "=", input.employee]);
39863
+ filters.push([
39864
+ "employee",
39865
+ "=",
39866
+ await resolveEmployee(ctx.client, input.employee)
39867
+ ]);
39251
39868
  }
39252
39869
  if (input.status) {
39253
39870
  filters.push(["status", "=", input.status]);
@@ -39321,7 +39938,12 @@ var hrTools = [
39321
39938
  type: "string",
39322
39939
  description: "Filter by status (Draft, Submitted, Cancelled)",
39323
39940
  enum: ["Draft", "Submitted", "Cancelled"]
39324
- }
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" }
39325
39947
  }
39326
39948
  },
39327
39949
  handler: async (input, ctx) => {
@@ -39333,6 +39955,12 @@ var hrTools = [
39333
39955
  if (input.status) {
39334
39956
  filters.push(["status", "=", input.status]);
39335
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
+ }
39336
39964
  const docs = await ctx.client.list("Payroll Entry", {
39337
39965
  fields: [
39338
39966
  "name",
@@ -39364,7 +39992,10 @@ var hrTools = [
39364
39992
  type: "object",
39365
39993
  properties: {
39366
39994
  limit: { type: "number", description: "Max results (default 20)" },
39367
- 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
+ },
39368
39999
  status: {
39369
40000
  type: "string",
39370
40001
  description: "Filter by status (Draft, Submitted, Cancelled)",
@@ -39374,14 +40005,23 @@ var hrTools = [
39374
40005
  type: "string",
39375
40006
  description: "Filter by approval status (Pending, Approved, Rejected)",
39376
40007
  enum: ["Pending", "Approved", "Rejected"]
39377
- }
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" }
39378
40014
  }
39379
40015
  },
39380
40016
  handler: async (input, ctx) => {
39381
40017
  const limit = input.limit ?? 20;
39382
40018
  const filters = [];
39383
40019
  if (input.employee) {
39384
- filters.push(["employee", "=", input.employee]);
40020
+ filters.push([
40021
+ "employee",
40022
+ "=",
40023
+ await resolveEmployee(ctx.client, input.employee)
40024
+ ]);
39385
40025
  }
39386
40026
  if (input.status) {
39387
40027
  filters.push(["status", "=", input.status]);
@@ -39389,6 +40029,12 @@ var hrTools = [
39389
40029
  if (input.approval_status) {
39390
40030
  filters.push(["approval_status", "=", input.approval_status]);
39391
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
+ }
39392
40038
  const docs = await ctx.client.list("Expense Claim", {
39393
40039
  fields: [
39394
40040
  "name",
@@ -39490,7 +40136,7 @@ var hrTools = [
39490
40136
  properties: {
39491
40137
  employee: {
39492
40138
  type: "string",
39493
- description: "Employee ID (e.g. HR-EMP-00001)"
40139
+ description: "Employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
39494
40140
  }
39495
40141
  },
39496
40142
  required: ["employee"]
@@ -39500,7 +40146,11 @@ var hrTools = [
39500
40146
  throw new Error("[erpnext_leave_balance] 'employee' is required");
39501
40147
  }
39502
40148
  const filters = [
39503
- ["employee", "=", input.employee],
40149
+ [
40150
+ "employee",
40151
+ "=",
40152
+ await resolveEmployee(ctx.client, input.employee)
40153
+ ],
39504
40154
  ["docstatus", "=", 1]
39505
40155
  ];
39506
40156
  const docs = await ctx.client.list("Leave Allocation", {
@@ -39701,7 +40351,12 @@ var projectTools = [
39701
40351
  description: "Filter by status (Open, Completed, Cancelled)",
39702
40352
  enum: ["Open", "Completed", "Cancelled"]
39703
40353
  },
39704
- 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" }
39705
40360
  }
39706
40361
  },
39707
40362
  handler: async (input, ctx) => {
@@ -39713,6 +40368,12 @@ var projectTools = [
39713
40368
  if (input.company) {
39714
40369
  filters.push(["company", "=", input.company]);
39715
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
+ }
39716
40377
  const docs = await ctx.client.list("Project", {
39717
40378
  fields: [
39718
40379
  "name",
@@ -39775,7 +40436,12 @@ var projectTools = [
39775
40436
  type: "string",
39776
40437
  description: "Filter by priority (Low, Medium, High, Urgent)",
39777
40438
  enum: ["Low", "Medium", "High", "Urgent"]
39778
- }
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" }
39779
40445
  }
39780
40446
  },
39781
40447
  handler: async (input, ctx) => {
@@ -39790,6 +40456,12 @@ var projectTools = [
39790
40456
  if (input.priority) {
39791
40457
  filters.push(["priority", "=", input.priority]);
39792
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
+ }
39793
40465
  const docs = await ctx.client.list("Task", {
39794
40466
  fields: [
39795
40467
  "name",
@@ -40032,19 +40704,31 @@ var projectTools = [
40032
40704
  type: "object",
40033
40705
  properties: {
40034
40706
  limit: { type: "number", description: "Max results (default 20)" },
40035
- 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
+ },
40036
40711
  project: { type: "string", description: "Filter by project name" },
40037
40712
  status: {
40038
40713
  type: "string",
40039
40714
  description: "Filter by status (Draft, Submitted, Cancelled)"
40040
- }
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" }
40041
40721
  }
40042
40722
  },
40043
40723
  handler: async (input, ctx) => {
40044
40724
  const limit = input.limit ?? 20;
40045
40725
  const filters = [];
40046
40726
  if (input.employee) {
40047
- filters.push(["employee", "=", input.employee]);
40727
+ filters.push([
40728
+ "employee",
40729
+ "=",
40730
+ await resolveEmployee(ctx.client, input.employee)
40731
+ ]);
40048
40732
  }
40049
40733
  if (input.project) {
40050
40734
  filters.push(["project", "=", input.project]);
@@ -40052,6 +40736,12 @@ var projectTools = [
40052
40736
  if (input.status) {
40053
40737
  filters.push(["status", "=", input.status]);
40054
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
+ }
40055
40745
  const docs = await ctx.client.list("Timesheet", {
40056
40746
  fields: [
40057
40747
  "name",
@@ -40294,7 +40984,10 @@ var purchasingTools = [
40294
40984
  type: "object",
40295
40985
  properties: {
40296
40986
  limit: { type: "number", description: "Max results (default 20)" },
40297
- 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
+ },
40298
40991
  status: {
40299
40992
  type: "string",
40300
40993
  description: "Filter by status (Draft, To Receive and Bill, To Bill, Completed, Cancelled, etc.)"
@@ -40310,7 +41003,11 @@ var purchasingTools = [
40310
41003
  const limit = input.limit ?? 20;
40311
41004
  const filters = [];
40312
41005
  if (input.supplier) {
40313
- filters.push(["supplier", "=", input.supplier]);
41006
+ filters.push([
41007
+ "supplier",
41008
+ "=",
41009
+ await resolveSupplier(ctx.client, input.supplier)
41010
+ ]);
40314
41011
  }
40315
41012
  if (input.status) {
40316
41013
  filters.push(["status", "=", input.status]);
@@ -40442,7 +41139,10 @@ var purchasingTools = [
40442
41139
  type: "object",
40443
41140
  properties: {
40444
41141
  limit: { type: "number", description: "Max results (default 20)" },
40445
- 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
+ },
40446
41146
  status: {
40447
41147
  type: "string",
40448
41148
  description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
@@ -40458,7 +41158,11 @@ var purchasingTools = [
40458
41158
  const limit = input.limit ?? 20;
40459
41159
  const filters = [];
40460
41160
  if (input.supplier) {
40461
- filters.push(["supplier", "=", input.supplier]);
41161
+ filters.push([
41162
+ "supplier",
41163
+ "=",
41164
+ await resolveSupplier(ctx.client, input.supplier)
41165
+ ]);
40462
41166
  }
40463
41167
  if (input.status) {
40464
41168
  filters.push(["status", "=", input.status]);
@@ -40528,7 +41232,10 @@ var purchasingTools = [
40528
41232
  type: "object",
40529
41233
  properties: {
40530
41234
  limit: { type: "number", description: "Max results (default 20)" },
40531
- 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
+ },
40532
41239
  status: {
40533
41240
  type: "string",
40534
41241
  description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
@@ -40544,7 +41251,11 @@ var purchasingTools = [
40544
41251
  const limit = input.limit ?? 20;
40545
41252
  const filters = [];
40546
41253
  if (input.supplier) {
40547
- filters.push(["supplier", "=", input.supplier]);
41254
+ filters.push([
41255
+ "supplier",
41256
+ "=",
41257
+ await resolveSupplier(ctx.client, input.supplier)
41258
+ ]);
40548
41259
  }
40549
41260
  if (input.status) {
40550
41261
  filters.push(["status", "=", input.status]);
@@ -40613,22 +41324,40 @@ var purchasingTools = [
40613
41324
  type: "object",
40614
41325
  properties: {
40615
41326
  limit: { type: "number", description: "Max results (default 20)" },
40616
- 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
+ },
40617
41331
  status: {
40618
41332
  type: "string",
40619
41333
  description: "Filter by status (Draft, Submitted, Ordered, Lost, Cancelled)"
40620
- }
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" }
40621
41340
  }
40622
41341
  },
40623
41342
  handler: async (input, ctx) => {
40624
41343
  const limit = input.limit ?? 20;
40625
41344
  const filters = [];
40626
41345
  if (input.supplier) {
40627
- filters.push(["supplier", "=", input.supplier]);
41346
+ filters.push([
41347
+ "supplier",
41348
+ "=",
41349
+ await resolveSupplier(ctx.client, input.supplier)
41350
+ ]);
40628
41351
  }
40629
41352
  if (input.status) {
40630
41353
  filters.push(["status", "=", input.status]);
40631
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
+ }
40632
41361
  const docs = await ctx.client.list("Supplier Quotation", {
40633
41362
  fields: [
40634
41363
  "name",
@@ -40664,7 +41393,10 @@ var deliveryTools = [
40664
41393
  type: "object",
40665
41394
  properties: {
40666
41395
  limit: { type: "number", description: "Max results (default 20)" },
40667
- 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
+ },
40668
41400
  status: {
40669
41401
  type: "string",
40670
41402
  description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
@@ -40680,7 +41412,11 @@ var deliveryTools = [
40680
41412
  const limit = input.limit ?? 20;
40681
41413
  const filters = [];
40682
41414
  if (input.customer) {
40683
- filters.push(["customer", "=", input.customer]);
41415
+ filters.push([
41416
+ "customer",
41417
+ "=",
41418
+ await resolveCustomer(ctx.client, input.customer)
41419
+ ]);
40684
41420
  }
40685
41421
  if (input.status) {
40686
41422
  filters.push(["status", "=", input.status]);
@@ -40899,7 +41635,7 @@ var manufacturingTools = [
40899
41635
  limit: { type: "number", description: "Max results (default 20)" },
40900
41636
  item: {
40901
41637
  type: "string",
40902
- description: "Filter by finished goods item code"
41638
+ description: "Filter by finished goods item code or name (e.g. 'ITEM-001' or 'Widget A')"
40903
41639
  },
40904
41640
  is_active: {
40905
41641
  type: "boolean",
@@ -40915,7 +41651,11 @@ var manufacturingTools = [
40915
41651
  const limit = input.limit ?? 20;
40916
41652
  const filters = [];
40917
41653
  if (input.item) {
40918
- filters.push(["item", "=", input.item]);
41654
+ filters.push([
41655
+ "item",
41656
+ "=",
41657
+ await resolveItem(ctx.client, input.item)
41658
+ ]);
40919
41659
  }
40920
41660
  if (input.is_active !== void 0) {
40921
41661
  filters.push(["is_active", "=", input.is_active ? 1 : 0]);
@@ -40983,7 +41723,7 @@ var manufacturingTools = [
40983
41723
  limit: { type: "number", description: "Max results (default 20)" },
40984
41724
  production_item: {
40985
41725
  type: "string",
40986
- 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')"
40987
41727
  },
40988
41728
  status: {
40989
41729
  type: "string",
@@ -41003,7 +41743,11 @@ var manufacturingTools = [
41003
41743
  const limit = input.limit ?? 20;
41004
41744
  const filters = [];
41005
41745
  if (input.production_item) {
41006
- filters.push(["production_item", "=", input.production_item]);
41746
+ filters.push([
41747
+ "production_item",
41748
+ "=",
41749
+ await resolveItem(ctx.client, input.production_item)
41750
+ ]);
41007
41751
  }
41008
41752
  if (input.status) {
41009
41753
  filters.push(["status", "=", input.status]);
@@ -41327,9 +42071,14 @@ var crmTools = [
41327
42071
  type: "string",
41328
42072
  description: "Filter by assigned sales rep (user)"
41329
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
+ },
41330
42079
  party_name: {
41331
42080
  type: "string",
41332
- description: "Filter by customer or lead name"
42081
+ description: "Filter by party \u2014 ID or name (e.g. customer/lead name). Requires 'opportunity_from'."
41333
42082
  }
41334
42083
  }
41335
42084
  },
@@ -41347,7 +42096,20 @@ var crmTools = [
41347
42096
  ]);
41348
42097
  }
41349
42098
  if (input.party_name) {
41350
- 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
+ ]);
41351
42113
  }
41352
42114
  const docs = await ctx.client.list("Opportunity", {
41353
42115
  fields: [
@@ -41475,7 +42237,12 @@ var crmTools = [
41475
42237
  campaign_type: {
41476
42238
  type: "string",
41477
42239
  description: "Filter by campaign type"
41478
- }
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" }
41479
42246
  }
41480
42247
  },
41481
42248
  handler: async (input, ctx) => {
@@ -41484,6 +42251,12 @@ var crmTools = [
41484
42251
  if (input.campaign_type) {
41485
42252
  filters.push(["campaign_type", "=", input.campaign_type]);
41486
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
+ }
41487
42260
  const docs = await ctx.client.list("Campaign", {
41488
42261
  fields: [
41489
42262
  "name",
@@ -41531,7 +42304,15 @@ var assetsTools = [
41531
42304
  location: { type: "string", description: "Filter by location" },
41532
42305
  custodian: {
41533
42306
  type: "string",
41534
- 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"
41535
42316
  }
41536
42317
  }
41537
42318
  },
@@ -41548,7 +42329,17 @@ var assetsTools = [
41548
42329
  filters.push(["location", "=", input.location]);
41549
42330
  }
41550
42331
  if (input.custodian) {
41551
- 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]);
41552
42343
  }
41553
42344
  const docs = await ctx.client.list("Asset", {
41554
42345
  fields: [
@@ -42002,11 +42793,13 @@ var operationsTools = [
42002
42793
  }
42003
42794
  const doc = await ctx.client.get(
42004
42795
  input.doctype,
42005
- input.name
42796
+ input.name,
42797
+ { skipCache: true }
42006
42798
  );
42007
42799
  const result = await ctx.client.callMethod("frappe.client.submit", {
42008
42800
  doc: { ...doc, doctype: input.doctype }
42009
42801
  });
42802
+ ctx.client.invalidate(input.doctype, input.name);
42010
42803
  return {
42011
42804
  data: result,
42012
42805
  message: `${input.doctype} ${input.name} submitted successfully`,
@@ -42046,6 +42839,7 @@ var operationsTools = [
42046
42839
  doctype: input.doctype,
42047
42840
  name: input.name
42048
42841
  });
42842
+ ctx.client.invalidate(input.doctype, input.name);
42049
42843
  return {
42050
42844
  data: result,
42051
42845
  message: `${input.doctype} ${input.name} cancelled successfully`,
@@ -44414,628 +45208,334 @@ var ISSUE_ALLOWED_TRANSITIONS = [
44414
45208
  { fromColumn: "resolved", toColumn: "closed", allowed: true, label: "Close" },
44415
45209
  { fromColumn: "closed", toColumn: "open", allowed: true, label: "Reopen" },
44416
45210
  { fromColumn: "closed", toColumn: "replied", allowed: true, label: "Reply" }
44417
- ];
44418
- function columnIdForIssueStatus(status) {
44419
- return COLUMN_BY_STATUS3.get(String(status ?? "Open")) ?? "open";
44420
- }
44421
- function buildIssueCard(row) {
44422
- const status = String(row.status ?? "Open");
44423
- const columnId = columnIdForIssueStatus(status);
44424
- const priority = typeof row.priority === "string" ? row.priority : void 0;
44425
- let subtitle;
44426
- if (typeof row.customer === "string" && row.customer.length > 0) {
44427
- subtitle = row.customer;
44428
- } else if (typeof row.raised_by === "string" && row.raised_by.length > 0) {
44429
- subtitle = row.raised_by;
44430
- }
44431
- const badges = [];
44432
- if (priority) badges.push({ label: priority, tone: priorityTone(priority) });
44433
- const slaDate = row.resolution_by;
44434
- if (isDateOverdue(slaDate) && columnId !== "resolved" && columnId !== "closed") {
44435
- badges.push({ label: "SLA breach", tone: "error" });
44436
- }
44437
- const metrics = [];
44438
- if (typeof row.raised_by === "string" && row.raised_by.length > 0) {
44439
- metrics.push({ label: "Raised By", value: row.raised_by });
44440
- }
44441
- const slaDisplay = formatShortDate(slaDate);
44442
- if (slaDisplay) metrics.push({ label: "SLA", value: slaDisplay });
44443
- const openedDisplay = formatShortDate(row.opening_date);
44444
- if (openedDisplay) metrics.push({ label: "Opened", value: openedDisplay });
44445
- const resolvedDisplay = formatShortDate(row.resolution_date);
44446
- if (resolvedDisplay) {
44447
- metrics.push({ label: "Resolved", value: resolvedDisplay });
44448
- }
44449
- const assignee = parseFirstAssignee(row._assign);
44450
- return {
44451
- id: String(row.name ?? ""),
44452
- title: String(row.subject ?? row.name ?? "Untitled issue"),
44453
- subtitle,
44454
- columnId,
44455
- accent: ISSUE_COLUMNS.find((column) => column.id === columnId)?.color,
44456
- badges,
44457
- metrics,
44458
- dueDate: typeof slaDate === "string" ? slaDate : void 0,
44459
- assignee
44460
- };
44461
- }
44462
- var issueKanbanAdapter = {
44463
- doctype: "Issue",
44464
- getColumns() {
44465
- return ISSUE_COLUMNS.map((column) => ({
44466
- id: column.id,
44467
- label: column.label,
44468
- color: column.color,
44469
- count: 0
44470
- }));
44471
- },
44472
- getAllowedTransitions() {
44473
- return ISSUE_ALLOWED_TRANSITIONS;
44474
- },
44475
- getListFields() {
44476
- return ISSUE_LIST_FIELDS;
44477
- },
44478
- buildListFilters(input) {
44479
- const filters = [];
44480
- if (typeof input.status === "string" && input.status.length > 0) {
44481
- filters.push(["status", "=", input.status]);
44482
- }
44483
- if (typeof input.priority === "string" && input.priority.length > 0) {
44484
- filters.push(["priority", "=", input.priority]);
44485
- }
44486
- if (typeof input.customer === "string" && input.customer.length > 0) {
44487
- filters.push(["customer", "=", input.customer]);
44488
- }
44489
- if (typeof input.raised_by === "string" && input.raised_by.length > 0) {
44490
- filters.push(["raised_by", "=", input.raised_by]);
44491
- }
44492
- return filters;
44493
- },
44494
- buildCards(rows) {
44495
- return rows.map(buildIssueCard);
44496
- },
44497
- validateTransition(move) {
44498
- const match2 = ISSUE_ALLOWED_TRANSITIONS.find(
44499
- (transition) => transition.fromColumn === move.fromColumn && transition.toColumn === move.toColumn && transition.allowed
44500
- );
44501
- if (!match2) {
44502
- return { allowed: false, reason: "Issue transition is not allowed" };
44503
- }
44504
- return { allowed: true };
44505
- },
44506
- async executeMove(move, ctx) {
44507
- const currentIssue = await ctx.client.get("Issue", move.cardId);
44508
- const serverColumn = columnIdForIssueStatus(currentIssue.status);
44509
- if (serverColumn !== move.fromColumn) {
44510
- return {
44511
- ok: false,
44512
- cardId: move.cardId,
44513
- fromColumn: serverColumn,
44514
- toColumn: move.toColumn,
44515
- errorMessage: `Issue moved on the server from ${move.fromColumn} to ${serverColumn}. Refresh the board and try again.`
44516
- };
44517
- }
44518
- const validation = this.validateTransition(move);
44519
- if (!validation.allowed) {
44520
- return {
44521
- ok: false,
44522
- cardId: move.cardId,
44523
- fromColumn: move.fromColumn,
44524
- toColumn: move.toColumn,
44525
- errorMessage: validation.reason
44526
- };
44527
- }
44528
- const status = STATUS_BY_COLUMN3.get(move.toColumn);
44529
- if (!status) {
44530
- return {
44531
- ok: false,
44532
- cardId: move.cardId,
44533
- fromColumn: move.fromColumn,
44534
- toColumn: move.toColumn,
44535
- errorMessage: "Unknown Issue column"
44536
- };
44537
- }
44538
- const serverIssue = await ctx.client.update("Issue", move.cardId, {
44539
- status
44540
- });
44541
- return {
44542
- ok: true,
44543
- cardId: move.cardId,
44544
- fromColumn: serverColumn,
44545
- toColumn: move.toColumn,
44546
- serverCard: buildIssueCard(serverIssue)
44547
- };
44548
- }
44549
- };
44550
-
44551
- // src/tools/kanban.ts
44552
- var ADAPTERS = {
44553
- task: taskKanbanAdapter,
44554
- opportunity: opportunityKanbanAdapter,
44555
- issue: issueKanbanAdapter
44556
- };
44557
- function getAdapter(doctype) {
44558
- const definition = getKanbanBoardDefinition(doctype);
44559
- if (!definition) {
44560
- throw new Error(`[erpnext_kanban] Unsupported kanban doctype: ${doctype}`);
44561
- }
44562
- const adapter = ADAPTERS[definition.adapterKey];
44563
- if (!adapter) {
44564
- throw new Error(`[erpnext_kanban] No adapter registered for ${doctype}`);
44565
- }
44566
- return { definition, adapter };
44567
- }
44568
- function withColumnCounts(columns, cards) {
44569
- const counts = /* @__PURE__ */ new Map();
44570
- for (const card of cards) {
44571
- counts.set(card.columnId, (counts.get(card.columnId) ?? 0) + 1);
44572
- }
44573
- return columns.map((column) => ({
44574
- ...column,
44575
- count: counts.get(column.id) ?? 0
44576
- }));
44577
- }
44578
- var kanbanTools = [
44579
- {
44580
- name: "erpnext_kanban_get_board",
44581
- annotations: { readOnlyHint: true },
44582
- category: "kanban",
44583
- _meta: KANBAN_META,
44584
- description: "Get a normalized kanban board for a supported ERPNext DocType. Supports Task, Opportunity, and Issue, with pagination and MCP App metadata.",
44585
- inputSchema: {
44586
- type: "object",
44587
- properties: {
44588
- doctype: {
44589
- type: "string",
44590
- description: "Kanban-enabled ERPNext DocType",
44591
- enum: ["Task", "Opportunity", "Issue"]
44592
- },
44593
- limit: { type: "number", description: "Page size (default 50)" },
44594
- offset: {
44595
- type: "number",
44596
- description: "Pagination offset (default 0)"
44597
- },
44598
- project: {
44599
- type: "string",
44600
- description: "Optional Task project filter"
44601
- },
44602
- priority: {
44603
- type: "string",
44604
- description: "Optional Task priority filter",
44605
- enum: ["Low", "Medium", "High", "Urgent"]
44606
- },
44607
- status: {
44608
- type: "string",
44609
- description: "Optional Opportunity status filter",
44610
- enum: ["Open", "Replied", "Quotation", "Converted", "Closed", "Lost"]
44611
- },
44612
- opportunity_owner: {
44613
- type: "string",
44614
- description: "Optional Opportunity owner filter"
44615
- },
44616
- party_name: {
44617
- type: "string",
44618
- description: "Optional Opportunity party filter"
44619
- },
44620
- customer: {
44621
- type: "string",
44622
- description: "Optional Issue customer filter"
44623
- },
44624
- raised_by: {
44625
- type: "string",
44626
- description: "Optional Issue reporter email filter"
44627
- }
44628
- },
44629
- required: ["doctype"]
44630
- },
44631
- handler: async (input, ctx) => {
44632
- const doctype = String(input.doctype ?? "");
44633
- const { definition, adapter } = getAdapter(doctype);
44634
- const limit = Math.max(1, Number(input.limit ?? 50));
44635
- const offset = Math.max(0, Number(input.offset ?? 0));
44636
- const rows = await ctx.client.list(doctype, {
44637
- fields: adapter.getListFields(),
44638
- filters: adapter.buildListFilters(input),
44639
- limit: limit + 1,
44640
- limit_start: offset,
44641
- order_by: "modified desc"
44642
- });
44643
- const hasMore = rows.length > limit;
44644
- const visibleRows = rows.slice(0, limit);
44645
- const cards = adapter.buildCards(visibleRows);
44646
- const board = {
44647
- boardId: `${doctype.toLowerCase()}-board`,
44648
- title: definition.title,
44649
- doctype,
44650
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
44651
- moveToolName: definition.moveToolName,
44652
- refreshArguments: { ...input, doctype, limit, offset },
44653
- columns: withColumnCounts(adapter.getColumns(), cards),
44654
- cards,
44655
- allowedTransitions: adapter.getAllowedTransitions(),
44656
- capabilities: { canMoveCards: true },
44657
- pagination: {
44658
- limit,
44659
- offset,
44660
- loadedCount: offset + visibleRows.length,
44661
- hasMore
44662
- }
44663
- };
44664
- return {
44665
- ...board,
44666
- _meta: KANBAN_META
44667
- };
44668
- }
44669
- },
44670
- {
44671
- name: "erpnext_kanban_move_card",
44672
- category: "kanban",
44673
- description: "Move a kanban card for a supported ERPNext DocType. Returns structured success or business error details for MCP App reconciliation.",
44674
- inputSchema: {
44675
- type: "object",
44676
- properties: {
44677
- doctype: {
44678
- type: "string",
44679
- description: "Kanban-enabled ERPNext DocType",
44680
- enum: ["Task", "Opportunity", "Issue"]
44681
- },
44682
- card_id: { type: "string", description: "Card/document identifier" },
44683
- from_column: {
44684
- type: "string",
44685
- description: "Source column identifier"
44686
- },
44687
- to_column: {
44688
- type: "string",
44689
- description: "Destination column identifier"
44690
- }
44691
- },
44692
- required: ["doctype", "card_id", "from_column", "to_column"]
44693
- },
44694
- handler: async (input, ctx) => {
44695
- const doctype = String(input.doctype ?? "");
44696
- const { adapter } = getAdapter(doctype);
44697
- return await adapter.executeMove({
44698
- doctype,
44699
- cardId: String(input.card_id ?? ""),
44700
- fromColumn: String(input.from_column ?? ""),
44701
- toColumn: String(input.to_column ?? "")
44702
- }, ctx);
44703
- }
44704
- }
44705
- ];
44706
-
44707
- // src/tools/mod.ts
44708
- var toolsByCategory = {
44709
- sales: salesTools,
44710
- inventory: inventoryTools,
44711
- accounting: accountingTools,
44712
- hr: hrTools,
44713
- project: projectTools,
44714
- purchasing: purchasingTools,
44715
- delivery: deliveryTools,
44716
- manufacturing: manufacturingTools,
44717
- crm: crmTools,
44718
- assets: assetsTools,
44719
- operations: operationsTools,
44720
- setup: setupTools,
44721
- analytics: analyticsTools,
44722
- kanban: kanbanTools
44723
- };
44724
- var allTools = [
44725
- ...salesTools,
44726
- ...inventoryTools,
44727
- ...accountingTools,
44728
- ...hrTools,
44729
- ...projectTools,
44730
- ...purchasingTools,
44731
- ...deliveryTools,
44732
- ...manufacturingTools,
44733
- ...crmTools,
44734
- ...assetsTools,
44735
- ...operationsTools,
44736
- ...setupTools,
44737
- ...analyticsTools,
44738
- ...kanbanTools
44739
- ];
44740
- function getToolsByCategory(category) {
44741
- return toolsByCategory[category] ?? [];
44742
- }
44743
-
44744
- // src/runtime.ts
44745
- var isDeno2 = typeof globalThis.Deno?.version?.deno === "string";
44746
- var impl2 = isDeno2 ? await Promise.resolve().then(() => (init_runtime_deno2(), runtime_deno_exports2)) : await Promise.resolve().then(() => (init_runtime_node2(), runtime_node_exports2));
44747
- var env6 = impl2.env;
44748
- var readTextFile6 = impl2.readTextFile;
44749
- var statSync3 = impl2.statSync;
44750
- var readDirSync3 = impl2.readDirSync;
44751
- var getArgs3 = impl2.getArgs;
44752
- var exit3 = impl2.exit;
44753
- var onSignal3 = impl2.onSignal;
44754
-
44755
- // src/api/frappe-client.ts
44756
- var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
44757
- var DEFAULT_RETRY_METHODS = ["GET"];
44758
- var FrappeAPIError = class extends Error {
44759
- /**
44760
- * @param message - Human-readable error description
44761
- * @param status - HTTP status code (0 for network errors, 408 for timeouts)
44762
- * @param body - Raw response body (parsed JSON object or plain text string)
44763
- * @param retryAfterMs - When the server sent a `Retry-After` header on a
44764
- * retryable status (typically 429), the parsed delay
44765
- * in ms. Used by the retry loop; absent otherwise.
44766
- */
44767
- constructor(message2, status, body, retryAfterMs) {
44768
- super(`[FrappeClient] ${message2} (HTTP ${status})`);
44769
- this.status = status;
44770
- this.body = body;
44771
- this.retryAfterMs = retryAfterMs;
44772
- this.name = "FrappeAPIError";
44773
- }
44774
- };
44775
- function parseRetryAfter(raw2) {
44776
- if (!raw2) return void 0;
44777
- const trimmed = raw2.trim();
44778
- const seconds = Number(trimmed);
44779
- if (Number.isFinite(seconds) && seconds >= 0) {
44780
- return Math.round(seconds * 1e3);
44781
- }
44782
- const date3 = Date.parse(trimmed);
44783
- if (Number.isFinite(date3)) {
44784
- return Math.max(0, date3 - Date.now());
44785
- }
44786
- return void 0;
44787
- }
44788
- function extractServerMessages(raw2) {
44789
- if (!raw2 || typeof raw2 !== "string") return void 0;
44790
- try {
44791
- const outer = JSON.parse(raw2);
44792
- if (!Array.isArray(outer)) return void 0;
44793
- const msgs = [];
44794
- for (const item of outer) {
44795
- try {
44796
- const inner = JSON.parse(item);
44797
- if (typeof inner === "object" && inner?.message) {
44798
- msgs.push(inner.message);
44799
- } else if (typeof inner === "string") {
44800
- msgs.push(inner);
44801
- }
44802
- } catch {
44803
- if (typeof item === "string") msgs.push(item);
44804
- }
44805
- }
44806
- return msgs.length > 0 ? msgs.join("; ") : void 0;
44807
- } catch {
44808
- return void 0;
44809
- }
44810
- }
44811
- var FrappeClient = class {
44812
- baseUrl;
44813
- authHeader;
44814
- timeoutMs;
44815
- retries;
44816
- retryStatuses;
44817
- retryBackoffMs;
44818
- retryMethods;
44819
- constructor(config2) {
44820
- this.baseUrl = config2.baseUrl.replace(/\/$/, "");
44821
- this.authHeader = `token ${config2.apiKey}:${config2.apiSecret}`;
44822
- this.timeoutMs = config2.timeoutMs ?? 3e4;
44823
- this.retries = config2.retries ?? 3;
44824
- this.retryStatuses = config2.retryStatuses ?? DEFAULT_RETRY_STATUSES;
44825
- this.retryBackoffMs = config2.retryBackoffMs ?? 200;
44826
- this.retryMethods = config2.retryMethods ?? DEFAULT_RETRY_METHODS;
44827
- }
44828
- // ── Private HTTP helpers ────────────────────────────────────────────────────
44829
- buildHeaders() {
44830
- return {
44831
- "Authorization": this.authHeader,
44832
- "Accept": "application/json",
44833
- "Content-Type": "application/json"
44834
- };
45211
+ ];
45212
+ function columnIdForIssueStatus(status) {
45213
+ return COLUMN_BY_STATUS3.get(String(status ?? "Open")) ?? "open";
45214
+ }
45215
+ function buildIssueCard(row) {
45216
+ const status = String(row.status ?? "Open");
45217
+ const columnId = columnIdForIssueStatus(status);
45218
+ const priority = typeof row.priority === "string" ? row.priority : void 0;
45219
+ let subtitle;
45220
+ if (typeof row.customer === "string" && row.customer.length > 0) {
45221
+ subtitle = row.customer;
45222
+ } else if (typeof row.raised_by === "string" && row.raised_by.length > 0) {
45223
+ subtitle = row.raised_by;
44835
45224
  }
44836
- /**
44837
- * Decide whether an error is worth retrying.
44838
- * Network errors (status 0) and timeouts (status 408) are always retryable;
44839
- * other statuses are checked against `retryStatuses`.
44840
- */
44841
- isRetryable(err, method) {
44842
- if (!this.retryMethods.includes(method)) return false;
44843
- if (!(err instanceof FrappeAPIError)) return false;
44844
- if (err.status === 0) return true;
44845
- return this.retryStatuses.includes(err.status);
45225
+ const badges = [];
45226
+ if (priority) badges.push({ label: priority, tone: priorityTone(priority) });
45227
+ const slaDate = row.resolution_by;
45228
+ if (isDateOverdue(slaDate) && columnId !== "resolved" && columnId !== "closed") {
45229
+ badges.push({ label: "SLA breach", tone: "error" });
44846
45230
  }
44847
- /** Compute the backoff delay for a given attempt (0-indexed). */
44848
- computeBackoff(attempt, err) {
44849
- if (err instanceof FrappeAPIError && err.retryAfterMs !== void 0) {
44850
- return err.retryAfterMs;
44851
- }
44852
- return this.retryBackoffMs * 2 ** attempt;
45231
+ const metrics = [];
45232
+ if (typeof row.raised_by === "string" && row.raised_by.length > 0) {
45233
+ metrics.push({ label: "Raised By", value: row.raised_by });
44853
45234
  }
44854
- async request(method, path, body) {
44855
- let lastError;
44856
- for (let attempt = 0; attempt <= this.retries; attempt++) {
44857
- try {
44858
- return await this.requestOnce(method, path, body);
44859
- } catch (err) {
44860
- lastError = err;
44861
- if (attempt === this.retries || !this.isRetryable(err, method)) {
44862
- throw err;
44863
- }
44864
- const delay = this.computeBackoff(attempt, err);
44865
- if (delay > 0) {
44866
- await new Promise((resolve) => setTimeout(resolve, delay));
44867
- }
44868
- }
44869
- }
44870
- throw lastError;
45235
+ const slaDisplay = formatShortDate(slaDate);
45236
+ if (slaDisplay) metrics.push({ label: "SLA", value: slaDisplay });
45237
+ const openedDisplay = formatShortDate(row.opening_date);
45238
+ if (openedDisplay) metrics.push({ label: "Opened", value: openedDisplay });
45239
+ const resolvedDisplay = formatShortDate(row.resolution_date);
45240
+ if (resolvedDisplay) {
45241
+ metrics.push({ label: "Resolved", value: resolvedDisplay });
44871
45242
  }
44872
- async requestOnce(method, path, body) {
44873
- const url = `${this.baseUrl}${path}`;
44874
- const controller = new AbortController();
44875
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
44876
- let response;
44877
- try {
44878
- response = await fetch(url, {
44879
- method,
44880
- headers: this.buildHeaders(),
44881
- body: body !== void 0 ? JSON.stringify(body) : void 0,
44882
- signal: controller.signal
44883
- });
44884
- } catch (err) {
44885
- clearTimeout(timer);
44886
- if (err instanceof Error && err.name === "AbortError") {
44887
- throw new FrappeAPIError(
44888
- `Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
44889
- 408,
44890
- null
44891
- );
44892
- }
44893
- throw new FrappeAPIError(
44894
- `Network error on ${method} ${path}: ${err.message}`,
44895
- 0,
44896
- null
44897
- );
44898
- }
44899
- clearTimeout(timer);
44900
- const rawText = await response.text();
44901
- const contentType = response.headers.get("content-type") ?? "";
44902
- let responseBody = rawText;
44903
- if (contentType.includes("application/json") && rawText.length > 0) {
44904
- try {
44905
- responseBody = JSON.parse(rawText);
44906
- } catch {
44907
- responseBody = rawText;
44908
- }
45243
+ const assignee = parseFirstAssignee(row._assign);
45244
+ return {
45245
+ id: String(row.name ?? ""),
45246
+ title: String(row.subject ?? row.name ?? "Untitled issue"),
45247
+ subtitle,
45248
+ columnId,
45249
+ accent: ISSUE_COLUMNS.find((column) => column.id === columnId)?.color,
45250
+ badges,
45251
+ metrics,
45252
+ dueDate: typeof slaDate === "string" ? slaDate : void 0,
45253
+ assignee
45254
+ };
45255
+ }
45256
+ var issueKanbanAdapter = {
45257
+ doctype: "Issue",
45258
+ getColumns() {
45259
+ return ISSUE_COLUMNS.map((column) => ({
45260
+ id: column.id,
45261
+ label: column.label,
45262
+ color: column.color,
45263
+ count: 0
45264
+ }));
45265
+ },
45266
+ getAllowedTransitions() {
45267
+ return ISSUE_ALLOWED_TRANSITIONS;
45268
+ },
45269
+ getListFields() {
45270
+ return ISSUE_LIST_FIELDS;
45271
+ },
45272
+ buildListFilters(input) {
45273
+ const filters = [];
45274
+ if (typeof input.status === "string" && input.status.length > 0) {
45275
+ filters.push(["status", "=", input.status]);
44909
45276
  }
44910
- if (!response.ok) {
44911
- let msg = response.statusText;
44912
- if (typeof responseBody === "object" && responseBody !== null) {
44913
- const rb = responseBody;
44914
- const excType = rb.exc_type;
44915
- const baseMsg = rb.message ?? excType ?? response.statusText;
44916
- const serverDetails = extractServerMessages(rb._server_messages);
44917
- msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
44918
- } else if (typeof responseBody === "string" && responseBody.length > 0) {
44919
- msg = responseBody.slice(0, 200);
44920
- }
44921
- const retryAfterMs = parseRetryAfter(
44922
- response.headers.get("retry-after")
44923
- );
44924
- throw new FrappeAPIError(
44925
- `${method} ${path} failed: ${msg}`,
44926
- response.status,
44927
- responseBody,
44928
- retryAfterMs
44929
- );
45277
+ if (typeof input.priority === "string" && input.priority.length > 0) {
45278
+ filters.push(["priority", "=", input.priority]);
44930
45279
  }
44931
- return responseBody;
44932
- }
44933
- // ── Resource CRUD ───────────────────────────────────────────────────────────
44934
- /**
44935
- * List documents of a DocType.
44936
- * Frappe list API: GET /api/resource/{doctype}?fields=...&filters=...
44937
- */
44938
- async list(doctype, options = {}) {
44939
- const params = new URLSearchParams();
44940
- if (options.fields && options.fields.length > 0) {
44941
- params.set("fields", JSON.stringify(options.fields));
45280
+ if (typeof input.customer === "string" && input.customer.length > 0) {
45281
+ filters.push(["customer", "=", input.customer]);
44942
45282
  }
44943
- if (options.filters && options.filters.length > 0) {
44944
- params.set("filters", JSON.stringify(options.filters));
45283
+ if (typeof input.raised_by === "string" && input.raised_by.length > 0) {
45284
+ filters.push(["raised_by", "=", input.raised_by]);
44945
45285
  }
44946
- if (options.order_by) {
44947
- params.set("order_by", options.order_by);
45286
+ return filters;
45287
+ },
45288
+ buildCards(rows) {
45289
+ return rows.map(buildIssueCard);
45290
+ },
45291
+ validateTransition(move) {
45292
+ const match2 = ISSUE_ALLOWED_TRANSITIONS.find(
45293
+ (transition) => transition.fromColumn === move.fromColumn && transition.toColumn === move.toColumn && transition.allowed
45294
+ );
45295
+ if (!match2) {
45296
+ return { allowed: false, reason: "Issue transition is not allowed" };
44948
45297
  }
44949
- if (options.limit !== void 0) {
44950
- params.set("limit", String(options.limit));
45298
+ return { allowed: true };
45299
+ },
45300
+ async executeMove(move, ctx) {
45301
+ const currentIssue = await ctx.client.get("Issue", move.cardId);
45302
+ const serverColumn = columnIdForIssueStatus(currentIssue.status);
45303
+ if (serverColumn !== move.fromColumn) {
45304
+ return {
45305
+ ok: false,
45306
+ cardId: move.cardId,
45307
+ fromColumn: serverColumn,
45308
+ toColumn: move.toColumn,
45309
+ errorMessage: `Issue moved on the server from ${move.fromColumn} to ${serverColumn}. Refresh the board and try again.`
45310
+ };
44951
45311
  }
44952
- if (options.limit_start !== void 0) {
44953
- params.set("limit_start", String(options.limit_start));
45312
+ const validation = this.validateTransition(move);
45313
+ if (!validation.allowed) {
45314
+ return {
45315
+ ok: false,
45316
+ cardId: move.cardId,
45317
+ fromColumn: move.fromColumn,
45318
+ toColumn: move.toColumn,
45319
+ errorMessage: validation.reason
45320
+ };
44954
45321
  }
44955
- params.set("as_dict", "1");
44956
- const query = params.toString() ? `?${params.toString()}` : "";
44957
- const res = await this.request(
44958
- "GET",
44959
- `/api/resource/${encodeURIComponent(doctype)}${query}`
44960
- );
44961
- return res.data ?? [];
44962
- }
44963
- /**
44964
- * Get a single document by name.
44965
- * GET /api/resource/{doctype}/{name}
44966
- */
44967
- async get(doctype, name) {
44968
- const res = await this.request(
44969
- "GET",
44970
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
44971
- );
44972
- return res.data;
44973
- }
44974
- /**
44975
- * Create a new document.
44976
- * POST /api/resource/{doctype}
44977
- */
44978
- async create(doctype, data) {
44979
- const res = await this.request(
44980
- "POST",
44981
- `/api/resource/${encodeURIComponent(doctype)}`,
44982
- { data: { ...data, doctype } }
44983
- );
44984
- return res.data;
44985
- }
44986
- /**
44987
- * Update an existing document (partial update).
44988
- * PUT /api/resource/{doctype}/{name}
44989
- */
44990
- async update(doctype, name, data) {
44991
- const res = await this.request(
44992
- "PUT",
44993
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`,
44994
- { data }
44995
- );
44996
- return res.data;
45322
+ const status = STATUS_BY_COLUMN3.get(move.toColumn);
45323
+ if (!status) {
45324
+ return {
45325
+ ok: false,
45326
+ cardId: move.cardId,
45327
+ fromColumn: move.fromColumn,
45328
+ toColumn: move.toColumn,
45329
+ errorMessage: "Unknown Issue column"
45330
+ };
45331
+ }
45332
+ const serverIssue = await ctx.client.update("Issue", move.cardId, {
45333
+ status
45334
+ });
45335
+ return {
45336
+ ok: true,
45337
+ cardId: move.cardId,
45338
+ fromColumn: serverColumn,
45339
+ toColumn: move.toColumn,
45340
+ serverCard: buildIssueCard(serverIssue)
45341
+ };
44997
45342
  }
44998
- /**
44999
- * Delete a document.
45000
- * DELETE /api/resource/{doctype}/{name}
45001
- */
45002
- async delete(doctype, name) {
45003
- await this.request(
45004
- "DELETE",
45005
- `/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
45006
- );
45343
+ };
45344
+
45345
+ // src/tools/kanban.ts
45346
+ var ADAPTERS = {
45347
+ task: taskKanbanAdapter,
45348
+ opportunity: opportunityKanbanAdapter,
45349
+ issue: issueKanbanAdapter
45350
+ };
45351
+ function getAdapter(doctype) {
45352
+ const definition = getKanbanBoardDefinition(doctype);
45353
+ if (!definition) {
45354
+ throw new Error(`[erpnext_kanban] Unsupported kanban doctype: ${doctype}`);
45007
45355
  }
45008
- /**
45009
- * Call a whitelisted Frappe method.
45010
- * POST /api/method/{method}
45011
- */
45012
- async callMethod(method, args = {}) {
45013
- const res = await this.request(
45014
- "POST",
45015
- `/api/method/${method}`,
45016
- args
45017
- );
45018
- return res.message;
45356
+ const adapter = ADAPTERS[definition.adapterKey];
45357
+ if (!adapter) {
45358
+ throw new Error(`[erpnext_kanban] No adapter registered for ${doctype}`);
45019
45359
  }
45020
- };
45021
- var _client = null;
45022
- function getFrappeClient() {
45023
- if (_client) return _client;
45024
- const url = env6("ERPNEXT_URL");
45025
- const apiKey = env6("ERPNEXT_API_KEY");
45026
- const apiSecret = env6("ERPNEXT_API_SECRET");
45027
- if (!url) {
45028
- throw new Error(
45029
- "[lib/erpnext] ERPNEXT_URL is required. Set it to your ERPNext instance URL, e.g. http://localhost:8000"
45030
- );
45360
+ return { definition, adapter };
45361
+ }
45362
+ function withColumnCounts(columns, cards) {
45363
+ const counts = /* @__PURE__ */ new Map();
45364
+ for (const card of cards) {
45365
+ counts.set(card.columnId, (counts.get(card.columnId) ?? 0) + 1);
45031
45366
  }
45032
- if (!apiKey || !apiSecret) {
45033
- throw new Error(
45034
- "[lib/erpnext] ERPNEXT_API_KEY and ERPNEXT_API_SECRET are required. Generate them in ERPNext: User Settings \u2192 API Access."
45035
- );
45367
+ return columns.map((column) => ({
45368
+ ...column,
45369
+ count: counts.get(column.id) ?? 0
45370
+ }));
45371
+ }
45372
+ var kanbanTools = [
45373
+ {
45374
+ name: "erpnext_kanban_get_board",
45375
+ annotations: { readOnlyHint: true },
45376
+ category: "kanban",
45377
+ _meta: KANBAN_META,
45378
+ description: "Get a normalized kanban board for a supported ERPNext DocType. Supports Task, Opportunity, and Issue, with pagination and MCP App metadata.",
45379
+ inputSchema: {
45380
+ type: "object",
45381
+ properties: {
45382
+ doctype: {
45383
+ type: "string",
45384
+ description: "Kanban-enabled ERPNext DocType",
45385
+ enum: ["Task", "Opportunity", "Issue"]
45386
+ },
45387
+ limit: { type: "number", description: "Page size (default 50)" },
45388
+ offset: {
45389
+ type: "number",
45390
+ description: "Pagination offset (default 0)"
45391
+ },
45392
+ project: {
45393
+ type: "string",
45394
+ description: "Optional Task project filter"
45395
+ },
45396
+ priority: {
45397
+ type: "string",
45398
+ description: "Optional Task priority filter",
45399
+ enum: ["Low", "Medium", "High", "Urgent"]
45400
+ },
45401
+ status: {
45402
+ type: "string",
45403
+ description: "Optional Opportunity status filter",
45404
+ enum: ["Open", "Replied", "Quotation", "Converted", "Closed", "Lost"]
45405
+ },
45406
+ opportunity_owner: {
45407
+ type: "string",
45408
+ description: "Optional Opportunity owner filter"
45409
+ },
45410
+ party_name: {
45411
+ type: "string",
45412
+ description: "Optional Opportunity party filter"
45413
+ },
45414
+ customer: {
45415
+ type: "string",
45416
+ description: "Optional Issue customer filter"
45417
+ },
45418
+ raised_by: {
45419
+ type: "string",
45420
+ description: "Optional Issue reporter email filter"
45421
+ }
45422
+ },
45423
+ required: ["doctype"]
45424
+ },
45425
+ handler: async (input, ctx) => {
45426
+ const doctype = String(input.doctype ?? "");
45427
+ const { definition, adapter } = getAdapter(doctype);
45428
+ const limit = Math.max(1, Number(input.limit ?? 50));
45429
+ const offset = Math.max(0, Number(input.offset ?? 0));
45430
+ const rows = await ctx.client.list(doctype, {
45431
+ fields: adapter.getListFields(),
45432
+ filters: adapter.buildListFilters(input),
45433
+ limit: limit + 1,
45434
+ limit_start: offset,
45435
+ order_by: "modified desc"
45436
+ });
45437
+ const hasMore = rows.length > limit;
45438
+ const visibleRows = rows.slice(0, limit);
45439
+ const cards = adapter.buildCards(visibleRows);
45440
+ const board = {
45441
+ boardId: `${doctype.toLowerCase()}-board`,
45442
+ title: definition.title,
45443
+ doctype,
45444
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
45445
+ moveToolName: definition.moveToolName,
45446
+ refreshArguments: { ...input, doctype, limit, offset },
45447
+ columns: withColumnCounts(adapter.getColumns(), cards),
45448
+ cards,
45449
+ allowedTransitions: adapter.getAllowedTransitions(),
45450
+ capabilities: { canMoveCards: true },
45451
+ pagination: {
45452
+ limit,
45453
+ offset,
45454
+ loadedCount: offset + visibleRows.length,
45455
+ hasMore
45456
+ }
45457
+ };
45458
+ return {
45459
+ ...board,
45460
+ _meta: KANBAN_META
45461
+ };
45462
+ }
45463
+ },
45464
+ {
45465
+ name: "erpnext_kanban_move_card",
45466
+ category: "kanban",
45467
+ description: "Move a kanban card for a supported ERPNext DocType. Returns structured success or business error details for MCP App reconciliation.",
45468
+ inputSchema: {
45469
+ type: "object",
45470
+ properties: {
45471
+ doctype: {
45472
+ type: "string",
45473
+ description: "Kanban-enabled ERPNext DocType",
45474
+ enum: ["Task", "Opportunity", "Issue"]
45475
+ },
45476
+ card_id: { type: "string", description: "Card/document identifier" },
45477
+ from_column: {
45478
+ type: "string",
45479
+ description: "Source column identifier"
45480
+ },
45481
+ to_column: {
45482
+ type: "string",
45483
+ description: "Destination column identifier"
45484
+ }
45485
+ },
45486
+ required: ["doctype", "card_id", "from_column", "to_column"]
45487
+ },
45488
+ handler: async (input, ctx) => {
45489
+ const doctype = String(input.doctype ?? "");
45490
+ const { adapter } = getAdapter(doctype);
45491
+ return await adapter.executeMove({
45492
+ doctype,
45493
+ cardId: String(input.card_id ?? ""),
45494
+ fromColumn: String(input.from_column ?? ""),
45495
+ toColumn: String(input.to_column ?? "")
45496
+ }, ctx);
45497
+ }
45036
45498
  }
45037
- _client = new FrappeClient({ baseUrl: url, apiKey, apiSecret });
45038
- return _client;
45499
+ ];
45500
+
45501
+ // src/tools/mod.ts
45502
+ var toolsByCategory = {
45503
+ sales: salesTools,
45504
+ inventory: inventoryTools,
45505
+ accounting: accountingTools,
45506
+ hr: hrTools,
45507
+ project: projectTools,
45508
+ purchasing: purchasingTools,
45509
+ delivery: deliveryTools,
45510
+ manufacturing: manufacturingTools,
45511
+ crm: crmTools,
45512
+ assets: assetsTools,
45513
+ operations: operationsTools,
45514
+ setup: setupTools,
45515
+ analytics: analyticsTools,
45516
+ kanban: kanbanTools
45517
+ };
45518
+ var allTools = [
45519
+ ...salesTools,
45520
+ ...inventoryTools,
45521
+ ...accountingTools,
45522
+ ...hrTools,
45523
+ ...projectTools,
45524
+ ...purchasingTools,
45525
+ ...deliveryTools,
45526
+ ...manufacturingTools,
45527
+ ...crmTools,
45528
+ ...assetsTools,
45529
+ ...operationsTools,
45530
+ ...setupTools,
45531
+ ...analyticsTools,
45532
+ ...kanbanTools
45533
+ ];
45534
+ function getToolsByCategory(category) {
45535
+ return toolsByCategory[category] ?? [];
45536
+ }
45537
+ function getToolByName(name) {
45538
+ return allTools.find((t) => t.name === name);
45039
45539
  }
45040
45540
 
45041
45541
  // src/tools/ui-refresh.ts
@@ -45332,6 +45832,37 @@ function resolveViewerDistPath2(moduleUrl, viewerName, exists) {
45332
45832
  return null;
45333
45833
  }
45334
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
+
45335
45866
  // server.ts
45336
45867
  var DEFAULT_HTTP_PORT = 3012;
45337
45868
  async function main() {
@@ -45356,7 +45887,7 @@ async function main() {
45356
45887
  );
45357
45888
  const server = new McpApp({
45358
45889
  name: "mcp-erpnext",
45359
- version: "2.4.2",
45890
+ version: "2.5.0",
45360
45891
  maxConcurrent: 10,
45361
45892
  backpressureStrategy: "queue",
45362
45893
  validateSchema: true,
@@ -45401,6 +45932,9 @@ async function main() {
45401
45932
  console.error(
45402
45933
  `[mcp-erpnext] Initialized \u2014 ${toolsClient.count} tools${categories ? ` (categories: ${categories.join(", ")})` : ""}`
45403
45934
  );
45935
+ warmCache().catch((err) => {
45936
+ console.error("[mcp-erpnext] Cache warm failed (non-fatal):", err);
45937
+ });
45404
45938
  if (httpFlag) {
45405
45939
  const isLoopback = hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
45406
45940
  if (!isLoopback) {