@centry-digital/bukku-mcp 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/build/config/env.d.ts +1 -0
  2. package/build/config/env.d.ts.map +1 -0
  3. package/build/config/env.js +2 -1
  4. package/build/index.d.ts +1 -0
  5. package/build/index.d.ts.map +1 -0
  6. package/build/index.js +1813 -44
  7. package/build/tools/custom/account-tools.d.ts +2 -1
  8. package/build/tools/custom/account-tools.d.ts.map +1 -0
  9. package/build/tools/custom/account-tools.js +2 -2
  10. package/build/tools/custom/contact-archive.d.ts +2 -1
  11. package/build/tools/custom/contact-archive.d.ts.map +1 -0
  12. package/build/tools/custom/contact-archive.js +2 -2
  13. package/build/tools/custom/control-panel-archive.d.ts +2 -1
  14. package/build/tools/custom/control-panel-archive.d.ts.map +1 -0
  15. package/build/tools/custom/control-panel-archive.js +2 -2
  16. package/build/tools/custom/file-upload.d.ts +2 -1
  17. package/build/tools/custom/file-upload.d.ts.map +1 -0
  18. package/build/tools/custom/file-upload.js +2 -2
  19. package/build/tools/custom/journal-entry-tools.d.ts +2 -1
  20. package/build/tools/custom/journal-entry-tools.d.ts.map +1 -0
  21. package/build/tools/custom/journal-entry-tools.js +2 -3
  22. package/build/tools/custom/location-tools.d.ts +2 -1
  23. package/build/tools/custom/location-tools.d.ts.map +1 -0
  24. package/build/tools/custom/location-tools.js +2 -2
  25. package/build/tools/custom/product-archive.d.ts +2 -1
  26. package/build/tools/custom/product-archive.d.ts.map +1 -0
  27. package/build/tools/custom/product-archive.js +2 -2
  28. package/build/tools/custom/reference-data.d.ts +3 -2
  29. package/build/tools/custom/reference-data.d.ts.map +1 -0
  30. package/build/tools/custom/reference-data.js +2 -2
  31. package/build/tools/factory.d.ts +2 -2
  32. package/build/tools/factory.d.ts.map +1 -0
  33. package/build/tools/factory.js +2 -2
  34. package/build/tools/registry.d.ts +2 -1
  35. package/build/tools/registry.d.ts.map +1 -0
  36. package/build/tools/registry.js +3 -41
  37. package/package.json +9 -22
  38. package/LICENSE +0 -21
  39. package/README.md +0 -269
  40. package/build/client/bukku-client.d.ts +0 -62
  41. package/build/client/bukku-client.js +0 -195
  42. package/build/errors/transform.d.ts +0 -14
  43. package/build/errors/transform.js +0 -141
  44. package/build/errors/transform.test.d.ts +0 -1
  45. package/build/errors/transform.test.js +0 -101
  46. package/build/tools/cache/reference-cache.d.ts +0 -42
  47. package/build/tools/cache/reference-cache.js +0 -63
  48. package/build/tools/configs/account.d.ts +0 -17
  49. package/build/tools/configs/account.js +0 -28
  50. package/build/tools/configs/bank-money-in.d.ts +0 -10
  51. package/build/tools/configs/bank-money-in.js +0 -22
  52. package/build/tools/configs/bank-money-out.d.ts +0 -10
  53. package/build/tools/configs/bank-money-out.js +0 -22
  54. package/build/tools/configs/bank-transfer.d.ts +0 -11
  55. package/build/tools/configs/bank-transfer.js +0 -23
  56. package/build/tools/configs/contact-group.d.ts +0 -11
  57. package/build/tools/configs/contact-group.js +0 -19
  58. package/build/tools/configs/contact.d.ts +0 -14
  59. package/build/tools/configs/contact.js +0 -25
  60. package/build/tools/configs/delivery-order.d.ts +0 -8
  61. package/build/tools/configs/delivery-order.js +0 -20
  62. package/build/tools/configs/file.d.ts +0 -18
  63. package/build/tools/configs/file.js +0 -26
  64. package/build/tools/configs/goods-received-note.d.ts +0 -8
  65. package/build/tools/configs/goods-received-note.js +0 -20
  66. package/build/tools/configs/journal-entry.d.ts +0 -14
  67. package/build/tools/configs/journal-entry.js +0 -26
  68. package/build/tools/configs/location.d.ts +0 -20
  69. package/build/tools/configs/location.js +0 -28
  70. package/build/tools/configs/product-bundle.d.ts +0 -18
  71. package/build/tools/configs/product-bundle.js +0 -29
  72. package/build/tools/configs/product-group.d.ts +0 -14
  73. package/build/tools/configs/product-group.js +0 -22
  74. package/build/tools/configs/product.d.ts +0 -24
  75. package/build/tools/configs/product.js +0 -35
  76. package/build/tools/configs/purchase-bill.d.ts +0 -9
  77. package/build/tools/configs/purchase-bill.js +0 -21
  78. package/build/tools/configs/purchase-credit-note.d.ts +0 -8
  79. package/build/tools/configs/purchase-credit-note.js +0 -20
  80. package/build/tools/configs/purchase-order.d.ts +0 -8
  81. package/build/tools/configs/purchase-order.js +0 -20
  82. package/build/tools/configs/purchase-payment.d.ts +0 -8
  83. package/build/tools/configs/purchase-payment.js +0 -20
  84. package/build/tools/configs/purchase-refund.d.ts +0 -8
  85. package/build/tools/configs/purchase-refund.js +0 -20
  86. package/build/tools/configs/sales-credit-note.d.ts +0 -8
  87. package/build/tools/configs/sales-credit-note.js +0 -20
  88. package/build/tools/configs/sales-invoice.d.ts +0 -8
  89. package/build/tools/configs/sales-invoice.js +0 -20
  90. package/build/tools/configs/sales-order.d.ts +0 -8
  91. package/build/tools/configs/sales-order.js +0 -20
  92. package/build/tools/configs/sales-payment.d.ts +0 -8
  93. package/build/tools/configs/sales-payment.js +0 -20
  94. package/build/tools/configs/sales-quote.d.ts +0 -8
  95. package/build/tools/configs/sales-quote.js +0 -20
  96. package/build/tools/configs/sales-refund.d.ts +0 -8
  97. package/build/tools/configs/sales-refund.js +0 -20
  98. package/build/tools/configs/tag-group.d.ts +0 -11
  99. package/build/tools/configs/tag-group.js +0 -22
  100. package/build/tools/configs/tag.d.ts +0 -11
  101. package/build/tools/configs/tag.js +0 -22
  102. package/build/tools/validation/double-entry.d.ts +0 -46
  103. package/build/tools/validation/double-entry.js +0 -66
  104. package/build/types/api-responses.d.ts +0 -21
  105. package/build/types/api-responses.js +0 -6
  106. package/build/types/bukku.d.ts +0 -93
  107. package/build/types/bukku.js +0 -11
  108. package/build/utils/logger.d.ts +0 -6
  109. package/build/utils/logger.js +0 -8
package/build/index.js CHANGED
@@ -1,52 +1,1821 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Bukku MCP Server Entry Point
4
- *
5
- * Startup sequence:
6
- * 1. Validate environment variables (BUKKU_API_TOKEN, BUKKU_COMPANY_SUBDOMAIN)
7
- * 2. Create BukkuClient with validated env
8
- * 3. Validate API token via lightweight API call
9
- * 4. Create MCP server and register tools
10
- * 5. Connect via stdio transport
11
- * 6. Log startup to stderr
12
- */
2
+ #!/usr/bin/env node
3
+
4
+ // packages/mcp/src/index.ts
13
5
  import { readFileSync } from "node:fs";
14
6
  import { fileURLToPath } from "node:url";
