@casys/mcp-erpnext 2.4.2 → 2.6.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 +53 -37
- package/mcp-erpnext.mjs +1604 -646
- package/package.json +1 -1
package/mcp-erpnext.mjs
CHANGED
|
@@ -34542,7 +34542,7 @@ function isCloudflareWorkers() {
|
|
|
34542
34542
|
var USER_AGENT;
|
|
34543
34543
|
if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) {
|
|
34544
34544
|
const NAME = "jose";
|
|
34545
|
-
const VERSION2 = "v6.2.
|
|
34545
|
+
const VERSION2 = "v6.2.4";
|
|
34546
34546
|
USER_AGENT = `${NAME}/${VERSION2}`;
|
|
34547
34547
|
}
|
|
34548
34548
|
var customFetch = Symbol();
|
|
@@ -37314,6 +37314,65 @@ function defaultHumanName(name) {
|
|
|
37314
37314
|
return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
37315
37315
|
}
|
|
37316
37316
|
|
|
37317
|
+
// node_modules/@casys/mcp-server/src/auth/static-token-provider.ts
|
|
37318
|
+
var StaticTokenAuthProvider = class extends AuthProvider {
|
|
37319
|
+
tokens;
|
|
37320
|
+
authInfo;
|
|
37321
|
+
resource;
|
|
37322
|
+
resourceMetadataUrl;
|
|
37323
|
+
scopesSupported;
|
|
37324
|
+
constructor(tokens, options) {
|
|
37325
|
+
super();
|
|
37326
|
+
if (tokens.length === 0) {
|
|
37327
|
+
throw new Error(
|
|
37328
|
+
"[StaticTokenAuthProvider] `tokens` must contain at least one token"
|
|
37329
|
+
);
|
|
37330
|
+
}
|
|
37331
|
+
if (tokens.some((t) => t.trim().length === 0)) {
|
|
37332
|
+
throw new Error(
|
|
37333
|
+
"[StaticTokenAuthProvider] `tokens` must not contain empty entries"
|
|
37334
|
+
);
|
|
37335
|
+
}
|
|
37336
|
+
if (!options.resource?.trim()) {
|
|
37337
|
+
throw new Error("[StaticTokenAuthProvider] `resource` is required");
|
|
37338
|
+
}
|
|
37339
|
+
const resourceUrl = httpsUrl(options.resource);
|
|
37340
|
+
this.tokens = new Set(tokens.map((t) => t.trim()));
|
|
37341
|
+
const scopes = Object.freeze([...options.scopes ?? []]);
|
|
37342
|
+
this.authInfo = Object.freeze({
|
|
37343
|
+
subject: options.subject ?? "static-token-user",
|
|
37344
|
+
scopes
|
|
37345
|
+
});
|
|
37346
|
+
this.resource = options.resource;
|
|
37347
|
+
this.scopesSupported = options.scopesSupported ? Object.freeze([...options.scopesSupported]) : scopes.length > 0 ? scopes : void 0;
|
|
37348
|
+
if (options.resourceMetadataUrl?.trim()) {
|
|
37349
|
+
this.resourceMetadataUrl = httpsUrl(options.resourceMetadataUrl);
|
|
37350
|
+
} else {
|
|
37351
|
+
const parsed = new URL(resourceUrl);
|
|
37352
|
+
const pathPart = parsed.pathname === "/" ? "" : parsed.pathname;
|
|
37353
|
+
this.resourceMetadataUrl = httpsUrl(
|
|
37354
|
+
`${parsed.origin}/.well-known/oauth-protected-resource${pathPart}${parsed.search}`
|
|
37355
|
+
);
|
|
37356
|
+
}
|
|
37357
|
+
}
|
|
37358
|
+
async verifyToken(token) {
|
|
37359
|
+
return this.tokens.has(token.trim()) ? this.authInfo : null;
|
|
37360
|
+
}
|
|
37361
|
+
getResourceMetadata() {
|
|
37362
|
+
return {
|
|
37363
|
+
resource: this.resource,
|
|
37364
|
+
resource_metadata_url: this.resourceMetadataUrl,
|
|
37365
|
+
// No authorization server: static tokens are provisioned out of band.
|
|
37366
|
+
authorization_servers: [],
|
|
37367
|
+
scopes_supported: this.scopesSupported,
|
|
37368
|
+
bearer_methods_supported: ["header"]
|
|
37369
|
+
};
|
|
37370
|
+
}
|
|
37371
|
+
};
|
|
37372
|
+
function createStaticTokenAuthProvider(tokens, options) {
|
|
37373
|
+
return new StaticTokenAuthProvider(tokens, options);
|
|
37374
|
+
}
|
|
37375
|
+
|
|
37317
37376
|
// node_modules/@casys/mcp-server/src/inspector/launcher.ts
|
|
37318
37377
|
async function launchInspector(serverCommand, serverArgs, options) {
|
|
37319
37378
|
const { spawn } = await import("node:child_process");
|
|
@@ -37389,97 +37448,714 @@ var KANBAN_META = viewer("kanban-viewer");
|
|
|
37389
37448
|
var KPI_META = viewer("kpi-viewer");
|
|
37390
37449
|
var FUNNEL_META = viewer("funnel-viewer");
|
|
37391
37450
|
|
|
37392
|
-
// src/
|
|
37393
|
-
|
|
37394
|
-
|
|
37451
|
+
// src/runtime.ts
|
|
37452
|
+
var isDeno2 = typeof globalThis.Deno?.version?.deno === "string";
|
|
37453
|
+
var impl2 = isDeno2 ? await Promise.resolve().then(() => (init_runtime_deno2(), runtime_deno_exports2)) : await Promise.resolve().then(() => (init_runtime_node2(), runtime_node_exports2));
|
|
37454
|
+
var env6 = impl2.env;
|
|
37455
|
+
var readTextFile6 = impl2.readTextFile;
|
|
37456
|
+
var statSync3 = impl2.statSync;
|
|
37457
|
+
var readDirSync3 = impl2.readDirSync;
|
|
37458
|
+
var getArgs3 = impl2.getArgs;
|
|
37459
|
+
var exit3 = impl2.exit;
|
|
37460
|
+
var onSignal3 = impl2.onSignal;
|
|
37461
|
+
|
|
37462
|
+
// src/cache/memory.ts
|
|
37463
|
+
var MemoryCache = class {
|
|
37464
|
+
store = /* @__PURE__ */ new Map();
|
|
37465
|
+
get(key) {
|
|
37466
|
+
const entry = this.store.get(key);
|
|
37467
|
+
if (!entry) return void 0;
|
|
37468
|
+
if (Date.now() >= entry.expiresAt) {
|
|
37469
|
+
this.store.delete(key);
|
|
37470
|
+
return void 0;
|
|
37471
|
+
}
|
|
37472
|
+
return structuredClone(entry.value);
|
|
37473
|
+
}
|
|
37474
|
+
set(key, value, ttlMs) {
|
|
37475
|
+
this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
37476
|
+
}
|
|
37477
|
+
delete(key) {
|
|
37478
|
+
this.store.delete(key);
|
|
37479
|
+
}
|
|
37480
|
+
deleteByPrefix(prefix) {
|
|
37481
|
+
for (const key of this.store.keys()) {
|
|
37482
|
+
if (key.startsWith(prefix)) this.store.delete(key);
|
|
37483
|
+
}
|
|
37484
|
+
}
|
|
37485
|
+
clear() {
|
|
37486
|
+
this.store.clear();
|
|
37487
|
+
}
|
|
37488
|
+
};
|
|
37489
|
+
|
|
37490
|
+
// src/cache/noop.ts
|
|
37491
|
+
var NoopCache = class {
|
|
37492
|
+
get(_key) {
|
|
37493
|
+
return void 0;
|
|
37494
|
+
}
|
|
37495
|
+
set(_key, _value, _ttlMs) {
|
|
37496
|
+
}
|
|
37497
|
+
delete(_key) {
|
|
37498
|
+
}
|
|
37499
|
+
deleteByPrefix(_prefix) {
|
|
37500
|
+
}
|
|
37501
|
+
clear() {
|
|
37502
|
+
}
|
|
37503
|
+
};
|
|
37504
|
+
|
|
37505
|
+
// src/cache/cache.ts
|
|
37506
|
+
var DEFAULT_CACHE_TTL_MS = 15e3;
|
|
37507
|
+
var _cache = null;
|
|
37508
|
+
function getCacheTtlMs() {
|
|
37509
|
+
const raw2 = env6("MCP_CACHE_TTL_MS");
|
|
37510
|
+
if (raw2 === void 0) return DEFAULT_CACHE_TTL_MS;
|
|
37511
|
+
const parsed = Number(raw2);
|
|
37512
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
37513
|
+
console.error(
|
|
37514
|
+
`[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).`
|
|
37515
|
+
);
|
|
37516
|
+
return DEFAULT_CACHE_TTL_MS;
|
|
37517
|
+
}
|
|
37518
|
+
return parsed;
|
|
37519
|
+
}
|
|
37520
|
+
function getCache() {
|
|
37521
|
+
if (_cache) return _cache;
|
|
37522
|
+
const enabled = env6("MCP_CACHE_ENABLED") !== "false";
|
|
37523
|
+
_cache = enabled ? new MemoryCache() : new NoopCache();
|
|
37524
|
+
return _cache;
|
|
37525
|
+
}
|
|
37526
|
+
|
|
37527
|
+
// src/api/frappe-client.ts
|
|
37528
|
+
function stableStringify(value) {
|
|
37529
|
+
if (Array.isArray(value)) {
|
|
37530
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
37531
|
+
}
|
|
37532
|
+
if (value !== null && typeof value === "object") {
|
|
37533
|
+
const keys = Object.keys(value).sort();
|
|
37534
|
+
return `{${keys.map(
|
|
37535
|
+
(k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`
|
|
37536
|
+
).join(",")}}`;
|
|
37537
|
+
}
|
|
37538
|
+
return JSON.stringify(value);
|
|
37539
|
+
}
|
|
37540
|
+
var DEFAULT_RETRY_STATUSES = [408, 429, 502, 503, 504];
|
|
37541
|
+
var DEFAULT_RETRY_METHODS = ["GET"];
|
|
37542
|
+
var DEFAULT_MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
37543
|
+
function decodeBase64File(contentBase64, maxBytes) {
|
|
37544
|
+
if (contentBase64.length === 0) {
|
|
37545
|
+
throw new Error("[FrappeClient] File content must not be empty");
|
|
37546
|
+
}
|
|
37547
|
+
const unpadded = contentBase64.replace(/=+$/, "");
|
|
37548
|
+
const padding = contentBase64.slice(unpadded.length);
|
|
37549
|
+
if (!/^[A-Za-z0-9+/]+$/.test(unpadded) || !/^={0,2}$/.test(padding) || unpadded.length % 4 === 1) {
|
|
37550
|
+
throw new Error("[FrappeClient] File content must be valid base64");
|
|
37551
|
+
}
|
|
37552
|
+
const decodedSize = Math.floor(unpadded.length * 6 / 8);
|
|
37553
|
+
if (decodedSize > maxBytes) {
|
|
37395
37554
|
throw new Error(
|
|
37396
|
-
`[${
|
|
37555
|
+
`[FrappeClient] Decoded file size ${decodedSize} bytes exceeds the ${maxBytes}-byte upload limit`
|
|
37397
37556
|
);
|
|
37398
37557
|
}
|
|
37399
|
-
|
|
37400
|
-
|
|
37558
|
+
const normalized = unpadded + "=".repeat((4 - unpadded.length % 4) % 4);
|
|
37559
|
+
let binary;
|
|
37560
|
+
try {
|
|
37561
|
+
binary = atob(normalized);
|
|
37562
|
+
} catch {
|
|
37563
|
+
throw new Error("[FrappeClient] File content must be valid base64");
|
|
37564
|
+
}
|
|
37565
|
+
if (binary.length === 0) {
|
|
37566
|
+
throw new Error("[FrappeClient] File content must not be empty");
|
|
37567
|
+
}
|
|
37568
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
37569
|
+
}
|
|
37570
|
+
var FrappeAPIError = class extends Error {
|
|
37571
|
+
/**
|
|
37572
|
+
* @param message - Human-readable error description
|
|
37573
|
+
* @param status - HTTP status code (0 for network errors, 408 for timeouts)
|
|
37574
|
+
* @param body - Raw response body (parsed JSON object or plain text string)
|
|
37575
|
+
* @param retryAfterMs - When the server sent a `Retry-After` header on a
|
|
37576
|
+
* retryable status (typically 429), the parsed delay
|
|
37577
|
+
* in ms. Used by the retry loop; absent otherwise.
|
|
37578
|
+
*/
|
|
37579
|
+
constructor(message2, status, body, retryAfterMs) {
|
|
37580
|
+
super(`[FrappeClient] ${message2} (HTTP ${status})`);
|
|
37581
|
+
this.status = status;
|
|
37582
|
+
this.body = body;
|
|
37583
|
+
this.retryAfterMs = retryAfterMs;
|
|
37584
|
+
this.name = "FrappeAPIError";
|
|
37585
|
+
}
|
|
37586
|
+
};
|
|
37587
|
+
function parseRetryAfter(raw2) {
|
|
37588
|
+
if (!raw2) return void 0;
|
|
37589
|
+
const trimmed = raw2.trim();
|
|
37590
|
+
const seconds = Number(trimmed);
|
|
37591
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
37592
|
+
return Math.round(seconds * 1e3);
|
|
37593
|
+
}
|
|
37594
|
+
const date3 = Date.parse(trimmed);
|
|
37595
|
+
if (Number.isFinite(date3)) {
|
|
37596
|
+
return Math.max(0, date3 - Date.now());
|
|
37597
|
+
}
|
|
37598
|
+
return void 0;
|
|
37599
|
+
}
|
|
37600
|
+
function extractServerMessages(raw2) {
|
|
37601
|
+
if (!raw2 || typeof raw2 !== "string") return void 0;
|
|
37602
|
+
try {
|
|
37603
|
+
const outer = JSON.parse(raw2);
|
|
37604
|
+
if (!Array.isArray(outer)) return void 0;
|
|
37605
|
+
const msgs = [];
|
|
37606
|
+
for (const item of outer) {
|
|
37607
|
+
try {
|
|
37608
|
+
const inner = JSON.parse(item);
|
|
37609
|
+
if (typeof inner === "object" && inner?.message) {
|
|
37610
|
+
msgs.push(inner.message);
|
|
37611
|
+
} else if (typeof inner === "string") {
|
|
37612
|
+
msgs.push(inner);
|
|
37613
|
+
}
|
|
37614
|
+
} catch {
|
|
37615
|
+
if (typeof item === "string") msgs.push(item);
|
|
37616
|
+
}
|
|
37617
|
+
}
|
|
37618
|
+
return msgs.length > 0 ? msgs.join("; ") : void 0;
|
|
37619
|
+
} catch {
|
|
37620
|
+
return void 0;
|
|
37621
|
+
}
|
|
37622
|
+
}
|
|
37623
|
+
var FrappeClient = class {
|
|
37624
|
+
baseUrl;
|
|
37625
|
+
authHeader;
|
|
37626
|
+
timeoutMs;
|
|
37627
|
+
maxUploadBytes;
|
|
37628
|
+
retries;
|
|
37629
|
+
retryStatuses;
|
|
37630
|
+
retryBackoffMs;
|
|
37631
|
+
retryMethods;
|
|
37632
|
+
cache;
|
|
37633
|
+
constructor(config2) {
|
|
37634
|
+
this.baseUrl = config2.baseUrl.replace(/\/$/, "");
|
|
37635
|
+
this.authHeader = `token ${config2.apiKey}:${config2.apiSecret}`;
|
|
37636
|
+
this.timeoutMs = config2.timeoutMs ?? 3e4;
|
|
37637
|
+
this.maxUploadBytes = config2.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
|
|
37638
|
+
if (!Number.isInteger(this.maxUploadBytes) || this.maxUploadBytes <= 0) {
|
|
37401
37639
|
throw new Error(
|
|
37402
|
-
|
|
37640
|
+
"[FrappeClient] maxUploadBytes must be a positive integer"
|
|
37403
37641
|
);
|
|
37404
37642
|
}
|
|
37405
|
-
|
|
37406
|
-
|
|
37407
|
-
|
|
37408
|
-
|
|
37643
|
+
this.retries = config2.retries ?? 3;
|
|
37644
|
+
this.retryStatuses = config2.retryStatuses ?? DEFAULT_RETRY_STATUSES;
|
|
37645
|
+
this.retryBackoffMs = config2.retryBackoffMs ?? 200;
|
|
37646
|
+
this.retryMethods = config2.retryMethods ?? DEFAULT_RETRY_METHODS;
|
|
37647
|
+
this.cache = config2.cache ?? new MemoryCache();
|
|
37648
|
+
}
|
|
37649
|
+
// ── Private HTTP helpers ────────────────────────────────────────────────────
|
|
37650
|
+
buildHeaders(includeJsonContentType = true) {
|
|
37651
|
+
const headers = {
|
|
37652
|
+
"Authorization": this.authHeader,
|
|
37653
|
+
"Accept": "application/json"
|
|
37409
37654
|
};
|
|
37410
|
-
if (
|
|
37411
|
-
|
|
37655
|
+
if (includeJsonContentType) {
|
|
37656
|
+
headers["Content-Type"] = "application/json";
|
|
37412
37657
|
}
|
|
37413
|
-
|
|
37414
|
-
|
|
37658
|
+
return headers;
|
|
37659
|
+
}
|
|
37660
|
+
/**
|
|
37661
|
+
* Decide whether an error is worth retrying.
|
|
37662
|
+
* Network errors (status 0) and timeouts (status 408) are always retryable;
|
|
37663
|
+
* other statuses are checked against `retryStatuses`.
|
|
37664
|
+
*/
|
|
37665
|
+
isRetryable(err, method) {
|
|
37666
|
+
if (!this.retryMethods.includes(method)) return false;
|
|
37667
|
+
if (!(err instanceof FrappeAPIError)) return false;
|
|
37668
|
+
if (err.status === 0) return true;
|
|
37669
|
+
return this.retryStatuses.includes(err.status);
|
|
37670
|
+
}
|
|
37671
|
+
/** Compute the backoff delay for a given attempt (0-indexed). */
|
|
37672
|
+
computeBackoff(attempt, err) {
|
|
37673
|
+
if (err instanceof FrappeAPIError && err.retryAfterMs !== void 0) {
|
|
37674
|
+
return err.retryAfterMs;
|
|
37415
37675
|
}
|
|
37416
|
-
return
|
|
37417
|
-
}
|
|
37418
|
-
|
|
37419
|
-
|
|
37420
|
-
|
|
37421
|
-
|
|
37422
|
-
|
|
37423
|
-
|
|
37424
|
-
|
|
37425
|
-
|
|
37426
|
-
|
|
37427
|
-
|
|
37428
|
-
|
|
37429
|
-
|
|
37430
|
-
|
|
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)"
|
|
37676
|
+
return this.retryBackoffMs * 2 ** attempt;
|
|
37677
|
+
}
|
|
37678
|
+
async request(method, path, body, multipart = false) {
|
|
37679
|
+
let lastError;
|
|
37680
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
37681
|
+
try {
|
|
37682
|
+
return await this.requestOnce(method, path, body, multipart);
|
|
37683
|
+
} catch (err) {
|
|
37684
|
+
lastError = err;
|
|
37685
|
+
if (attempt === this.retries || !this.isRetryable(err, method)) {
|
|
37686
|
+
throw err;
|
|
37687
|
+
}
|
|
37688
|
+
const delay = this.computeBackoff(attempt, err);
|
|
37689
|
+
if (delay > 0) {
|
|
37690
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
37439
37691
|
}
|
|
37440
37692
|
}
|
|
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]);
|
|
37450
|
-
}
|
|
37451
|
-
if (input.territory) {
|
|
37452
|
-
filters.push(["territory", "=", input.territory]);
|
|
37453
|
-
}
|
|
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
|
-
};
|
|
37473
37693
|
}
|
|
37474
|
-
|
|
37475
|
-
|
|
37476
|
-
|
|
37477
|
-
|
|
37478
|
-
|
|
37479
|
-
|
|
37480
|
-
|
|
37481
|
-
|
|
37482
|
-
|
|
37694
|
+
throw lastError;
|
|
37695
|
+
}
|
|
37696
|
+
async requestOnce(method, path, body, multipart = false) {
|
|
37697
|
+
const url = `${this.baseUrl}${path}`;
|
|
37698
|
+
const controller = new AbortController();
|
|
37699
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
37700
|
+
let response;
|
|
37701
|
+
try {
|
|
37702
|
+
response = await fetch(url, {
|
|
37703
|
+
method,
|
|
37704
|
+
headers: this.buildHeaders(!multipart),
|
|
37705
|
+
body: body === void 0 ? void 0 : multipart ? body : JSON.stringify(body),
|
|
37706
|
+
signal: controller.signal
|
|
37707
|
+
});
|
|
37708
|
+
} catch (err) {
|
|
37709
|
+
clearTimeout(timer);
|
|
37710
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
37711
|
+
throw new FrappeAPIError(
|
|
37712
|
+
`Request timed out after ${this.timeoutMs}ms: ${method} ${path}`,
|
|
37713
|
+
408,
|
|
37714
|
+
null
|
|
37715
|
+
);
|
|
37716
|
+
}
|
|
37717
|
+
throw new FrappeAPIError(
|
|
37718
|
+
`Network error on ${method} ${path}: ${err.message}`,
|
|
37719
|
+
0,
|
|
37720
|
+
null
|
|
37721
|
+
);
|
|
37722
|
+
}
|
|
37723
|
+
clearTimeout(timer);
|
|
37724
|
+
const rawText = await response.text();
|
|
37725
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
37726
|
+
let responseBody = rawText;
|
|
37727
|
+
if (contentType.includes("application/json") && rawText.length > 0) {
|
|
37728
|
+
try {
|
|
37729
|
+
responseBody = JSON.parse(rawText);
|
|
37730
|
+
} catch {
|
|
37731
|
+
responseBody = rawText;
|
|
37732
|
+
}
|
|
37733
|
+
}
|
|
37734
|
+
if (!response.ok) {
|
|
37735
|
+
let msg = response.statusText;
|
|
37736
|
+
if (typeof responseBody === "object" && responseBody !== null) {
|
|
37737
|
+
const rb = responseBody;
|
|
37738
|
+
const excType = rb.exc_type;
|
|
37739
|
+
const baseMsg = rb.message ?? excType ?? response.statusText;
|
|
37740
|
+
const serverDetails = extractServerMessages(rb._server_messages);
|
|
37741
|
+
msg = serverDetails ? `${baseMsg}: ${serverDetails}` : baseMsg;
|
|
37742
|
+
} else if (typeof responseBody === "string" && responseBody.length > 0) {
|
|
37743
|
+
msg = responseBody.slice(0, 200);
|
|
37744
|
+
}
|
|
37745
|
+
const retryAfterMs = parseRetryAfter(
|
|
37746
|
+
response.headers.get("retry-after")
|
|
37747
|
+
);
|
|
37748
|
+
throw new FrappeAPIError(
|
|
37749
|
+
`${method} ${path} failed: ${msg}`,
|
|
37750
|
+
response.status,
|
|
37751
|
+
responseBody,
|
|
37752
|
+
retryAfterMs
|
|
37753
|
+
);
|
|
37754
|
+
}
|
|
37755
|
+
return responseBody;
|
|
37756
|
+
}
|
|
37757
|
+
// ── Resource CRUD ───────────────────────────────────────────────────────────
|
|
37758
|
+
/**
|
|
37759
|
+
* List documents of a DocType.
|
|
37760
|
+
* Frappe list API: GET /api/resource/{doctype}?fields=...&filters=...
|
|
37761
|
+
*
|
|
37762
|
+
* Pass `{ skipCache: true }` to force a fresh read — e.g. for aggregate/KPI
|
|
37763
|
+
* tools that read across doctypes other than the one a preceding mutation
|
|
37764
|
+
* invalidated (see `invalidate()` below for why that gap exists). The fresh
|
|
37765
|
+
* result still refreshes the cache for subsequent normal reads.
|
|
37766
|
+
*/
|
|
37767
|
+
async list(doctype, options = {}, opts = {}) {
|
|
37768
|
+
const cacheKey = `list:${doctype}:${stableStringify(options)}`;
|
|
37769
|
+
if (!opts.skipCache) {
|
|
37770
|
+
const cached2 = this.cache.get(cacheKey);
|
|
37771
|
+
if (cached2 !== void 0) return cached2;
|
|
37772
|
+
}
|
|
37773
|
+
const params = new URLSearchParams();
|
|
37774
|
+
if (options.fields && options.fields.length > 0) {
|
|
37775
|
+
params.set("fields", JSON.stringify(options.fields));
|
|
37776
|
+
}
|
|
37777
|
+
if (options.filters && options.filters.length > 0) {
|
|
37778
|
+
params.set("filters", JSON.stringify(options.filters));
|
|
37779
|
+
}
|
|
37780
|
+
if (options.order_by) {
|
|
37781
|
+
params.set("order_by", options.order_by);
|
|
37782
|
+
}
|
|
37783
|
+
if (options.limit !== void 0) {
|
|
37784
|
+
params.set("limit", String(options.limit));
|
|
37785
|
+
}
|
|
37786
|
+
if (options.limit_start !== void 0) {
|
|
37787
|
+
params.set("limit_start", String(options.limit_start));
|
|
37788
|
+
}
|
|
37789
|
+
params.set("as_dict", "1");
|
|
37790
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
37791
|
+
const res = await this.request(
|
|
37792
|
+
"GET",
|
|
37793
|
+
`/api/resource/${encodeURIComponent(doctype)}${query}`
|
|
37794
|
+
);
|
|
37795
|
+
const docs = res.data ?? [];
|
|
37796
|
+
this.cache.set(cacheKey, docs, getCacheTtlMs());
|
|
37797
|
+
return docs;
|
|
37798
|
+
}
|
|
37799
|
+
/**
|
|
37800
|
+
* Get a single document by name.
|
|
37801
|
+
* GET /api/resource/{doctype}/{name}
|
|
37802
|
+
*
|
|
37803
|
+
* Pass `{ skipCache: true }` to force a fresh read — required before any
|
|
37804
|
+
* operation relying on the doc's `modified` timestamp for optimistic
|
|
37805
|
+
* locking (see erpnext_doc_submit/erpnext_doc_cancel in operations.ts).
|
|
37806
|
+
* The fresh result still refreshes the cache for subsequent normal reads.
|
|
37807
|
+
*/
|
|
37808
|
+
async get(doctype, name, opts = {}) {
|
|
37809
|
+
const cacheKey = `get:${doctype}:${name}`;
|
|
37810
|
+
if (!opts.skipCache) {
|
|
37811
|
+
const cached2 = this.cache.get(cacheKey);
|
|
37812
|
+
if (cached2 !== void 0) return cached2;
|
|
37813
|
+
}
|
|
37814
|
+
const res = await this.request(
|
|
37815
|
+
"GET",
|
|
37816
|
+
`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
|
|
37817
|
+
);
|
|
37818
|
+
this.cache.set(cacheKey, res.data, getCacheTtlMs());
|
|
37819
|
+
return res.data;
|
|
37820
|
+
}
|
|
37821
|
+
/**
|
|
37822
|
+
* Clear cached entries for a doctype (list results, resolveLink's
|
|
37823
|
+
* negative-match cache) and, if `name` is given, the cached single-document
|
|
37824
|
+
* read too. Called automatically after create/update/delete; call explicitly
|
|
37825
|
+
* after any mutation that bypasses those methods (e.g.
|
|
37826
|
+
* frappe.client.submit/cancel via callMethod).
|
|
37827
|
+
*
|
|
37828
|
+
* Known limitation: this only clears the mutated doctype. Frappe mutations
|
|
37829
|
+
* commonly cascade — submitting a Sales Order also writes Bin/GL
|
|
37830
|
+
* Entry/Sales Invoice rows — and those doctypes' cached `list()` results
|
|
37831
|
+
* aren't invalidated here. Aggregate/KPI tools that read across doctypes
|
|
37832
|
+
* can therefore serve up-to-TTL-stale numbers right after a mutation; pass
|
|
37833
|
+
* `{ skipCache: true }` to `list()` in those tools if that staleness isn't
|
|
37834
|
+
* acceptable for a given call site.
|
|
37835
|
+
*/
|
|
37836
|
+
invalidate(doctype, name) {
|
|
37837
|
+
this.cache.deleteByPrefix(`list:${doctype}:`);
|
|
37838
|
+
this.cache.deleteByPrefix(`resolve:miss:${doctype}:`);
|
|
37839
|
+
if (name) this.cache.delete(`get:${doctype}:${name}`);
|
|
37840
|
+
}
|
|
37841
|
+
/**
|
|
37842
|
+
* Create a new document.
|
|
37843
|
+
* POST /api/resource/{doctype}
|
|
37844
|
+
*/
|
|
37845
|
+
async create(doctype, data) {
|
|
37846
|
+
const res = await this.request(
|
|
37847
|
+
"POST",
|
|
37848
|
+
`/api/resource/${encodeURIComponent(doctype)}`,
|
|
37849
|
+
{ data: { ...data, doctype } }
|
|
37850
|
+
);
|
|
37851
|
+
this.invalidate(doctype, res.data.name);
|
|
37852
|
+
return res.data;
|
|
37853
|
+
}
|
|
37854
|
+
/**
|
|
37855
|
+
* Update an existing document (partial update).
|
|
37856
|
+
* PUT /api/resource/{doctype}/{name}
|
|
37857
|
+
*/
|
|
37858
|
+
async update(doctype, name, data) {
|
|
37859
|
+
const res = await this.request(
|
|
37860
|
+
"PUT",
|
|
37861
|
+
`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`,
|
|
37862
|
+
{ data }
|
|
37863
|
+
);
|
|
37864
|
+
this.invalidate(doctype, name);
|
|
37865
|
+
return res.data;
|
|
37866
|
+
}
|
|
37867
|
+
/**
|
|
37868
|
+
* Delete a document.
|
|
37869
|
+
* DELETE /api/resource/{doctype}/{name}
|
|
37870
|
+
*/
|
|
37871
|
+
async delete(doctype, name) {
|
|
37872
|
+
await this.request(
|
|
37873
|
+
"DELETE",
|
|
37874
|
+
`/api/resource/${encodeURIComponent(doctype)}/${encodeURIComponent(name)}`
|
|
37875
|
+
);
|
|
37876
|
+
this.invalidate(doctype, name);
|
|
37877
|
+
}
|
|
37878
|
+
/**
|
|
37879
|
+
* Upload file bytes and attach the native File document to another document.
|
|
37880
|
+
* POST /api/method/upload_file
|
|
37881
|
+
*/
|
|
37882
|
+
async uploadFile(input) {
|
|
37883
|
+
const fileName = input.fileName.trim();
|
|
37884
|
+
const attachedToDoctype = input.attachedToDoctype.trim();
|
|
37885
|
+
const attachedToName = input.attachedToName.trim();
|
|
37886
|
+
const attachedToField = input.attachedToField?.trim();
|
|
37887
|
+
if (!fileName || /[\\/\0]/.test(fileName)) {
|
|
37888
|
+
throw new Error(
|
|
37889
|
+
"[FrappeClient] fileName must be a filename without path separators"
|
|
37890
|
+
);
|
|
37891
|
+
}
|
|
37892
|
+
if (!attachedToDoctype) {
|
|
37893
|
+
throw new Error("[FrappeClient] attachedToDoctype must not be empty");
|
|
37894
|
+
}
|
|
37895
|
+
if (!attachedToName) {
|
|
37896
|
+
throw new Error("[FrappeClient] attachedToName must not be empty");
|
|
37897
|
+
}
|
|
37898
|
+
const bytes = decodeBase64File(
|
|
37899
|
+
input.contentBase64,
|
|
37900
|
+
this.maxUploadBytes
|
|
37901
|
+
);
|
|
37902
|
+
const fileBuffer = new ArrayBuffer(bytes.byteLength);
|
|
37903
|
+
new Uint8Array(fileBuffer).set(bytes);
|
|
37904
|
+
const form = new FormData();
|
|
37905
|
+
form.append("file", new Blob([fileBuffer]), fileName);
|
|
37906
|
+
form.append("doctype", attachedToDoctype);
|
|
37907
|
+
form.append("docname", attachedToName);
|
|
37908
|
+
if (attachedToField) {
|
|
37909
|
+
form.append("fieldname", attachedToField);
|
|
37910
|
+
}
|
|
37911
|
+
form.append("is_private", input.isPrivate === false ? "0" : "1");
|
|
37912
|
+
const res = await this.request(
|
|
37913
|
+
"POST",
|
|
37914
|
+
"/api/method/upload_file",
|
|
37915
|
+
form,
|
|
37916
|
+
true
|
|
37917
|
+
);
|
|
37918
|
+
const file = res.message;
|
|
37919
|
+
this.invalidate("File", file.name);
|
|
37920
|
+
this.invalidate(attachedToDoctype, attachedToName);
|
|
37921
|
+
if (attachedToField) {
|
|
37922
|
+
await this.update(attachedToDoctype, attachedToName, {
|
|
37923
|
+
[attachedToField]: file.file_url
|
|
37924
|
+
});
|
|
37925
|
+
}
|
|
37926
|
+
return file;
|
|
37927
|
+
}
|
|
37928
|
+
/**
|
|
37929
|
+
* Call a whitelisted Frappe method.
|
|
37930
|
+
* POST /api/method/{method}
|
|
37931
|
+
*/
|
|
37932
|
+
async callMethod(method, args = {}) {
|
|
37933
|
+
const res = await this.request(
|
|
37934
|
+
"POST",
|
|
37935
|
+
`/api/method/${method}`,
|
|
37936
|
+
args
|
|
37937
|
+
);
|
|
37938
|
+
return res.message;
|
|
37939
|
+
}
|
|
37940
|
+
};
|
|
37941
|
+
var _client = null;
|
|
37942
|
+
function getFrappeClient() {
|
|
37943
|
+
if (_client) return _client;
|
|
37944
|
+
const url = env6("ERPNEXT_URL");
|
|
37945
|
+
const apiKey = env6("ERPNEXT_API_KEY");
|
|
37946
|
+
const apiSecret = env6("ERPNEXT_API_SECRET");
|
|
37947
|
+
const maxUploadBytesRaw = env6("ERPNEXT_MAX_UPLOAD_BYTES");
|
|
37948
|
+
if (!url) {
|
|
37949
|
+
throw new Error(
|
|
37950
|
+
"[lib/erpnext] ERPNEXT_URL is required. Set it to your ERPNext instance URL, e.g. http://localhost:8000"
|
|
37951
|
+
);
|
|
37952
|
+
}
|
|
37953
|
+
if (!apiKey || !apiSecret) {
|
|
37954
|
+
throw new Error(
|
|
37955
|
+
"[lib/erpnext] ERPNEXT_API_KEY and ERPNEXT_API_SECRET are required. Generate them in ERPNext: User Settings \u2192 API Access."
|
|
37956
|
+
);
|
|
37957
|
+
}
|
|
37958
|
+
_client = new FrappeClient({
|
|
37959
|
+
baseUrl: url,
|
|
37960
|
+
apiKey,
|
|
37961
|
+
apiSecret,
|
|
37962
|
+
cache: getCache(),
|
|
37963
|
+
maxUploadBytes: maxUploadBytesRaw?.trim() ? Number(maxUploadBytesRaw) : void 0
|
|
37964
|
+
});
|
|
37965
|
+
return _client;
|
|
37966
|
+
}
|
|
37967
|
+
|
|
37968
|
+
// src/api/resolve.ts
|
|
37969
|
+
var NEGATIVE_CACHE_TTL_MS = 15e3;
|
|
37970
|
+
var EXACT_MATCH_PROBE_LIMIT = 2;
|
|
37971
|
+
var PARTIAL_MATCH_PROBE_LIMIT = 5;
|
|
37972
|
+
async function resolveUnique(client, doctype, identifier, searchField, op, value, probeLimit, ambiguityHint) {
|
|
37973
|
+
const rows = await client.list(doctype, {
|
|
37974
|
+
filters: [[searchField, op, value]],
|
|
37975
|
+
fields: ["name", searchField],
|
|
37976
|
+
limit: probeLimit
|
|
37977
|
+
});
|
|
37978
|
+
if (rows.length === 0) return void 0;
|
|
37979
|
+
if (rows.length === 1) return rows[0].name;
|
|
37980
|
+
const candidates = rows.map((r) => `${r.name} (${r[searchField]})`).join(
|
|
37981
|
+
", "
|
|
37982
|
+
);
|
|
37983
|
+
const suffix = rows.length === probeLimit ? ", and possibly more" : "";
|
|
37984
|
+
throw new Error(
|
|
37985
|
+
`[resolveLink] Ambiguous ${doctype} identifier "${identifier}": did you mean ${candidates}${suffix}? ${ambiguityHint}`
|
|
37986
|
+
);
|
|
37987
|
+
}
|
|
37988
|
+
async function resolveLink(client, doctype, identifier, searchField, options = {}) {
|
|
37989
|
+
const { allowPartialMatch = true } = options;
|
|
37990
|
+
const cache2 = getCache();
|
|
37991
|
+
const missKey = `resolve:miss:${doctype}:${identifier}`;
|
|
37992
|
+
if (cache2.get(missKey) === void 0) {
|
|
37993
|
+
try {
|
|
37994
|
+
await client.get(doctype, identifier);
|
|
37995
|
+
return identifier;
|
|
37996
|
+
} catch (e) {
|
|
37997
|
+
if (!(e instanceof FrappeAPIError) || e.status !== 404) throw e;
|
|
37998
|
+
cache2.set(missKey, true, NEGATIVE_CACHE_TTL_MS);
|
|
37999
|
+
}
|
|
38000
|
+
}
|
|
38001
|
+
const exact = await resolveUnique(
|
|
38002
|
+
client,
|
|
38003
|
+
doctype,
|
|
38004
|
+
identifier,
|
|
38005
|
+
searchField,
|
|
38006
|
+
"=",
|
|
38007
|
+
identifier,
|
|
38008
|
+
EXACT_MATCH_PROBE_LIMIT,
|
|
38009
|
+
"Please pass the record's ID directly."
|
|
38010
|
+
);
|
|
38011
|
+
if (exact !== void 0) return exact;
|
|
38012
|
+
if (allowPartialMatch) {
|
|
38013
|
+
const partial2 = await resolveUnique(
|
|
38014
|
+
client,
|
|
38015
|
+
doctype,
|
|
38016
|
+
identifier,
|
|
38017
|
+
searchField,
|
|
38018
|
+
"like",
|
|
38019
|
+
`%${identifier}%`,
|
|
38020
|
+
PARTIAL_MATCH_PROBE_LIMIT,
|
|
38021
|
+
"Please pass an exact value."
|
|
38022
|
+
);
|
|
38023
|
+
if (partial2 !== void 0) return partial2;
|
|
38024
|
+
}
|
|
38025
|
+
throw new Error(`[resolveLink] No ${doctype} found matching "${identifier}"`);
|
|
38026
|
+
}
|
|
38027
|
+
function resolveEmployee(client, identifier) {
|
|
38028
|
+
return resolveLink(client, "Employee", identifier, "employee_name");
|
|
38029
|
+
}
|
|
38030
|
+
function resolveCustomer(client, identifier) {
|
|
38031
|
+
return resolveLink(client, "Customer", identifier, "customer_name");
|
|
38032
|
+
}
|
|
38033
|
+
function resolveSupplier(client, identifier) {
|
|
38034
|
+
return resolveLink(client, "Supplier", identifier, "supplier_name");
|
|
38035
|
+
}
|
|
38036
|
+
function resolveItem(client, identifier) {
|
|
38037
|
+
return resolveLink(client, "Item", identifier, "item_name");
|
|
38038
|
+
}
|
|
38039
|
+
var DYNAMIC_LINK_SEARCH_FIELDS = {
|
|
38040
|
+
Customer: "customer_name",
|
|
38041
|
+
Supplier: "supplier_name",
|
|
38042
|
+
Employee: "employee_name",
|
|
38043
|
+
Lead: "lead_name"
|
|
38044
|
+
};
|
|
38045
|
+
async function resolveDynamicLink(client, targetDoctype, identifier, options = {}) {
|
|
38046
|
+
const searchField = DYNAMIC_LINK_SEARCH_FIELDS[targetDoctype];
|
|
38047
|
+
if (!searchField) return identifier;
|
|
38048
|
+
return resolveLink(client, targetDoctype, identifier, searchField, options);
|
|
38049
|
+
}
|
|
38050
|
+
|
|
38051
|
+
// src/tools/submit-helpers.ts
|
|
38052
|
+
function withRoundedTotalFallback(doc) {
|
|
38053
|
+
const hasNullRoundedTotal = "base_rounded_total" in doc && doc.base_rounded_total == null || "rounded_total" in doc && doc.rounded_total == null;
|
|
38054
|
+
if (!hasNullRoundedTotal || doc.disable_rounded_total) {
|
|
38055
|
+
return doc;
|
|
38056
|
+
}
|
|
38057
|
+
return { ...doc, disable_rounded_total: 1 };
|
|
38058
|
+
}
|
|
38059
|
+
function roundedTotalFallbackWarning(original, patched) {
|
|
38060
|
+
if (patched.disable_rounded_total === 1 && original.disable_rounded_total !== 1) {
|
|
38061
|
+
return [
|
|
38062
|
+
"disable_rounded_total auto-set \u2014 rounded totals were null (rounding not configured on this instance)"
|
|
38063
|
+
];
|
|
38064
|
+
}
|
|
38065
|
+
return [];
|
|
38066
|
+
}
|
|
38067
|
+
|
|
38068
|
+
// src/tools/sales.ts
|
|
38069
|
+
function mapLineItems(items, options) {
|
|
38070
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
38071
|
+
throw new Error(
|
|
38072
|
+
`[${options.toolName}] 'items' must be a non-empty array`
|
|
38073
|
+
);
|
|
38074
|
+
}
|
|
38075
|
+
return items.map((item) => {
|
|
38076
|
+
if (!item.item_code || item.qty == null || item.rate == null) {
|
|
38077
|
+
throw new Error(
|
|
38078
|
+
`[${options.toolName}] Each item must have item_code, qty, and rate`
|
|
38079
|
+
);
|
|
38080
|
+
}
|
|
38081
|
+
const mapped = {
|
|
38082
|
+
item_code: item.item_code,
|
|
38083
|
+
qty: item.qty,
|
|
38084
|
+
rate: item.rate
|
|
38085
|
+
};
|
|
38086
|
+
if (options.defaultDeliveryDate !== void 0) {
|
|
38087
|
+
mapped.delivery_date = options.defaultDeliveryDate;
|
|
38088
|
+
}
|
|
38089
|
+
if (options.includeWarehouse !== false && item.warehouse) {
|
|
38090
|
+
mapped.warehouse = item.warehouse;
|
|
38091
|
+
}
|
|
38092
|
+
return mapped;
|
|
38093
|
+
});
|
|
38094
|
+
}
|
|
38095
|
+
var salesTools = [
|
|
38096
|
+
// ── Customers ─────────────────────────────────────────────────────────────
|
|
38097
|
+
{
|
|
38098
|
+
name: "erpnext_customer_list",
|
|
38099
|
+
annotations: { readOnlyHint: true },
|
|
38100
|
+
_meta: DOCLIST_META,
|
|
38101
|
+
description: "List ERPNext customers. Returns active customers by default. Fields: name, customer_name, customer_group, territory, email_id, disabled.",
|
|
38102
|
+
category: "sales",
|
|
38103
|
+
inputSchema: {
|
|
38104
|
+
type: "object",
|
|
38105
|
+
properties: {
|
|
38106
|
+
limit: { type: "number", description: "Max results (default 20)" },
|
|
38107
|
+
customer_group: {
|
|
38108
|
+
type: "string",
|
|
38109
|
+
description: "Filter by customer group"
|
|
38110
|
+
},
|
|
38111
|
+
territory: { type: "string", description: "Filter by territory" },
|
|
38112
|
+
include_disabled: {
|
|
38113
|
+
type: "boolean",
|
|
38114
|
+
description: "Include disabled customers (default false)"
|
|
38115
|
+
}
|
|
38116
|
+
}
|
|
38117
|
+
},
|
|
38118
|
+
handler: async (input, ctx) => {
|
|
38119
|
+
const limit = input.limit ?? 20;
|
|
38120
|
+
const filters = [];
|
|
38121
|
+
if (!input.include_disabled) {
|
|
38122
|
+
filters.push(["disabled", "=", 0]);
|
|
38123
|
+
}
|
|
38124
|
+
if (input.customer_group) {
|
|
38125
|
+
filters.push(["customer_group", "=", input.customer_group]);
|
|
38126
|
+
}
|
|
38127
|
+
if (input.territory) {
|
|
38128
|
+
filters.push(["territory", "=", input.territory]);
|
|
38129
|
+
}
|
|
38130
|
+
const docs = await ctx.client.list("Customer", {
|
|
38131
|
+
fields: [
|
|
38132
|
+
"name",
|
|
38133
|
+
"customer_name",
|
|
38134
|
+
"customer_group",
|
|
38135
|
+
"territory",
|
|
38136
|
+
"email_id",
|
|
38137
|
+
"disabled"
|
|
38138
|
+
],
|
|
38139
|
+
filters,
|
|
38140
|
+
limit,
|
|
38141
|
+
order_by: "modified desc"
|
|
38142
|
+
});
|
|
38143
|
+
return {
|
|
38144
|
+
doctype: "Customer",
|
|
38145
|
+
count: docs.length,
|
|
38146
|
+
data: docs,
|
|
38147
|
+
_meta: DOCLIST_META
|
|
38148
|
+
};
|
|
38149
|
+
}
|
|
38150
|
+
},
|
|
38151
|
+
{
|
|
38152
|
+
name: "erpnext_customer_get",
|
|
38153
|
+
annotations: { readOnlyHint: true },
|
|
38154
|
+
description: "Get a single ERPNext customer by name (ID). Returns all fields including contact details.",
|
|
38155
|
+
category: "sales",
|
|
38156
|
+
inputSchema: {
|
|
38157
|
+
type: "object",
|
|
38158
|
+
properties: {
|
|
37483
38159
|
name: { type: "string", description: "Customer name (ID)" }
|
|
37484
38160
|
},
|
|
37485
38161
|
required: ["name"]
|
|
@@ -37592,7 +38268,10 @@ var salesTools = [
|
|
|
37592
38268
|
type: "object",
|
|
37593
38269
|
properties: {
|
|
37594
38270
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
37595
|
-
customer: {
|
|
38271
|
+
customer: {
|
|
38272
|
+
type: "string",
|
|
38273
|
+
description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
|
|
38274
|
+
},
|
|
37596
38275
|
status: {
|
|
37597
38276
|
type: "string",
|
|
37598
38277
|
description: "Filter by status (Draft, To Deliver and Bill, Completed, Cancelled, etc.)"
|
|
@@ -37608,7 +38287,11 @@ var salesTools = [
|
|
|
37608
38287
|
const limit = input.limit ?? 20;
|
|
37609
38288
|
const filters = [];
|
|
37610
38289
|
if (input.customer) {
|
|
37611
|
-
filters.push([
|
|
38290
|
+
filters.push([
|
|
38291
|
+
"customer",
|
|
38292
|
+
"=",
|
|
38293
|
+
await resolveCustomer(ctx.client, input.customer)
|
|
38294
|
+
]);
|
|
37612
38295
|
}
|
|
37613
38296
|
if (input.status) {
|
|
37614
38297
|
filters.push(["status", "=", input.status]);
|
|
@@ -37822,13 +38505,20 @@ var salesTools = [
|
|
|
37822
38505
|
if (!input.name) {
|
|
37823
38506
|
throw new Error("[erpnext_sales_order_submit] 'name' is required");
|
|
37824
38507
|
}
|
|
37825
|
-
const doc = await ctx.client.get("Sales Order", input.name
|
|
38508
|
+
const doc = await ctx.client.get("Sales Order", input.name, {
|
|
38509
|
+
skipCache: true
|
|
38510
|
+
});
|
|
38511
|
+
const docWithDoctype = { ...doc, doctype: "Sales Order" };
|
|
38512
|
+
const patchedDoc = withRoundedTotalFallback(docWithDoctype);
|
|
37826
38513
|
const result = await ctx.client.callMethod("frappe.client.submit", {
|
|
37827
|
-
doc:
|
|
38514
|
+
doc: patchedDoc
|
|
37828
38515
|
});
|
|
38516
|
+
ctx.client.invalidate("Sales Order", input.name);
|
|
38517
|
+
const warnings = roundedTotalFallbackWarning(docWithDoctype, patchedDoc);
|
|
37829
38518
|
return {
|
|
37830
38519
|
data: result,
|
|
37831
|
-
message: `Sales Order ${input.name} submitted successfully
|
|
38520
|
+
message: `Sales Order ${input.name} submitted successfully`,
|
|
38521
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
37832
38522
|
};
|
|
37833
38523
|
}
|
|
37834
38524
|
},
|
|
@@ -37855,6 +38545,7 @@ var salesTools = [
|
|
|
37855
38545
|
doctype: "Sales Order",
|
|
37856
38546
|
name: input.name
|
|
37857
38547
|
});
|
|
38548
|
+
ctx.client.invalidate("Sales Order", input.name);
|
|
37858
38549
|
return {
|
|
37859
38550
|
data: result,
|
|
37860
38551
|
message: `Sales Order ${input.name} cancelled successfully`
|
|
@@ -37872,7 +38563,10 @@ var salesTools = [
|
|
|
37872
38563
|
type: "object",
|
|
37873
38564
|
properties: {
|
|
37874
38565
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
37875
|
-
customer: {
|
|
38566
|
+
customer: {
|
|
38567
|
+
type: "string",
|
|
38568
|
+
description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
|
|
38569
|
+
},
|
|
37876
38570
|
status: {
|
|
37877
38571
|
type: "string",
|
|
37878
38572
|
description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
|
|
@@ -37888,7 +38582,11 @@ var salesTools = [
|
|
|
37888
38582
|
const limit = input.limit ?? 20;
|
|
37889
38583
|
const filters = [];
|
|
37890
38584
|
if (input.customer) {
|
|
37891
|
-
filters.push([
|
|
38585
|
+
filters.push([
|
|
38586
|
+
"customer",
|
|
38587
|
+
"=",
|
|
38588
|
+
await resolveCustomer(ctx.client, input.customer)
|
|
38589
|
+
]);
|
|
37892
38590
|
}
|
|
37893
38591
|
if (input.status) {
|
|
37894
38592
|
filters.push(["status", "=", input.status]);
|
|
@@ -38054,14 +38752,21 @@ var salesTools = [
|
|
|
38054
38752
|
if (!input.name) {
|
|
38055
38753
|
throw new Error("[erpnext_sales_invoice_submit] 'name' is required");
|
|
38056
38754
|
}
|
|
38057
|
-
const doc = await ctx.client.get("Sales Invoice", input.name
|
|
38755
|
+
const doc = await ctx.client.get("Sales Invoice", input.name, {
|
|
38756
|
+
skipCache: true
|
|
38757
|
+
});
|
|
38758
|
+
const docWithDoctype = { ...doc, doctype: "Sales Invoice" };
|
|
38759
|
+
const patchedDoc = withRoundedTotalFallback(docWithDoctype);
|
|
38058
38760
|
const result = await ctx.client.callMethod("frappe.client.submit", {
|
|
38059
|
-
doc:
|
|
38761
|
+
doc: patchedDoc
|
|
38060
38762
|
});
|
|
38763
|
+
ctx.client.invalidate("Sales Invoice", input.name);
|
|
38764
|
+
const warnings = roundedTotalFallbackWarning(docWithDoctype, patchedDoc);
|
|
38061
38765
|
return {
|
|
38062
38766
|
data: result,
|
|
38063
38767
|
message: `Sales Invoice ${input.name} submitted successfully`,
|
|
38064
|
-
_meta: INVOICE_META
|
|
38768
|
+
_meta: INVOICE_META,
|
|
38769
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
38065
38770
|
};
|
|
38066
38771
|
}
|
|
38067
38772
|
},
|
|
@@ -38076,22 +38781,54 @@ var salesTools = [
|
|
|
38076
38781
|
type: "object",
|
|
38077
38782
|
properties: {
|
|
38078
38783
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
38079
|
-
|
|
38784
|
+
quotation_to: {
|
|
38785
|
+
type: "string",
|
|
38786
|
+
description: "Party type: Customer or Lead. Required when 'party_name' is set, so the party name/ID can be resolved against the right doctype.",
|
|
38787
|
+
enum: ["Customer", "Lead"]
|
|
38788
|
+
},
|
|
38789
|
+
party_name: {
|
|
38790
|
+
type: "string",
|
|
38791
|
+
description: "Filter by party \u2014 ID or name (e.g. customer/lead name). Requires 'quotation_to'."
|
|
38792
|
+
},
|
|
38080
38793
|
status: {
|
|
38081
38794
|
type: "string",
|
|
38082
38795
|
description: "Filter by status (Draft, Open, Replied, Ordered, Lost, Cancelled)"
|
|
38083
|
-
}
|
|
38796
|
+
},
|
|
38797
|
+
date_from: {
|
|
38798
|
+
type: "string",
|
|
38799
|
+
description: "Start date filter YYYY-MM-DD"
|
|
38800
|
+
},
|
|
38801
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
38084
38802
|
}
|
|
38085
38803
|
},
|
|
38086
38804
|
handler: async (input, ctx) => {
|
|
38087
38805
|
const limit = input.limit ?? 20;
|
|
38088
38806
|
const filters = [];
|
|
38089
38807
|
if (input.party_name) {
|
|
38090
|
-
|
|
38808
|
+
if (!input.quotation_to) {
|
|
38809
|
+
throw new Error(
|
|
38810
|
+
"[erpnext_quotation_list] 'quotation_to' is required when filtering by 'party_name'"
|
|
38811
|
+
);
|
|
38812
|
+
}
|
|
38813
|
+
filters.push([
|
|
38814
|
+
"party_name",
|
|
38815
|
+
"=",
|
|
38816
|
+
await resolveDynamicLink(
|
|
38817
|
+
ctx.client,
|
|
38818
|
+
input.quotation_to,
|
|
38819
|
+
input.party_name
|
|
38820
|
+
)
|
|
38821
|
+
]);
|
|
38091
38822
|
}
|
|
38092
38823
|
if (input.status) {
|
|
38093
38824
|
filters.push(["status", "=", input.status]);
|
|
38094
38825
|
}
|
|
38826
|
+
if (input.date_from) {
|
|
38827
|
+
filters.push(["transaction_date", ">=", input.date_from]);
|
|
38828
|
+
}
|
|
38829
|
+
if (input.date_to) {
|
|
38830
|
+
filters.push(["transaction_date", "<=", input.date_to]);
|
|
38831
|
+
}
|
|
38095
38832
|
const docs = await ctx.client.list("Quotation", {
|
|
38096
38833
|
fields: [
|
|
38097
38834
|
"name",
|
|
@@ -38151,7 +38888,7 @@ var salesTools = [
|
|
|
38151
38888
|
},
|
|
38152
38889
|
party_name: {
|
|
38153
38890
|
type: "string",
|
|
38154
|
-
description: "Customer or Lead name"
|
|
38891
|
+
description: "Customer or Lead \u2014 ID or name"
|
|
38155
38892
|
},
|
|
38156
38893
|
items: {
|
|
38157
38894
|
type: "array",
|
|
@@ -38204,7 +38941,12 @@ var salesTools = [
|
|
|
38204
38941
|
});
|
|
38205
38942
|
const data = {
|
|
38206
38943
|
quotation_to: input.quotation_to,
|
|
38207
|
-
party_name:
|
|
38944
|
+
party_name: await resolveDynamicLink(
|
|
38945
|
+
ctx.client,
|
|
38946
|
+
input.quotation_to,
|
|
38947
|
+
input.party_name,
|
|
38948
|
+
{ allowPartialMatch: false }
|
|
38949
|
+
),
|
|
38208
38950
|
items
|
|
38209
38951
|
};
|
|
38210
38952
|
if (input.transaction_date) {
|
|
@@ -38418,7 +39160,10 @@ var inventoryTools = [
|
|
|
38418
39160
|
type: "object",
|
|
38419
39161
|
properties: {
|
|
38420
39162
|
limit: { type: "number", description: "Max results (default 50)" },
|
|
38421
|
-
item_code: {
|
|
39163
|
+
item_code: {
|
|
39164
|
+
type: "string",
|
|
39165
|
+
description: "Filter by item code or name (e.g. 'ITEM-001' or 'Widget A')"
|
|
39166
|
+
},
|
|
38422
39167
|
warehouse: { type: "string", description: "Filter by warehouse" }
|
|
38423
39168
|
}
|
|
38424
39169
|
},
|
|
@@ -38426,7 +39171,11 @@ var inventoryTools = [
|
|
|
38426
39171
|
const limit = input.limit ?? 50;
|
|
38427
39172
|
const filters = [];
|
|
38428
39173
|
if (input.item_code) {
|
|
38429
|
-
filters.push([
|
|
39174
|
+
filters.push([
|
|
39175
|
+
"item_code",
|
|
39176
|
+
"=",
|
|
39177
|
+
await resolveItem(ctx.client, input.item_code)
|
|
39178
|
+
]);
|
|
38430
39179
|
}
|
|
38431
39180
|
if (input.warehouse) {
|
|
38432
39181
|
filters.push(["warehouse", "=", input.warehouse]);
|
|
@@ -38807,9 +39556,13 @@ var accountingTools = [
|
|
|
38807
39556
|
},
|
|
38808
39557
|
party_type: {
|
|
38809
39558
|
type: "string",
|
|
38810
|
-
description: "Filter by party type (Customer, Supplier, Employee)"
|
|
39559
|
+
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.",
|
|
39560
|
+
enum: ["Customer", "Supplier", "Employee"]
|
|
39561
|
+
},
|
|
39562
|
+
party: {
|
|
39563
|
+
type: "string",
|
|
39564
|
+
description: "Filter by party \u2014 ID or name (e.g. 'CUST-00001' or 'Acme Corp'). Requires 'party_type'."
|
|
38811
39565
|
},
|
|
38812
|
-
party: { type: "string", description: "Filter by party name" },
|
|
38813
39566
|
date_from: {
|
|
38814
39567
|
type: "string",
|
|
38815
39568
|
description: "Start date filter YYYY-MM-DD"
|
|
@@ -38826,7 +39579,20 @@ var accountingTools = [
|
|
|
38826
39579
|
filters.push(["party_type", "=", input.party_type]);
|
|
38827
39580
|
}
|
|
38828
39581
|
if (input.party) {
|
|
38829
|
-
|
|
39582
|
+
if (!input.party_type) {
|
|
39583
|
+
throw new Error(
|
|
39584
|
+
"[erpnext_payment_entry_list] 'party_type' is required when filtering by 'party'"
|
|
39585
|
+
);
|
|
39586
|
+
}
|
|
39587
|
+
filters.push([
|
|
39588
|
+
"party",
|
|
39589
|
+
"=",
|
|
39590
|
+
await resolveDynamicLink(
|
|
39591
|
+
ctx.client,
|
|
39592
|
+
input.party_type,
|
|
39593
|
+
input.party
|
|
39594
|
+
)
|
|
39595
|
+
]);
|
|
38830
39596
|
}
|
|
38831
39597
|
if (input.date_from) {
|
|
38832
39598
|
filters.push(["posting_date", ">=", input.date_from]);
|
|
@@ -39030,7 +39796,10 @@ var hrTools = [
|
|
|
39030
39796
|
type: "object",
|
|
39031
39797
|
properties: {
|
|
39032
39798
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
39033
|
-
employee: {
|
|
39799
|
+
employee: {
|
|
39800
|
+
type: "string",
|
|
39801
|
+
description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
39802
|
+
},
|
|
39034
39803
|
status: {
|
|
39035
39804
|
type: "string",
|
|
39036
39805
|
description: "Filter by status (Present, Absent, Half Day, On Leave)",
|
|
@@ -39047,7 +39816,11 @@ var hrTools = [
|
|
|
39047
39816
|
const limit = input.limit ?? 20;
|
|
39048
39817
|
const filters = [];
|
|
39049
39818
|
if (input.employee) {
|
|
39050
|
-
filters.push([
|
|
39819
|
+
filters.push([
|
|
39820
|
+
"employee",
|
|
39821
|
+
"=",
|
|
39822
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
39823
|
+
]);
|
|
39051
39824
|
}
|
|
39052
39825
|
if (input.status) {
|
|
39053
39826
|
filters.push(["status", "=", input.status]);
|
|
@@ -39089,7 +39862,10 @@ var hrTools = [
|
|
|
39089
39862
|
type: "object",
|
|
39090
39863
|
properties: {
|
|
39091
39864
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
39092
|
-
employee: {
|
|
39865
|
+
employee: {
|
|
39866
|
+
type: "string",
|
|
39867
|
+
description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
39868
|
+
},
|
|
39093
39869
|
status: {
|
|
39094
39870
|
type: "string",
|
|
39095
39871
|
description: "Filter by status (Open, Approved, Rejected, Cancelled)",
|
|
@@ -39098,14 +39874,23 @@ var hrTools = [
|
|
|
39098
39874
|
leave_type: {
|
|
39099
39875
|
type: "string",
|
|
39100
39876
|
description: "Filter by leave type (e.g. Sick Leave)"
|
|
39101
|
-
}
|
|
39877
|
+
},
|
|
39878
|
+
date_from: {
|
|
39879
|
+
type: "string",
|
|
39880
|
+
description: "Start date filter YYYY-MM-DD"
|
|
39881
|
+
},
|
|
39882
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
39102
39883
|
}
|
|
39103
39884
|
},
|
|
39104
39885
|
handler: async (input, ctx) => {
|
|
39105
39886
|
const limit = input.limit ?? 20;
|
|
39106
39887
|
const filters = [];
|
|
39107
39888
|
if (input.employee) {
|
|
39108
|
-
filters.push([
|
|
39889
|
+
filters.push([
|
|
39890
|
+
"employee",
|
|
39891
|
+
"=",
|
|
39892
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
39893
|
+
]);
|
|
39109
39894
|
}
|
|
39110
39895
|
if (input.status) {
|
|
39111
39896
|
filters.push(["status", "=", input.status]);
|
|
@@ -39113,6 +39898,12 @@ var hrTools = [
|
|
|
39113
39898
|
if (input.leave_type) {
|
|
39114
39899
|
filters.push(["leave_type", "=", input.leave_type]);
|
|
39115
39900
|
}
|
|
39901
|
+
if (input.date_from) {
|
|
39902
|
+
filters.push(["from_date", ">=", input.date_from]);
|
|
39903
|
+
}
|
|
39904
|
+
if (input.date_to) {
|
|
39905
|
+
filters.push(["to_date", "<=", input.date_to]);
|
|
39906
|
+
}
|
|
39116
39907
|
const docs = await ctx.client.list("Leave Application", {
|
|
39117
39908
|
fields: [
|
|
39118
39909
|
"name",
|
|
@@ -39227,7 +40018,10 @@ var hrTools = [
|
|
|
39227
40018
|
type: "object",
|
|
39228
40019
|
properties: {
|
|
39229
40020
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
39230
|
-
employee: {
|
|
40021
|
+
employee: {
|
|
40022
|
+
type: "string",
|
|
40023
|
+
description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
40024
|
+
},
|
|
39231
40025
|
status: {
|
|
39232
40026
|
type: "string",
|
|
39233
40027
|
description: "Filter by status (Draft, Submitted, Cancelled)",
|
|
@@ -39247,7 +40041,11 @@ var hrTools = [
|
|
|
39247
40041
|
const limit = input.limit ?? 20;
|
|
39248
40042
|
const filters = [];
|
|
39249
40043
|
if (input.employee) {
|
|
39250
|
-
filters.push([
|
|
40044
|
+
filters.push([
|
|
40045
|
+
"employee",
|
|
40046
|
+
"=",
|
|
40047
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
40048
|
+
]);
|
|
39251
40049
|
}
|
|
39252
40050
|
if (input.status) {
|
|
39253
40051
|
filters.push(["status", "=", input.status]);
|
|
@@ -39321,7 +40119,12 @@ var hrTools = [
|
|
|
39321
40119
|
type: "string",
|
|
39322
40120
|
description: "Filter by status (Draft, Submitted, Cancelled)",
|
|
39323
40121
|
enum: ["Draft", "Submitted", "Cancelled"]
|
|
39324
|
-
}
|
|
40122
|
+
},
|
|
40123
|
+
date_from: {
|
|
40124
|
+
type: "string",
|
|
40125
|
+
description: "Start date filter YYYY-MM-DD"
|
|
40126
|
+
},
|
|
40127
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
39325
40128
|
}
|
|
39326
40129
|
},
|
|
39327
40130
|
handler: async (input, ctx) => {
|
|
@@ -39333,6 +40136,12 @@ var hrTools = [
|
|
|
39333
40136
|
if (input.status) {
|
|
39334
40137
|
filters.push(["status", "=", input.status]);
|
|
39335
40138
|
}
|
|
40139
|
+
if (input.date_from) {
|
|
40140
|
+
filters.push(["posting_date", ">=", input.date_from]);
|
|
40141
|
+
}
|
|
40142
|
+
if (input.date_to) {
|
|
40143
|
+
filters.push(["posting_date", "<=", input.date_to]);
|
|
40144
|
+
}
|
|
39336
40145
|
const docs = await ctx.client.list("Payroll Entry", {
|
|
39337
40146
|
fields: [
|
|
39338
40147
|
"name",
|
|
@@ -39364,7 +40173,10 @@ var hrTools = [
|
|
|
39364
40173
|
type: "object",
|
|
39365
40174
|
properties: {
|
|
39366
40175
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
39367
|
-
employee: {
|
|
40176
|
+
employee: {
|
|
40177
|
+
type: "string",
|
|
40178
|
+
description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
40179
|
+
},
|
|
39368
40180
|
status: {
|
|
39369
40181
|
type: "string",
|
|
39370
40182
|
description: "Filter by status (Draft, Submitted, Cancelled)",
|
|
@@ -39374,14 +40186,23 @@ var hrTools = [
|
|
|
39374
40186
|
type: "string",
|
|
39375
40187
|
description: "Filter by approval status (Pending, Approved, Rejected)",
|
|
39376
40188
|
enum: ["Pending", "Approved", "Rejected"]
|
|
39377
|
-
}
|
|
40189
|
+
},
|
|
40190
|
+
date_from: {
|
|
40191
|
+
type: "string",
|
|
40192
|
+
description: "Start date filter YYYY-MM-DD"
|
|
40193
|
+
},
|
|
40194
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
39378
40195
|
}
|
|
39379
40196
|
},
|
|
39380
40197
|
handler: async (input, ctx) => {
|
|
39381
40198
|
const limit = input.limit ?? 20;
|
|
39382
40199
|
const filters = [];
|
|
39383
40200
|
if (input.employee) {
|
|
39384
|
-
filters.push([
|
|
40201
|
+
filters.push([
|
|
40202
|
+
"employee",
|
|
40203
|
+
"=",
|
|
40204
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
40205
|
+
]);
|
|
39385
40206
|
}
|
|
39386
40207
|
if (input.status) {
|
|
39387
40208
|
filters.push(["status", "=", input.status]);
|
|
@@ -39389,6 +40210,12 @@ var hrTools = [
|
|
|
39389
40210
|
if (input.approval_status) {
|
|
39390
40211
|
filters.push(["approval_status", "=", input.approval_status]);
|
|
39391
40212
|
}
|
|
40213
|
+
if (input.date_from) {
|
|
40214
|
+
filters.push(["posting_date", ">=", input.date_from]);
|
|
40215
|
+
}
|
|
40216
|
+
if (input.date_to) {
|
|
40217
|
+
filters.push(["posting_date", "<=", input.date_to]);
|
|
40218
|
+
}
|
|
39392
40219
|
const docs = await ctx.client.list("Expense Claim", {
|
|
39393
40220
|
fields: [
|
|
39394
40221
|
"name",
|
|
@@ -39490,7 +40317,7 @@ var hrTools = [
|
|
|
39490
40317
|
properties: {
|
|
39491
40318
|
employee: {
|
|
39492
40319
|
type: "string",
|
|
39493
|
-
description: "Employee ID (e.g. HR-EMP-00001)"
|
|
40320
|
+
description: "Employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
39494
40321
|
}
|
|
39495
40322
|
},
|
|
39496
40323
|
required: ["employee"]
|
|
@@ -39500,7 +40327,11 @@ var hrTools = [
|
|
|
39500
40327
|
throw new Error("[erpnext_leave_balance] 'employee' is required");
|
|
39501
40328
|
}
|
|
39502
40329
|
const filters = [
|
|
39503
|
-
[
|
|
40330
|
+
[
|
|
40331
|
+
"employee",
|
|
40332
|
+
"=",
|
|
40333
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
40334
|
+
],
|
|
39504
40335
|
["docstatus", "=", 1]
|
|
39505
40336
|
];
|
|
39506
40337
|
const docs = await ctx.client.list("Leave Allocation", {
|
|
@@ -39701,7 +40532,12 @@ var projectTools = [
|
|
|
39701
40532
|
description: "Filter by status (Open, Completed, Cancelled)",
|
|
39702
40533
|
enum: ["Open", "Completed", "Cancelled"]
|
|
39703
40534
|
},
|
|
39704
|
-
company: { type: "string", description: "Filter by company" }
|
|
40535
|
+
company: { type: "string", description: "Filter by company" },
|
|
40536
|
+
date_from: {
|
|
40537
|
+
type: "string",
|
|
40538
|
+
description: "Start date filter YYYY-MM-DD"
|
|
40539
|
+
},
|
|
40540
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
39705
40541
|
}
|
|
39706
40542
|
},
|
|
39707
40543
|
handler: async (input, ctx) => {
|
|
@@ -39713,6 +40549,12 @@ var projectTools = [
|
|
|
39713
40549
|
if (input.company) {
|
|
39714
40550
|
filters.push(["company", "=", input.company]);
|
|
39715
40551
|
}
|
|
40552
|
+
if (input.date_from) {
|
|
40553
|
+
filters.push(["expected_start_date", ">=", input.date_from]);
|
|
40554
|
+
}
|
|
40555
|
+
if (input.date_to) {
|
|
40556
|
+
filters.push(["expected_end_date", "<=", input.date_to]);
|
|
40557
|
+
}
|
|
39716
40558
|
const docs = await ctx.client.list("Project", {
|
|
39717
40559
|
fields: [
|
|
39718
40560
|
"name",
|
|
@@ -39775,7 +40617,12 @@ var projectTools = [
|
|
|
39775
40617
|
type: "string",
|
|
39776
40618
|
description: "Filter by priority (Low, Medium, High, Urgent)",
|
|
39777
40619
|
enum: ["Low", "Medium", "High", "Urgent"]
|
|
39778
|
-
}
|
|
40620
|
+
},
|
|
40621
|
+
date_from: {
|
|
40622
|
+
type: "string",
|
|
40623
|
+
description: "Start date filter YYYY-MM-DD"
|
|
40624
|
+
},
|
|
40625
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
39779
40626
|
}
|
|
39780
40627
|
},
|
|
39781
40628
|
handler: async (input, ctx) => {
|
|
@@ -39790,6 +40637,12 @@ var projectTools = [
|
|
|
39790
40637
|
if (input.priority) {
|
|
39791
40638
|
filters.push(["priority", "=", input.priority]);
|
|
39792
40639
|
}
|
|
40640
|
+
if (input.date_from) {
|
|
40641
|
+
filters.push(["exp_start_date", ">=", input.date_from]);
|
|
40642
|
+
}
|
|
40643
|
+
if (input.date_to) {
|
|
40644
|
+
filters.push(["exp_end_date", "<=", input.date_to]);
|
|
40645
|
+
}
|
|
39793
40646
|
const docs = await ctx.client.list("Task", {
|
|
39794
40647
|
fields: [
|
|
39795
40648
|
"name",
|
|
@@ -40032,19 +40885,31 @@ var projectTools = [
|
|
|
40032
40885
|
type: "object",
|
|
40033
40886
|
properties: {
|
|
40034
40887
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40035
|
-
employee: {
|
|
40888
|
+
employee: {
|
|
40889
|
+
type: "string",
|
|
40890
|
+
description: "Filter by employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
40891
|
+
},
|
|
40036
40892
|
project: { type: "string", description: "Filter by project name" },
|
|
40037
40893
|
status: {
|
|
40038
40894
|
type: "string",
|
|
40039
40895
|
description: "Filter by status (Draft, Submitted, Cancelled)"
|
|
40040
|
-
}
|
|
40896
|
+
},
|
|
40897
|
+
date_from: {
|
|
40898
|
+
type: "string",
|
|
40899
|
+
description: "Start date filter YYYY-MM-DD"
|
|
40900
|
+
},
|
|
40901
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
40041
40902
|
}
|
|
40042
40903
|
},
|
|
40043
40904
|
handler: async (input, ctx) => {
|
|
40044
40905
|
const limit = input.limit ?? 20;
|
|
40045
40906
|
const filters = [];
|
|
40046
40907
|
if (input.employee) {
|
|
40047
|
-
filters.push([
|
|
40908
|
+
filters.push([
|
|
40909
|
+
"employee",
|
|
40910
|
+
"=",
|
|
40911
|
+
await resolveEmployee(ctx.client, input.employee)
|
|
40912
|
+
]);
|
|
40048
40913
|
}
|
|
40049
40914
|
if (input.project) {
|
|
40050
40915
|
filters.push(["project", "=", input.project]);
|
|
@@ -40052,6 +40917,12 @@ var projectTools = [
|
|
|
40052
40917
|
if (input.status) {
|
|
40053
40918
|
filters.push(["status", "=", input.status]);
|
|
40054
40919
|
}
|
|
40920
|
+
if (input.date_from) {
|
|
40921
|
+
filters.push(["start_date", ">=", input.date_from]);
|
|
40922
|
+
}
|
|
40923
|
+
if (input.date_to) {
|
|
40924
|
+
filters.push(["end_date", "<=", input.date_to]);
|
|
40925
|
+
}
|
|
40055
40926
|
const docs = await ctx.client.list("Timesheet", {
|
|
40056
40927
|
fields: [
|
|
40057
40928
|
"name",
|
|
@@ -40294,7 +41165,10 @@ var purchasingTools = [
|
|
|
40294
41165
|
type: "object",
|
|
40295
41166
|
properties: {
|
|
40296
41167
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40297
|
-
supplier: {
|
|
41168
|
+
supplier: {
|
|
41169
|
+
type: "string",
|
|
41170
|
+
description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
|
|
41171
|
+
},
|
|
40298
41172
|
status: {
|
|
40299
41173
|
type: "string",
|
|
40300
41174
|
description: "Filter by status (Draft, To Receive and Bill, To Bill, Completed, Cancelled, etc.)"
|
|
@@ -40310,7 +41184,11 @@ var purchasingTools = [
|
|
|
40310
41184
|
const limit = input.limit ?? 20;
|
|
40311
41185
|
const filters = [];
|
|
40312
41186
|
if (input.supplier) {
|
|
40313
|
-
filters.push([
|
|
41187
|
+
filters.push([
|
|
41188
|
+
"supplier",
|
|
41189
|
+
"=",
|
|
41190
|
+
await resolveSupplier(ctx.client, input.supplier)
|
|
41191
|
+
]);
|
|
40314
41192
|
}
|
|
40315
41193
|
if (input.status) {
|
|
40316
41194
|
filters.push(["status", "=", input.status]);
|
|
@@ -40442,7 +41320,10 @@ var purchasingTools = [
|
|
|
40442
41320
|
type: "object",
|
|
40443
41321
|
properties: {
|
|
40444
41322
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40445
|
-
supplier: {
|
|
41323
|
+
supplier: {
|
|
41324
|
+
type: "string",
|
|
41325
|
+
description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
|
|
41326
|
+
},
|
|
40446
41327
|
status: {
|
|
40447
41328
|
type: "string",
|
|
40448
41329
|
description: "Filter by status (Draft, Unpaid, Paid, Overdue, Cancelled, etc.)"
|
|
@@ -40458,7 +41339,11 @@ var purchasingTools = [
|
|
|
40458
41339
|
const limit = input.limit ?? 20;
|
|
40459
41340
|
const filters = [];
|
|
40460
41341
|
if (input.supplier) {
|
|
40461
|
-
filters.push([
|
|
41342
|
+
filters.push([
|
|
41343
|
+
"supplier",
|
|
41344
|
+
"=",
|
|
41345
|
+
await resolveSupplier(ctx.client, input.supplier)
|
|
41346
|
+
]);
|
|
40462
41347
|
}
|
|
40463
41348
|
if (input.status) {
|
|
40464
41349
|
filters.push(["status", "=", input.status]);
|
|
@@ -40528,7 +41413,10 @@ var purchasingTools = [
|
|
|
40528
41413
|
type: "object",
|
|
40529
41414
|
properties: {
|
|
40530
41415
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40531
|
-
supplier: {
|
|
41416
|
+
supplier: {
|
|
41417
|
+
type: "string",
|
|
41418
|
+
description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
|
|
41419
|
+
},
|
|
40532
41420
|
status: {
|
|
40533
41421
|
type: "string",
|
|
40534
41422
|
description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
|
|
@@ -40544,7 +41432,11 @@ var purchasingTools = [
|
|
|
40544
41432
|
const limit = input.limit ?? 20;
|
|
40545
41433
|
const filters = [];
|
|
40546
41434
|
if (input.supplier) {
|
|
40547
|
-
filters.push([
|
|
41435
|
+
filters.push([
|
|
41436
|
+
"supplier",
|
|
41437
|
+
"=",
|
|
41438
|
+
await resolveSupplier(ctx.client, input.supplier)
|
|
41439
|
+
]);
|
|
40548
41440
|
}
|
|
40549
41441
|
if (input.status) {
|
|
40550
41442
|
filters.push(["status", "=", input.status]);
|
|
@@ -40613,22 +41505,40 @@ var purchasingTools = [
|
|
|
40613
41505
|
type: "object",
|
|
40614
41506
|
properties: {
|
|
40615
41507
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40616
|
-
supplier: {
|
|
41508
|
+
supplier: {
|
|
41509
|
+
type: "string",
|
|
41510
|
+
description: "Filter by supplier ID or name (e.g. 'SUPP-00001' or 'Acme Supplies')"
|
|
41511
|
+
},
|
|
40617
41512
|
status: {
|
|
40618
41513
|
type: "string",
|
|
40619
41514
|
description: "Filter by status (Draft, Submitted, Ordered, Lost, Cancelled)"
|
|
40620
|
-
}
|
|
41515
|
+
},
|
|
41516
|
+
date_from: {
|
|
41517
|
+
type: "string",
|
|
41518
|
+
description: "Start date filter YYYY-MM-DD"
|
|
41519
|
+
},
|
|
41520
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
40621
41521
|
}
|
|
40622
41522
|
},
|
|
40623
41523
|
handler: async (input, ctx) => {
|
|
40624
41524
|
const limit = input.limit ?? 20;
|
|
40625
41525
|
const filters = [];
|
|
40626
41526
|
if (input.supplier) {
|
|
40627
|
-
filters.push([
|
|
41527
|
+
filters.push([
|
|
41528
|
+
"supplier",
|
|
41529
|
+
"=",
|
|
41530
|
+
await resolveSupplier(ctx.client, input.supplier)
|
|
41531
|
+
]);
|
|
40628
41532
|
}
|
|
40629
41533
|
if (input.status) {
|
|
40630
41534
|
filters.push(["status", "=", input.status]);
|
|
40631
41535
|
}
|
|
41536
|
+
if (input.date_from) {
|
|
41537
|
+
filters.push(["transaction_date", ">=", input.date_from]);
|
|
41538
|
+
}
|
|
41539
|
+
if (input.date_to) {
|
|
41540
|
+
filters.push(["transaction_date", "<=", input.date_to]);
|
|
41541
|
+
}
|
|
40632
41542
|
const docs = await ctx.client.list("Supplier Quotation", {
|
|
40633
41543
|
fields: [
|
|
40634
41544
|
"name",
|
|
@@ -40664,7 +41574,10 @@ var deliveryTools = [
|
|
|
40664
41574
|
type: "object",
|
|
40665
41575
|
properties: {
|
|
40666
41576
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40667
|
-
customer: {
|
|
41577
|
+
customer: {
|
|
41578
|
+
type: "string",
|
|
41579
|
+
description: "Filter by customer ID or name (e.g. 'CUST-00001' or 'Acme Corp')"
|
|
41580
|
+
},
|
|
40668
41581
|
status: {
|
|
40669
41582
|
type: "string",
|
|
40670
41583
|
description: "Filter by status (Draft, To Bill, Completed, Cancelled, etc.)"
|
|
@@ -40680,7 +41593,11 @@ var deliveryTools = [
|
|
|
40680
41593
|
const limit = input.limit ?? 20;
|
|
40681
41594
|
const filters = [];
|
|
40682
41595
|
if (input.customer) {
|
|
40683
|
-
filters.push([
|
|
41596
|
+
filters.push([
|
|
41597
|
+
"customer",
|
|
41598
|
+
"=",
|
|
41599
|
+
await resolveCustomer(ctx.client, input.customer)
|
|
41600
|
+
]);
|
|
40684
41601
|
}
|
|
40685
41602
|
if (input.status) {
|
|
40686
41603
|
filters.push(["status", "=", input.status]);
|
|
@@ -40899,7 +41816,7 @@ var manufacturingTools = [
|
|
|
40899
41816
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40900
41817
|
item: {
|
|
40901
41818
|
type: "string",
|
|
40902
|
-
description: "Filter by finished goods item code"
|
|
41819
|
+
description: "Filter by finished goods item code or name (e.g. 'ITEM-001' or 'Widget A')"
|
|
40903
41820
|
},
|
|
40904
41821
|
is_active: {
|
|
40905
41822
|
type: "boolean",
|
|
@@ -40915,7 +41832,11 @@ var manufacturingTools = [
|
|
|
40915
41832
|
const limit = input.limit ?? 20;
|
|
40916
41833
|
const filters = [];
|
|
40917
41834
|
if (input.item) {
|
|
40918
|
-
filters.push([
|
|
41835
|
+
filters.push([
|
|
41836
|
+
"item",
|
|
41837
|
+
"=",
|
|
41838
|
+
await resolveItem(ctx.client, input.item)
|
|
41839
|
+
]);
|
|
40919
41840
|
}
|
|
40920
41841
|
if (input.is_active !== void 0) {
|
|
40921
41842
|
filters.push(["is_active", "=", input.is_active ? 1 : 0]);
|
|
@@ -40983,7 +41904,7 @@ var manufacturingTools = [
|
|
|
40983
41904
|
limit: { type: "number", description: "Max results (default 20)" },
|
|
40984
41905
|
production_item: {
|
|
40985
41906
|
type: "string",
|
|
40986
|
-
description: "Filter by item being produced"
|
|
41907
|
+
description: "Filter by item being produced \u2014 item code or name (e.g. 'ITEM-001' or 'Widget A')"
|
|
40987
41908
|
},
|
|
40988
41909
|
status: {
|
|
40989
41910
|
type: "string",
|
|
@@ -41003,7 +41924,11 @@ var manufacturingTools = [
|
|
|
41003
41924
|
const limit = input.limit ?? 20;
|
|
41004
41925
|
const filters = [];
|
|
41005
41926
|
if (input.production_item) {
|
|
41006
|
-
filters.push([
|
|
41927
|
+
filters.push([
|
|
41928
|
+
"production_item",
|
|
41929
|
+
"=",
|
|
41930
|
+
await resolveItem(ctx.client, input.production_item)
|
|
41931
|
+
]);
|
|
41007
41932
|
}
|
|
41008
41933
|
if (input.status) {
|
|
41009
41934
|
filters.push(["status", "=", input.status]);
|
|
@@ -41327,9 +42252,14 @@ var crmTools = [
|
|
|
41327
42252
|
type: "string",
|
|
41328
42253
|
description: "Filter by assigned sales rep (user)"
|
|
41329
42254
|
},
|
|
42255
|
+
opportunity_from: {
|
|
42256
|
+
type: "string",
|
|
42257
|
+
description: "Party type: Customer or Lead. Required when 'party_name' is set, so the party name/ID can be resolved against the right doctype.",
|
|
42258
|
+
enum: ["Customer", "Lead"]
|
|
42259
|
+
},
|
|
41330
42260
|
party_name: {
|
|
41331
42261
|
type: "string",
|
|
41332
|
-
description: "Filter by
|
|
42262
|
+
description: "Filter by party \u2014 ID or name (e.g. customer/lead name). Requires 'opportunity_from'."
|
|
41333
42263
|
}
|
|
41334
42264
|
}
|
|
41335
42265
|
},
|
|
@@ -41347,7 +42277,20 @@ var crmTools = [
|
|
|
41347
42277
|
]);
|
|
41348
42278
|
}
|
|
41349
42279
|
if (input.party_name) {
|
|
41350
|
-
|
|
42280
|
+
if (!input.opportunity_from) {
|
|
42281
|
+
throw new Error(
|
|
42282
|
+
"[erpnext_opportunity_list] 'opportunity_from' is required when filtering by 'party_name'"
|
|
42283
|
+
);
|
|
42284
|
+
}
|
|
42285
|
+
filters.push([
|
|
42286
|
+
"party_name",
|
|
42287
|
+
"=",
|
|
42288
|
+
await resolveDynamicLink(
|
|
42289
|
+
ctx.client,
|
|
42290
|
+
input.opportunity_from,
|
|
42291
|
+
input.party_name
|
|
42292
|
+
)
|
|
42293
|
+
]);
|
|
41351
42294
|
}
|
|
41352
42295
|
const docs = await ctx.client.list("Opportunity", {
|
|
41353
42296
|
fields: [
|
|
@@ -41475,7 +42418,12 @@ var crmTools = [
|
|
|
41475
42418
|
campaign_type: {
|
|
41476
42419
|
type: "string",
|
|
41477
42420
|
description: "Filter by campaign type"
|
|
41478
|
-
}
|
|
42421
|
+
},
|
|
42422
|
+
date_from: {
|
|
42423
|
+
type: "string",
|
|
42424
|
+
description: "Start date filter YYYY-MM-DD"
|
|
42425
|
+
},
|
|
42426
|
+
date_to: { type: "string", description: "End date filter YYYY-MM-DD" }
|
|
41479
42427
|
}
|
|
41480
42428
|
},
|
|
41481
42429
|
handler: async (input, ctx) => {
|
|
@@ -41484,6 +42432,12 @@ var crmTools = [
|
|
|
41484
42432
|
if (input.campaign_type) {
|
|
41485
42433
|
filters.push(["campaign_type", "=", input.campaign_type]);
|
|
41486
42434
|
}
|
|
42435
|
+
if (input.date_from) {
|
|
42436
|
+
filters.push(["start_date", ">=", input.date_from]);
|
|
42437
|
+
}
|
|
42438
|
+
if (input.date_to) {
|
|
42439
|
+
filters.push(["end_date", "<=", input.date_to]);
|
|
42440
|
+
}
|
|
41487
42441
|
const docs = await ctx.client.list("Campaign", {
|
|
41488
42442
|
fields: [
|
|
41489
42443
|
"name",
|
|
@@ -41531,7 +42485,15 @@ var assetsTools = [
|
|
|
41531
42485
|
location: { type: "string", description: "Filter by location" },
|
|
41532
42486
|
custodian: {
|
|
41533
42487
|
type: "string",
|
|
41534
|
-
description: "Filter by custodian (
|
|
42488
|
+
description: "Filter by custodian \u2014 employee ID or name (e.g. 'HR-EMP-00001' or 'John Doe')"
|
|
42489
|
+
},
|
|
42490
|
+
date_from: {
|
|
42491
|
+
type: "string",
|
|
42492
|
+
description: "Purchase date start filter YYYY-MM-DD"
|
|
42493
|
+
},
|
|
42494
|
+
date_to: {
|
|
42495
|
+
type: "string",
|
|
42496
|
+
description: "Purchase date end filter YYYY-MM-DD"
|
|
41535
42497
|
}
|
|
41536
42498
|
}
|
|
41537
42499
|
},
|
|
@@ -41548,7 +42510,17 @@ var assetsTools = [
|
|
|
41548
42510
|
filters.push(["location", "=", input.location]);
|
|
41549
42511
|
}
|
|
41550
42512
|
if (input.custodian) {
|
|
41551
|
-
filters.push([
|
|
42513
|
+
filters.push([
|
|
42514
|
+
"custodian",
|
|
42515
|
+
"=",
|
|
42516
|
+
await resolveEmployee(ctx.client, input.custodian)
|
|
42517
|
+
]);
|
|
42518
|
+
}
|
|
42519
|
+
if (input.date_from) {
|
|
42520
|
+
filters.push(["purchase_date", ">=", input.date_from]);
|
|
42521
|
+
}
|
|
42522
|
+
if (input.date_to) {
|
|
42523
|
+
filters.push(["purchase_date", "<=", input.date_to]);
|
|
41552
42524
|
}
|
|
41553
42525
|
const docs = await ctx.client.list("Asset", {
|
|
41554
42526
|
fields: [
|
|
@@ -41830,27 +42802,115 @@ var assetsTools = [
|
|
|
41830
42802
|
properties: {
|
|
41831
42803
|
limit: { type: "number", description: "Max results (default 20)" }
|
|
41832
42804
|
}
|
|
41833
|
-
},
|
|
41834
|
-
handler: async (input, ctx) => {
|
|
41835
|
-
const limit = input.limit ?? 20;
|
|
41836
|
-
const docs = await ctx.client.list("Asset Category", {
|
|
41837
|
-
fields: ["name", "asset_category_name"],
|
|
41838
|
-
filters: [],
|
|
41839
|
-
limit,
|
|
41840
|
-
order_by: "modified desc"
|
|
42805
|
+
},
|
|
42806
|
+
handler: async (input, ctx) => {
|
|
42807
|
+
const limit = input.limit ?? 20;
|
|
42808
|
+
const docs = await ctx.client.list("Asset Category", {
|
|
42809
|
+
fields: ["name", "asset_category_name"],
|
|
42810
|
+
filters: [],
|
|
42811
|
+
limit,
|
|
42812
|
+
order_by: "modified desc"
|
|
42813
|
+
});
|
|
42814
|
+
return {
|
|
42815
|
+
doctype: "Asset Category",
|
|
42816
|
+
count: docs.length,
|
|
42817
|
+
data: docs,
|
|
42818
|
+
_meta: DOCLIST_META
|
|
42819
|
+
};
|
|
42820
|
+
}
|
|
42821
|
+
}
|
|
42822
|
+
];
|
|
42823
|
+
|
|
42824
|
+
// src/tools/operations.ts
|
|
42825
|
+
var operationsTools = [
|
|
42826
|
+
// ── File Attachments ───────────────────────────────────────────────────────
|
|
42827
|
+
{
|
|
42828
|
+
name: "erpnext_file_upload",
|
|
42829
|
+
annotations: { destructiveHint: true },
|
|
42830
|
+
description: "Upload base64-encoded file content and attach it to any ERPNext document. Files are private by default.",
|
|
42831
|
+
category: "operations",
|
|
42832
|
+
inputSchema: {
|
|
42833
|
+
type: "object",
|
|
42834
|
+
properties: {
|
|
42835
|
+
file_name: {
|
|
42836
|
+
type: "string",
|
|
42837
|
+
description: "Filename only, without a path.",
|
|
42838
|
+
minLength: 1
|
|
42839
|
+
},
|
|
42840
|
+
content_base64: {
|
|
42841
|
+
type: "string",
|
|
42842
|
+
description: "File content as standard base64 (not a data URL).",
|
|
42843
|
+
minLength: 1
|
|
42844
|
+
},
|
|
42845
|
+
attached_to_doctype: {
|
|
42846
|
+
type: "string",
|
|
42847
|
+
description: "DocType of the document to attach the file to.",
|
|
42848
|
+
minLength: 1
|
|
42849
|
+
},
|
|
42850
|
+
attached_to_name: {
|
|
42851
|
+
type: "string",
|
|
42852
|
+
description: "Name/ID of the document to attach the file to.",
|
|
42853
|
+
minLength: 1
|
|
42854
|
+
},
|
|
42855
|
+
attached_to_field: {
|
|
42856
|
+
type: "string",
|
|
42857
|
+
description: "Optional Attach or Attach Image field to populate with the uploaded file."
|
|
42858
|
+
},
|
|
42859
|
+
is_private: {
|
|
42860
|
+
type: "boolean",
|
|
42861
|
+
description: "Whether the attachment is private. Defaults to true.",
|
|
42862
|
+
default: true
|
|
42863
|
+
}
|
|
42864
|
+
},
|
|
42865
|
+
required: [
|
|
42866
|
+
"file_name",
|
|
42867
|
+
"content_base64",
|
|
42868
|
+
"attached_to_doctype",
|
|
42869
|
+
"attached_to_name"
|
|
42870
|
+
]
|
|
42871
|
+
},
|
|
42872
|
+
handler: async (input, ctx) => {
|
|
42873
|
+
const requiredStrings = [
|
|
42874
|
+
"file_name",
|
|
42875
|
+
"content_base64",
|
|
42876
|
+
"attached_to_doctype",
|
|
42877
|
+
"attached_to_name"
|
|
42878
|
+
];
|
|
42879
|
+
for (const field of requiredStrings) {
|
|
42880
|
+
if (typeof input[field] !== "string" || !input[field].trim()) {
|
|
42881
|
+
throw new Error(
|
|
42882
|
+
`[erpnext_file_upload] '${field}' must be a non-empty string`
|
|
42883
|
+
);
|
|
42884
|
+
}
|
|
42885
|
+
}
|
|
42886
|
+
const fileName = input.file_name;
|
|
42887
|
+
if (/[\\/\0]/.test(fileName)) {
|
|
42888
|
+
throw new Error(
|
|
42889
|
+
"[erpnext_file_upload] 'file_name' must be a filename without a path"
|
|
42890
|
+
);
|
|
42891
|
+
}
|
|
42892
|
+
if (input.is_private !== void 0 && typeof input.is_private !== "boolean") {
|
|
42893
|
+
throw new Error("[erpnext_file_upload] 'is_private' must be a boolean");
|
|
42894
|
+
}
|
|
42895
|
+
if (input.attached_to_field !== void 0 && (typeof input.attached_to_field !== "string" || !input.attached_to_field.trim())) {
|
|
42896
|
+
throw new Error(
|
|
42897
|
+
"[erpnext_file_upload] 'attached_to_field' must be a non-empty string"
|
|
42898
|
+
);
|
|
42899
|
+
}
|
|
42900
|
+
const file = await ctx.client.uploadFile({
|
|
42901
|
+
fileName,
|
|
42902
|
+
contentBase64: input.content_base64,
|
|
42903
|
+
attachedToDoctype: input.attached_to_doctype,
|
|
42904
|
+
attachedToName: input.attached_to_name,
|
|
42905
|
+
...input.attached_to_field !== void 0 ? { attachedToField: input.attached_to_field.trim() } : {},
|
|
42906
|
+
isPrivate: input.is_private === void 0 ? true : input.is_private
|
|
41841
42907
|
});
|
|
41842
42908
|
return {
|
|
41843
|
-
|
|
41844
|
-
|
|
41845
|
-
data: docs,
|
|
41846
|
-
_meta: DOCLIST_META
|
|
42909
|
+
data: file,
|
|
42910
|
+
message: `${fileName} attached to ${input.attached_to_doctype} ${input.attached_to_name}`
|
|
41847
42911
|
};
|
|
41848
42912
|
}
|
|
41849
|
-
}
|
|
41850
|
-
];
|
|
41851
|
-
|
|
41852
|
-
// src/tools/operations.ts
|
|
41853
|
-
var operationsTools = [
|
|
42913
|
+
},
|
|
41854
42914
|
// ── Generic Create ──────────────────────────────────────────────────────────
|
|
41855
42915
|
{
|
|
41856
42916
|
name: "erpnext_doc_create",
|
|
@@ -42002,16 +43062,22 @@ var operationsTools = [
|
|
|
42002
43062
|
}
|
|
42003
43063
|
const doc = await ctx.client.get(
|
|
42004
43064
|
input.doctype,
|
|
42005
|
-
input.name
|
|
43065
|
+
input.name,
|
|
43066
|
+
{ skipCache: true }
|
|
42006
43067
|
);
|
|
43068
|
+
const docWithDoctype = { ...doc, doctype: input.doctype };
|
|
43069
|
+
const patchedDoc = withRoundedTotalFallback(docWithDoctype);
|
|
42007
43070
|
const result = await ctx.client.callMethod("frappe.client.submit", {
|
|
42008
|
-
doc:
|
|
43071
|
+
doc: patchedDoc
|
|
42009
43072
|
});
|
|
43073
|
+
ctx.client.invalidate(input.doctype, input.name);
|
|
43074
|
+
const warnings = roundedTotalFallbackWarning(docWithDoctype, patchedDoc);
|
|
42010
43075
|
return {
|
|
42011
43076
|
data: result,
|
|
42012
43077
|
message: `${input.doctype} ${input.name} submitted successfully`,
|
|
42013
43078
|
doctype: input.doctype,
|
|
42014
|
-
name: input.name
|
|
43079
|
+
name: input.name,
|
|
43080
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
42015
43081
|
};
|
|
42016
43082
|
}
|
|
42017
43083
|
},
|
|
@@ -42046,6 +43112,7 @@ var operationsTools = [
|
|
|
42046
43112
|
doctype: input.doctype,
|
|
42047
43113
|
name: input.name
|
|
42048
43114
|
});
|
|
43115
|
+
ctx.client.invalidate(input.doctype, input.name);
|
|
42049
43116
|
return {
|
|
42050
43117
|
data: result,
|
|
42051
43118
|
message: `${input.doctype} ${input.name} cancelled successfully`,
|
|
@@ -44542,500 +45609,206 @@ var issueKanbanAdapter = {
|
|
|
44542
45609
|
ok: true,
|
|
44543
45610
|
cardId: move.cardId,
|
|
44544
45611
|
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";
|
|
45612
|
+
toColumn: move.toColumn,
|
|
45613
|
+
serverCard: buildIssueCard(serverIssue)
|
|
45614
|
+
};
|
|
44773
45615
|
}
|
|
44774
45616
|
};
|
|
44775
|
-
|
|
44776
|
-
|
|
44777
|
-
|
|
44778
|
-
|
|
44779
|
-
|
|
44780
|
-
|
|
45617
|
+
|
|
45618
|
+
// src/tools/kanban.ts
|
|
45619
|
+
var ADAPTERS = {
|
|
45620
|
+
task: taskKanbanAdapter,
|
|
45621
|
+
opportunity: opportunityKanbanAdapter,
|
|
45622
|
+
issue: issueKanbanAdapter
|
|
45623
|
+
};
|
|
45624
|
+
function getAdapter(doctype) {
|
|
45625
|
+
const definition = getKanbanBoardDefinition(doctype);
|
|
45626
|
+
if (!definition) {
|
|
45627
|
+
throw new Error(`[erpnext_kanban] Unsupported kanban doctype: ${doctype}`);
|
|
44781
45628
|
}
|
|
44782
|
-
const
|
|
44783
|
-
if (
|
|
44784
|
-
|
|
45629
|
+
const adapter = ADAPTERS[definition.adapterKey];
|
|
45630
|
+
if (!adapter) {
|
|
45631
|
+
throw new Error(`[erpnext_kanban] No adapter registered for ${doctype}`);
|
|
44785
45632
|
}
|
|
44786
|
-
return
|
|
45633
|
+
return { definition, adapter };
|
|
44787
45634
|
}
|
|
44788
|
-
function
|
|
44789
|
-
|
|
44790
|
-
|
|
44791
|
-
|
|
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;
|
|
45635
|
+
function withColumnCounts(columns, cards) {
|
|
45636
|
+
const counts = /* @__PURE__ */ new Map();
|
|
45637
|
+
for (const card of cards) {
|
|
45638
|
+
counts.set(card.columnId, (counts.get(card.columnId) ?? 0) + 1);
|
|
44809
45639
|
}
|
|
45640
|
+
return columns.map((column) => ({
|
|
45641
|
+
...column,
|
|
45642
|
+
count: counts.get(column.id) ?? 0
|
|
45643
|
+
}));
|
|
44810
45644
|
}
|
|
44811
|
-
var
|
|
44812
|
-
|
|
44813
|
-
|
|
44814
|
-
|
|
44815
|
-
|
|
44816
|
-
|
|
44817
|
-
|
|
44818
|
-
|
|
44819
|
-
|
|
44820
|
-
|
|
44821
|
-
|
|
44822
|
-
|
|
44823
|
-
|
|
44824
|
-
|
|
44825
|
-
|
|
44826
|
-
|
|
44827
|
-
|
|
44828
|
-
|
|
44829
|
-
|
|
44830
|
-
|
|
44831
|
-
|
|
44832
|
-
|
|
44833
|
-
|
|
44834
|
-
|
|
44835
|
-
|
|
44836
|
-
|
|
44837
|
-
|
|
44838
|
-
|
|
44839
|
-
|
|
44840
|
-
|
|
44841
|
-
|
|
44842
|
-
|
|
44843
|
-
|
|
44844
|
-
|
|
44845
|
-
|
|
44846
|
-
|
|
44847
|
-
|
|
44848
|
-
|
|
44849
|
-
|
|
44850
|
-
|
|
44851
|
-
|
|
44852
|
-
|
|
44853
|
-
|
|
44854
|
-
|
|
44855
|
-
|
|
44856
|
-
|
|
44857
|
-
|
|
44858
|
-
|
|
44859
|
-
|
|
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));
|
|
45645
|
+
var kanbanTools = [
|
|
45646
|
+
{
|
|
45647
|
+
name: "erpnext_kanban_get_board",
|
|
45648
|
+
annotations: { readOnlyHint: true },
|
|
45649
|
+
category: "kanban",
|
|
45650
|
+
_meta: KANBAN_META,
|
|
45651
|
+
description: "Get a normalized kanban board for a supported ERPNext DocType. Supports Task, Opportunity, and Issue, with pagination and MCP App metadata.",
|
|
45652
|
+
inputSchema: {
|
|
45653
|
+
type: "object",
|
|
45654
|
+
properties: {
|
|
45655
|
+
doctype: {
|
|
45656
|
+
type: "string",
|
|
45657
|
+
description: "Kanban-enabled ERPNext DocType",
|
|
45658
|
+
enum: ["Task", "Opportunity", "Issue"]
|
|
45659
|
+
},
|
|
45660
|
+
limit: { type: "number", description: "Page size (default 50)" },
|
|
45661
|
+
offset: {
|
|
45662
|
+
type: "number",
|
|
45663
|
+
description: "Pagination offset (default 0)"
|
|
45664
|
+
},
|
|
45665
|
+
project: {
|
|
45666
|
+
type: "string",
|
|
45667
|
+
description: "Optional Task project filter"
|
|
45668
|
+
},
|
|
45669
|
+
priority: {
|
|
45670
|
+
type: "string",
|
|
45671
|
+
description: "Optional Task priority filter",
|
|
45672
|
+
enum: ["Low", "Medium", "High", "Urgent"]
|
|
45673
|
+
},
|
|
45674
|
+
status: {
|
|
45675
|
+
type: "string",
|
|
45676
|
+
description: "Optional Opportunity status filter",
|
|
45677
|
+
enum: ["Open", "Replied", "Quotation", "Converted", "Closed", "Lost"]
|
|
45678
|
+
},
|
|
45679
|
+
opportunity_owner: {
|
|
45680
|
+
type: "string",
|
|
45681
|
+
description: "Optional Opportunity owner filter"
|
|
45682
|
+
},
|
|
45683
|
+
party_name: {
|
|
45684
|
+
type: "string",
|
|
45685
|
+
description: "Optional Opportunity party filter"
|
|
45686
|
+
},
|
|
45687
|
+
customer: {
|
|
45688
|
+
type: "string",
|
|
45689
|
+
description: "Optional Issue customer filter"
|
|
45690
|
+
},
|
|
45691
|
+
raised_by: {
|
|
45692
|
+
type: "string",
|
|
45693
|
+
description: "Optional Issue reporter email filter"
|
|
44867
45694
|
}
|
|
44868
|
-
}
|
|
44869
|
-
|
|
44870
|
-
|
|
44871
|
-
|
|
44872
|
-
|
|
44873
|
-
|
|
44874
|
-
|
|
44875
|
-
|
|
44876
|
-
|
|
44877
|
-
|
|
44878
|
-
|
|
44879
|
-
|
|
44880
|
-
|
|
44881
|
-
|
|
44882
|
-
signal: controller.signal
|
|
45695
|
+
},
|
|
45696
|
+
required: ["doctype"]
|
|
45697
|
+
},
|
|
45698
|
+
handler: async (input, ctx) => {
|
|
45699
|
+
const doctype = String(input.doctype ?? "");
|
|
45700
|
+
const { definition, adapter } = getAdapter(doctype);
|
|
45701
|
+
const limit = Math.max(1, Number(input.limit ?? 50));
|
|
45702
|
+
const offset = Math.max(0, Number(input.offset ?? 0));
|
|
45703
|
+
const rows = await ctx.client.list(doctype, {
|
|
45704
|
+
fields: adapter.getListFields(),
|
|
45705
|
+
filters: adapter.buildListFilters(input),
|
|
45706
|
+
limit: limit + 1,
|
|
45707
|
+
limit_start: offset,
|
|
45708
|
+
order_by: "modified desc"
|
|
44883
45709
|
});
|
|
44884
|
-
|
|
44885
|
-
|
|
44886
|
-
|
|
44887
|
-
|
|
44888
|
-
|
|
44889
|
-
|
|
44890
|
-
|
|
44891
|
-
)
|
|
44892
|
-
|
|
44893
|
-
|
|
44894
|
-
|
|
44895
|
-
|
|
44896
|
-
|
|
44897
|
-
|
|
44898
|
-
|
|
44899
|
-
|
|
44900
|
-
|
|
44901
|
-
|
|
44902
|
-
|
|
44903
|
-
|
|
44904
|
-
|
|
44905
|
-
|
|
44906
|
-
|
|
44907
|
-
|
|
44908
|
-
}
|
|
44909
|
-
}
|
|
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
|
-
);
|
|
44930
|
-
}
|
|
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));
|
|
44942
|
-
}
|
|
44943
|
-
if (options.filters && options.filters.length > 0) {
|
|
44944
|
-
params.set("filters", JSON.stringify(options.filters));
|
|
44945
|
-
}
|
|
44946
|
-
if (options.order_by) {
|
|
44947
|
-
params.set("order_by", options.order_by);
|
|
44948
|
-
}
|
|
44949
|
-
if (options.limit !== void 0) {
|
|
44950
|
-
params.set("limit", String(options.limit));
|
|
45710
|
+
const hasMore = rows.length > limit;
|
|
45711
|
+
const visibleRows = rows.slice(0, limit);
|
|
45712
|
+
const cards = adapter.buildCards(visibleRows);
|
|
45713
|
+
const board = {
|
|
45714
|
+
boardId: `${doctype.toLowerCase()}-board`,
|
|
45715
|
+
title: definition.title,
|
|
45716
|
+
doctype,
|
|
45717
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
45718
|
+
moveToolName: definition.moveToolName,
|
|
45719
|
+
refreshArguments: { ...input, doctype, limit, offset },
|
|
45720
|
+
columns: withColumnCounts(adapter.getColumns(), cards),
|
|
45721
|
+
cards,
|
|
45722
|
+
allowedTransitions: adapter.getAllowedTransitions(),
|
|
45723
|
+
capabilities: { canMoveCards: true },
|
|
45724
|
+
pagination: {
|
|
45725
|
+
limit,
|
|
45726
|
+
offset,
|
|
45727
|
+
loadedCount: offset + visibleRows.length,
|
|
45728
|
+
hasMore
|
|
45729
|
+
}
|
|
45730
|
+
};
|
|
45731
|
+
return {
|
|
45732
|
+
...board,
|
|
45733
|
+
_meta: KANBAN_META
|
|
45734
|
+
};
|
|
44951
45735
|
}
|
|
44952
|
-
|
|
44953
|
-
|
|
45736
|
+
},
|
|
45737
|
+
{
|
|
45738
|
+
name: "erpnext_kanban_move_card",
|
|
45739
|
+
category: "kanban",
|
|
45740
|
+
description: "Move a kanban card for a supported ERPNext DocType. Returns structured success or business error details for MCP App reconciliation.",
|
|
45741
|
+
inputSchema: {
|
|
45742
|
+
type: "object",
|
|
45743
|
+
properties: {
|
|
45744
|
+
doctype: {
|
|
45745
|
+
type: "string",
|
|
45746
|
+
description: "Kanban-enabled ERPNext DocType",
|
|
45747
|
+
enum: ["Task", "Opportunity", "Issue"]
|
|
45748
|
+
},
|
|
45749
|
+
card_id: { type: "string", description: "Card/document identifier" },
|
|
45750
|
+
from_column: {
|
|
45751
|
+
type: "string",
|
|
45752
|
+
description: "Source column identifier"
|
|
45753
|
+
},
|
|
45754
|
+
to_column: {
|
|
45755
|
+
type: "string",
|
|
45756
|
+
description: "Destination column identifier"
|
|
45757
|
+
}
|
|
45758
|
+
},
|
|
45759
|
+
required: ["doctype", "card_id", "from_column", "to_column"]
|
|
45760
|
+
},
|
|
45761
|
+
handler: async (input, ctx) => {
|
|
45762
|
+
const doctype = String(input.doctype ?? "");
|
|
45763
|
+
const { adapter } = getAdapter(doctype);
|
|
45764
|
+
return await adapter.executeMove({
|
|
45765
|
+
doctype,
|
|
45766
|
+
cardId: String(input.card_id ?? ""),
|
|
45767
|
+
fromColumn: String(input.from_column ?? ""),
|
|
45768
|
+
toColumn: String(input.to_column ?? "")
|
|
45769
|
+
}, ctx);
|
|
44954
45770
|
}
|
|
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;
|
|
44997
|
-
}
|
|
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
|
-
);
|
|
45007
|
-
}
|
|
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;
|
|
45019
45771
|
}
|
|
45772
|
+
];
|
|
45773
|
+
|
|
45774
|
+
// src/tools/mod.ts
|
|
45775
|
+
var toolsByCategory = {
|
|
45776
|
+
sales: salesTools,
|
|
45777
|
+
inventory: inventoryTools,
|
|
45778
|
+
accounting: accountingTools,
|
|
45779
|
+
hr: hrTools,
|
|
45780
|
+
project: projectTools,
|
|
45781
|
+
purchasing: purchasingTools,
|
|
45782
|
+
delivery: deliveryTools,
|
|
45783
|
+
manufacturing: manufacturingTools,
|
|
45784
|
+
crm: crmTools,
|
|
45785
|
+
assets: assetsTools,
|
|
45786
|
+
operations: operationsTools,
|
|
45787
|
+
setup: setupTools,
|
|
45788
|
+
analytics: analyticsTools,
|
|
45789
|
+
kanban: kanbanTools
|
|
45020
45790
|
};
|
|
45021
|
-
var
|
|
45022
|
-
|
|
45023
|
-
|
|
45024
|
-
|
|
45025
|
-
|
|
45026
|
-
|
|
45027
|
-
|
|
45028
|
-
|
|
45029
|
-
|
|
45030
|
-
|
|
45031
|
-
|
|
45032
|
-
|
|
45033
|
-
|
|
45034
|
-
|
|
45035
|
-
|
|
45036
|
-
|
|
45037
|
-
|
|
45038
|
-
return
|
|
45791
|
+
var allTools = [
|
|
45792
|
+
...salesTools,
|
|
45793
|
+
...inventoryTools,
|
|
45794
|
+
...accountingTools,
|
|
45795
|
+
...hrTools,
|
|
45796
|
+
...projectTools,
|
|
45797
|
+
...purchasingTools,
|
|
45798
|
+
...deliveryTools,
|
|
45799
|
+
...manufacturingTools,
|
|
45800
|
+
...crmTools,
|
|
45801
|
+
...assetsTools,
|
|
45802
|
+
...operationsTools,
|
|
45803
|
+
...setupTools,
|
|
45804
|
+
...analyticsTools,
|
|
45805
|
+
...kanbanTools
|
|
45806
|
+
];
|
|
45807
|
+
function getToolsByCategory(category) {
|
|
45808
|
+
return toolsByCategory[category] ?? [];
|
|
45809
|
+
}
|
|
45810
|
+
function getToolByName(name) {
|
|
45811
|
+
return allTools.find((t) => t.name === name);
|
|
45039
45812
|
}
|
|
45040
45813
|
|
|
45041
45814
|
// src/tools/ui-refresh.ts
|
|
@@ -45332,6 +46105,168 @@ function resolveViewerDistPath2(moduleUrl, viewerName, exists) {
|
|
|
45332
46105
|
return null;
|
|
45333
46106
|
}
|
|
45334
46107
|
|
|
46108
|
+
// src/auth/composite-provider.ts
|
|
46109
|
+
var CompositeAuthProvider = class extends AuthProvider {
|
|
46110
|
+
constructor(providers) {
|
|
46111
|
+
super();
|
|
46112
|
+
this.providers = providers;
|
|
46113
|
+
if (providers.length === 0) {
|
|
46114
|
+
throw new Error(
|
|
46115
|
+
"[CompositeAuthProvider] at least one provider is required"
|
|
46116
|
+
);
|
|
46117
|
+
}
|
|
46118
|
+
}
|
|
46119
|
+
async verifyToken(token) {
|
|
46120
|
+
for (const provider of this.providers) {
|
|
46121
|
+
const info = await provider.verifyToken(token);
|
|
46122
|
+
if (info) return info;
|
|
46123
|
+
}
|
|
46124
|
+
return null;
|
|
46125
|
+
}
|
|
46126
|
+
getResourceMetadata() {
|
|
46127
|
+
const metadata = this.providers.map(
|
|
46128
|
+
(provider) => provider.getResourceMetadata()
|
|
46129
|
+
);
|
|
46130
|
+
const primary = metadata[0];
|
|
46131
|
+
const scopesSupported = [
|
|
46132
|
+
...new Set(metadata.flatMap((item) => item.scopes_supported ?? []))
|
|
46133
|
+
];
|
|
46134
|
+
return {
|
|
46135
|
+
...primary,
|
|
46136
|
+
authorization_servers: [
|
|
46137
|
+
...new Set(metadata.flatMap((item) => item.authorization_servers))
|
|
46138
|
+
],
|
|
46139
|
+
...scopesSupported.length > 0 ? { scopes_supported: scopesSupported } : {},
|
|
46140
|
+
bearer_methods_supported: [
|
|
46141
|
+
...new Set(
|
|
46142
|
+
metadata.flatMap((item) => item.bearer_methods_supported)
|
|
46143
|
+
)
|
|
46144
|
+
]
|
|
46145
|
+
};
|
|
46146
|
+
}
|
|
46147
|
+
};
|
|
46148
|
+
|
|
46149
|
+
// src/auth/config.ts
|
|
46150
|
+
function unquote(value) {
|
|
46151
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
46152
|
+
return value.slice(1, -1);
|
|
46153
|
+
}
|
|
46154
|
+
return value;
|
|
46155
|
+
}
|
|
46156
|
+
function optionalEnvValue(value) {
|
|
46157
|
+
const trimmed = value?.trim();
|
|
46158
|
+
return trimmed ? unquote(trimmed) : void 0;
|
|
46159
|
+
}
|
|
46160
|
+
function loadAuthConfig2() {
|
|
46161
|
+
const single = optionalEnvValue(env6("MCP_AUTH_TOKEN"));
|
|
46162
|
+
const multi = env6("MCP_AUTH_TOKENS");
|
|
46163
|
+
const jwksUrl = optionalEnvValue(env6("MCP_OAUTH_JWKS_URL"));
|
|
46164
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
46165
|
+
if (single) tokens.add(single);
|
|
46166
|
+
if (multi) {
|
|
46167
|
+
for (const t of multi.split(",")) {
|
|
46168
|
+
const token = optionalEnvValue(t);
|
|
46169
|
+
if (token) tokens.add(token);
|
|
46170
|
+
}
|
|
46171
|
+
}
|
|
46172
|
+
if (tokens.size === 0 && !jwksUrl) return null;
|
|
46173
|
+
return {
|
|
46174
|
+
tokens,
|
|
46175
|
+
resource: optionalEnvValue(env6("MCP_AUTH_RESOURCE")),
|
|
46176
|
+
jwksUrl,
|
|
46177
|
+
audience: optionalEnvValue(env6("MCP_OAUTH_AUDIENCE")),
|
|
46178
|
+
issuer: optionalEnvValue(env6("MCP_OAUTH_ISSUER"))
|
|
46179
|
+
};
|
|
46180
|
+
}
|
|
46181
|
+
function buildAuthProvider(config2) {
|
|
46182
|
+
const providers = [];
|
|
46183
|
+
if (config2.tokens.size > 0) {
|
|
46184
|
+
if (!config2.resource) {
|
|
46185
|
+
throw new Error(
|
|
46186
|
+
"[mcp-erpnext] MCP_AUTH_RESOURCE is required alongside MCP_AUTH_TOKEN(S) \u2014 set it to this server's public URL, e.g. https://mcp.example.com"
|
|
46187
|
+
);
|
|
46188
|
+
}
|
|
46189
|
+
providers.push(
|
|
46190
|
+
createStaticTokenAuthProvider([...config2.tokens], {
|
|
46191
|
+
resource: config2.resource
|
|
46192
|
+
})
|
|
46193
|
+
);
|
|
46194
|
+
}
|
|
46195
|
+
if (config2.jwksUrl) {
|
|
46196
|
+
if (!config2.issuer) {
|
|
46197
|
+
throw new Error(
|
|
46198
|
+
"[mcp-erpnext] MCP_OAUTH_ISSUER is required alongside MCP_OAUTH_JWKS_URL"
|
|
46199
|
+
);
|
|
46200
|
+
}
|
|
46201
|
+
if (!config2.audience) {
|
|
46202
|
+
throw new Error(
|
|
46203
|
+
"[mcp-erpnext] MCP_OAUTH_AUDIENCE is required alongside MCP_OAUTH_JWKS_URL"
|
|
46204
|
+
);
|
|
46205
|
+
}
|
|
46206
|
+
if (!config2.resource) {
|
|
46207
|
+
throw new Error(
|
|
46208
|
+
"[mcp-erpnext] MCP_AUTH_RESOURCE is required alongside MCP_OAUTH_JWKS_URL"
|
|
46209
|
+
);
|
|
46210
|
+
}
|
|
46211
|
+
providers.push(
|
|
46212
|
+
createOIDCAuthProvider({
|
|
46213
|
+
issuer: config2.issuer,
|
|
46214
|
+
audience: config2.audience,
|
|
46215
|
+
jwksUri: config2.jwksUrl,
|
|
46216
|
+
resource: config2.resource
|
|
46217
|
+
})
|
|
46218
|
+
);
|
|
46219
|
+
}
|
|
46220
|
+
return providers.length === 1 ? providers[0] : new CompositeAuthProvider(providers);
|
|
46221
|
+
}
|
|
46222
|
+
|
|
46223
|
+
// src/cache/warm.ts
|
|
46224
|
+
async function warmCache() {
|
|
46225
|
+
const configured = env6("MCP_CACHE_WARM_TOOLS");
|
|
46226
|
+
if (!configured?.trim()) return;
|
|
46227
|
+
const client = getFrappeClient();
|
|
46228
|
+
const names = configured.split(",").map((s) => s.trim()).filter(Boolean);
|
|
46229
|
+
for (const name of names) {
|
|
46230
|
+
const tool = getToolByName(name);
|
|
46231
|
+
if (!tool) {
|
|
46232
|
+
console.error(
|
|
46233
|
+
`[mcp-erpnext] Cache warm: unknown tool "${name}", skipping`
|
|
46234
|
+
);
|
|
46235
|
+
continue;
|
|
46236
|
+
}
|
|
46237
|
+
if (!tool.annotations?.readOnlyHint) {
|
|
46238
|
+
console.error(
|
|
46239
|
+
`[mcp-erpnext] Cache warm: "${name}" is not read-only, refusing to call it, skipping`
|
|
46240
|
+
);
|
|
46241
|
+
continue;
|
|
46242
|
+
}
|
|
46243
|
+
try {
|
|
46244
|
+
await tool.handler({}, { client });
|
|
46245
|
+
} catch (err) {
|
|
46246
|
+
console.error(
|
|
46247
|
+
`[mcp-erpnext] Cache warm: "${name}" failed (non-fatal):`,
|
|
46248
|
+
err
|
|
46249
|
+
);
|
|
46250
|
+
}
|
|
46251
|
+
}
|
|
46252
|
+
}
|
|
46253
|
+
|
|
46254
|
+
// src/auth/resource-metadata-route.ts
|
|
46255
|
+
var ROOT_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
46256
|
+
function resourceMetadataRoute(provider) {
|
|
46257
|
+
const metadata = provider.getResourceMetadata();
|
|
46258
|
+
const path = new URL(metadata.resource_metadata_url).pathname;
|
|
46259
|
+
if (path === ROOT_METADATA_PATH) return void 0;
|
|
46260
|
+
return {
|
|
46261
|
+
method: "get",
|
|
46262
|
+
path,
|
|
46263
|
+
handler: () => jsonMetadata(metadata)
|
|
46264
|
+
};
|
|
46265
|
+
}
|
|
46266
|
+
function jsonMetadata(metadata) {
|
|
46267
|
+
return Response.json(metadata);
|
|
46268
|
+
}
|
|
46269
|
+
|
|
45335
46270
|
// server.ts
|
|
45336
46271
|
var DEFAULT_HTTP_PORT = 3012;
|
|
45337
46272
|
async function main() {
|
|
@@ -45351,15 +46286,19 @@ async function main() {
|
|
|
45351
46286
|
const httpPort = portArg ? parseInt(portArg.split("=")[1], 10) : DEFAULT_HTTP_PORT;
|
|
45352
46287
|
const hostnameArg = args.find((arg) => arg.startsWith("--hostname="));
|
|
45353
46288
|
const hostname = hostnameArg ? hostnameArg.split("=")[1] : "127.0.0.1";
|
|
46289
|
+
const authConfig = httpFlag ? loadAuthConfig2() : null;
|
|
46290
|
+
const authProvider = authConfig ? buildAuthProvider(authConfig) : void 0;
|
|
46291
|
+
const authMetadataRoute = authProvider ? resourceMetadataRoute(authProvider) : void 0;
|
|
45354
46292
|
const toolsClient = new ErpNextToolsClient(
|
|
45355
46293
|
categories ? { categories } : void 0
|
|
45356
46294
|
);
|
|
45357
46295
|
const server = new McpApp({
|
|
45358
46296
|
name: "mcp-erpnext",
|
|
45359
|
-
version: "2.
|
|
46297
|
+
version: "2.6.0",
|
|
45360
46298
|
maxConcurrent: 10,
|
|
45361
46299
|
backpressureStrategy: "queue",
|
|
45362
46300
|
validateSchema: true,
|
|
46301
|
+
auth: authProvider ? { provider: authProvider } : void 0,
|
|
45363
46302
|
logger: (msg) => console.error(`[mcp-erpnext] ${msg}`),
|
|
45364
46303
|
toolErrorMapper: (error2) => {
|
|
45365
46304
|
if (error2 instanceof FrappeAPIError) return error2.message;
|
|
@@ -45401,27 +46340,46 @@ async function main() {
|
|
|
45401
46340
|
console.error(
|
|
45402
46341
|
`[mcp-erpnext] Initialized \u2014 ${toolsClient.count} tools${categories ? ` (categories: ${categories.join(", ")})` : ""}`
|
|
45403
46342
|
);
|
|
46343
|
+
warmCache().catch((err) => {
|
|
46344
|
+
console.error("[mcp-erpnext] Cache warm failed (non-fatal):", err);
|
|
46345
|
+
});
|
|
45404
46346
|
if (httpFlag) {
|
|
45405
46347
|
const isLoopback = hostname === "127.0.0.1" || hostname === "::1" || hostname === "localhost";
|
|
45406
46348
|
if (!isLoopback) {
|
|
45407
46349
|
console.error(
|
|
45408
|
-
`[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
|
|
46350
|
+
`[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 configure auth via MCP_AUTH_TOKEN(S)/MCP_OAUTH_JWKS_URL.`
|
|
46351
|
+
);
|
|
46352
|
+
}
|
|
46353
|
+
onSignal3("SIGINT", () => {
|
|
46354
|
+
console.error("[mcp-erpnext] Shutting down...");
|
|
46355
|
+
exit3(0);
|
|
46356
|
+
});
|
|
46357
|
+
if (!authConfig) {
|
|
46358
|
+
console.error(
|
|
46359
|
+
"[mcp-erpnext] WARNING: No auth configured for HTTP mode. Set MCP_AUTH_TOKEN, MCP_AUTH_TOKENS, or MCP_OAUTH_JWKS_URL to restrict access."
|
|
45409
46360
|
);
|
|
45410
46361
|
}
|
|
45411
46362
|
await server.startHttp({
|
|
45412
46363
|
port: httpPort,
|
|
45413
46364
|
hostname,
|
|
45414
46365
|
cors: true,
|
|
46366
|
+
customRoutes: authMetadataRoute ? [authMetadataRoute] : void 0,
|
|
45415
46367
|
onListen: (info) => {
|
|
45416
46368
|
console.error(
|
|
45417
|
-
`[mcp-erpnext] HTTP server listening on http://${info.hostname}:${info.port}`
|
|
46369
|
+
`[mcp-erpnext] HTTP server listening${authConfig ? "" : " (unauthenticated)"} on http://${info.hostname}:${info.port}`
|
|
45418
46370
|
);
|
|
46371
|
+
if (authConfig) {
|
|
46372
|
+
const authMethods = [];
|
|
46373
|
+
if (authConfig.tokens.size > 0) {
|
|
46374
|
+
authMethods.push(`static tokens (${authConfig.tokens.size})`);
|
|
46375
|
+
}
|
|
46376
|
+
if (authConfig.jwksUrl) {
|
|
46377
|
+
authMethods.push(`OAuth JWT JWKS (${authConfig.jwksUrl})`);
|
|
46378
|
+
}
|
|
46379
|
+
console.error(`[mcp-erpnext] Auth: ${authMethods.join(", ")}`);
|
|
46380
|
+
}
|
|
45419
46381
|
}
|
|
45420
46382
|
});
|
|
45421
|
-
onSignal3("SIGINT", () => {
|
|
45422
|
-
console.error("[mcp-erpnext] Shutting down...");
|
|
45423
|
-
exit3(0);
|
|
45424
|
-
});
|
|
45425
46383
|
} else {
|
|
45426
46384
|
await server.start();
|
|
45427
46385
|
console.error("[mcp-erpnext] stdio mode ready");
|