@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.
- package/README.md +42 -29
- package/mcp-erpnext.mjs +903 -351
- 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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: {
|
|
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([
|
|
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
|
-
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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
|
-
[
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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: {
|
|
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([
|
|
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([
|
|
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([
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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([
|
|
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
|
-
|
|
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] : "
|
|
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.
|
|
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,
|