15
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
- import { validateEnv } from "./config/env.js";
18
- import { BukkuClient } from "./client/bukku-client.js";
19
- import { registerAllTools } from "./tools/registry.js";
20
- import { log } from "./utils/logger.js";
21
- // Read package.json version dynamically
22
- const packageJsonPath = new URL("../package.json", import.meta.url);
23
- const pkg = JSON.parse(readFileSync(fileURLToPath(packageJsonPath), "utf-8"));
24
- /**
25
- * Main entry point.
26
- * Exits process if configuration or authentication fails.
27
- */
28
- async function main() {
29
- // Step 1: Validate environment variables
30
- const env = validateEnv();
31
- // Step 2: Create authenticated Bukku API client
32
- const client = new BukkuClient(env);
33
- // Step 3: Validate API token
34
- await client.validateToken();
35
- // Step 4: Create MCP server
36
- const server = new McpServer({
37
- name: "bukku",
38
- version: pkg.version,
9
+
10
+ // packages/mcp/src/config/env.ts
11
+ import { z } from "zod";
12
+
13
+ // packages/core/build/utils/logger.js
14
+ function createLogger(prefix) {
15
+ return (message, ...args) => {
16
+ console.error(`[${prefix}] ${message}`, ...args);
17
+ };
18
+ }
19
+
20
+ // packages/core/build/client/bukku-client.js
21
+ import { readFile } from "node:fs/promises";
22
+ import { basename, extname } from "node:path";
23
+ var log = createLogger("bukku");
24
+ var BukkuClient = class {
25
+ baseUrl = "https://api.bukku.my";
26
+ token;
27
+ subdomain;
28
+ constructor(config) {
29
+ this.token = config.apiToken;
30
+ this.subdomain = config.companySubdomain;
31
+ }
32
+ /**
33
+ * Build headers for all requests.
34
+ * CRITICAL: Never log the actual token value - use "Bearer ***" for debugging.
35
+ */
36
+ getHeaders(includeContentType = false) {
37
+ const headers = {
38
+ Authorization: `Bearer ${this.token}`,
39
+ "Company-Subdomain": this.subdomain,
40
+ Accept: "application/json"
41
+ };
42
+ if (includeContentType) {
43
+ headers["Content-Type"] = "application/json";
44
+ }
45
+ return headers;
46
+ }
47
+ /**
48
+ * Map file extensions to MIME types for common file types.
49
+ * Returns null for unknown extensions.
50
+ */
51
+ getMimeType(extension) {
52
+ const mimeMap = {
53
+ ".pdf": "application/pdf",
54
+ ".png": "image/png",
55
+ ".jpg": "image/jpeg",
56
+ ".jpeg": "image/jpeg",
57
+ ".gif": "image/gif",
58
+ ".txt": "text/plain",
59
+ ".csv": "text/csv",
60
+ ".json": "application/json",
61
+ ".xml": "application/xml",
62
+ ".zip": "application/zip"
63
+ };
64
+ return mimeMap[extension.toLowerCase()] || null;
65
+ }
66
+ /**
67
+ * Build URL with query parameters.
68
+ */
69
+ buildUrl(path, params) {
70
+ const url = new URL(path, this.baseUrl);
71
+ if (params) {
72
+ for (const [key, value] of Object.entries(params)) {
73
+ if (value !== void 0) {
74
+ url.searchParams.append(key, String(value));
75
+ }
76
+ }
77
+ }
78
+ return url.toString();
79
+ }
80
+ /**
81
+ * GET request with optional query parameters.
82
+ */
83
+ async get(path, params) {
84
+ const url = this.buildUrl(path, params);
85
+ const response = await fetch(url, {
86
+ method: "GET",
87
+ headers: this.getHeaders()
39
88
  });
40
- // Step 5: Register all tools
41
- const toolCount = registerAllTools(server, client);
42
- // Step 6: Connect via stdio transport
43
- const transport = new StdioServerTransport();
44
- await server.connect(transport);
45
- // Step 7: Log startup to stderr
46
- log(`Bukku MCP server started (${toolCount} tools registered)`);
47
- }
48
- // Execute main and handle fatal errors
49
- main().catch((error) => {
50
- log("Fatal error during startup:", error);
89
+ if (!response.ok) {
90
+ throw response;
91
+ }
92
+ return response.json();
93
+ }
94
+ /**
95
+ * POST request with JSON body.
96
+ */
97
+ async post(path, body) {
98
+ const url = this.buildUrl(path);
99
+ const response = await fetch(url, {
100
+ method: "POST",
101
+ headers: this.getHeaders(true),
102
+ body: JSON.stringify(body)
103
+ });
104
+ if (!response.ok) {
105
+ throw response;
106
+ }
107
+ return response.json();
108
+ }
109
+ /**
110
+ * PUT request with JSON body.
111
+ */
112
+ async put(path, body) {
113
+ const url = this.buildUrl(path);
114
+ const response = await fetch(url, {
115
+ method: "PUT",
116
+ headers: this.getHeaders(true),
117
+ body: JSON.stringify(body)
118
+ });
119
+ if (!response.ok) {
120
+ throw response;
121
+ }
122
+ return response.json();
123
+ }
124
+ /**
125
+ * PATCH request with JSON body (for status updates).
126
+ */
127
+ async patch(path, body) {
128
+ const url = this.buildUrl(path);
129
+ const response = await fetch(url, {
130
+ method: "PATCH",
131
+ headers: this.getHeaders(true),
132
+ body: JSON.stringify(body)
133
+ });
134
+ if (!response.ok) {
135
+ throw response;
136
+ }
137
+ return response.json();
138
+ }
139
+ /**
140
+ * DELETE request.
141
+ */
142
+ async delete(path) {
143
+ const url = this.buildUrl(path);
144
+ const response = await fetch(url, {
145
+ method: "DELETE",
146
+ headers: this.getHeaders()
147
+ });
148
+ if (!response.ok) {
149
+ throw response;
150
+ }
151
+ }
152
+ /**
153
+ * POST multipart/form-data request for file uploads.
154
+ * Reads file from disk and sends as multipart form data.
155
+ * CRITICAL: Does NOT manually set Content-Type - fetch sets it automatically with boundary.
156
+ *
157
+ * @param path - API endpoint path
158
+ * @param filePath - Absolute path to file on disk
159
+ * @returns API response
160
+ */
161
+ async postMultipart(path, filePath) {
162
+ const url = this.buildUrl(path);
163
+ const fileBuffer = await readFile(filePath);
164
+ const fileName = basename(filePath);
165
+ const fileExtension = extname(filePath);
166
+ const mimeType = this.getMimeType(fileExtension) || "application/octet-stream";
167
+ const file = new File([fileBuffer], fileName, { type: mimeType });
168
+ const form = new FormData();
169
+ form.append("file", file);
170
+ const headers = this.getHeaders(false);
171
+ const response = await fetch(url, {
172
+ method: "POST",
173
+ headers,
174
+ body: form
175
+ });
176
+ if (!response.ok) {
177
+ throw response;
178
+ }
179
+ return response.json();
180
+ }
181
+ /**
182
+ * Validate token on startup by making a lightweight API call.
183
+ * Uses GET /contacts with page_size=1 to verify authentication.
184
+ * Exits process if token is invalid (401).
185
+ */
186
+ async validateToken() {
187
+ try {
188
+ await this.get("/contacts", { page_size: 1 });
189
+ log("Token validated successfully");
190
+ } catch (error) {
191
+ if (error instanceof Response && error.status === 401) {
192
+ log("Authentication Error\n");
193
+ log("The provided BUKKU_API_TOKEN is invalid or expired.\n");
194
+ log("Please check:");
195
+ log(" 1. Token is copied correctly (no extra spaces)");
196
+ log(" 2. API Access is enabled in Bukku Control Panel -> Integrations");
197
+ log(" 3. Token has not been revoked or regenerated\n");
198
+ process.exit(1);
199
+ }
200
+ log("Failed to validate token:", error);
201
+ process.exit(1);
202
+ }
203
+ }
204
+ };
205
+
206
+ // packages/core/build/errors/transform.js
207
+ function transformHttpError(status, body, operation) {
208
+ if (status === 401) {
209
+ return {
210
+ isError: true,
211
+ content: [
212
+ {
213
+ type: "text",
214
+ text: `Bukku authentication failed for "${operation}". The BUKKU_API_TOKEN environment variable is either missing or invalid. Please check your token and restart the server with the correct credentials.`
215
+ }
216
+ ]
217
+ };
218
+ }
219
+ if (status === 403) {
220
+ return {
221
+ isError: true,
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: `You don't have permission to "${operation}". Please check your Bukku account permissions and ensure you have access to this resource.`
226
+ }
227
+ ]
228
+ };
229
+ }
230
+ if (status === 404) {
231
+ return {
232
+ isError: true,
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: `I couldn't find that item when trying to "${operation}". Try listing the available items first to see what's accessible.`
237
+ }
238
+ ]
239
+ };
240
+ }
241
+ if (status === 400 || status === 422) {
242
+ const parsedBody = body;
243
+ const errors = parsedBody?.errors;
244
+ if (errors && typeof errors === "object") {
245
+ const errorMessages = Object.entries(errors).map(([field, messages]) => ` - ${field}: ${messages.join(", ")}`).join("\n");
246
+ return {
247
+ isError: true,
248
+ content: [
249
+ {
250
+ type: "text",
251
+ text: `Validation failed for "${operation}":
252
+ ${errorMessages}
253
+
254
+ Please fix these issues and try again.`
255
+ }
256
+ ]
257
+ };
258
+ } else {
259
+ const message = parsedBody?.message || "Invalid request";
260
+ return {
261
+ isError: true,
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: `${message} when trying to "${operation}". Please check your input and try again.`
266
+ }
267
+ ]
268
+ };
269
+ }
270
+ }
271
+ if (status === 503) {
272
+ return {
273
+ isError: true,
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: `Bukku is temporarily unavailable while trying to "${operation}". Please try again in a few moments.`
278
+ }
279
+ ]
280
+ };
281
+ }
282
+ if (status !== null && status >= 500) {
283
+ const parsedBody = body;
284
+ const bodyText = body ? `
285
+
286
+ Server response: ${JSON.stringify(parsedBody, null, 2)}` : "";
287
+ return {
288
+ isError: true,
289
+ content: [
290
+ {
291
+ type: "text",
292
+ text: `An unexpected error occurred on Bukku's servers while trying to "${operation}". Please try again, and if the issue persists, contact Bukku support.${bodyText}`
293
+ }
294
+ ]
295
+ };
296
+ }
297
+ return {
298
+ isError: true,
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: `An error occurred while trying to "${operation}". Please check your request and try again.`
303
+ }
304
+ ]
305
+ };
306
+ }
307
+ function transformNetworkError(error, operation) {
308
+ const errorMessage = error instanceof Error ? error.message : String(error);
309
+ if (errorMessage.includes("fetch") || errorMessage.includes("connect") || errorMessage.includes("network") || error instanceof TypeError) {
310
+ return {
311
+ isError: true,
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: `Couldn't connect to Bukku while trying to "${operation}". Please check your internet connection and ensure the Bukku API is accessible.`
316
+ }
317
+ ]
318
+ };
319
+ }
320
+ return {
321
+ isError: true,
322
+ content: [
323
+ {
324
+ type: "text",
325
+ text: `An unexpected error occurred while trying to "${operation}": ${errorMessage}. Please try again.`
326
+ }
327
+ ]
328
+ };
329
+ }
330
+
331
+ // packages/core/build/validation/double-entry.js
332
+ function validateDoubleEntry(lines, minLines = 2) {
333
+ if (lines.length < minLines) {
334
+ const lineWord = lines.length === 1 ? "line" : "lines";
335
+ return {
336
+ valid: false,
337
+ error: `Journal entries require at least ${minLines} line items (minimum one debit and one credit). This entry has ${lines.length} ${lineWord}.`
338
+ };
339
+ }
340
+ let totalDebits = 0;
341
+ let totalCredits = 0;
342
+ for (const line of lines) {
343
+ totalDebits += line.debit_amount || 0;
344
+ totalCredits += line.credit_amount || 0;
345
+ }
346
+ const difference = Math.abs(totalDebits - totalCredits);
347
+ const EPSILON = 0.01;
348
+ if (difference >= EPSILON) {
349
+ return {
350
+ valid: false,
351
+ error: `Journal entry is unbalanced. Total debits: ${totalDebits.toFixed(2)}, Total credits: ${totalCredits.toFixed(2)}. Difference: ${difference.toFixed(2)}. Debits must equal credits in double-entry accounting.`
352
+ };
353
+ }
354
+ return { valid: true };
355
+ }
356
+
357
+ // packages/core/build/cache/reference-cache.js
358
+ var ReferenceDataCache = class {
359
+ cache = /* @__PURE__ */ new Map();
360
+ ttlMs;
361
+ /**
362
+ * Create a new reference data cache
363
+ * @param ttlMs Time-to-live in milliseconds (default: 5 minutes)
364
+ */
365
+ constructor(ttlMs = 5 * 60 * 1e3) {
366
+ this.ttlMs = ttlMs;
367
+ }
368
+ /**
369
+ * Get cached data by key
370
+ * @param key Cache key (reference data type name)
371
+ * @returns Cached data or undefined if not found or expired
372
+ */
373
+ get(key) {
374
+ const entry = this.cache.get(key);
375
+ if (!entry)
376
+ return void 0;
377
+ if (Date.now() > entry.expiresAt) {
378
+ this.cache.delete(key);
379
+ return void 0;
380
+ }
381
+ return entry.data;
382
+ }
383
+ /**
384
+ * Store data in cache with TTL
385
+ * @param key Cache key (reference data type name)
386
+ * @param data Data to cache
387
+ */
388
+ set(key, data) {
389
+ this.cache.set(key, {
390
+ data,
391
+ expiresAt: Date.now() + this.ttlMs
392
+ });
393
+ }
394
+ /**
395
+ * Invalidate a specific cache entry
396
+ * @param key Cache key to invalidate
397
+ */
398
+ invalidate(key) {
399
+ this.cache.delete(key);
400
+ }
401
+ /**
402
+ * Clear all cached entries
403
+ */
404
+ clear() {
405
+ this.cache.clear();
406
+ }
407
+ };
408
+
409
+ // packages/core/build/entities/sales-invoice.js
410
+ var salesInvoiceConfig = {
411
+ entity: "sales-invoice",
412
+ apiBasePath: "/sales/invoices",
413
+ singularKey: "transaction",
414
+ pluralKey: "transactions",
415
+ description: "sales invoice",
416
+ operations: ["list", "get", "create", "update", "delete"],
417
+ hasStatusUpdate: true,
418
+ listFilters: ["contact_id", "email_status", "transfer_status", "payment_status"],
419
+ businessRules: {
420
+ delete: "Only draft and void invoices can be deleted. Ready or pending approval invoices cannot be deleted \u2014 use update-sales-invoice-status to void a ready invoice instead.",
421
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void invoice is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
422
+ },
423
+ cliGroup: "sales"
424
+ };
425
+
426
+ // packages/core/build/entities/sales-quote.js
427
+ var salesQuoteConfig = {
428
+ entity: "sales-quote",
429
+ apiBasePath: "/sales/quotes",
430
+ singularKey: "transaction",
431
+ pluralKey: "transactions",
432
+ description: "sales quote",
433
+ operations: ["list", "get", "create", "update", "delete"],
434
+ hasStatusUpdate: true,
435
+ listFilters: ["contact_id", "email_status", "transfer_status"],
436
+ businessRules: {
437
+ delete: "Only draft and void quotes can be deleted. Ready or pending approval quotes cannot be deleted \u2014 use update-sales-quote-status to void a ready quote instead.",
438
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void quote is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
439
+ },
440
+ cliGroup: "sales"
441
+ };
442
+
443
+ // packages/core/build/entities/sales-order.js
444
+ var salesOrderConfig = {
445
+ entity: "sales-order",
446
+ apiBasePath: "/sales/orders",
447
+ singularKey: "transaction",
448
+ pluralKey: "transactions",
449
+ description: "sales order",
450
+ operations: ["list", "get", "create", "update", "delete"],
451
+ hasStatusUpdate: true,
452
+ listFilters: ["contact_id", "email_status", "transfer_status"],
453
+ businessRules: {
454
+ delete: "Only draft and void orders can be deleted. Ready or pending approval orders cannot be deleted \u2014 use update-sales-order-status to void a ready order instead.",
455
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
456
+ },
457
+ cliGroup: "sales"
458
+ };
459
+
460
+ // packages/core/build/entities/sales-credit-note.js
461
+ var salesCreditNoteConfig = {
462
+ entity: "sales-credit-note",
463
+ apiBasePath: "/sales/credit_notes",
464
+ singularKey: "transaction",
465
+ pluralKey: "transactions",
466
+ description: "sales credit note",
467
+ operations: ["list", "get", "create", "update", "delete"],
468
+ hasStatusUpdate: true,
469
+ listFilters: ["contact_id", "email_status"],
470
+ businessRules: {
471
+ delete: "Only draft and void credit notes can be deleted. Ready or pending approval credit notes cannot be deleted \u2014 use update-sales-credit-note-status to void a ready credit note instead.",
472
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void credit note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
473
+ },
474
+ cliGroup: "sales"
475
+ };
476
+
477
+ // packages/core/build/entities/sales-payment.js
478
+ var salesPaymentConfig = {
479
+ entity: "sales-payment",
480
+ apiBasePath: "/sales/payments",
481
+ singularKey: "transaction",
482
+ pluralKey: "transactions",
483
+ description: "sales payment",
484
+ operations: ["list", "get", "create", "update", "delete"],
485
+ hasStatusUpdate: true,
486
+ listFilters: ["contact_id", "payment_mode"],
487
+ businessRules: {
488
+ delete: "Only draft and void payments can be deleted. Ready or pending approval payments cannot be deleted \u2014 use update-sales-payment-status to void a ready payment instead.",
489
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void payment is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
490
+ },
491
+ cliGroup: "sales"
492
+ };
493
+
494
+ // packages/core/build/entities/sales-refund.js
495
+ var salesRefundConfig = {
496
+ entity: "sales-refund",
497
+ apiBasePath: "/sales/refunds",
498
+ singularKey: "transaction",
499
+ pluralKey: "transactions",
500
+ description: "sales refund",
501
+ operations: ["list", "get", "create", "update", "delete"],
502
+ hasStatusUpdate: true,
503
+ listFilters: ["contact_id"],
504
+ businessRules: {
505
+ delete: "Only draft and void refunds can be deleted. Ready or pending approval refunds cannot be deleted \u2014 use update-sales-refund-status to void a ready refund instead.",
506
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void refund is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
507
+ },
508
+ cliGroup: "sales"
509
+ };
510
+
511
+ // packages/core/build/entities/delivery-order.js
512
+ var deliveryOrderConfig = {
513
+ entity: "delivery-order",
514
+ apiBasePath: "/sales/delivery_orders",
515
+ singularKey: "transaction",
516
+ pluralKey: "transactions",
517
+ description: "delivery order",
518
+ operations: ["list", "get", "create", "update", "delete"],
519
+ hasStatusUpdate: true,
520
+ listFilters: ["contact_id", "email_status", "transfer_status"],
521
+ businessRules: {
522
+ delete: "Only draft and void delivery orders can be deleted. Ready or pending approval delivery orders cannot be deleted \u2014 use update-delivery-order-status to void a ready delivery order instead.",
523
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void delivery order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
524
+ },
525
+ cliGroup: "sales"
526
+ };
527
+
528
+ // packages/core/build/entities/purchase-order.js
529
+ var purchaseOrderConfig = {
530
+ entity: "purchase-order",
531
+ apiBasePath: "/purchases/orders",
532
+ singularKey: "transaction",
533
+ pluralKey: "transactions",
534
+ description: "purchase order",
535
+ operations: ["list", "get", "create", "update", "delete"],
536
+ hasStatusUpdate: true,
537
+ listFilters: ["contact_id", "email_status", "transfer_status"],
538
+ businessRules: {
539
+ delete: "Only draft and void purchase orders can be deleted. Ready or pending approval purchase orders cannot be deleted \u2014 use update-purchase-order-status to void a ready purchase order instead.",
540
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void purchase order is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
541
+ },
542
+ cliGroup: "purchases"
543
+ };
544
+
545
+ // packages/core/build/entities/purchase-bill.js
546
+ var purchaseBillConfig = {
547
+ entity: "purchase-bill",
548
+ apiBasePath: "/purchases/bills",
549
+ singularKey: "transaction",
550
+ pluralKey: "transactions",
551
+ description: "purchase bill",
552
+ operations: ["list", "get", "create", "update", "delete"],
553
+ hasStatusUpdate: true,
554
+ listFilters: ["contact_id", "payment_status", "payment_mode"],
555
+ businessRules: {
556
+ delete: "Only draft and void bills can be deleted. Ready or pending approval bills cannot be deleted \u2014 use update-purchase-bill-status to void a ready bill instead.",
557
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void bill is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
558
+ },
559
+ cliGroup: "purchases"
560
+ };
561
+
562
+ // packages/core/build/entities/purchase-credit-note.js
563
+ var purchaseCreditNoteConfig = {
564
+ entity: "purchase-credit-note",
565
+ apiBasePath: "/purchases/credit_notes",
566
+ singularKey: "transaction",
567
+ pluralKey: "transactions",
568
+ description: "purchase credit note",
569
+ operations: ["list", "get", "create", "update", "delete"],
570
+ hasStatusUpdate: true,
571
+ listFilters: ["contact_id", "payment_status"],
572
+ businessRules: {
573
+ delete: "Only draft and void credit notes can be deleted. Ready or pending approval credit notes cannot be deleted \u2014 use update-purchase-credit-note-status to void a ready credit note instead.",
574
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void credit note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
575
+ },
576
+ cliGroup: "purchases"
577
+ };
578
+
579
+ // packages/core/build/entities/purchase-payment.js
580
+ var purchasePaymentConfig = {
581
+ entity: "purchase-payment",
582
+ apiBasePath: "/purchases/payments",
583
+ singularKey: "transaction",
584
+ pluralKey: "transactions",
585
+ description: "purchase payment",
586
+ operations: ["list", "get", "create", "update", "delete"],
587
+ hasStatusUpdate: true,
588
+ listFilters: ["contact_id", "email_status", "payment_status", "account_id"],
589
+ businessRules: {
590
+ delete: "Only draft and void payments can be deleted. Ready or pending approval payments cannot be deleted \u2014 use update-purchase-payment-status to void a ready payment instead.",
591
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void payment is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
592
+ },
593
+ cliGroup: "purchases"
594
+ };
595
+
596
+ // packages/core/build/entities/purchase-refund.js
597
+ var purchaseRefundConfig = {
598
+ entity: "purchase-refund",
599
+ apiBasePath: "/purchases/refunds",
600
+ singularKey: "transaction",
601
+ pluralKey: "transactions",
602
+ description: "purchase refund",
603
+ operations: ["list", "get", "create", "update", "delete"],
604
+ hasStatusUpdate: true,
605
+ listFilters: ["contact_id", "email_status", "payment_status", "account_id"],
606
+ businessRules: {
607
+ delete: "Only draft and void refunds can be deleted. Ready or pending approval refunds cannot be deleted \u2014 use update-purchase-refund-status to void a ready refund instead.",
608
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void refund is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
609
+ },
610
+ cliGroup: "purchases"
611
+ };
612
+
613
+ // packages/core/build/entities/goods-received-note.js
614
+ var goodsReceivedNoteConfig = {
615
+ entity: "goods-received-note",
616
+ apiBasePath: "/purchases/goods_received_notes",
617
+ singularKey: "transaction",
618
+ pluralKey: "transactions",
619
+ description: "goods received note",
620
+ operations: ["list", "get", "create", "update", "delete"],
621
+ hasStatusUpdate: true,
622
+ listFilters: ["contact_id", "email_status", "transfer_status"],
623
+ businessRules: {
624
+ delete: "Only draft and void goods received notes can be deleted. Ready or pending approval goods received notes cannot be deleted \u2014 use update-goods-received-note-status to void a ready goods received note instead.",
625
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void goods received note is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
626
+ },
627
+ cliGroup: "purchases"
628
+ };
629
+
630
+ // packages/core/build/entities/bank-money-in.js
631
+ var bankMoneyInConfig = {
632
+ entity: "bank-money-in",
633
+ apiBasePath: "/banking/incomes",
634
+ singularKey: "transaction",
635
+ pluralKey: "transactions",
636
+ description: "bank money in transaction",
637
+ operations: ["list", "get", "create", "update", "delete"],
638
+ hasStatusUpdate: true,
639
+ listFilters: ["contact_id", "account_id", "email_status"],
640
+ businessRules: {
641
+ delete: "Only draft and void money in transactions can be deleted. Ready or pending approval transactions cannot be deleted \u2014 use update-bank-money-in-status to void a ready transaction instead.",
642
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transaction is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
643
+ },
644
+ cliGroup: "banking"
645
+ };
646
+
647
+ // packages/core/build/entities/bank-money-out.js
648
+ var bankMoneyOutConfig = {
649
+ entity: "bank-money-out",
650
+ apiBasePath: "/banking/expenses",
651
+ singularKey: "transaction",
652
+ pluralKey: "transactions",
653
+ description: "bank money out transaction",
654
+ operations: ["list", "get", "create", "update", "delete"],
655
+ hasStatusUpdate: true,
656
+ listFilters: ["contact_id", "account_id", "email_status"],
657
+ businessRules: {
658
+ delete: "Only draft and void money out transactions can be deleted. Ready or pending approval transactions cannot be deleted \u2014 use update-bank-money-out-status to void a ready transaction instead.",
659
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transaction is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
660
+ },
661
+ cliGroup: "banking"
662
+ };
663
+
664
+ // packages/core/build/entities/bank-transfer.js
665
+ var bankTransferConfig = {
666
+ entity: "bank-transfer",
667
+ apiBasePath: "/banking/transfers",
668
+ singularKey: "transaction",
669
+ pluralKey: "transactions",
670
+ description: "bank transfer",
671
+ operations: ["list", "get", "create", "update", "delete"],
672
+ hasStatusUpdate: true,
673
+ listFilters: ["account_id"],
674
+ businessRules: {
675
+ delete: "Only draft and void transfers can be deleted. Ready or pending approval transfers cannot be deleted \u2014 use update-bank-transfer-status to void a ready transfer instead.",
676
+ statusTransitions: "Valid transitions: draft -> pending_approval, draft -> ready, pending_approval -> ready, ready -> void. A void transfer is final and cannot be changed. There is no way to revert from ready, pending_approval, or void back to draft."
677
+ },
678
+ cliGroup: "banking"
679
+ };
680
+
681
+ // packages/core/build/entities/contact.js
682
+ var contactConfig = {
683
+ entity: "contact",
684
+ apiBasePath: "/contacts",
685
+ singularKey: "contact",
686
+ pluralKey: "contacts",
687
+ description: "contact",
688
+ operations: ["list", "get", "create", "update", "delete"],
689
+ hasStatusUpdate: false,
690
+ listFilters: ["group_id", "status", "type", "is_myinvois_ready"],
691
+ businessRules: {
692
+ delete: "Only contacts with no linked transactions can be deleted. Archive instead if the contact has transaction history."
693
+ },
694
+ cliGroup: "contacts"
695
+ };
696
+
697
+ // packages/core/build/entities/contact-group.js
698
+ var contactGroupConfig = {
699
+ entity: "contact-group",
700
+ apiBasePath: "/contacts/groups",
701
+ singularKey: "group",
702
+ pluralKey: "groups",
703
+ description: "contact group",
704
+ operations: ["list", "get", "create", "update", "delete"],
705
+ hasStatusUpdate: false,
706
+ listFilters: [],
707
+ cliGroup: "contacts"
708
+ };
709
+
710
+ // packages/core/build/entities/product.js
711
+ var productConfig = {
712
+ entity: "product",
713
+ apiBasePath: "/products",
714
+ singularKey: "product",
715
+ pluralKey: "products",
716
+ description: "product. Use list-tax-codes to find valid tax code IDs for sale_tax_code_id and purchase_tax_code_id. Use list-accounts to find valid account IDs for sale_account_id, purchase_account_id, and inventory_account_id. Use list-product-groups to find group IDs for group_ids.",
717
+ operations: ["list", "get", "create", "update", "delete"],
718
+ hasStatusUpdate: false,
719
+ listFilters: ["search", "stock_level", "mode", "type", "include_archived"],
720
+ businessRules: {
721
+ delete: "Only products that are not used in any transactions can be deleted. Archive instead if the product has transaction history."
722
+ },
723
+ cliGroup: "products"
724
+ };
725
+
726
+ // packages/core/build/entities/product-bundle.js
727
+ var productBundleConfig = {
728
+ entity: "product-bundle",
729
+ apiBasePath: "/products/bundles",
730
+ singularKey: "bundle",
731
+ pluralKey: "bundles",
732
+ description: "product bundle. Bundles aggregate multiple products with optional discounts. To list bundles, use list-products with type=bundle. Use list-products to find product IDs for bundle items.",
733
+ operations: ["get", "create", "update", "delete"],
734
+ hasStatusUpdate: false,
735
+ listFilters: [],
736
+ businessRules: {
737
+ delete: "Only bundles that are not used in any transactions can be deleted."
738
+ },
739
+ cliGroup: "products"
740
+ };
741
+
742
+ // packages/core/build/entities/product-group.js
743
+ var productGroupConfig = {
744
+ entity: "product-group",
745
+ apiBasePath: "/products/groups",
746
+ singularKey: "group",
747
+ pluralKey: "groups",
748
+ description: "product group. Groups organize products into categories. Use list-products to find product IDs for the product_ids array.",
749
+ operations: ["list", "get", "create", "update", "delete"],
750
+ hasStatusUpdate: false,
751
+ listFilters: [],
752
+ cliGroup: "products"
753
+ };
754
+
755
+ // packages/core/build/entities/journal-entry.js
756
+ var journalEntryConfig = {
757
+ entity: "journal-entry",
758
+ apiBasePath: "/journal_entries",
759
+ singularKey: "transaction",
760
+ pluralKey: "transactions",
761
+ description: "journal entry",
762
+ operations: ["list", "get", "delete"],
763
+ hasStatusUpdate: true,
764
+ listFilters: [],
765
+ businessRules: {
766
+ delete: "Only draft and void journal entries can be deleted. Ready journal entries cannot be deleted -- use update-journal-entry-status to void a ready entry instead.",
767
+ statusTransitions: "Valid transitions: draft -> ready, ready -> void. A void entry is final and cannot be changed."
768
+ },
769
+ cliGroup: "accounting"
770
+ };
771
+
772
+ // packages/core/build/entities/account.js
773
+ var accountConfig = {
774
+ entity: "account",
775
+ apiBasePath: "/accounts",
776
+ singularKey: "account",
777
+ pluralKey: "accounts",
778
+ description: "account",
779
+ operations: ["get", "create", "update", "delete"],
780
+ hasStatusUpdate: false,
781
+ listFilters: [],
782
+ businessRules: {
783
+ delete: "Only accounts that are not assigned to locked system type, have no children, and are not used in transactions can be deleted. Use archive-account instead if the account has transaction history."
784
+ },
785
+ cliGroup: "accounting"
786
+ };
787
+
788
+ // packages/core/build/entities/file.js
789
+ var fileConfig = {
790
+ entity: "file",
791
+ apiBasePath: "/files",
792
+ singularKey: "file",
793
+ pluralKey: "files",
794
+ description: "file. Files are typically attached to sales and purchase transactions using file_ids arrays. Use upload-file to add new files.",
795
+ operations: ["list", "get"],
796
+ hasStatusUpdate: false,
797
+ listFilters: [],
798
+ cliGroup: "files"
799
+ };
800
+
801
+ // packages/core/build/entities/location.js
802
+ var locationConfig = {
803
+ entity: "location",
804
+ apiBasePath: "/locations",
805
+ singularKey: "location",
806
+ pluralKey: "locations",
807
+ description: "location for multi-branch accounting. Use locations to track which branch or office a transaction belongs to.",
808
+ operations: ["list", "create"],
809
+ hasStatusUpdate: false,
810
+ listFilters: ["include_archived"],
811
+ cliGroup: "control-panel"
812
+ };
813
+
814
+ // packages/core/build/entities/tag.js
815
+ var tagConfig = {
816
+ entity: "tag",
817
+ apiBasePath: "/tags",
818
+ singularKey: "tag",
819
+ pluralKey: "tags",
820
+ description: "tag for categorizing transactions and documents. Tags must belong to a tag group \u2014 use list-tag-groups to find available groups and their tag_group_id before creating a tag.",
821
+ operations: ["list", "get", "create", "update", "delete"],
822
+ hasStatusUpdate: false,
823
+ listFilters: [],
824
+ businessRules: {
825
+ delete: "API may restrict deletion if tag is referenced by transactions. Archive the tag instead if deletion fails."
826
+ },
827
+ cliGroup: "control-panel"
828
+ };
829
+
830
+ // packages/core/build/entities/tag-group.js
831
+ var tagGroupConfig = {
832
+ entity: "tag-group",
833
+ apiBasePath: "/tags/groups",
834
+ singularKey: "tag_group",
835
+ pluralKey: "tag_groups",
836
+ description: "tag group for organizing tags into categories. Tag groups contain tags as children \u2014 the list response includes nested tag arrays for each group.",
837
+ operations: ["list", "get", "create", "update", "delete"],
838
+ hasStatusUpdate: false,
839
+ listFilters: ["include_archived"],
840
+ businessRules: {
841
+ delete: "API may restrict deletion if tag group contains tags or is referenced by transactions. Archive instead, or manually delete child tags first."
842
+ },
843
+ cliGroup: "control-panel"
844
+ };
845
+
846
+ // packages/mcp/src/config/env.ts
847
+ var log2 = createLogger("bukku-mcp");
848
+ var envSchema = z.object({
849
+ BUKKU_API_TOKEN: z.string().min(1, "BUKKU_API_TOKEN is required"),
850
+ BUKKU_COMPANY_SUBDOMAIN: z.string().min(1, "BUKKU_COMPANY_SUBDOMAIN is required")
851
+ });
852
+ function validateEnv() {
853
+ const result = envSchema.safeParse(process.env);
854
+ if (!result.success) {
855
+ log2("Configuration Error\n");
856
+ log2("Missing required environment variables:");
857
+ for (const issue of result.error.issues) {
858
+ log2(` - ${issue.path.join(".")}: ${issue.message}`);
859
+ }
860
+ log2("\nSetup checklist:");
861
+ log2(" 1. Go to Bukku web app -> Control Panel -> Integrations");
862
+ log2(" 2. Turn on API Access and copy the Access Token");
863
+ log2(" 3. Set BUKKU_API_TOKEN=<your-token>");
864
+ log2(" 4. Set BUKKU_COMPANY_SUBDOMAIN=<your-subdomain>");
865
+ log2(" (e.g., 'mycompany' from mycompany.bukku.my)");
866
+ log2(" 5. Restart Claude Desktop\n");
51
867
  process.exit(1);
868
+ }
869
+ return result.data;
870
+ }
871
+
872
+ // packages/mcp/src/tools/factory.ts
873
+ import { z as z2 } from "zod";
874
+ var log3 = createLogger("bukku-mcp");
875
+ function registerCrudTools(server, client, config) {
876
+ let toolCount = 0;
877
+ if (config.operations.includes("list")) {
878
+ const listName = `list-${config.entity}s`;
879
+ const listDescription = `List ${config.description}s with optional filters`;
880
+ const listSchema = {
881
+ page: z2.number().optional().describe("Page number for pagination"),
882
+ page_size: z2.number().optional().describe("Number of items per page"),
883
+ search: z2.string().optional().describe("Search query"),
884
+ date_from: z2.string().optional().describe("Filter by start date (YYYY-MM-DD)"),
885
+ date_to: z2.string().optional().describe("Filter by end date (YYYY-MM-DD)"),
886
+ status: z2.string().optional().describe("Filter by status"),
887
+ sort_by: z2.string().optional().describe("Field to sort by"),
888
+ sort_dir: z2.enum(["asc", "desc"]).optional().describe("Sort direction")
889
+ };
890
+ if (config.listFilters) {
891
+ for (const filter of config.listFilters) {
892
+ listSchema[filter] = z2.string().optional().describe(`Filter by ${filter}`);
893
+ }
894
+ }
895
+ server.tool(
896
+ listName,
897
+ listDescription,
898
+ listSchema,
899
+ async (params) => {
900
+ try {
901
+ const result = await client.get(config.apiBasePath, params);
902
+ return {
903
+ content: [
904
+ {
905
+ type: "text",
906
+ text: JSON.stringify(result, null, 2)
907
+ }
908
+ ]
909
+ };
910
+ } catch (error) {
911
+ if (error instanceof Response) {
912
+ const body = await error.json().catch(() => null);
913
+ return transformHttpError(error.status, body, listName);
914
+ }
915
+ return transformNetworkError(error, listName);
916
+ }
917
+ }
918
+ );
919
+ toolCount++;
920
+ log3(`Registered tool: ${listName}`);
921
+ }
922
+ if (config.operations.includes("get")) {
923
+ const getName = `get-${config.entity}`;
924
+ const getDescription = `Get a ${config.description} by ID`;
925
+ server.tool(
926
+ getName,
927
+ getDescription,
928
+ {
929
+ id: z2.number().describe(`The ${config.description} ID`)
930
+ },
931
+ async (params) => {
932
+ try {
933
+ const result = await client.get(`${config.apiBasePath}/${params.id}`);
934
+ return {
935
+ content: [
936
+ {
937
+ type: "text",
938
+ text: JSON.stringify(result, null, 2)
939
+ }
940
+ ]
941
+ };
942
+ } catch (error) {
943
+ if (error instanceof Response) {
944
+ const body = await error.json().catch(() => null);
945
+ return transformHttpError(error.status, body, getName);
946
+ }
947
+ return transformNetworkError(error, getName);
948
+ }
949
+ }
950
+ );
951
+ toolCount++;
952
+ log3(`Registered tool: ${getName}`);
953
+ }
954
+ if (config.operations.includes("create")) {
955
+ const createName = `create-${config.entity}`;
956
+ const createDescription = `Create a new ${config.description}`;
957
+ server.tool(
958
+ createName,
959
+ createDescription,
960
+ {
961
+ data: z2.record(z2.string(), z2.unknown()).describe("Data for the new item")
962
+ },
963
+ async (params) => {
964
+ try {
965
+ const result = await client.post(config.apiBasePath, params.data);
966
+ return {
967
+ content: [
968
+ {
969
+ type: "text",
970
+ text: JSON.stringify(result, null, 2)
971
+ }
972
+ ]
973
+ };
974
+ } catch (error) {
975
+ if (error instanceof Response) {
976
+ const body = await error.json().catch(() => null);
977
+ return transformHttpError(error.status, body, createName);
978
+ }
979
+ return transformNetworkError(error, createName);
980
+ }
981
+ }
982
+ );
983
+ toolCount++;
984
+ log3(`Registered tool: ${createName}`);
985
+ }
986
+ if (config.operations.includes("update")) {
987
+ const updateName = `update-${config.entity}`;
988
+ const updateDescription = `Update an existing ${config.description}`;
989
+ server.tool(
990
+ updateName,
991
+ updateDescription,
992
+ {
993
+ id: z2.number().describe(`The ${config.description} ID`),
994
+ data: z2.record(z2.string(), z2.unknown()).describe("Updated data")
995
+ },
996
+ async (params) => {
997
+ try {
998
+ const result = await client.put(`${config.apiBasePath}/${params.id}`, params.data);
999
+ return {
1000
+ content: [
1001
+ {
1002
+ type: "text",
1003
+ text: JSON.stringify(result, null, 2)
1004
+ }
1005
+ ]
1006
+ };
1007
+ } catch (error) {
1008
+ if (error instanceof Response) {
1009
+ const body = await error.json().catch(() => null);
1010
+ return transformHttpError(error.status, body, updateName);
1011
+ }
1012
+ return transformNetworkError(error, updateName);
1013
+ }
1014
+ }
1015
+ );
1016
+ toolCount++;
1017
+ log3(`Registered tool: ${updateName}`);
1018
+ }
1019
+ if (config.operations.includes("delete")) {
1020
+ const deleteName = `delete-${config.entity}`;
1021
+ const deleteDescription = `Delete a ${config.description}. ${config.businessRules?.delete ?? ""}`.trim();
1022
+ server.tool(
1023
+ deleteName,
1024
+ deleteDescription,
1025
+ {
1026
+ id: z2.number().describe(`The ${config.description} ID`)
1027
+ },
1028
+ async (params) => {
1029
+ try {
1030
+ await client.delete(`${config.apiBasePath}/${params.id}`);
1031
+ return {
1032
+ content: [
1033
+ {
1034
+ type: "text",
1035
+ text: `Successfully deleted ${config.description} with ID ${params.id}`
1036
+ }
1037
+ ]
1038
+ };
1039
+ } catch (error) {
1040
+ if (error instanceof Response) {
1041
+ const body = await error.json().catch(() => null);
1042
+ return transformHttpError(error.status, body, deleteName);
1043
+ }
1044
+ return transformNetworkError(error, deleteName);
1045
+ }
1046
+ }
1047
+ );
1048
+ toolCount++;
1049
+ log3(`Registered tool: ${deleteName}`);
1050
+ }
1051
+ if (config.hasStatusUpdate) {
1052
+ const statusName = `update-${config.entity}-status`;
1053
+ const statusDescription = `Update the status of a ${config.description}. ${config.businessRules?.statusTransitions ?? ""}`.trim();
1054
+ server.tool(
1055
+ statusName,
1056
+ statusDescription,
1057
+ {
1058
+ id: z2.number().describe(`The ${config.description} ID`),
1059
+ status: z2.string().describe("New status value")
1060
+ },
1061
+ async (params) => {
1062
+ try {
1063
+ const result = await client.patch(`${config.apiBasePath}/${params.id}`, {
1064
+ status: params.status
1065
+ });
1066
+ return {
1067
+ content: [
1068
+ {
1069
+ type: "text",
1070
+ text: JSON.stringify(result, null, 2)
1071
+ }
1072
+ ]
1073
+ };
1074
+ } catch (error) {
1075
+ if (error instanceof Response) {
1076
+ const body = await error.json().catch(() => null);
1077
+ return transformHttpError(error.status, body, statusName);
1078
+ }
1079
+ return transformNetworkError(error, statusName);
1080
+ }
1081
+ }
1082
+ );
1083
+ toolCount++;
1084
+ log3(`Registered tool: ${statusName}`);
1085
+ }
1086
+ return toolCount;
1087
+ }
1088
+
1089
+ // packages/mcp/src/tools/custom/contact-archive.ts
1090
+ import { z as z3 } from "zod";
1091
+ var log4 = createLogger("bukku-mcp");
1092
+ function registerContactArchiveTools(server, client) {
1093
+ server.tool(
1094
+ "archive-contact",
1095
+ "Archive a contact (hide from active lists). Use this for contacts with transaction history instead of deleting them.",
1096
+ {
1097
+ id: z3.number().describe("The contact ID")
1098
+ },
1099
+ async (params) => {
1100
+ try {
1101
+ const result = await client.patch(`/contacts/${params.id}`, {
1102
+ is_archived: true
1103
+ });
1104
+ return {
1105
+ content: [
1106
+ {
1107
+ type: "text",
1108
+ text: JSON.stringify(result, null, 2)
1109
+ }
1110
+ ]
1111
+ };
1112
+ } catch (error) {
1113
+ if (error instanceof Response) {
1114
+ const body = await error.json().catch(() => null);
1115
+ return transformHttpError(error.status, body, "archive-contact");
1116
+ }
1117
+ return transformNetworkError(error, "archive-contact");
1118
+ }
1119
+ }
1120
+ );
1121
+ log4("Registered tool: archive-contact");
1122
+ server.tool(
1123
+ "unarchive-contact",
1124
+ "Unarchive a contact (restore to active lists).",
1125
+ {
1126
+ id: z3.number().describe("The contact ID")
1127
+ },
1128
+ async (params) => {
1129
+ try {
1130
+ const result = await client.patch(`/contacts/${params.id}`, {
1131
+ is_archived: false
1132
+ });
1133
+ return {
1134
+ content: [
1135
+ {
1136
+ type: "text",
1137
+ text: JSON.stringify(result, null, 2)
1138
+ }
1139
+ ]
1140
+ };
1141
+ } catch (error) {
1142
+ if (error instanceof Response) {
1143
+ const body = await error.json().catch(() => null);
1144
+ return transformHttpError(error.status, body, "unarchive-contact");
1145
+ }
1146
+ return transformNetworkError(error, "unarchive-contact");
1147
+ }
1148
+ }
1149
+ );
1150
+ log4("Registered tool: unarchive-contact");
1151
+ return 2;
1152
+ }
1153
+
1154
+ // packages/mcp/src/tools/custom/product-archive.ts
1155
+ import { z as z4 } from "zod";
1156
+ var log5 = createLogger("bukku-mcp");
1157
+ function registerProductArchiveTools(server, client) {
1158
+ server.tool(
1159
+ "archive-product",
1160
+ "Archive a product (hide from active lists). Use this for products with transaction history instead of deleting them.",
1161
+ {
1162
+ id: z4.number().describe("The product ID")
1163
+ },
1164
+ async (params) => {
1165
+ try {
1166
+ const result = await client.patch(`/products/${params.id}`, {
1167
+ is_archived: true
1168
+ });
1169
+ return {
1170
+ content: [
1171
+ {
1172
+ type: "text",
1173
+ text: JSON.stringify(result, null, 2)
1174
+ }
1175
+ ]
1176
+ };
1177
+ } catch (error) {
1178
+ if (error instanceof Response) {
1179
+ const body = await error.json().catch(() => null);
1180
+ return transformHttpError(error.status, body, "archive-product");
1181
+ }
1182
+ return transformNetworkError(error, "archive-product");
1183
+ }
1184
+ }
1185
+ );
1186
+ log5("Registered tool: archive-product");
1187
+ server.tool(
1188
+ "unarchive-product",
1189
+ "Unarchive a product (restore to active lists).",
1190
+ {
1191
+ id: z4.number().describe("The product ID")
1192
+ },
1193
+ async (params) => {
1194
+ try {
1195
+ const result = await client.patch(`/products/${params.id}`, {
1196
+ is_archived: false
1197
+ });
1198
+ return {
1199
+ content: [
1200
+ {
1201
+ type: "text",
1202
+ text: JSON.stringify(result, null, 2)
1203
+ }
1204
+ ]
1205
+ };
1206
+ } catch (error) {
1207
+ if (error instanceof Response) {
1208
+ const body = await error.json().catch(() => null);
1209
+ return transformHttpError(error.status, body, "unarchive-product");
1210
+ }
1211
+ return transformNetworkError(error, "unarchive-product");
1212
+ }
1213
+ }
1214
+ );
1215
+ log5("Registered tool: unarchive-product");
1216
+ server.tool(
1217
+ "archive-product-bundle",
1218
+ "Archive a product bundle (hide from active lists). Use this for bundles with transaction history instead of deleting them.",
1219
+ {
1220
+ id: z4.number().describe("The product bundle ID")
1221
+ },
1222
+ async (params) => {
1223
+ try {
1224
+ const result = await client.patch(`/products/bundles/${params.id}`, {
1225
+ is_archived: true
1226
+ });
1227
+ return {
1228
+ content: [
1229
+ {
1230
+ type: "text",
1231
+ text: JSON.stringify(result, null, 2)
1232
+ }
1233
+ ]
1234
+ };
1235
+ } catch (error) {
1236
+ if (error instanceof Response) {
1237
+ const body = await error.json().catch(() => null);
1238
+ return transformHttpError(error.status, body, "archive-product-bundle");
1239
+ }
1240
+ return transformNetworkError(error, "archive-product-bundle");
1241
+ }
1242
+ }
1243
+ );
1244
+ log5("Registered tool: archive-product-bundle");
1245
+ server.tool(
1246
+ "unarchive-product-bundle",
1247
+ "Unarchive a product bundle (restore to active lists).",
1248
+ {
1249
+ id: z4.number().describe("The product bundle ID")
1250
+ },
1251
+ async (params) => {
1252
+ try {
1253
+ const result = await client.patch(`/products/bundles/${params.id}`, {
1254
+ is_archived: false
1255
+ });
1256
+ return {
1257
+ content: [
1258
+ {
1259
+ type: "text",
1260
+ text: JSON.stringify(result, null, 2)
1261
+ }
1262
+ ]
1263
+ };
1264
+ } catch (error) {
1265
+ if (error instanceof Response) {
1266
+ const body = await error.json().catch(() => null);
1267
+ return transformHttpError(error.status, body, "unarchive-product-bundle");
1268
+ }
1269
+ return transformNetworkError(error, "unarchive-product-bundle");
1270
+ }
1271
+ }
1272
+ );
1273
+ log5("Registered tool: unarchive-product-bundle");
1274
+ return 4;
1275
+ }
1276
+
1277
+ // packages/mcp/src/tools/custom/reference-data.ts
1278
+ var log6 = createLogger("bukku-mcp");
1279
+ var REFERENCE_TYPES = [
1280
+ {
1281
+ type: "tax_codes",
1282
+ toolName: "list-tax-codes",
1283
+ description: "List all tax codes (tax rate definitions for invoices and purchases). Use to find valid tax_code_id values before creating sales invoices, purchase bills, or products."
1284
+ },
1285
+ {
1286
+ type: "currencies",
1287
+ toolName: "list-currencies",
1288
+ description: "List all activated currencies. Use to find valid currency_code values for multi-currency transactions."
1289
+ },
1290
+ {
1291
+ type: "payment_methods",
1292
+ toolName: "list-payment-methods",
1293
+ description: "List all payment methods (e.g., Bank Transfer, Cash, Credit Card). Use to find valid payment_method_id values for payments and refunds."
1294
+ },
1295
+ {
1296
+ type: "terms",
1297
+ toolName: "list-terms",
1298
+ description: "List all payment terms (e.g., Net 30, Due on Receipt). Use to find valid term_id values when creating invoices or bills."
1299
+ },
1300
+ {
1301
+ type: "accounts",
1302
+ toolName: "list-accounts",
1303
+ description: "List all accounts from the chart of accounts. Use to find valid account IDs for sale_account_id, purchase_account_id, and inventory_account_id in products, and for journal entries."
1304
+ },
1305
+ {
1306
+ type: "price_levels",
1307
+ toolName: "list-price-levels",
1308
+ description: "List all price levels (custom pricing tiers for volume discounts). Use to find valid price_level_id values for product custom pricing."
1309
+ },
1310
+ {
1311
+ type: "countries",
1312
+ toolName: "list-countries",
1313
+ description: "List all countries ordered by name. Use to find valid country codes for contacts and locations."
1314
+ },
1315
+ {
1316
+ type: "classification_code_list",
1317
+ toolName: "list-classification-codes",
1318
+ description: "List all product classification codes (Malaysia LHDN e-Invoice). Use to find valid classification_code values for products."
1319
+ },
1320
+ {
1321
+ type: "numberings",
1322
+ toolName: "list-numberings",
1323
+ description: "List all document numbering schemes. Use to find valid numbering IDs for transactions."
1324
+ },
1325
+ {
1326
+ type: "state_list",
1327
+ toolName: "list-states",
1328
+ description: "List all geographic states/provinces for addresses."
1329
+ }
1330
+ ];
1331
+ function registerReferenceDataTools(server, client, cache) {
1332
+ for (const { type, toolName, description } of REFERENCE_TYPES) {
1333
+ server.tool(
1334
+ toolName,
1335
+ description,
1336
+ {},
1337
+ async () => {
1338
+ const cached = cache.get(type);
1339
+ if (cached) {
1340
+ return {
1341
+ content: [{ type: "text", text: JSON.stringify(cached, null, 2) }]
1342
+ };
1343
+ }
1344
+ try {
1345
+ const result = await client.post("/v2/lists", {
1346
+ lists: [type],
1347
+ params: []
1348
+ });
1349
+ cache.set(type, result);
1350
+ return {
1351
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1352
+ };
1353
+ } catch (error) {
1354
+ if (error instanceof Response) {
1355
+ const body = await error.json().catch(() => null);
1356
+ return transformHttpError(error.status, body, toolName);
1357
+ }
1358
+ return transformNetworkError(error, toolName);
1359
+ }
1360
+ }
1361
+ );
1362
+ log6(`Registered tool: ${toolName}`);
1363
+ }
1364
+ return REFERENCE_TYPES.length;
1365
+ }
1366
+
1367
+ // packages/mcp/src/tools/custom/journal-entry-tools.ts
1368
+ import { z as z5 } from "zod";
1369
+ var log7 = createLogger("bukku-mcp");
1370
+ function registerJournalEntryTools(server, client) {
1371
+ server.tool(
1372
+ "create-journal-entry",
1373
+ "Create a new journal entry. Use list-accounts to find valid account IDs for line items. Journal entries must have balanced debits and credits (total debits = total credits).",
1374
+ {
1375
+ data: z5.record(z5.string(), z5.unknown()).describe("Journal entry data including journal_items array with debit/credit amounts and account IDs")
1376
+ },
1377
+ async (params) => {
1378
+ try {
1379
+ const journalItems = params.data.journal_items;
1380
+ if (journalItems && Array.isArray(journalItems)) {
1381
+ const validation = validateDoubleEntry(journalItems);
1382
+ if (!validation.valid) {
1383
+ return {
1384
+ isError: true,
1385
+ content: [
1386
+ {
1387
+ type: "text",
1388
+ text: validation.error
1389
+ }
1390
+ ]
1391
+ };
1392
+ }
1393
+ }
1394
+ const result = await client.post("/journal_entries", params.data);
1395
+ return {
1396
+ content: [
1397
+ {
1398
+ type: "text",
1399
+ text: JSON.stringify(result, null, 2)
1400
+ }
1401
+ ]
1402
+ };
1403
+ } catch (error) {
1404
+ if (error instanceof Response) {
1405
+ const body = await error.json().catch(() => null);
1406
+ return transformHttpError(error.status, body, "create-journal-entry");
1407
+ }
1408
+ return transformNetworkError(error, "create-journal-entry");
1409
+ }
1410
+ }
1411
+ );
1412
+ log7("Registered tool: create-journal-entry");
1413
+ server.tool(
1414
+ "update-journal-entry",
1415
+ "Update an existing journal entry. If updating line items, journal entries must have balanced debits and credits (total debits = total credits).",
1416
+ {
1417
+ id: z5.number().describe("The journal entry ID"),
1418
+ data: z5.record(z5.string(), z5.unknown()).describe("Updated journal entry data")
1419
+ },
1420
+ async (params) => {
1421
+ try {
1422
+ const journalItems = params.data.journal_items;
1423
+ if (journalItems && Array.isArray(journalItems)) {
1424
+ const validation = validateDoubleEntry(journalItems);
1425
+ if (!validation.valid) {
1426
+ return {
1427
+ isError: true,
1428
+ content: [
1429
+ {
1430
+ type: "text",
1431
+ text: validation.error
1432
+ }
1433
+ ]
1434
+ };
1435
+ }
1436
+ }
1437
+ const result = await client.put(`/journal_entries/${params.id}`, params.data);
1438
+ return {
1439
+ content: [
1440
+ {
1441
+ type: "text",
1442
+ text: JSON.stringify(result, null, 2)
1443
+ }
1444
+ ]
1445
+ };
1446
+ } catch (error) {
1447
+ if (error instanceof Response) {
1448
+ const body = await error.json().catch(() => null);
1449
+ return transformHttpError(error.status, body, "update-journal-entry");
1450
+ }
1451
+ return transformNetworkError(error, "update-journal-entry");
1452
+ }
1453
+ }
1454
+ );
1455
+ log7("Registered tool: update-journal-entry");
1456
+ return 2;
1457
+ }
1458
+
1459
+ // packages/mcp/src/tools/custom/account-tools.ts
1460
+ import { z as z6 } from "zod";
1461
+ var log8 = createLogger("bukku-mcp");
1462
+ function registerAccountCustomTools(server, client) {
1463
+ server.tool(
1464
+ "search-accounts",
1465
+ "Search and filter accounts from the chart of accounts. Supports filtering by category and archived status. For a quick cached account lookup (e.g., to find account IDs for journal entries), use list-accounts instead.",
1466
+ {
1467
+ search: z6.string().optional().describe("Search by account name, code, or description"),
1468
+ category: z6.enum(["assets", "liabilities", "equity", "income", "expenses"]).optional().describe("Filter by account category"),
1469
+ is_archived: z6.boolean().optional().describe("Filter by archived status (default: false, showing only active accounts)"),
1470
+ sort_by: z6.enum(["code", "name", "balance"]).optional().describe("Sort field (default: code)"),
1471
+ sort_dir: z6.enum(["asc", "desc"]).optional().describe("Sort direction (default: asc)"),
1472
+ page: z6.number().optional().describe("Page number for pagination"),
1473
+ page_size: z6.number().optional().describe("Number of items per page")
1474
+ },
1475
+ async (params) => {
1476
+ try {
1477
+ const queryParams = {
1478
+ search: params.search,
1479
+ category: params.category,
1480
+ sort_by: params.sort_by,
1481
+ sort_dir: params.sort_dir,
1482
+ page: params.page,
1483
+ page_size: params.page_size
1484
+ };
1485
+ if (params.is_archived !== void 0) {
1486
+ queryParams.is_archived = params.is_archived ? "true" : "false";
1487
+ }
1488
+ const result = await client.get("/accounts", queryParams);
1489
+ return {
1490
+ content: [
1491
+ {
1492
+ type: "text",
1493
+ text: JSON.stringify(result, null, 2)
1494
+ }
1495
+ ]
1496
+ };
1497
+ } catch (error) {
1498
+ if (error instanceof Response) {
1499
+ const body = await error.json().catch(() => null);
1500
+ return transformHttpError(error.status, body, "search-accounts");
1501
+ }
1502
+ return transformNetworkError(error, "search-accounts");
1503
+ }
1504
+ }
1505
+ );
1506
+ log8("Registered tool: search-accounts");
1507
+ server.tool(
1508
+ "archive-account",
1509
+ "Archive an account (hide from active lists). Use this instead of delete for accounts with transaction history. Archived accounts are hidden by default but can be reactivated.",
1510
+ {
1511
+ id: z6.number().describe("The account ID")
1512
+ },
1513
+ async (params) => {
1514
+ try {
1515
+ const result = await client.patch(`/accounts/${params.id}`, {
1516
+ is_archived: true
1517
+ });
1518
+ return {
1519
+ content: [
1520
+ {
1521
+ type: "text",
1522
+ text: JSON.stringify(result, null, 2)
1523
+ }
1524
+ ]
1525
+ };
1526
+ } catch (error) {
1527
+ if (error instanceof Response) {
1528
+ const body = await error.json().catch(() => null);
1529
+ return transformHttpError(error.status, body, "archive-account");
1530
+ }
1531
+ return transformNetworkError(error, "archive-account");
1532
+ }
1533
+ }
1534
+ );
1535
+ log8("Registered tool: archive-account");
1536
+ server.tool(
1537
+ "unarchive-account",
1538
+ "Unarchive an account (restore to active lists).",
1539
+ {
1540
+ id: z6.number().describe("The account ID")
1541
+ },
1542
+ async (params) => {
1543
+ try {
1544
+ const result = await client.patch(`/accounts/${params.id}`, {
1545
+ is_archived: false
1546
+ });
1547
+ return {
1548
+ content: [
1549
+ {
1550
+ type: "text",
1551
+ text: JSON.stringify(result, null, 2)
1552
+ }
1553
+ ]
1554
+ };
1555
+ } catch (error) {
1556
+ if (error instanceof Response) {
1557
+ const body = await error.json().catch(() => null);
1558
+ return transformHttpError(error.status, body, "unarchive-account");
1559
+ }
1560
+ return transformNetworkError(error, "unarchive-account");
1561
+ }
1562
+ }
1563
+ );
1564
+ log8("Registered tool: unarchive-account");
1565
+ return 3;
1566
+ }
1567
+
1568
+ // packages/mcp/src/tools/custom/file-upload.ts
1569
+ import { z as z7 } from "zod";
1570
+ var log9 = createLogger("bukku-mcp");
1571
+ function registerFileUploadTool(server, client) {
1572
+ server.tool(
1573
+ "upload-file",
1574
+ "Upload a file to Bukku. Returns file object with id, url, and metadata. Use the returned file id in transaction file_ids arrays to attach files to sales invoices, purchase bills, and other documents.",
1575
+ {
1576
+ file_path: z7.string().describe("Absolute path to the file on disk")
1577
+ },
1578
+ async (params) => {
1579
+ try {
1580
+ const result = await client.postMultipart("/files", params.file_path);
1581
+ return {
1582
+ content: [
1583
+ {
1584
+ type: "text",
1585
+ text: JSON.stringify(result, null, 2)
1586
+ }
1587
+ ]
1588
+ };
1589
+ } catch (error) {
1590
+ if (error instanceof Response) {
1591
+ const body = await error.json().catch(() => null);
1592
+ return transformHttpError(error.status, body, "upload-file");
1593
+ }
1594
+ return transformNetworkError(error, "upload-file");
1595
+ }
1596
+ }
1597
+ );
1598
+ log9("Registered tool: upload-file");
1599
+ return 1;
1600
+ }
1601
+
1602
+ // packages/mcp/src/tools/custom/location-tools.ts
1603
+ import { z as z8 } from "zod";
1604
+ var log10 = createLogger("bukku-mcp");
1605
+ function registerLocationTools(server, client) {
1606
+ server.tool(
1607
+ "get-location",
1608
+ "Get a location for multi-branch accounting by ID",
1609
+ {
1610
+ id: z8.number().describe("The location ID")
1611
+ },
1612
+ async (params) => {
1613
+ try {
1614
+ const result = await client.get(`/location/${params.id}`);
1615
+ return {
1616
+ content: [
1617
+ {
1618
+ type: "text",
1619
+ text: JSON.stringify(result, null, 2)
1620
+ }
1621
+ ]
1622
+ };
1623
+ } catch (error) {
1624
+ if (error instanceof Response) {
1625
+ const body = await error.json().catch(() => null);
1626
+ return transformHttpError(error.status, body, "get-location");
1627
+ }
1628
+ return transformNetworkError(error, "get-location");
1629
+ }
1630
+ }
1631
+ );
1632
+ log10("Registered tool: get-location");
1633
+ server.tool(
1634
+ "update-location",
1635
+ "Update an existing location for multi-branch accounting",
1636
+ {
1637
+ id: z8.number().describe("The location ID"),
1638
+ data: z8.record(z8.string(), z8.unknown()).describe("Updated data")
1639
+ },
1640
+ async (params) => {
1641
+ try {
1642
+ const result = await client.put(`/location/${params.id}`, params.data);
1643
+ return {
1644
+ content: [
1645
+ {
1646
+ type: "text",
1647
+ text: JSON.stringify(result, null, 2)
1648
+ }
1649
+ ]
1650
+ };
1651
+ } catch (error) {
1652
+ if (error instanceof Response) {
1653
+ const body = await error.json().catch(() => null);
1654
+ return transformHttpError(error.status, body, "update-location");
1655
+ }
1656
+ return transformNetworkError(error, "update-location");
1657
+ }
1658
+ }
1659
+ );
1660
+ log10("Registered tool: update-location");
1661
+ server.tool(
1662
+ "delete-location",
1663
+ "Delete a location for multi-branch accounting. API may restrict deletion if location is referenced by transactions. Archive instead if deletion fails.",
1664
+ {
1665
+ id: z8.number().describe("The location ID")
1666
+ },
1667
+ async (params) => {
1668
+ try {
1669
+ await client.delete(`/location/${params.id}`);
1670
+ return {
1671
+ content: [
1672
+ {
1673
+ type: "text",
1674
+ text: `Successfully deleted location with ID ${params.id}`
1675
+ }
1676
+ ]
1677
+ };
1678
+ } catch (error) {
1679
+ if (error instanceof Response) {
1680
+ const body = await error.json().catch(() => null);
1681
+ return transformHttpError(error.status, body, "delete-location");
1682
+ }
1683
+ return transformNetworkError(error, "delete-location");
1684
+ }
1685
+ }
1686
+ );
1687
+ log10("Registered tool: delete-location");
1688
+ return 3;
1689
+ }
1690
+
1691
+ // packages/mcp/src/tools/custom/control-panel-archive.ts
1692
+ import { z as z9 } from "zod";
1693
+ var log11 = createLogger("bukku-mcp");
1694
+ function registerControlPanelArchiveTools(server, client) {
1695
+ server.tool(
1696
+ "archive-location",
1697
+ "Archive a location (hide from active lists). Use this for locations referenced by transactions instead of deleting them.",
1698
+ {
1699
+ id: z9.number().describe("The location ID")
1700
+ },
1701
+ async (params) => {
1702
+ try {
1703
+ const result = await client.patch(`/location/${params.id}`, {
1704
+ is_archived: true
1705
+ });
1706
+ return {
1707
+ content: [
1708
+ {
1709
+ type: "text",
1710
+ text: JSON.stringify(result, null, 2)
1711
+ }
1712
+ ]
1713
+ };
1714
+ } catch (error) {
1715
+ if (error instanceof Response) {
1716
+ const body = await error.json().catch(() => null);
1717
+ return transformHttpError(error.status, body, "archive-location");
1718
+ }
1719
+ return transformNetworkError(error, "archive-location");
1720
+ }
1721
+ }
1722
+ );
1723
+ log11("Registered tool: archive-location");
1724
+ server.tool(
1725
+ "unarchive-location",
1726
+ "Unarchive a location (restore to active lists).",
1727
+ {
1728
+ id: z9.number().describe("The location ID")
1729
+ },
1730
+ async (params) => {
1731
+ try {
1732
+ const result = await client.patch(`/location/${params.id}`, {
1733
+ is_archived: false
1734
+ });
1735
+ return {
1736
+ content: [
1737
+ {
1738
+ type: "text",
1739
+ text: JSON.stringify(result, null, 2)
1740
+ }
1741
+ ]
1742
+ };
1743
+ } catch (error) {
1744
+ if (error instanceof Response) {
1745
+ const body = await error.json().catch(() => null);
1746
+ return transformHttpError(error.status, body, "unarchive-location");
1747
+ }
1748
+ return transformNetworkError(error, "unarchive-location");
1749
+ }
1750
+ }
1751
+ );
1752
+ log11("Registered tool: unarchive-location");
1753
+ return 2;
1754
+ }
1755
+
1756
+ // packages/mcp/src/tools/registry.ts
1757
+ function registerAllTools(server, client) {
1758
+ let totalTools = 0;
1759
+ totalTools += registerCrudTools(server, client, salesQuoteConfig);
1760
+ totalTools += registerCrudTools(server, client, salesOrderConfig);
1761
+ totalTools += registerCrudTools(server, client, deliveryOrderConfig);
1762
+ totalTools += registerCrudTools(server, client, salesInvoiceConfig);
1763
+ totalTools += registerCrudTools(server, client, salesCreditNoteConfig);
1764
+ totalTools += registerCrudTools(server, client, salesPaymentConfig);
1765
+ totalTools += registerCrudTools(server, client, salesRefundConfig);
1766
+ totalTools += registerCrudTools(server, client, purchaseOrderConfig);
1767
+ totalTools += registerCrudTools(server, client, goodsReceivedNoteConfig);
1768
+ totalTools += registerCrudTools(server, client, purchaseBillConfig);
1769
+ totalTools += registerCrudTools(server, client, purchaseCreditNoteConfig);
1770
+ totalTools += registerCrudTools(server, client, purchasePaymentConfig);
1771
+ totalTools += registerCrudTools(server, client, purchaseRefundConfig);
1772
+ totalTools += registerCrudTools(server, client, bankMoneyInConfig);
1773
+ totalTools += registerCrudTools(server, client, bankMoneyOutConfig);
1774
+ totalTools += registerCrudTools(server, client, bankTransferConfig);
1775
+ totalTools += registerCrudTools(server, client, contactConfig);
1776
+ totalTools += registerCrudTools(server, client, contactGroupConfig);
1777
+ totalTools += registerContactArchiveTools(server, client);
1778
+ totalTools += registerCrudTools(server, client, productConfig);
1779
+ totalTools += registerCrudTools(server, client, productBundleConfig);
1780
+ totalTools += registerCrudTools(server, client, productGroupConfig);
1781
+ totalTools += registerProductArchiveTools(server, client);
1782
+ const referenceCache = new ReferenceDataCache();
1783
+ totalTools += registerReferenceDataTools(server, client, referenceCache);
1784
+ totalTools += registerCrudTools(server, client, journalEntryConfig);
1785
+ totalTools += registerCrudTools(server, client, accountConfig);
1786
+ totalTools += registerJournalEntryTools(server, client);
1787
+ totalTools += registerAccountCustomTools(server, client);
1788
+ totalTools += registerCrudTools(server, client, fileConfig);
1789
+ totalTools += registerFileUploadTool(server, client);
1790
+ totalTools += registerCrudTools(server, client, locationConfig);
1791
+ totalTools += registerLocationTools(server, client);
1792
+ totalTools += registerCrudTools(server, client, tagConfig);
1793
+ totalTools += registerCrudTools(server, client, tagGroupConfig);
1794
+ totalTools += registerControlPanelArchiveTools(server, client);
1795
+ return totalTools;
1796
+ }
1797
+
1798
+ // packages/mcp/src/index.ts
1799
+ var log12 = createLogger("bukku-mcp");
1800
+ var packageJsonPath = new URL("../package.json", import.meta.url);
1801
+ var pkg = JSON.parse(readFileSync(fileURLToPath(packageJsonPath), "utf-8"));
1802
+ async function main() {
1803
+ const env = validateEnv();
1804
+ const client = new BukkuClient({
1805
+ apiToken: env.BUKKU_API_TOKEN,
1806
+ companySubdomain: env.BUKKU_COMPANY_SUBDOMAIN
1807
+ });
1808
+ await client.validateToken();
1809
+ const server = new McpServer({
1810
+ name: "bukku",
1811
+ version: pkg.version
1812
+ });
1813
+ const toolCount = registerAllTools(server, client);
1814
+ const transport = new StdioServerTransport();
1815
+ await server.connect(transport);
1816
+ log12(`Bukku MCP server started (${toolCount} tools registered)`);
1817
+ }
1818
+ main().catch((error) => {
1819
+ log12("Fatal error during startup:", error);
1820
+ process.exit(1);
52
1821
  });