@hisaabo/mcp 0.1.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/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/README.md +155 -0
- package/dist/bin/index.js +2682 -0
- package/dist/bin/index.js.map +1 -0
- package/dist/index.js +2681 -0
- package/package.json +26 -0
- package/src/client.ts +1315 -0
- package/src/index.ts +64 -0
- package/src/lib/errors.ts +49 -0
- package/src/lib/pagination.ts +35 -0
- package/src/resources/index.ts +211 -0
- package/src/server.ts +55 -0
- package/src/tools/bankAccount.ts +200 -0
- package/src/tools/dashboard.ts +110 -0
- package/src/tools/expense.ts +175 -0
- package/src/tools/gst.ts +90 -0
- package/src/tools/import.ts +298 -0
- package/src/tools/invoice.ts +256 -0
- package/src/tools/item.ts +262 -0
- package/src/tools/party.ts +266 -0
- package/src/tools/payment.ts +222 -0
- package/src/tools/reports.ts +246 -0
- package/src/tools/shipment.ts +222 -0
- package/src/tools/store.ts +177 -0
- package/src/tools/target.ts +143 -0
- package/tsconfig.json +8 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/client.ts
|
|
8
|
+
import superjson from "superjson";
|
|
9
|
+
var HisaaboApiError = class extends Error {
|
|
10
|
+
constructor(hisaaboError) {
|
|
11
|
+
super(formatHisaaboError(hisaaboError));
|
|
12
|
+
this.hisaaboError = hisaaboError;
|
|
13
|
+
this.name = "HisaaboApiError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function formatHisaaboError(err) {
|
|
17
|
+
switch (err.code) {
|
|
18
|
+
case "unauthorized":
|
|
19
|
+
return `Authentication required: ${err.message}. Check that HISAABO_TOKEN is set and not expired.`;
|
|
20
|
+
case "forbidden":
|
|
21
|
+
return `Permission denied: ${err.message}`;
|
|
22
|
+
case "not_found":
|
|
23
|
+
return `Not found: ${err.resource}`;
|
|
24
|
+
case "validation_failed":
|
|
25
|
+
return `Validation failed:
|
|
26
|
+
` + Object.entries(err.fields).map(([field, msgs]) => ` ${field}: ${msgs.join(", ")}`).join("\n");
|
|
27
|
+
case "api_error":
|
|
28
|
+
return `API error: ${err.message}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalizeTrpcError(raw) {
|
|
32
|
+
if (!raw || typeof raw !== "object") {
|
|
33
|
+
return { code: "api_error", message: "Unknown error from API" };
|
|
34
|
+
}
|
|
35
|
+
const err = raw;
|
|
36
|
+
const code = err["code"];
|
|
37
|
+
const message = err["message"] ?? "Unknown error";
|
|
38
|
+
if (code === "UNAUTHORIZED") return { code: "unauthorized", message };
|
|
39
|
+
if (code === "FORBIDDEN") return { code: "forbidden", message };
|
|
40
|
+
if (code === "NOT_FOUND") return { code: "not_found", resource: message };
|
|
41
|
+
if (code === "BAD_REQUEST") {
|
|
42
|
+
const data = err["data"];
|
|
43
|
+
const zodError = data?.["zodError"];
|
|
44
|
+
if (zodError?.fieldErrors) {
|
|
45
|
+
return { code: "validation_failed", fields: zodError.fieldErrors };
|
|
46
|
+
}
|
|
47
|
+
return { code: "validation_failed", fields: { _: [message] } };
|
|
48
|
+
}
|
|
49
|
+
return { code: "api_error", message };
|
|
50
|
+
}
|
|
51
|
+
var HisaaboClient = class {
|
|
52
|
+
constructor(config2) {
|
|
53
|
+
this.config = config2;
|
|
54
|
+
this.apiUrl = config2.apiUrl;
|
|
55
|
+
}
|
|
56
|
+
/** Base API URL, exposed for tools that need to construct URLs (e.g. PDF download). */
|
|
57
|
+
apiUrl;
|
|
58
|
+
buildHeaders() {
|
|
59
|
+
return {
|
|
60
|
+
"Authorization": `Bearer ${this.config.token}`,
|
|
61
|
+
"x-business-id": this.config.businessId,
|
|
62
|
+
"x-tenant-id": this.config.tenantId,
|
|
63
|
+
"x-client-type": "mcp"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async unwrap(res) {
|
|
67
|
+
const body = await res.json();
|
|
68
|
+
if (typeof body !== "object" || body === null) {
|
|
69
|
+
throw new HisaaboApiError({ code: "api_error", message: "Unexpected response format from API" });
|
|
70
|
+
}
|
|
71
|
+
const envelope = body;
|
|
72
|
+
if (!res.ok || "error" in envelope) {
|
|
73
|
+
throw new HisaaboApiError(normalizeTrpcError(envelope["error"] ?? { code: "api_error", message: `HTTP ${res.status}` }));
|
|
74
|
+
}
|
|
75
|
+
const result = envelope["result"];
|
|
76
|
+
if (!result) {
|
|
77
|
+
throw new HisaaboApiError({ code: "api_error", message: "Missing result in API response" });
|
|
78
|
+
}
|
|
79
|
+
const data = result["data"];
|
|
80
|
+
return superjson.deserialize(data);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Call a tRPC query procedure.
|
|
84
|
+
* Queries use GET with SuperJSON-serialized input as a URL param.
|
|
85
|
+
*/
|
|
86
|
+
async query(path, input) {
|
|
87
|
+
const url = new URL(`${this.config.apiUrl}/api/trpc/${path}`);
|
|
88
|
+
if (input !== void 0) {
|
|
89
|
+
url.searchParams.set("input", JSON.stringify(superjson.serialize(input)));
|
|
90
|
+
}
|
|
91
|
+
const res = await fetch(url.toString(), { headers: this.buildHeaders() });
|
|
92
|
+
return this.unwrap(res);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Call a tRPC mutation procedure.
|
|
96
|
+
* Mutations use POST with SuperJSON-serialized body.
|
|
97
|
+
*/
|
|
98
|
+
async mutate(path, input) {
|
|
99
|
+
const res = await fetch(`${this.config.apiUrl}/api/trpc/${path}`, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { ...this.buildHeaders(), "Content-Type": "application/json" },
|
|
102
|
+
body: JSON.stringify(superjson.serialize(input))
|
|
103
|
+
});
|
|
104
|
+
return this.unwrap(res);
|
|
105
|
+
}
|
|
106
|
+
// ── Namespaced procedure accessors ──────────────────────────────────────
|
|
107
|
+
get invoice() {
|
|
108
|
+
const c = this;
|
|
109
|
+
return {
|
|
110
|
+
list(input) {
|
|
111
|
+
return c.query("invoice.list", input);
|
|
112
|
+
},
|
|
113
|
+
create(input) {
|
|
114
|
+
return c.mutate("invoice.create", input);
|
|
115
|
+
},
|
|
116
|
+
get(id) {
|
|
117
|
+
return c.query("invoice.get", { id });
|
|
118
|
+
},
|
|
119
|
+
updateStatus(id, status) {
|
|
120
|
+
return c.mutate("invoice.updateStatus", { id, status });
|
|
121
|
+
},
|
|
122
|
+
delete(id) {
|
|
123
|
+
return c.mutate("invoice.delete", { id });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
get party() {
|
|
128
|
+
const c = this;
|
|
129
|
+
return {
|
|
130
|
+
list(input) {
|
|
131
|
+
return c.query("party.list", input);
|
|
132
|
+
},
|
|
133
|
+
create(input) {
|
|
134
|
+
return c.mutate("party.create", input);
|
|
135
|
+
},
|
|
136
|
+
get(id) {
|
|
137
|
+
return c.query("party.get", { id });
|
|
138
|
+
},
|
|
139
|
+
ledger(partyId, input) {
|
|
140
|
+
return c.query("party.ledger", { partyId, ...input });
|
|
141
|
+
},
|
|
142
|
+
update(id, data) {
|
|
143
|
+
return c.mutate("party.update", { id, data });
|
|
144
|
+
},
|
|
145
|
+
delete(id) {
|
|
146
|
+
return c.mutate("party.delete", { id });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
get item() {
|
|
151
|
+
const c = this;
|
|
152
|
+
return {
|
|
153
|
+
list(input) {
|
|
154
|
+
return c.query("item.list", input);
|
|
155
|
+
},
|
|
156
|
+
create(input) {
|
|
157
|
+
return c.mutate("item.create", input);
|
|
158
|
+
},
|
|
159
|
+
get(id) {
|
|
160
|
+
return c.query("item.get", { id });
|
|
161
|
+
},
|
|
162
|
+
adjustStock(input) {
|
|
163
|
+
return c.mutate("item.adjustStock", input);
|
|
164
|
+
},
|
|
165
|
+
update(id, data) {
|
|
166
|
+
return c.mutate("item.update", { id, data });
|
|
167
|
+
},
|
|
168
|
+
delete(id) {
|
|
169
|
+
return c.mutate("item.delete", { id });
|
|
170
|
+
},
|
|
171
|
+
categories() {
|
|
172
|
+
return c.query("item.categories");
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
get payment() {
|
|
177
|
+
const c = this;
|
|
178
|
+
return {
|
|
179
|
+
list(input) {
|
|
180
|
+
return c.query("payment.list", input);
|
|
181
|
+
},
|
|
182
|
+
create(input) {
|
|
183
|
+
return c.mutate("payment.create", input);
|
|
184
|
+
},
|
|
185
|
+
getById(id) {
|
|
186
|
+
return c.query("payment.getById", { id });
|
|
187
|
+
},
|
|
188
|
+
update(input) {
|
|
189
|
+
return c.mutate("payment.update", input);
|
|
190
|
+
},
|
|
191
|
+
delete(id) {
|
|
192
|
+
return c.mutate("payment.delete", { id });
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
get expense() {
|
|
197
|
+
const c = this;
|
|
198
|
+
return {
|
|
199
|
+
list(input) {
|
|
200
|
+
return c.query("expense.list", input);
|
|
201
|
+
},
|
|
202
|
+
create(input) {
|
|
203
|
+
return c.mutate("expense.create", input);
|
|
204
|
+
},
|
|
205
|
+
update(id, data) {
|
|
206
|
+
return c.mutate("expense.update", { id, data });
|
|
207
|
+
},
|
|
208
|
+
delete(id) {
|
|
209
|
+
return c.mutate("expense.delete", { id });
|
|
210
|
+
},
|
|
211
|
+
categories() {
|
|
212
|
+
return c.query("expense.categories");
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
get dashboard() {
|
|
217
|
+
const c = this;
|
|
218
|
+
return {
|
|
219
|
+
summary(input) {
|
|
220
|
+
return c.query("dashboard.summary", input);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
get business() {
|
|
225
|
+
const c = this;
|
|
226
|
+
return {
|
|
227
|
+
get() {
|
|
228
|
+
return c.query("business.get");
|
|
229
|
+
},
|
|
230
|
+
list() {
|
|
231
|
+
return c.query("business.list");
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
get gst() {
|
|
236
|
+
const c = this;
|
|
237
|
+
return {
|
|
238
|
+
gstr1(input) {
|
|
239
|
+
return c.query("gst.gstr1", input);
|
|
240
|
+
},
|
|
241
|
+
gstr3b(input) {
|
|
242
|
+
return c.query("gst.gstr3b", input);
|
|
243
|
+
},
|
|
244
|
+
gstr1CSV(input) {
|
|
245
|
+
return c.query("gst.gstr1CSV", input);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
get shipment() {
|
|
250
|
+
const c = this;
|
|
251
|
+
return {
|
|
252
|
+
list(input) {
|
|
253
|
+
return c.query("shipment.list", input);
|
|
254
|
+
},
|
|
255
|
+
get(id) {
|
|
256
|
+
return c.query("shipment.getById", { id });
|
|
257
|
+
},
|
|
258
|
+
create(input) {
|
|
259
|
+
return c.mutate("shipment.create", input);
|
|
260
|
+
},
|
|
261
|
+
update(input) {
|
|
262
|
+
return c.mutate("shipment.update", input);
|
|
263
|
+
},
|
|
264
|
+
delete(id) {
|
|
265
|
+
return c.mutate("shipment.delete", { id });
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
get bankAccount() {
|
|
270
|
+
const c = this;
|
|
271
|
+
return {
|
|
272
|
+
list() {
|
|
273
|
+
return c.query("bankAccount.list");
|
|
274
|
+
},
|
|
275
|
+
get(id) {
|
|
276
|
+
return c.query("bankAccount.getById", { id });
|
|
277
|
+
},
|
|
278
|
+
create(input) {
|
|
279
|
+
return c.mutate("bankAccount.create", input);
|
|
280
|
+
},
|
|
281
|
+
update(id, data) {
|
|
282
|
+
return c.mutate("bankAccount.update", { id, data });
|
|
283
|
+
},
|
|
284
|
+
delete(id) {
|
|
285
|
+
return c.mutate("bankAccount.delete", { id });
|
|
286
|
+
},
|
|
287
|
+
transfer(input) {
|
|
288
|
+
return c.mutate("bankAccount.transfer", input);
|
|
289
|
+
},
|
|
290
|
+
listTransactions(input) {
|
|
291
|
+
return c.query("bankAccount.listTransactions", input);
|
|
292
|
+
},
|
|
293
|
+
summary() {
|
|
294
|
+
return c.query("bankAccount.summary");
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
get reports() {
|
|
299
|
+
const c = this;
|
|
300
|
+
return {
|
|
301
|
+
daybook(input) {
|
|
302
|
+
return c.query("reports.daybook", input);
|
|
303
|
+
},
|
|
304
|
+
outstanding(input) {
|
|
305
|
+
return c.query("reports.outstanding", input);
|
|
306
|
+
},
|
|
307
|
+
taxSummary(input) {
|
|
308
|
+
return c.query("reports.taxSummary", input);
|
|
309
|
+
},
|
|
310
|
+
itemSales(input) {
|
|
311
|
+
return c.query("reports.itemSales", input);
|
|
312
|
+
},
|
|
313
|
+
stockSummary(input) {
|
|
314
|
+
return c.query("reports.stockSummary", input);
|
|
315
|
+
},
|
|
316
|
+
partyStatement(input) {
|
|
317
|
+
return c.query("reports.partyStatement", input);
|
|
318
|
+
},
|
|
319
|
+
paymentSummary(input) {
|
|
320
|
+
return c.query("reports.paymentSummary", input);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
get store() {
|
|
325
|
+
const c = this;
|
|
326
|
+
return {
|
|
327
|
+
getSettings() {
|
|
328
|
+
return c.query("store.getSettings");
|
|
329
|
+
},
|
|
330
|
+
updateSettings(input) {
|
|
331
|
+
return c.mutate("store.updateSettings", input);
|
|
332
|
+
},
|
|
333
|
+
listOrders(input) {
|
|
334
|
+
return c.query("store.listOrders", input);
|
|
335
|
+
},
|
|
336
|
+
getOrder(id) {
|
|
337
|
+
return c.query("store.getOrder", { id });
|
|
338
|
+
},
|
|
339
|
+
updateOrderStatus(input) {
|
|
340
|
+
return c.mutate("store.updateOrderStatus", input);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
get target() {
|
|
345
|
+
const c = this;
|
|
346
|
+
return {
|
|
347
|
+
list(input) {
|
|
348
|
+
return c.query("target.list", input);
|
|
349
|
+
},
|
|
350
|
+
create(input) {
|
|
351
|
+
return c.mutate("target.create", input);
|
|
352
|
+
},
|
|
353
|
+
getProgress(id) {
|
|
354
|
+
return c.query("target.getProgress", { id });
|
|
355
|
+
},
|
|
356
|
+
update(input) {
|
|
357
|
+
return c.mutate("target.update", input);
|
|
358
|
+
},
|
|
359
|
+
delete(id) {
|
|
360
|
+
return c.mutate("target.delete", { id });
|
|
361
|
+
},
|
|
362
|
+
myTargets() {
|
|
363
|
+
return c.query("target.myTargets");
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
get import() {
|
|
368
|
+
const c = this;
|
|
369
|
+
return {
|
|
370
|
+
importParties(input) {
|
|
371
|
+
return c.mutate("import.importParties", input);
|
|
372
|
+
},
|
|
373
|
+
importItems(input) {
|
|
374
|
+
return c.mutate("import.importItems", input);
|
|
375
|
+
},
|
|
376
|
+
importInvoices(input) {
|
|
377
|
+
return c.mutate("import.importInvoices", input);
|
|
378
|
+
},
|
|
379
|
+
importPayments(input) {
|
|
380
|
+
return c.mutate("import.importPayments", input);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// src/tools/invoice.ts
|
|
387
|
+
import { z } from "zod";
|
|
388
|
+
|
|
389
|
+
// src/lib/errors.ts
|
|
390
|
+
function wrapTool(handler) {
|
|
391
|
+
return async (input) => {
|
|
392
|
+
try {
|
|
393
|
+
return await handler(input);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
const hisaaboErr = toHisaaboError(err);
|
|
396
|
+
return {
|
|
397
|
+
isError: true,
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: formatHisaaboError(hisaaboErr)
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function toHisaaboError(err) {
|
|
409
|
+
if (err instanceof HisaaboApiError) {
|
|
410
|
+
return err.hisaaboError;
|
|
411
|
+
}
|
|
412
|
+
const message = err instanceof Error ? err.message : "An unexpected error occurred. Check server logs.";
|
|
413
|
+
return { code: "api_error", message };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/lib/pagination.ts
|
|
417
|
+
var envCap = parseInt(process.env.HISAABO_MCP_PAGE_SIZE ?? "25", 10);
|
|
418
|
+
var MAX_PAGE_SIZE = Math.min(Math.max(isNaN(envCap) ? 25 : envCap, 1), 50);
|
|
419
|
+
function withPaginationMeta(result) {
|
|
420
|
+
return {
|
|
421
|
+
...result,
|
|
422
|
+
hasMore: result.total > result.page * result.limit
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/tools/invoice.ts
|
|
427
|
+
var INVOICE_STATUS = ["draft", "unfulfilled", "sent", "paid", "partial", "overdue", "cancelled"];
|
|
428
|
+
var DOCUMENT_TYPE = ["invoice", "quotation", "credit_note", "debit_note", "delivery_challan", "proforma", "sales_return", "purchase_return"];
|
|
429
|
+
function registerInvoiceTools(server2, client2) {
|
|
430
|
+
server2.tool(
|
|
431
|
+
"invoice_list",
|
|
432
|
+
[
|
|
433
|
+
"List invoices for the active business. Returns up to 25 invoices per page with pagination metadata.",
|
|
434
|
+
"Use `page` to fetch subsequent pages when `hasMore` is true in the response.",
|
|
435
|
+
"Example: to find all unpaid invoices from a customer, set status='sent' or status='partial' and party_id=<uuid>.",
|
|
436
|
+
"To find overdue invoices across all customers, set status='overdue'."
|
|
437
|
+
].join(" "),
|
|
438
|
+
{
|
|
439
|
+
type: z.enum(["sale", "purchase"]).optional().describe("sale = customer invoices, purchase = supplier bills. Omit to return both."),
|
|
440
|
+
document_type: z.enum(DOCUMENT_TYPE).optional().describe("Filter by document type (default: invoice). Use 'quotation' for quotes, 'credit_note' for credits."),
|
|
441
|
+
status: z.enum(INVOICE_STATUS).optional().describe("Filter by status. Common values: 'sent' (awaiting payment), 'paid', 'overdue', 'draft'."),
|
|
442
|
+
party_id: z.string().uuid().optional().describe("UUID of a specific customer or supplier to filter by."),
|
|
443
|
+
from_date: z.string().datetime().optional().describe("Start of invoice date range (ISO 8601, e.g. '2024-04-01T00:00:00Z')."),
|
|
444
|
+
to_date: z.string().datetime().optional().describe("End of invoice date range (ISO 8601)."),
|
|
445
|
+
search: z.string().max(200).optional().describe("Search by invoice number or party name (partial match)."),
|
|
446
|
+
sort_by: z.enum(["date", "amount", "number"]).optional().describe("Sort field. Defaults to date descending."),
|
|
447
|
+
sort_dir: z.enum(["asc", "desc"]).optional().describe("Sort direction. Defaults to desc."),
|
|
448
|
+
page: z.number().int().min(1).default(1).describe("Page number (1-indexed). Use with hasMore in the response to paginate.")
|
|
449
|
+
},
|
|
450
|
+
wrapTool(async (input) => {
|
|
451
|
+
const result = await client2.invoice.list({
|
|
452
|
+
type: input.type,
|
|
453
|
+
documentType: input.document_type,
|
|
454
|
+
status: input.status,
|
|
455
|
+
partyId: input.party_id,
|
|
456
|
+
fromDate: input.from_date,
|
|
457
|
+
toDate: input.to_date,
|
|
458
|
+
search: input.search,
|
|
459
|
+
sortBy: input.sort_by,
|
|
460
|
+
sortDir: input.sort_dir,
|
|
461
|
+
page: input.page,
|
|
462
|
+
limit: MAX_PAGE_SIZE
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
content: [{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
468
|
+
}]
|
|
469
|
+
};
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
server2.tool(
|
|
473
|
+
"invoice_create",
|
|
474
|
+
[
|
|
475
|
+
"Create a new invoice or bill for the active business. Returns the created invoice with its assigned invoice number.",
|
|
476
|
+
"For sale invoices (customer billing), set type='sale'. For purchase bills (supplier invoices), set type='purchase'.",
|
|
477
|
+
"Each line item requires a description, quantity (decimal string), and unit_price (decimal string, no currency symbol).",
|
|
478
|
+
"Monetary values are always decimal strings, e.g. '1500.00' not 1500.",
|
|
479
|
+
"If a line item corresponds to an inventory item, set item_id to link it and update stock automatically.",
|
|
480
|
+
"Example: { party_id: 'uuid', type: 'sale', line_items: [{ description: 'Web Design', quantity: '1.00', unit_price: '15000.00', tax_percent: '18.00' }] }"
|
|
481
|
+
].join(" "),
|
|
482
|
+
{
|
|
483
|
+
party_id: z.string().uuid().describe("UUID of the customer (for sales) or supplier (for purchases). Use party_list to find UUIDs."),
|
|
484
|
+
type: z.enum(["sale", "purchase"]).describe("'sale' for customer invoices (money coming in), 'purchase' for supplier bills (money going out)."),
|
|
485
|
+
document_type: z.enum(DOCUMENT_TYPE).optional().describe("Document type (default: 'invoice'). Use 'quotation' to create a quote instead of an invoice."),
|
|
486
|
+
line_items: z.array(z.object({
|
|
487
|
+
description: z.string().min(1).max(500).describe("Product or service name/description."),
|
|
488
|
+
quantity: z.string().regex(/^\d+(\.\d{1,3})?$/).describe("Quantity as decimal string, e.g. '1.000', '7.500', '100'."),
|
|
489
|
+
unit_price: z.string().regex(/^\d+(\.\d{1,2})?$/).describe("Price per unit as decimal string, e.g. '250.00', '15000.00'."),
|
|
490
|
+
tax_percent: z.string().regex(/^\d+(\.\d{1,2})?$/).default("0").describe("GST/tax rate percentage as decimal string: '0', '5.00', '12.00', '18.00', '28.00'."),
|
|
491
|
+
discount_percent: z.string().regex(/^\d+(\.\d{1,2})?$/).default("0").describe("Line-level discount percentage, e.g. '10.00' for 10% off."),
|
|
492
|
+
item_id: z.string().uuid().optional().describe("Link to an inventory item UUID to auto-fill price and update stock (optional).")
|
|
493
|
+
})).min(1).describe("At least one line item is required."),
|
|
494
|
+
invoice_date: z.string().datetime().optional().describe("Invoice date (ISO 8601). Defaults to today if omitted."),
|
|
495
|
+
due_date: z.string().datetime().optional().describe("Payment due date (ISO 8601). Set to enforce credit terms."),
|
|
496
|
+
notes: z.string().max(2e3).optional().describe("Notes visible on the printed invoice, e.g. bank account details or thank-you message."),
|
|
497
|
+
terms_and_conditions: z.string().max(2e3).optional().describe("Terms and conditions text printed on the invoice."),
|
|
498
|
+
invoice_discount: z.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Invoice-level discount. Interpretation depends on invoice_discount_type."),
|
|
499
|
+
invoice_discount_type: z.enum(["amount", "percent"]).optional().describe("Whether invoice_discount is a flat amount or percentage (default: 'amount')."),
|
|
500
|
+
round_off: z.string().regex(/^-?\d+(\.\d{1,2})?$/).optional().describe("Round-off adjustment for the total, e.g. '0.50' or '-0.25' (typically small). Default '0'."),
|
|
501
|
+
reference_document_id: z.string().uuid().optional().describe("UUID of a source document (e.g. quotation UUID when converting quote to invoice).")
|
|
502
|
+
},
|
|
503
|
+
wrapTool(async (input) => {
|
|
504
|
+
const invoice = await client2.invoice.create({
|
|
505
|
+
partyId: input.party_id,
|
|
506
|
+
type: input.type,
|
|
507
|
+
documentType: input.document_type,
|
|
508
|
+
invoiceDate: input.invoice_date,
|
|
509
|
+
dueDate: input.due_date,
|
|
510
|
+
notes: input.notes,
|
|
511
|
+
termsAndConditions: input.terms_and_conditions,
|
|
512
|
+
invoiceDiscount: input.invoice_discount,
|
|
513
|
+
invoiceDiscountType: input.invoice_discount_type,
|
|
514
|
+
roundOff: input.round_off,
|
|
515
|
+
referenceDocumentId: input.reference_document_id,
|
|
516
|
+
lineItems: input.line_items.map((li) => ({
|
|
517
|
+
description: li.description,
|
|
518
|
+
quantity: li.quantity,
|
|
519
|
+
unitPrice: li.unit_price,
|
|
520
|
+
taxPercent: li.tax_percent,
|
|
521
|
+
discountPercent: li.discount_percent,
|
|
522
|
+
itemId: li.item_id
|
|
523
|
+
}))
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
content: [{
|
|
527
|
+
type: "text",
|
|
528
|
+
text: JSON.stringify(invoice, null, 2)
|
|
529
|
+
}]
|
|
530
|
+
};
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
server2.tool(
|
|
534
|
+
"invoice_get",
|
|
535
|
+
[
|
|
536
|
+
"Get the full details of a single invoice, including all line items, payment history, and outstanding balance.",
|
|
537
|
+
"Use this after invoice_list to get complete details. The 'balanceDue' field shows how much is still owed.",
|
|
538
|
+
"Payments already applied to this invoice appear in the response."
|
|
539
|
+
].join(" "),
|
|
540
|
+
{
|
|
541
|
+
invoice_id: z.string().uuid().describe("Invoice UUID from invoice_list or invoice_create.")
|
|
542
|
+
},
|
|
543
|
+
wrapTool(async (input) => {
|
|
544
|
+
const invoice = await client2.invoice.get(input.invoice_id);
|
|
545
|
+
return {
|
|
546
|
+
content: [{
|
|
547
|
+
type: "text",
|
|
548
|
+
text: JSON.stringify(invoice, null, 2)
|
|
549
|
+
}]
|
|
550
|
+
};
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
server2.tool(
|
|
554
|
+
"invoice_update_status",
|
|
555
|
+
[
|
|
556
|
+
"Change the status of an invoice.",
|
|
557
|
+
"Typical workflow: create (draft) \u2192 mark sent \u2192 payment received (handled by payment_create, which auto-updates status to 'paid' or 'partial').",
|
|
558
|
+
"Use this tool to manually set status to 'sent' (invoice delivered), 'cancelled' (void the invoice), or 'overdue' (mark past due).",
|
|
559
|
+
"Note: 'paid' and 'partial' status is normally set automatically when payments are recorded \u2014 prefer payment_create over forcing 'paid' here."
|
|
560
|
+
].join(" "),
|
|
561
|
+
{
|
|
562
|
+
invoice_id: z.string().uuid().describe("Invoice UUID."),
|
|
563
|
+
status: z.enum(INVOICE_STATUS).describe("New status. 'sent' = delivered to customer. 'cancelled' = voided. 'draft' = revert to draft.")
|
|
564
|
+
},
|
|
565
|
+
wrapTool(async (input) => {
|
|
566
|
+
const result = await client2.invoice.updateStatus(input.invoice_id, input.status);
|
|
567
|
+
return {
|
|
568
|
+
content: [{
|
|
569
|
+
type: "text",
|
|
570
|
+
text: JSON.stringify(result, null, 2)
|
|
571
|
+
}]
|
|
572
|
+
};
|
|
573
|
+
})
|
|
574
|
+
);
|
|
575
|
+
server2.tool(
|
|
576
|
+
"invoice_delete",
|
|
577
|
+
[
|
|
578
|
+
"Soft-delete an invoice (marks it as cancelled and hides it from lists).",
|
|
579
|
+
"Admins can delete any invoice. Seller managers can only delete unpaid invoices created within the last 2 hours.",
|
|
580
|
+
"Paid invoices cannot be deleted \u2014 void them by recording a credit note instead.",
|
|
581
|
+
"This is a soft delete \u2014 the invoice is not physically removed from the database."
|
|
582
|
+
].join(" "),
|
|
583
|
+
{
|
|
584
|
+
invoice_id: z.string().uuid().describe("Invoice UUID to delete.")
|
|
585
|
+
},
|
|
586
|
+
wrapTool(async (input) => {
|
|
587
|
+
const result = await client2.invoice.delete(input.invoice_id);
|
|
588
|
+
return {
|
|
589
|
+
content: [{
|
|
590
|
+
type: "text",
|
|
591
|
+
text: JSON.stringify(result, null, 2)
|
|
592
|
+
}]
|
|
593
|
+
};
|
|
594
|
+
})
|
|
595
|
+
);
|
|
596
|
+
server2.tool(
|
|
597
|
+
"invoice_pdf_url",
|
|
598
|
+
[
|
|
599
|
+
"Get the URL to download or view an invoice as a PDF.",
|
|
600
|
+
"The URL requires the HISAABO_TOKEN for authentication (pass as a Bearer token).",
|
|
601
|
+
"Use format='a4' for standard invoices and format='thermal' for 80mm receipt printing.",
|
|
602
|
+
"The URL is valid for as long as the session token is valid."
|
|
603
|
+
].join(" "),
|
|
604
|
+
{
|
|
605
|
+
invoice_id: z.string().uuid().describe("Invoice UUID."),
|
|
606
|
+
format: z.enum(["a4", "thermal"]).default("a4").describe("'a4' for standard A4 invoice PDF, 'thermal' for 80mm thermal receipt.")
|
|
607
|
+
},
|
|
608
|
+
wrapTool(async (input) => {
|
|
609
|
+
const url = `${client2.apiUrl}/api/invoice/${input.invoice_id}/pdf?format=${input.format}`;
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: "text",
|
|
613
|
+
text: JSON.stringify({
|
|
614
|
+
url,
|
|
615
|
+
note: "Fetch this URL with the Authorization: Bearer <HISAABO_TOKEN> header to download the PDF.",
|
|
616
|
+
format: input.format
|
|
617
|
+
}, null, 2)
|
|
618
|
+
}]
|
|
619
|
+
};
|
|
620
|
+
})
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/tools/party.ts
|
|
625
|
+
import { z as z2 } from "zod";
|
|
626
|
+
function registerPartyTools(server2, client2) {
|
|
627
|
+
server2.tool(
|
|
628
|
+
"party_list",
|
|
629
|
+
[
|
|
630
|
+
"List customers and suppliers (parties) for the active business.",
|
|
631
|
+
"The 'balance' field in each result shows the outstanding amount: positive = they owe you (receivable), negative = you owe them (payable).",
|
|
632
|
+
"Use filter='outstanding' to find parties with unpaid balances. Use filter='overdue' for parties with overdue invoices.",
|
|
633
|
+
"Use this tool to find party UUIDs before calling invoice_create or payment_create."
|
|
634
|
+
].join(" "),
|
|
635
|
+
{
|
|
636
|
+
type: z2.enum(["customer", "supplier"]).optional().describe("'customer' for buyers, 'supplier' for vendors. Omit to return both."),
|
|
637
|
+
filter: z2.enum(["all", "customer", "supplier", "outstanding", "overdue"]).optional().describe("'outstanding' = parties with unpaid balance, 'overdue' = parties with overdue invoices."),
|
|
638
|
+
search: z2.string().max(200).optional().describe("Search by party name, phone, email, or GSTIN (partial match)."),
|
|
639
|
+
category: z2.string().max(100).optional().describe("Filter by party category tag."),
|
|
640
|
+
sort_by: z2.enum(["name", "balance"]).optional().describe("Sort by name (alphabetical) or balance (largest first)."),
|
|
641
|
+
sort_dir: z2.enum(["asc", "desc"]).optional().describe("Sort direction."),
|
|
642
|
+
page: z2.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
643
|
+
},
|
|
644
|
+
wrapTool(async (input) => {
|
|
645
|
+
const result = await client2.party.list({
|
|
646
|
+
type: input.type,
|
|
647
|
+
filter: input.filter,
|
|
648
|
+
search: input.search,
|
|
649
|
+
category: input.category,
|
|
650
|
+
sortBy: input.sort_by,
|
|
651
|
+
sortDir: input.sort_dir,
|
|
652
|
+
page: input.page,
|
|
653
|
+
limit: MAX_PAGE_SIZE
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
content: [{
|
|
657
|
+
type: "text",
|
|
658
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
659
|
+
}]
|
|
660
|
+
};
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
server2.tool(
|
|
664
|
+
"party_create",
|
|
665
|
+
[
|
|
666
|
+
"Create a new customer or supplier party.",
|
|
667
|
+
"Minimum required: type ('customer' or 'supplier') and name.",
|
|
668
|
+
"For GST-registered parties, provide gstin (format: 22AAAAA0000A1Z5). For unregistered, omit it.",
|
|
669
|
+
"opening_balance sets their starting account balance: positive = they owe you, negative = you owe them."
|
|
670
|
+
].join(" "),
|
|
671
|
+
{
|
|
672
|
+
type: z2.enum(["customer", "supplier"]).describe("'customer' for buyers/clients, 'supplier' for vendors/sellers."),
|
|
673
|
+
name: z2.string().min(1).max(200).describe("Full name of the customer or business."),
|
|
674
|
+
phone: z2.string().max(15).optional().describe("Phone number (digits only, no country code prefix required)."),
|
|
675
|
+
email: z2.string().email().optional().describe("Email address for sending invoices."),
|
|
676
|
+
gstin: z2.string().regex(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/).optional().describe("GST Identification Number (15 characters), e.g. '22AAAAA0000A1Z5'. Omit if unregistered."),
|
|
677
|
+
pan: z2.string().optional().describe("PAN (Permanent Account Number), 10 characters."),
|
|
678
|
+
billing_address: z2.string().max(500).optional().describe("Full billing address."),
|
|
679
|
+
city: z2.string().max(100).optional(),
|
|
680
|
+
state: z2.string().max(100).optional(),
|
|
681
|
+
pincode: z2.string().max(10).optional(),
|
|
682
|
+
opening_balance: z2.string().regex(/^-?\d+(\.\d{1,2})?$/).optional().describe("Starting balance as decimal string. Positive = they owe you, negative = you owe them. Default '0'."),
|
|
683
|
+
category: z2.string().max(100).optional().describe("Category tag for grouping parties, e.g. 'retail', 'wholesale', 'government'."),
|
|
684
|
+
credit_period_days: z2.number().int().min(0).max(365).optional().describe("Number of days before payment is due (e.g. 30 for net-30 terms)."),
|
|
685
|
+
credit_limit: z2.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Maximum credit amount as decimal string.")
|
|
686
|
+
},
|
|
687
|
+
wrapTool(async (input) => {
|
|
688
|
+
const party = await client2.party.create({
|
|
689
|
+
type: input.type,
|
|
690
|
+
name: input.name,
|
|
691
|
+
phone: input.phone,
|
|
692
|
+
email: input.email,
|
|
693
|
+
gstin: input.gstin,
|
|
694
|
+
pan: input.pan,
|
|
695
|
+
billingAddress: input.billing_address,
|
|
696
|
+
city: input.city,
|
|
697
|
+
state: input.state,
|
|
698
|
+
pincode: input.pincode,
|
|
699
|
+
openingBalance: input.opening_balance,
|
|
700
|
+
category: input.category,
|
|
701
|
+
creditPeriodDays: input.credit_period_days,
|
|
702
|
+
creditLimit: input.credit_limit
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
content: [{
|
|
706
|
+
type: "text",
|
|
707
|
+
text: JSON.stringify(party, null, 2)
|
|
708
|
+
}]
|
|
709
|
+
};
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
server2.tool(
|
|
713
|
+
"party_get",
|
|
714
|
+
[
|
|
715
|
+
"Get full details of a single customer or supplier, including their current outstanding balance.",
|
|
716
|
+
"'balance' shows net amount: positive = they owe you, negative = you owe them.",
|
|
717
|
+
"Use this to answer questions like 'What does Acme Corp owe us?' or 'What is our balance with Supplier X?'"
|
|
718
|
+
].join(" "),
|
|
719
|
+
{
|
|
720
|
+
party_id: z2.string().uuid().describe("Party UUID from party_list.")
|
|
721
|
+
},
|
|
722
|
+
wrapTool(async (input) => {
|
|
723
|
+
const party = await client2.party.get(input.party_id);
|
|
724
|
+
return {
|
|
725
|
+
content: [{
|
|
726
|
+
type: "text",
|
|
727
|
+
text: JSON.stringify(party, null, 2)
|
|
728
|
+
}]
|
|
729
|
+
};
|
|
730
|
+
})
|
|
731
|
+
);
|
|
732
|
+
server2.tool(
|
|
733
|
+
"party_update",
|
|
734
|
+
[
|
|
735
|
+
"Update an existing customer or supplier's details.",
|
|
736
|
+
"Only provide fields you want to change \u2014 all other fields remain unchanged.",
|
|
737
|
+
"Note: 'type' (customer/supplier) cannot be changed after creation."
|
|
738
|
+
].join(" "),
|
|
739
|
+
{
|
|
740
|
+
party_id: z2.string().uuid().describe("Party UUID to update."),
|
|
741
|
+
name: z2.string().min(1).max(200).optional().describe("Updated name."),
|
|
742
|
+
phone: z2.string().max(15).optional().describe("Updated phone number."),
|
|
743
|
+
email: z2.string().email().optional().describe("Updated email address."),
|
|
744
|
+
gstin: z2.string().regex(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/).optional().describe("Updated GSTIN (15 characters)."),
|
|
745
|
+
billing_address: z2.string().max(500).optional().describe("Updated billing address."),
|
|
746
|
+
shipping_address: z2.string().max(500).optional().describe("Updated shipping address."),
|
|
747
|
+
city: z2.string().max(100).optional().describe("Updated city."),
|
|
748
|
+
state: z2.string().max(100).optional().describe("Updated state."),
|
|
749
|
+
pincode: z2.string().max(10).optional().describe("Updated PIN code."),
|
|
750
|
+
category: z2.string().max(100).optional().describe("Updated category tag."),
|
|
751
|
+
credit_period_days: z2.number().int().min(0).max(365).optional().describe("Updated credit period in days."),
|
|
752
|
+
credit_limit: z2.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated credit limit as decimal string."),
|
|
753
|
+
contact_person_name: z2.string().max(200).optional().describe("Updated contact person name.")
|
|
754
|
+
},
|
|
755
|
+
wrapTool(async (input) => {
|
|
756
|
+
const { party_id, ...fields } = input;
|
|
757
|
+
const party = await client2.party.update(party_id, {
|
|
758
|
+
name: fields.name,
|
|
759
|
+
phone: fields.phone,
|
|
760
|
+
email: fields.email,
|
|
761
|
+
gstin: fields.gstin,
|
|
762
|
+
billingAddress: fields.billing_address,
|
|
763
|
+
shippingAddress: fields.shipping_address,
|
|
764
|
+
city: fields.city,
|
|
765
|
+
state: fields.state,
|
|
766
|
+
pincode: fields.pincode,
|
|
767
|
+
category: fields.category,
|
|
768
|
+
creditPeriodDays: fields.credit_period_days,
|
|
769
|
+
creditLimit: fields.credit_limit,
|
|
770
|
+
contactPersonName: fields.contact_person_name
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
content: [{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: JSON.stringify(party, null, 2)
|
|
776
|
+
}]
|
|
777
|
+
};
|
|
778
|
+
})
|
|
779
|
+
);
|
|
780
|
+
server2.tool(
|
|
781
|
+
"party_delete",
|
|
782
|
+
[
|
|
783
|
+
"Permanently delete a customer or supplier. Requires admin role.",
|
|
784
|
+
"Warning: this is a hard delete. Associated invoices and payments are not deleted, but the party reference will be broken.",
|
|
785
|
+
"Consider deactivating or archiving instead \u2014 only delete if the party was created in error."
|
|
786
|
+
].join(" "),
|
|
787
|
+
{
|
|
788
|
+
party_id: z2.string().uuid().describe("Party UUID to delete.")
|
|
789
|
+
},
|
|
790
|
+
wrapTool(async (input) => {
|
|
791
|
+
const result = await client2.party.delete(input.party_id);
|
|
792
|
+
return {
|
|
793
|
+
content: [{
|
|
794
|
+
type: "text",
|
|
795
|
+
text: JSON.stringify(result, null, 2)
|
|
796
|
+
}]
|
|
797
|
+
};
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
server2.tool(
|
|
801
|
+
"party_ledger",
|
|
802
|
+
[
|
|
803
|
+
"Get the full transaction ledger (account statement) for a customer or supplier.",
|
|
804
|
+
"Shows all invoices, payments, and credit notes in chronological order with running balance.",
|
|
805
|
+
"Use this to answer 'Show me all transactions with Customer X' or 'What is the payment history for this supplier?'",
|
|
806
|
+
"The closing_balance field is the current net balance for the party."
|
|
807
|
+
].join(" "),
|
|
808
|
+
{
|
|
809
|
+
party_id: z2.string().uuid().describe("Party UUID."),
|
|
810
|
+
from_date: z2.string().datetime().optional().describe("Start date for ledger entries (ISO 8601). Omit for all-time."),
|
|
811
|
+
to_date: z2.string().datetime().optional().describe("End date for ledger entries (ISO 8601)."),
|
|
812
|
+
page: z2.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
813
|
+
},
|
|
814
|
+
wrapTool(async (input) => {
|
|
815
|
+
const ledger = await client2.party.ledger(input.party_id, {
|
|
816
|
+
fromDate: input.from_date,
|
|
817
|
+
toDate: input.to_date,
|
|
818
|
+
page: input.page,
|
|
819
|
+
limit: MAX_PAGE_SIZE
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
content: [{
|
|
823
|
+
type: "text",
|
|
824
|
+
text: JSON.stringify(ledger, null, 2)
|
|
825
|
+
}]
|
|
826
|
+
};
|
|
827
|
+
})
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/tools/item.ts
|
|
832
|
+
import { z as z3 } from "zod";
|
|
833
|
+
function registerItemTools(server2, client2) {
|
|
834
|
+
server2.tool(
|
|
835
|
+
"item_list",
|
|
836
|
+
[
|
|
837
|
+
"List inventory items and services for the active business.",
|
|
838
|
+
"The 'stockQuantity' field shows current stock. Items with stock below 'lowStockAlert' are low-stock.",
|
|
839
|
+
"Use low_stock=true to find items that need restocking.",
|
|
840
|
+
"Use this to find item UUIDs before creating invoices (linking item_id speeds up invoice creation and updates stock)."
|
|
841
|
+
].join(" "),
|
|
842
|
+
{
|
|
843
|
+
search: z3.string().max(200).optional().describe("Search by item name, SKU, or HSN code."),
|
|
844
|
+
category: z3.string().max(100).optional().describe("Filter by category."),
|
|
845
|
+
item_type: z3.enum(["product", "service"]).optional().describe("'product' for physical goods with stock, 'service' for billable services (no stock tracking)."),
|
|
846
|
+
low_stock: z3.boolean().optional().describe("If true, return only items where current stock is below the low-stock alert threshold."),
|
|
847
|
+
page: z3.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
848
|
+
},
|
|
849
|
+
wrapTool(async (input) => {
|
|
850
|
+
const result = await client2.item.list({
|
|
851
|
+
search: input.search,
|
|
852
|
+
category: input.category,
|
|
853
|
+
itemType: input.item_type,
|
|
854
|
+
lowStock: input.low_stock,
|
|
855
|
+
page: input.page,
|
|
856
|
+
limit: MAX_PAGE_SIZE
|
|
857
|
+
});
|
|
858
|
+
return {
|
|
859
|
+
content: [{
|
|
860
|
+
type: "text",
|
|
861
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
862
|
+
}]
|
|
863
|
+
};
|
|
864
|
+
})
|
|
865
|
+
);
|
|
866
|
+
server2.tool(
|
|
867
|
+
"item_create",
|
|
868
|
+
[
|
|
869
|
+
"Create a new inventory item or service.",
|
|
870
|
+
"For physical products, set item_type='product' and provide initial stock_quantity.",
|
|
871
|
+
"For billable services (consulting, installation, etc.), set item_type='service' \u2014 stock is not tracked.",
|
|
872
|
+
"Monetary values (sale_price, purchase_price) are decimal strings without currency symbols: '250.00' not '\u20B9250'.",
|
|
873
|
+
"Setting low_stock_alert triggers warnings when stock falls below that level."
|
|
874
|
+
].join(" "),
|
|
875
|
+
{
|
|
876
|
+
name: z3.string().min(1).max(200).describe("Item name as it should appear on invoices."),
|
|
877
|
+
item_type: z3.enum(["product", "service"]).default("product").describe("'product' for physical goods, 'service' for services (no stock)."),
|
|
878
|
+
unit: z3.enum(["pcs", "kg", "g", "l", "ml", "m", "cm", "ft", "in", "box", "dozen", "pair", "set", "pkt", "bun", "pouch", "jar", "btl", "bag", "ton", "pack", "pet", "person", "other"]).optional().describe("Unit of measurement. Default 'pcs'. Use 'kg' for weight, 'l' for liquids, etc."),
|
|
879
|
+
sale_price: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Default selling price per unit as decimal string, e.g. '250.00'."),
|
|
880
|
+
purchase_price: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Default purchase/cost price per unit as decimal string."),
|
|
881
|
+
tax_percent: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Default GST/tax rate percentage, e.g. '18.00'. Default '0'."),
|
|
882
|
+
stock_quantity: z3.string().regex(/^-?\d+(\.\d{1,3})?$/).optional().describe("Opening stock quantity as decimal string, e.g. '100.000'. Default '0'."),
|
|
883
|
+
low_stock_alert: z3.string().regex(/^\d+(\.\d{1,3})?$/).optional().describe("Alert threshold: warn when stock falls below this quantity."),
|
|
884
|
+
hsn: z3.string().max(20).optional().describe("HSN (Harmonized System of Nomenclature) code for GST compliance."),
|
|
885
|
+
sku: z3.string().max(50).optional().describe("SKU (Stock Keeping Unit) \u2014 your internal product code."),
|
|
886
|
+
description: z3.string().max(1e3).optional().describe("Internal description (not shown on invoices)."),
|
|
887
|
+
category: z3.string().max(100).optional().describe("Category for grouping, e.g. 'Electronics', 'Raw Materials'.")
|
|
888
|
+
},
|
|
889
|
+
wrapTool(async (input) => {
|
|
890
|
+
const item = await client2.item.create({
|
|
891
|
+
name: input.name,
|
|
892
|
+
itemType: input.item_type,
|
|
893
|
+
unit: input.unit,
|
|
894
|
+
salePrice: input.sale_price,
|
|
895
|
+
purchasePrice: input.purchase_price,
|
|
896
|
+
taxPercent: input.tax_percent,
|
|
897
|
+
stockQuantity: input.stock_quantity,
|
|
898
|
+
lowStockAlert: input.low_stock_alert,
|
|
899
|
+
hsn: input.hsn,
|
|
900
|
+
sku: input.sku,
|
|
901
|
+
description: input.description,
|
|
902
|
+
category: input.category
|
|
903
|
+
});
|
|
904
|
+
return {
|
|
905
|
+
content: [{
|
|
906
|
+
type: "text",
|
|
907
|
+
text: JSON.stringify(item, null, 2)
|
|
908
|
+
}]
|
|
909
|
+
};
|
|
910
|
+
})
|
|
911
|
+
);
|
|
912
|
+
server2.tool(
|
|
913
|
+
"item_get",
|
|
914
|
+
[
|
|
915
|
+
"Get full details of a single inventory item, including current stock quantity and variant information.",
|
|
916
|
+
"Use this to check current stock levels or get the full item spec before creating invoices."
|
|
917
|
+
].join(" "),
|
|
918
|
+
{
|
|
919
|
+
item_id: z3.string().uuid().describe("Item UUID from item_list.")
|
|
920
|
+
},
|
|
921
|
+
wrapTool(async (input) => {
|
|
922
|
+
const item = await client2.item.get(input.item_id);
|
|
923
|
+
return {
|
|
924
|
+
content: [{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: JSON.stringify(item, null, 2)
|
|
927
|
+
}]
|
|
928
|
+
};
|
|
929
|
+
})
|
|
930
|
+
);
|
|
931
|
+
server2.tool(
|
|
932
|
+
"item_update",
|
|
933
|
+
[
|
|
934
|
+
"Update an existing inventory item's details, pricing, or stock settings.",
|
|
935
|
+
"Only provide fields you want to change \u2014 all other fields remain unchanged.",
|
|
936
|
+
"Changing sale_price or purchase_price updates the default price for future invoices but does not retroactively change past invoice line items."
|
|
937
|
+
].join(" "),
|
|
938
|
+
{
|
|
939
|
+
item_id: z3.string().uuid().describe("Item UUID to update."),
|
|
940
|
+
name: z3.string().min(1).max(200).optional().describe("Updated item name."),
|
|
941
|
+
sale_price: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated selling price per unit as decimal string."),
|
|
942
|
+
purchase_price: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated purchase/cost price per unit as decimal string."),
|
|
943
|
+
tax_percent: z3.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated GST/tax rate percentage, e.g. '18.00'."),
|
|
944
|
+
low_stock_alert: z3.string().regex(/^\d+(\.\d{1,3})?$/).optional().describe("Updated low-stock alert threshold."),
|
|
945
|
+
hsn: z3.string().max(20).optional().describe("Updated HSN code."),
|
|
946
|
+
sku: z3.string().max(50).optional().describe("Updated SKU."),
|
|
947
|
+
description: z3.string().max(1e3).optional().describe("Updated internal description."),
|
|
948
|
+
category: z3.string().max(100).optional().describe("Updated category.")
|
|
949
|
+
},
|
|
950
|
+
wrapTool(async (input) => {
|
|
951
|
+
const { item_id, ...fields } = input;
|
|
952
|
+
const item = await client2.item.update(item_id, {
|
|
953
|
+
name: fields.name,
|
|
954
|
+
salePrice: fields.sale_price,
|
|
955
|
+
purchasePrice: fields.purchase_price,
|
|
956
|
+
taxPercent: fields.tax_percent,
|
|
957
|
+
lowStockAlert: fields.low_stock_alert,
|
|
958
|
+
hsn: fields.hsn,
|
|
959
|
+
sku: fields.sku,
|
|
960
|
+
description: fields.description,
|
|
961
|
+
category: fields.category
|
|
962
|
+
});
|
|
963
|
+
return {
|
|
964
|
+
content: [{
|
|
965
|
+
type: "text",
|
|
966
|
+
text: JSON.stringify(item, null, 2)
|
|
967
|
+
}]
|
|
968
|
+
};
|
|
969
|
+
})
|
|
970
|
+
);
|
|
971
|
+
server2.tool(
|
|
972
|
+
"item_delete",
|
|
973
|
+
[
|
|
974
|
+
"Permanently delete an inventory item. Requires admin role.",
|
|
975
|
+
"Warning: this is a hard delete \u2014 it removes the item record and its variants.",
|
|
976
|
+
"Existing invoice line items that reference this item are not deleted (they retain the data at time of invoicing).",
|
|
977
|
+
"Only delete if the item was created in error. For discontinued items, consider just setting them inactive."
|
|
978
|
+
].join(" "),
|
|
979
|
+
{
|
|
980
|
+
item_id: z3.string().uuid().describe("Item UUID to delete.")
|
|
981
|
+
},
|
|
982
|
+
wrapTool(async (input) => {
|
|
983
|
+
const result = await client2.item.delete(input.item_id);
|
|
984
|
+
return {
|
|
985
|
+
content: [{
|
|
986
|
+
type: "text",
|
|
987
|
+
text: JSON.stringify(result, null, 2)
|
|
988
|
+
}]
|
|
989
|
+
};
|
|
990
|
+
})
|
|
991
|
+
);
|
|
992
|
+
server2.tool(
|
|
993
|
+
"item_categories",
|
|
994
|
+
[
|
|
995
|
+
"Get a list of all distinct item categories used in the business.",
|
|
996
|
+
"Use this to discover valid category names before filtering item_list by category or creating items."
|
|
997
|
+
].join(" "),
|
|
998
|
+
{},
|
|
999
|
+
wrapTool(async (_input) => {
|
|
1000
|
+
const categories = await client2.item.categories();
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{
|
|
1003
|
+
type: "text",
|
|
1004
|
+
text: JSON.stringify(categories, null, 2)
|
|
1005
|
+
}]
|
|
1006
|
+
};
|
|
1007
|
+
})
|
|
1008
|
+
);
|
|
1009
|
+
server2.tool(
|
|
1010
|
+
"item_adjust_stock",
|
|
1011
|
+
[
|
|
1012
|
+
"Record a manual stock adjustment for an inventory item.",
|
|
1013
|
+
"Use a positive adjustment to add stock (e.g. '+50' for stock received) and a negative adjustment to remove stock (e.g. '-5' for damaged goods).",
|
|
1014
|
+
"Every adjustment is recorded in the audit log \u2014 always provide a reason.",
|
|
1015
|
+
"Example: to record receiving 100 units from a supplier, set adjustment='+100' and reason='Stock received from Supplier X'."
|
|
1016
|
+
].join(" "),
|
|
1017
|
+
{
|
|
1018
|
+
item_id: z3.string().uuid().describe("Item UUID from item_list."),
|
|
1019
|
+
adjustment: z3.string().regex(/^[+-]?\d+(\.\d{1,3})?$/).describe("Signed quantity change as decimal string. '+50' or '50' to add, '-10' to subtract."),
|
|
1020
|
+
reason: z3.string().max(500).optional().describe("Reason for the adjustment, e.g. 'Stock received from supplier', 'Damaged goods write-off'.")
|
|
1021
|
+
},
|
|
1022
|
+
wrapTool(async (input) => {
|
|
1023
|
+
const result = await client2.item.adjustStock({
|
|
1024
|
+
itemId: input.item_id,
|
|
1025
|
+
adjustment: input.adjustment,
|
|
1026
|
+
reason: input.reason
|
|
1027
|
+
});
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: JSON.stringify(result, null, 2)
|
|
1032
|
+
}]
|
|
1033
|
+
};
|
|
1034
|
+
})
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/tools/payment.ts
|
|
1039
|
+
import { z as z4 } from "zod";
|
|
1040
|
+
var PAYMENT_MODES = ["cash", "bank", "upi", "cheque", "other"];
|
|
1041
|
+
function registerPaymentTools(server2, client2) {
|
|
1042
|
+
server2.tool(
|
|
1043
|
+
"payment_create",
|
|
1044
|
+
[
|
|
1045
|
+
"Record a payment received from a customer or made to a supplier.",
|
|
1046
|
+
"If the payment is for a specific invoice, set invoice_id \u2014 the invoice status will update automatically to 'paid' or 'partial'.",
|
|
1047
|
+
"If the payment is not tied to a specific invoice (advance payment or bulk payment), omit invoice_id \u2014 it applies to the party's account balance.",
|
|
1048
|
+
"To split a payment across multiple invoices, use the allocations array instead of invoice_id.",
|
|
1049
|
+
"Example: customer paid invoice INV-001 by UPI: { party_id: '<uuid>', amount: '5000.00', mode: 'upi', invoice_id: '<uuid>' }"
|
|
1050
|
+
].join(" "),
|
|
1051
|
+
{
|
|
1052
|
+
party_id: z4.string().uuid().describe("UUID of the customer (payment received) or supplier (payment made)."),
|
|
1053
|
+
amount: z4.string().regex(/^\d+(\.\d{1,2})?$/).describe("Payment amount as decimal string, e.g. '5000.00'."),
|
|
1054
|
+
mode: z4.enum(PAYMENT_MODES).describe("Payment method: 'cash', 'bank' (bank transfer/NEFT/RTGS), 'upi', 'cheque', or 'other'."),
|
|
1055
|
+
invoice_id: z4.string().uuid().optional().describe("Link this payment to a specific invoice UUID. The invoice status updates automatically. Omit for advance payments."),
|
|
1056
|
+
discount: z4.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Discount or write-off amount applied alongside this payment, e.g. '100.00'. Default '0'."),
|
|
1057
|
+
reference_number: z4.string().max(100).optional().describe("Transaction reference, UTR number, cheque number, or any payment identifier."),
|
|
1058
|
+
payment_date: z4.string().datetime().optional().describe("Date the payment was received/made (ISO 8601). Defaults to today."),
|
|
1059
|
+
notes: z4.string().max(500).optional().describe("Internal notes about this payment."),
|
|
1060
|
+
bank_account_id: z4.string().uuid().optional().describe("Bank/cash account UUID to record this transaction against (for cash flow tracking)."),
|
|
1061
|
+
allocations: z4.array(z4.object({
|
|
1062
|
+
invoice_id: z4.string().uuid().describe("Invoice UUID to allocate part of this payment to."),
|
|
1063
|
+
amount: z4.string().regex(/^\d+(\.\d{1,2})?$/).describe("Amount to allocate to this invoice.")
|
|
1064
|
+
})).optional().describe("Allocate a single payment across multiple invoices. Use instead of invoice_id when splitting a payment.")
|
|
1065
|
+
},
|
|
1066
|
+
wrapTool(async (input) => {
|
|
1067
|
+
const payment = await client2.payment.create({
|
|
1068
|
+
partyId: input.party_id,
|
|
1069
|
+
amount: input.amount,
|
|
1070
|
+
mode: input.mode,
|
|
1071
|
+
invoiceId: input.invoice_id,
|
|
1072
|
+
discount: input.discount,
|
|
1073
|
+
referenceNumber: input.reference_number,
|
|
1074
|
+
paymentDate: input.payment_date,
|
|
1075
|
+
notes: input.notes,
|
|
1076
|
+
bankAccountId: input.bank_account_id,
|
|
1077
|
+
allocations: input.allocations?.map((a) => ({
|
|
1078
|
+
invoiceId: a.invoice_id,
|
|
1079
|
+
amount: a.amount
|
|
1080
|
+
}))
|
|
1081
|
+
});
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{
|
|
1084
|
+
type: "text",
|
|
1085
|
+
text: JSON.stringify(payment, null, 2)
|
|
1086
|
+
}]
|
|
1087
|
+
};
|
|
1088
|
+
})
|
|
1089
|
+
);
|
|
1090
|
+
server2.tool(
|
|
1091
|
+
"payment_get",
|
|
1092
|
+
[
|
|
1093
|
+
"Get full details of a single payment, including the invoices it was applied to.",
|
|
1094
|
+
"The 'linkedInvoices' field shows which invoices received allocations from this payment."
|
|
1095
|
+
].join(" "),
|
|
1096
|
+
{
|
|
1097
|
+
payment_id: z4.string().uuid().describe("Payment UUID from payment_list.")
|
|
1098
|
+
},
|
|
1099
|
+
wrapTool(async (input) => {
|
|
1100
|
+
const payment = await client2.payment.getById(input.payment_id);
|
|
1101
|
+
return {
|
|
1102
|
+
content: [{
|
|
1103
|
+
type: "text",
|
|
1104
|
+
text: JSON.stringify(payment, null, 2)
|
|
1105
|
+
}]
|
|
1106
|
+
};
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
1109
|
+
server2.tool(
|
|
1110
|
+
"payment_update",
|
|
1111
|
+
[
|
|
1112
|
+
"Update an existing payment record. This reverses the old allocation and re-applies the new one.",
|
|
1113
|
+
"The linked invoice's amountPaid and status are recalculated automatically.",
|
|
1114
|
+
"Only provide fields you want to change.",
|
|
1115
|
+
"Warning: updating a payment recalculates invoice statuses \u2014 ensure the new amount/allocation is correct."
|
|
1116
|
+
].join(" "),
|
|
1117
|
+
{
|
|
1118
|
+
payment_id: z4.string().uuid().describe("Payment UUID to update."),
|
|
1119
|
+
amount: z4.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("New payment amount as decimal string."),
|
|
1120
|
+
mode: z4.enum(PAYMENT_MODES).optional().describe("Updated payment mode."),
|
|
1121
|
+
discount: z4.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated discount/write-off amount."),
|
|
1122
|
+
reference_number: z4.string().max(100).optional().nullable().describe("Updated transaction reference number."),
|
|
1123
|
+
payment_date: z4.string().datetime().optional().describe("Updated payment date (ISO 8601)."),
|
|
1124
|
+
notes: z4.string().max(500).optional().nullable().describe("Updated internal notes."),
|
|
1125
|
+
bank_account_id: z4.string().uuid().optional().nullable().describe("Updated bank account UUID. Null to unlink from any account."),
|
|
1126
|
+
allocations: z4.array(z4.object({
|
|
1127
|
+
invoice_id: z4.string().uuid().describe("Invoice UUID to allocate part of this payment to."),
|
|
1128
|
+
amount: z4.string().regex(/^\d+(\.\d{1,2})?$/).describe("Amount to allocate.")
|
|
1129
|
+
})).optional().describe("Updated invoice allocations. Replaces all existing allocations.")
|
|
1130
|
+
},
|
|
1131
|
+
wrapTool(async (input) => {
|
|
1132
|
+
const payment = await client2.payment.update({
|
|
1133
|
+
id: input.payment_id,
|
|
1134
|
+
amount: input.amount,
|
|
1135
|
+
mode: input.mode,
|
|
1136
|
+
discount: input.discount,
|
|
1137
|
+
referenceNumber: input.reference_number,
|
|
1138
|
+
paymentDate: input.payment_date,
|
|
1139
|
+
notes: input.notes,
|
|
1140
|
+
bankAccountId: input.bank_account_id,
|
|
1141
|
+
allocations: input.allocations?.map((a) => ({
|
|
1142
|
+
invoiceId: a.invoice_id,
|
|
1143
|
+
amount: a.amount
|
|
1144
|
+
}))
|
|
1145
|
+
});
|
|
1146
|
+
return {
|
|
1147
|
+
content: [{
|
|
1148
|
+
type: "text",
|
|
1149
|
+
text: JSON.stringify(payment, null, 2)
|
|
1150
|
+
}]
|
|
1151
|
+
};
|
|
1152
|
+
})
|
|
1153
|
+
);
|
|
1154
|
+
server2.tool(
|
|
1155
|
+
"payment_delete",
|
|
1156
|
+
[
|
|
1157
|
+
"Soft-delete a payment record. Requires admin role.",
|
|
1158
|
+
"Deleting a payment reverses the invoice allocation: the linked invoice's amountPaid is reduced and its status is recalculated.",
|
|
1159
|
+
"The bank account balance is also reversed if the payment was recorded against an account."
|
|
1160
|
+
].join(" "),
|
|
1161
|
+
{
|
|
1162
|
+
payment_id: z4.string().uuid().describe("Payment UUID to delete.")
|
|
1163
|
+
},
|
|
1164
|
+
wrapTool(async (input) => {
|
|
1165
|
+
const result = await client2.payment.delete(input.payment_id);
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{
|
|
1168
|
+
type: "text",
|
|
1169
|
+
text: JSON.stringify(result, null, 2)
|
|
1170
|
+
}]
|
|
1171
|
+
};
|
|
1172
|
+
})
|
|
1173
|
+
);
|
|
1174
|
+
server2.tool(
|
|
1175
|
+
"payment_list",
|
|
1176
|
+
[
|
|
1177
|
+
"List payments for the active business, filtered by party, invoice, or date range.",
|
|
1178
|
+
"Use party_id to see all payments from/to a specific customer or supplier.",
|
|
1179
|
+
"Use invoice_id to see all payments applied to a specific invoice."
|
|
1180
|
+
].join(" "),
|
|
1181
|
+
{
|
|
1182
|
+
party_id: z4.string().uuid().optional().describe("Filter by customer or supplier UUID."),
|
|
1183
|
+
invoice_id: z4.string().uuid().optional().describe("Filter payments linked to a specific invoice."),
|
|
1184
|
+
from_date: z4.string().datetime().optional().describe("Start date (ISO 8601)."),
|
|
1185
|
+
to_date: z4.string().datetime().optional().describe("End date (ISO 8601)."),
|
|
1186
|
+
search: z4.string().max(200).optional().describe("Search by payment number, party name, or reference number."),
|
|
1187
|
+
page: z4.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
1188
|
+
},
|
|
1189
|
+
wrapTool(async (input) => {
|
|
1190
|
+
const result = await client2.payment.list({
|
|
1191
|
+
partyId: input.party_id,
|
|
1192
|
+
invoiceId: input.invoice_id,
|
|
1193
|
+
fromDate: input.from_date,
|
|
1194
|
+
toDate: input.to_date,
|
|
1195
|
+
search: input.search,
|
|
1196
|
+
page: input.page,
|
|
1197
|
+
limit: MAX_PAGE_SIZE
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
content: [{
|
|
1201
|
+
type: "text",
|
|
1202
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
1203
|
+
}]
|
|
1204
|
+
};
|
|
1205
|
+
})
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/tools/expense.ts
|
|
1210
|
+
import { z as z5 } from "zod";
|
|
1211
|
+
var PAYMENT_MODES2 = ["cash", "bank", "upi", "cheque", "other"];
|
|
1212
|
+
function registerExpenseTools(server2, client2) {
|
|
1213
|
+
server2.tool(
|
|
1214
|
+
"expense_create",
|
|
1215
|
+
[
|
|
1216
|
+
"Record a business expense (operating cost, overhead, etc.).",
|
|
1217
|
+
"Category is a free-form label \u2014 use consistent names like 'Rent', 'Utilities', 'Salaries', 'Travel'.",
|
|
1218
|
+
"Expenses appear in the dashboard summary's 'totalExpenses' and affect the P&L."
|
|
1219
|
+
].join(" "),
|
|
1220
|
+
{
|
|
1221
|
+
category: z5.string().min(1).max(100).describe("Expense category, e.g. 'Rent', 'Electricity', 'Office Supplies', 'Travel'."),
|
|
1222
|
+
amount: z5.string().regex(/^\d+(\.\d{1,2})?$/).describe("Expense amount as decimal string, e.g. '12000.00'."),
|
|
1223
|
+
mode: z5.enum(PAYMENT_MODES2).describe("Payment method used: 'cash', 'bank', 'upi', 'cheque', or 'other'."),
|
|
1224
|
+
description: z5.string().max(500).optional().describe("Additional details, e.g. 'Office rent for March 2024'."),
|
|
1225
|
+
expense_date: z5.string().datetime().optional().describe("Date of the expense (ISO 8601). Defaults to today."),
|
|
1226
|
+
reference_number: z5.string().max(100).optional().describe("Bill number, receipt number, or transaction ID.")
|
|
1227
|
+
},
|
|
1228
|
+
wrapTool(async (input) => {
|
|
1229
|
+
const expense = await client2.expense.create({
|
|
1230
|
+
category: input.category,
|
|
1231
|
+
amount: input.amount,
|
|
1232
|
+
mode: input.mode,
|
|
1233
|
+
description: input.description,
|
|
1234
|
+
expenseDate: input.expense_date,
|
|
1235
|
+
referenceNumber: input.reference_number
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
content: [{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: JSON.stringify(expense, null, 2)
|
|
1241
|
+
}]
|
|
1242
|
+
};
|
|
1243
|
+
})
|
|
1244
|
+
);
|
|
1245
|
+
server2.tool(
|
|
1246
|
+
"expense_update",
|
|
1247
|
+
[
|
|
1248
|
+
"Update an existing expense record.",
|
|
1249
|
+
"Only provide fields you want to change \u2014 all other fields remain unchanged."
|
|
1250
|
+
].join(" "),
|
|
1251
|
+
{
|
|
1252
|
+
expense_id: z5.string().uuid().describe("Expense UUID to update."),
|
|
1253
|
+
category: z5.string().min(1).max(100).optional().describe("Updated expense category."),
|
|
1254
|
+
amount: z5.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated expense amount as decimal string."),
|
|
1255
|
+
mode: z5.enum(PAYMENT_MODES2).optional().describe("Updated payment method."),
|
|
1256
|
+
description: z5.string().max(500).optional().describe("Updated description."),
|
|
1257
|
+
expense_date: z5.string().datetime().optional().describe("Updated expense date (ISO 8601)."),
|
|
1258
|
+
reference_number: z5.string().max(100).optional().describe("Updated bill number or receipt number.")
|
|
1259
|
+
},
|
|
1260
|
+
wrapTool(async (input) => {
|
|
1261
|
+
const expense = await client2.expense.update(input.expense_id, {
|
|
1262
|
+
category: input.category,
|
|
1263
|
+
amount: input.amount,
|
|
1264
|
+
mode: input.mode,
|
|
1265
|
+
description: input.description,
|
|
1266
|
+
expenseDate: input.expense_date,
|
|
1267
|
+
referenceNumber: input.reference_number
|
|
1268
|
+
});
|
|
1269
|
+
return {
|
|
1270
|
+
content: [{
|
|
1271
|
+
type: "text",
|
|
1272
|
+
text: JSON.stringify(expense, null, 2)
|
|
1273
|
+
}]
|
|
1274
|
+
};
|
|
1275
|
+
})
|
|
1276
|
+
);
|
|
1277
|
+
server2.tool(
|
|
1278
|
+
"expense_delete",
|
|
1279
|
+
[
|
|
1280
|
+
"Soft-delete an expense record. Requires admin role.",
|
|
1281
|
+
"The expense is hidden from all lists and reports after deletion."
|
|
1282
|
+
].join(" "),
|
|
1283
|
+
{
|
|
1284
|
+
expense_id: z5.string().uuid().describe("Expense UUID to delete.")
|
|
1285
|
+
},
|
|
1286
|
+
wrapTool(async (input) => {
|
|
1287
|
+
const result = await client2.expense.delete(input.expense_id);
|
|
1288
|
+
return {
|
|
1289
|
+
content: [{
|
|
1290
|
+
type: "text",
|
|
1291
|
+
text: JSON.stringify(result, null, 2)
|
|
1292
|
+
}]
|
|
1293
|
+
};
|
|
1294
|
+
})
|
|
1295
|
+
);
|
|
1296
|
+
server2.tool(
|
|
1297
|
+
"expense_categories",
|
|
1298
|
+
[
|
|
1299
|
+
"Get a list of all distinct expense categories used in the business.",
|
|
1300
|
+
"Use this to discover existing category names before filtering expense_list or creating new expenses with consistent categories."
|
|
1301
|
+
].join(" "),
|
|
1302
|
+
{},
|
|
1303
|
+
wrapTool(async (_input) => {
|
|
1304
|
+
const categories = await client2.expense.categories();
|
|
1305
|
+
return {
|
|
1306
|
+
content: [{
|
|
1307
|
+
type: "text",
|
|
1308
|
+
text: JSON.stringify(categories, null, 2)
|
|
1309
|
+
}]
|
|
1310
|
+
};
|
|
1311
|
+
})
|
|
1312
|
+
);
|
|
1313
|
+
server2.tool(
|
|
1314
|
+
"expense_list",
|
|
1315
|
+
[
|
|
1316
|
+
"List business expenses, optionally filtered by category or date range.",
|
|
1317
|
+
"Use this to answer 'How much did we spend on rent this year?' or 'Show me all expenses in March'."
|
|
1318
|
+
].join(" "),
|
|
1319
|
+
{
|
|
1320
|
+
category: z5.string().max(100).optional().describe("Filter by expense category (exact match)."),
|
|
1321
|
+
from_date: z5.string().datetime().optional().describe("Start date (ISO 8601)."),
|
|
1322
|
+
to_date: z5.string().datetime().optional().describe("End date (ISO 8601)."),
|
|
1323
|
+
search: z5.string().max(200).optional().describe("Search by description or reference number."),
|
|
1324
|
+
page: z5.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
1325
|
+
},
|
|
1326
|
+
wrapTool(async (input) => {
|
|
1327
|
+
const result = await client2.expense.list({
|
|
1328
|
+
category: input.category,
|
|
1329
|
+
fromDate: input.from_date,
|
|
1330
|
+
toDate: input.to_date,
|
|
1331
|
+
search: input.search,
|
|
1332
|
+
page: input.page,
|
|
1333
|
+
limit: MAX_PAGE_SIZE
|
|
1334
|
+
});
|
|
1335
|
+
return {
|
|
1336
|
+
content: [{
|
|
1337
|
+
type: "text",
|
|
1338
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
1339
|
+
}]
|
|
1340
|
+
};
|
|
1341
|
+
})
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/tools/dashboard.ts
|
|
1346
|
+
import { z as z6 } from "zod";
|
|
1347
|
+
function resolvePeriod(period) {
|
|
1348
|
+
const now = /* @__PURE__ */ new Date();
|
|
1349
|
+
if (!period || period === "this-fy") {
|
|
1350
|
+
return {};
|
|
1351
|
+
}
|
|
1352
|
+
if (period === "this-month") {
|
|
1353
|
+
const from = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1354
|
+
const to = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
|
|
1355
|
+
return { fromDate: from.toISOString(), toDate: to.toISOString() };
|
|
1356
|
+
}
|
|
1357
|
+
if (period === "last-month") {
|
|
1358
|
+
const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
1359
|
+
const to = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
|
|
1360
|
+
return { fromDate: from.toISOString(), toDate: to.toISOString() };
|
|
1361
|
+
}
|
|
1362
|
+
if (period === "this-quarter") {
|
|
1363
|
+
const quarter = Math.floor(now.getMonth() / 3);
|
|
1364
|
+
const from = new Date(now.getFullYear(), quarter * 3, 1);
|
|
1365
|
+
const to = new Date(now.getFullYear(), (quarter + 1) * 3, 0, 23, 59, 59, 999);
|
|
1366
|
+
return { fromDate: from.toISOString(), toDate: to.toISOString() };
|
|
1367
|
+
}
|
|
1368
|
+
if (period === "this-year") {
|
|
1369
|
+
const from = new Date(now.getFullYear(), 0, 1);
|
|
1370
|
+
const to = new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999);
|
|
1371
|
+
return { fromDate: from.toISOString(), toDate: to.toISOString() };
|
|
1372
|
+
}
|
|
1373
|
+
return {};
|
|
1374
|
+
}
|
|
1375
|
+
function registerDashboardTools(server2, client2) {
|
|
1376
|
+
server2.tool(
|
|
1377
|
+
"dashboard_summary",
|
|
1378
|
+
[
|
|
1379
|
+
"Get a financial summary for the active business: total sales, purchases, expenses, receivables, payables, and cash position.",
|
|
1380
|
+
"'receivable' = total amount customers owe you (outstanding invoices).",
|
|
1381
|
+
"'payable' = total amount you owe suppliers.",
|
|
1382
|
+
"'cashInHand' = cash balance across all cash/bank accounts.",
|
|
1383
|
+
"Use period to select the time window. Defaults to the current financial year (April\u2013March for Indian businesses).",
|
|
1384
|
+
"Use this to answer questions like 'How much revenue did we make this month?' or 'What are our total outstanding receivables?'"
|
|
1385
|
+
].join(" "),
|
|
1386
|
+
{
|
|
1387
|
+
period: z6.enum(["this-fy", "this-month", "last-month", "this-quarter", "this-year", "custom"]).optional().describe(
|
|
1388
|
+
"'this-fy' = current financial year (default). 'this-month' = current calendar month. 'last-month' = previous calendar month. 'this-quarter' = current calendar quarter. 'this-year' = current calendar year. 'custom' = use from_date and to_date."
|
|
1389
|
+
),
|
|
1390
|
+
from_date: z6.string().datetime().optional().describe("Custom start date (ISO 8601). Only used when period='custom'."),
|
|
1391
|
+
to_date: z6.string().datetime().optional().describe("Custom end date (ISO 8601). Only used when period='custom'.")
|
|
1392
|
+
},
|
|
1393
|
+
wrapTool(async (input) => {
|
|
1394
|
+
let dateRange;
|
|
1395
|
+
if (input.period === "custom") {
|
|
1396
|
+
dateRange = {
|
|
1397
|
+
fromDate: input.from_date,
|
|
1398
|
+
toDate: input.to_date
|
|
1399
|
+
};
|
|
1400
|
+
} else {
|
|
1401
|
+
dateRange = resolvePeriod(input.period);
|
|
1402
|
+
}
|
|
1403
|
+
const summary = await client2.dashboard.summary(
|
|
1404
|
+
Object.keys(dateRange).length > 0 ? dateRange : void 0
|
|
1405
|
+
);
|
|
1406
|
+
return {
|
|
1407
|
+
content: [{
|
|
1408
|
+
type: "text",
|
|
1409
|
+
text: JSON.stringify(
|
|
1410
|
+
{
|
|
1411
|
+
...summary,
|
|
1412
|
+
_period: input.period ?? "this-fy",
|
|
1413
|
+
_note: "All monetary values are decimal strings in the business's currency (default: INR)."
|
|
1414
|
+
},
|
|
1415
|
+
null,
|
|
1416
|
+
2
|
|
1417
|
+
)
|
|
1418
|
+
}]
|
|
1419
|
+
};
|
|
1420
|
+
})
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/tools/gst.ts
|
|
1425
|
+
import { z as z7 } from "zod";
|
|
1426
|
+
var CURRENT_YEAR = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1427
|
+
function registerGstTools(server2, client2) {
|
|
1428
|
+
server2.tool(
|
|
1429
|
+
"gst_report_csv",
|
|
1430
|
+
[
|
|
1431
|
+
"Get GSTR-1 data as a CSV string ready for upload to the GST portal.",
|
|
1432
|
+
"Returns the CSV content and a suggested filename (e.g. 'GSTR1_March_2024.csv').",
|
|
1433
|
+
"Save the CSV content to a file and upload it at https://www.gst.gov.in/.",
|
|
1434
|
+
"Month is 1\u201312 (1 = January, 3 = March, etc.)."
|
|
1435
|
+
].join(" "),
|
|
1436
|
+
{
|
|
1437
|
+
month: z7.number().int().min(1).max(12).describe("Month number (1 = January, 12 = December)."),
|
|
1438
|
+
year: z7.number().int().min(2020).max(CURRENT_YEAR + 1).describe(`Year, e.g. ${CURRENT_YEAR}.`)
|
|
1439
|
+
},
|
|
1440
|
+
wrapTool(async (input) => {
|
|
1441
|
+
const result = await client2.gst.gstr1CSV({ month: input.month, year: input.year });
|
|
1442
|
+
return {
|
|
1443
|
+
content: [{
|
|
1444
|
+
type: "text",
|
|
1445
|
+
text: JSON.stringify(result, null, 2)
|
|
1446
|
+
}]
|
|
1447
|
+
};
|
|
1448
|
+
})
|
|
1449
|
+
);
|
|
1450
|
+
server2.tool(
|
|
1451
|
+
"gst_report",
|
|
1452
|
+
[
|
|
1453
|
+
"Generate a GST report (GSTR1 or GSTR3B) for a specific month and year.",
|
|
1454
|
+
"GSTR1 = outward supplies summary (sales). GSTR3B = monthly return summary (sales + purchases + ITC).",
|
|
1455
|
+
"Returns JSON data \u2014 use this to answer 'What is our GST liability for March 2024?' or 'How much ITC can we claim this month?'",
|
|
1456
|
+
"Month is 1\u201312 (1 = January, 3 = March, etc.).",
|
|
1457
|
+
"Example: { report_type: 'gstr3b', month: 3, year: 2024 } for March 2024 GSTR3B."
|
|
1458
|
+
].join(" "),
|
|
1459
|
+
{
|
|
1460
|
+
report_type: z7.enum(["gstr1", "gstr3b"]).describe("'gstr1' for outward supplies (sales) report. 'gstr3b' for monthly summary return."),
|
|
1461
|
+
month: z7.number().int().min(1).max(12).describe("Month number (1 = January, 12 = December)."),
|
|
1462
|
+
year: z7.number().int().min(2020).max(CURRENT_YEAR + 1).describe(`Year, e.g. ${CURRENT_YEAR}.`)
|
|
1463
|
+
},
|
|
1464
|
+
wrapTool(async (input) => {
|
|
1465
|
+
const report = input.report_type === "gstr1" ? await client2.gst.gstr1({ month: input.month, year: input.year }) : await client2.gst.gstr3b({ month: input.month, year: input.year });
|
|
1466
|
+
const monthName = new Date(input.year, input.month - 1, 1).toLocaleString("en-IN", { month: "long" });
|
|
1467
|
+
return {
|
|
1468
|
+
content: [{
|
|
1469
|
+
type: "text",
|
|
1470
|
+
text: JSON.stringify(
|
|
1471
|
+
{
|
|
1472
|
+
...report,
|
|
1473
|
+
_meta: {
|
|
1474
|
+
reportType: input.report_type.toUpperCase(),
|
|
1475
|
+
period: `${monthName} ${input.year}`
|
|
1476
|
+
}
|
|
1477
|
+
},
|
|
1478
|
+
null,
|
|
1479
|
+
2
|
|
1480
|
+
)
|
|
1481
|
+
}]
|
|
1482
|
+
};
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/tools/shipment.ts
|
|
1488
|
+
import { z as z8 } from "zod";
|
|
1489
|
+
var SHIPMENT_STATUSES = ["pending", "shipped", "in_transit", "delivered", "returned"];
|
|
1490
|
+
function registerShipmentTools(server2, client2) {
|
|
1491
|
+
server2.tool(
|
|
1492
|
+
"shipment_list",
|
|
1493
|
+
[
|
|
1494
|
+
"List shipments for the active business with optional filters.",
|
|
1495
|
+
"Use status='pending' to find shipments not yet dispatched, or status='in_transit' for active deliveries.",
|
|
1496
|
+
"Filter by invoice_id to see all shipments for a specific invoice, or by party_id for a specific customer."
|
|
1497
|
+
].join(" "),
|
|
1498
|
+
{
|
|
1499
|
+
status: z8.enum(SHIPMENT_STATUSES).optional().describe("Filter by shipment status: 'pending', 'shipped', 'in_transit', 'delivered', or 'returned'."),
|
|
1500
|
+
invoice_id: z8.string().uuid().optional().describe("Filter shipments linked to a specific invoice UUID."),
|
|
1501
|
+
party_id: z8.string().uuid().optional().describe("Filter shipments for a specific customer/party UUID."),
|
|
1502
|
+
page: z8.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
1503
|
+
},
|
|
1504
|
+
wrapTool(async (input) => {
|
|
1505
|
+
const result = await client2.shipment.list({
|
|
1506
|
+
status: input.status,
|
|
1507
|
+
invoiceId: input.invoice_id,
|
|
1508
|
+
partyId: input.party_id,
|
|
1509
|
+
page: input.page,
|
|
1510
|
+
limit: MAX_PAGE_SIZE
|
|
1511
|
+
});
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{
|
|
1514
|
+
type: "text",
|
|
1515
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
1516
|
+
}]
|
|
1517
|
+
};
|
|
1518
|
+
})
|
|
1519
|
+
);
|
|
1520
|
+
server2.tool(
|
|
1521
|
+
"shipment_get",
|
|
1522
|
+
[
|
|
1523
|
+
"Get full details of a single shipment, including tracking info, carrier, address, and delivery dates.",
|
|
1524
|
+
"If the carrier is one of the 7 pre-configured Indian carriers (Delhivery, BlueDart, DTDC, Ecom Express, India Post, Shadowfax, Xpressbees), a tracking URL is auto-generated."
|
|
1525
|
+
].join(" "),
|
|
1526
|
+
{
|
|
1527
|
+
shipment_id: z8.string().uuid().describe("Shipment UUID from shipment_list.")
|
|
1528
|
+
},
|
|
1529
|
+
wrapTool(async (input) => {
|
|
1530
|
+
const shipment = await client2.shipment.get(input.shipment_id);
|
|
1531
|
+
return {
|
|
1532
|
+
content: [{
|
|
1533
|
+
type: "text",
|
|
1534
|
+
text: JSON.stringify(shipment, null, 2)
|
|
1535
|
+
}]
|
|
1536
|
+
};
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
server2.tool(
|
|
1540
|
+
"shipment_create",
|
|
1541
|
+
[
|
|
1542
|
+
"Create a new shipment record for an invoice or party.",
|
|
1543
|
+
"For known carriers (Delhivery, BlueDart, DTDC, Ecom Express, India Post, Shadowfax, Xpressbees), providing carrier + tracking_number auto-generates the tracking URL.",
|
|
1544
|
+
"For custom carriers, provide tracking_url manually.",
|
|
1545
|
+
"Cost is the shipping cost charged; weight is the parcel weight in kg."
|
|
1546
|
+
].join(" "),
|
|
1547
|
+
{
|
|
1548
|
+
invoice_id: z8.string().uuid().optional().describe("Link this shipment to an invoice UUID. Use shipment_list to avoid duplicates."),
|
|
1549
|
+
party_id: z8.string().uuid().optional().describe("Customer/party UUID for this shipment."),
|
|
1550
|
+
carrier: z8.string().max(100).optional().describe("Carrier name, e.g. 'Delhivery', 'BlueDart', 'DTDC'. Known carriers get auto-tracking URLs."),
|
|
1551
|
+
mode: z8.string().max(50).optional().describe("Delivery mode, e.g. 'surface', 'air', 'express'. Matches business custom shipping methods."),
|
|
1552
|
+
tracking_number: z8.string().max(200).optional().describe("AWB/tracking number from the carrier. Required for auto-generating tracking URL."),
|
|
1553
|
+
tracking_url: z8.string().max(500).optional().describe("Manual tracking URL if carrier is not in the pre-configured list."),
|
|
1554
|
+
cost: z8.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Shipping cost as decimal string, e.g. '250.00'. Default '0'."),
|
|
1555
|
+
weight: z8.string().regex(/^\d+(\.\d{1,3})?$/).optional().describe("Parcel weight in kg as decimal string, e.g. '2.500'."),
|
|
1556
|
+
shipping_address: z8.string().optional().describe("Delivery address (street/flat/block)."),
|
|
1557
|
+
shipping_city: z8.string().optional().describe("Delivery city."),
|
|
1558
|
+
shipping_pincode: z8.string().optional().describe("Delivery PIN code."),
|
|
1559
|
+
status: z8.enum(SHIPMENT_STATUSES).optional().describe("Initial status. Default 'pending'. Set 'shipped' if already dispatched."),
|
|
1560
|
+
shipment_date: z8.string().datetime().optional().describe("Date the parcel was dispatched (ISO 8601). Defaults to today."),
|
|
1561
|
+
estimated_delivery: z8.string().datetime().optional().describe("Expected delivery date (ISO 8601)."),
|
|
1562
|
+
notes: z8.string().optional().describe("Internal notes about this shipment.")
|
|
1563
|
+
},
|
|
1564
|
+
wrapTool(async (input) => {
|
|
1565
|
+
const shipment = await client2.shipment.create({
|
|
1566
|
+
invoiceId: input.invoice_id,
|
|
1567
|
+
partyId: input.party_id,
|
|
1568
|
+
carrier: input.carrier,
|
|
1569
|
+
mode: input.mode,
|
|
1570
|
+
trackingNumber: input.tracking_number,
|
|
1571
|
+
trackingUrl: input.tracking_url,
|
|
1572
|
+
cost: input.cost,
|
|
1573
|
+
weight: input.weight,
|
|
1574
|
+
shippingAddress: input.shipping_address,
|
|
1575
|
+
shippingCity: input.shipping_city,
|
|
1576
|
+
shippingPincode: input.shipping_pincode,
|
|
1577
|
+
status: input.status,
|
|
1578
|
+
shipmentDate: input.shipment_date,
|
|
1579
|
+
estimatedDelivery: input.estimated_delivery,
|
|
1580
|
+
notes: input.notes
|
|
1581
|
+
});
|
|
1582
|
+
return {
|
|
1583
|
+
content: [{
|
|
1584
|
+
type: "text",
|
|
1585
|
+
text: JSON.stringify(shipment, null, 2)
|
|
1586
|
+
}]
|
|
1587
|
+
};
|
|
1588
|
+
})
|
|
1589
|
+
);
|
|
1590
|
+
server2.tool(
|
|
1591
|
+
"shipment_update",
|
|
1592
|
+
[
|
|
1593
|
+
"Update a shipment's status, tracking information, or delivery dates.",
|
|
1594
|
+
"Use this to mark a shipment as shipped (set status='shipped'), in transit, or delivered.",
|
|
1595
|
+
"Setting status='delivered' automatically records the actual delivery date if not provided.",
|
|
1596
|
+
"Update tracking_number to trigger re-generation of the carrier tracking URL."
|
|
1597
|
+
].join(" "),
|
|
1598
|
+
{
|
|
1599
|
+
shipment_id: z8.string().uuid().describe("Shipment UUID to update."),
|
|
1600
|
+
carrier: z8.string().max(100).optional().describe("Updated carrier name."),
|
|
1601
|
+
mode: z8.string().max(50).optional().describe("Updated delivery mode."),
|
|
1602
|
+
tracking_number: z8.string().max(200).optional().describe("Updated tracking/AWB number. Re-generates tracking URL for known carriers."),
|
|
1603
|
+
tracking_url: z8.string().max(500).optional().describe("Manual tracking URL override."),
|
|
1604
|
+
cost: z8.string().regex(/^\d+(\.\d{1,2})?$/).optional().describe("Updated shipping cost as decimal string."),
|
|
1605
|
+
weight: z8.string().regex(/^\d+(\.\d{1,3})?$/).optional().describe("Updated parcel weight in kg."),
|
|
1606
|
+
status: z8.enum(SHIPMENT_STATUSES).optional().describe("New status. 'shipped' = dispatched. 'in_transit' = en route. 'delivered' = received. 'returned' = sent back."),
|
|
1607
|
+
shipment_date: z8.string().datetime().optional().describe("Updated dispatch date (ISO 8601)."),
|
|
1608
|
+
estimated_delivery: z8.string().datetime().optional().describe("Updated estimated delivery date (ISO 8601)."),
|
|
1609
|
+
actual_delivery: z8.string().datetime().optional().describe("Actual delivery date (ISO 8601). Auto-set when status becomes 'delivered' if omitted."),
|
|
1610
|
+
notes: z8.string().optional().describe("Updated notes.")
|
|
1611
|
+
},
|
|
1612
|
+
wrapTool(async (input) => {
|
|
1613
|
+
const shipment = await client2.shipment.update({
|
|
1614
|
+
id: input.shipment_id,
|
|
1615
|
+
carrier: input.carrier,
|
|
1616
|
+
mode: input.mode,
|
|
1617
|
+
trackingNumber: input.tracking_number,
|
|
1618
|
+
trackingUrl: input.tracking_url,
|
|
1619
|
+
cost: input.cost,
|
|
1620
|
+
weight: input.weight,
|
|
1621
|
+
status: input.status,
|
|
1622
|
+
shipmentDate: input.shipment_date,
|
|
1623
|
+
estimatedDelivery: input.estimated_delivery,
|
|
1624
|
+
actualDelivery: input.actual_delivery,
|
|
1625
|
+
notes: input.notes
|
|
1626
|
+
});
|
|
1627
|
+
return {
|
|
1628
|
+
content: [{
|
|
1629
|
+
type: "text",
|
|
1630
|
+
text: JSON.stringify(shipment, null, 2)
|
|
1631
|
+
}]
|
|
1632
|
+
};
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
server2.tool(
|
|
1636
|
+
"shipment_delete",
|
|
1637
|
+
[
|
|
1638
|
+
"Delete a shipment record. This is a hard delete \u2014 the shipment record is permanently removed.",
|
|
1639
|
+
"Requires admin role. Use with caution; prefer updating status to 'returned' instead of deleting."
|
|
1640
|
+
].join(" "),
|
|
1641
|
+
{
|
|
1642
|
+
shipment_id: z8.string().uuid().describe("Shipment UUID to delete.")
|
|
1643
|
+
},
|
|
1644
|
+
wrapTool(async (input) => {
|
|
1645
|
+
const result = await client2.shipment.delete(input.shipment_id);
|
|
1646
|
+
return {
|
|
1647
|
+
content: [{
|
|
1648
|
+
type: "text",
|
|
1649
|
+
text: JSON.stringify(result, null, 2)
|
|
1650
|
+
}]
|
|
1651
|
+
};
|
|
1652
|
+
})
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/tools/bankAccount.ts
|
|
1657
|
+
import { z as z9 } from "zod";
|
|
1658
|
+
var ACCOUNT_TYPES = ["savings", "current", "cash", "credit", "other"];
|
|
1659
|
+
function registerBankAccountTools(server2, client2) {
|
|
1660
|
+
server2.tool(
|
|
1661
|
+
"bank_account_list",
|
|
1662
|
+
[
|
|
1663
|
+
"List all bank and cash accounts for the active business, including current balances.",
|
|
1664
|
+
"Use this to find account UUIDs before recording payments or transfers.",
|
|
1665
|
+
"The 'currentBalance' field reflects the running balance after all recorded transactions.",
|
|
1666
|
+
"The default account (isDefault=true) is used automatically when no account is specified in payment_create."
|
|
1667
|
+
].join(" "),
|
|
1668
|
+
{},
|
|
1669
|
+
wrapTool(async (_input) => {
|
|
1670
|
+
const accounts = await client2.bankAccount.list();
|
|
1671
|
+
return {
|
|
1672
|
+
content: [{
|
|
1673
|
+
type: "text",
|
|
1674
|
+
text: JSON.stringify(accounts, null, 2)
|
|
1675
|
+
}]
|
|
1676
|
+
};
|
|
1677
|
+
})
|
|
1678
|
+
);
|
|
1679
|
+
server2.tool(
|
|
1680
|
+
"bank_account_get",
|
|
1681
|
+
[
|
|
1682
|
+
"Get details of a single bank account including its 20 most recent transactions.",
|
|
1683
|
+
"Use this to check an account's running balance and recent activity."
|
|
1684
|
+
].join(" "),
|
|
1685
|
+
{
|
|
1686
|
+
account_id: z9.string().uuid().describe("Bank account UUID from bank_account_list.")
|
|
1687
|
+
},
|
|
1688
|
+
wrapTool(async (input) => {
|
|
1689
|
+
const account = await client2.bankAccount.get(input.account_id);
|
|
1690
|
+
return {
|
|
1691
|
+
content: [{
|
|
1692
|
+
type: "text",
|
|
1693
|
+
text: JSON.stringify(account, null, 2)
|
|
1694
|
+
}]
|
|
1695
|
+
};
|
|
1696
|
+
})
|
|
1697
|
+
);
|
|
1698
|
+
server2.tool(
|
|
1699
|
+
"bank_account_create",
|
|
1700
|
+
[
|
|
1701
|
+
"Create a new bank account or cash account for the business.",
|
|
1702
|
+
"Use account_type='cash' for a physical cash register/petty cash account.",
|
|
1703
|
+
"Use account_type='savings' or 'current' for bank accounts.",
|
|
1704
|
+
"opening_balance sets the starting balance (e.g. the balance when you started using Hisaabo).",
|
|
1705
|
+
"Set is_default=true to make this the default account for payment recording."
|
|
1706
|
+
].join(" "),
|
|
1707
|
+
{
|
|
1708
|
+
account_name: z9.string().min(1).max(200).describe("Display name, e.g. 'HDFC Current Account' or 'Petty Cash'."),
|
|
1709
|
+
account_type: z9.enum(ACCOUNT_TYPES).describe("'savings', 'current', 'cash' (petty cash/physical cash), 'credit', or 'other'."),
|
|
1710
|
+
account_number: z9.string().max(34).optional().describe("Bank account number (not required for cash accounts)."),
|
|
1711
|
+
ifsc: z9.string().max(11).optional().describe("IFSC code, e.g. 'HDFC0001234'. Required for bank transfers."),
|
|
1712
|
+
bank_name: z9.string().max(200).optional().describe("Bank name, e.g. 'HDFC Bank', 'State Bank of India'."),
|
|
1713
|
+
opening_balance: z9.string().regex(/^-?\d+(\.\d{1,2})?$/).optional().describe("Opening balance as decimal string. Default '0'. Use the current account balance when onboarding."),
|
|
1714
|
+
is_default: z9.boolean().optional().describe("If true, this becomes the default account. Any previous default is cleared.")
|
|
1715
|
+
},
|
|
1716
|
+
wrapTool(async (input) => {
|
|
1717
|
+
const account = await client2.bankAccount.create({
|
|
1718
|
+
accountName: input.account_name,
|
|
1719
|
+
accountType: input.account_type,
|
|
1720
|
+
accountNumber: input.account_number,
|
|
1721
|
+
ifsc: input.ifsc,
|
|
1722
|
+
bankName: input.bank_name,
|
|
1723
|
+
openingBalance: input.opening_balance,
|
|
1724
|
+
isDefault: input.is_default
|
|
1725
|
+
});
|
|
1726
|
+
return {
|
|
1727
|
+
content: [{
|
|
1728
|
+
type: "text",
|
|
1729
|
+
text: JSON.stringify(account, null, 2)
|
|
1730
|
+
}]
|
|
1731
|
+
};
|
|
1732
|
+
})
|
|
1733
|
+
);
|
|
1734
|
+
server2.tool(
|
|
1735
|
+
"bank_account_transfer",
|
|
1736
|
+
[
|
|
1737
|
+
"Transfer funds between two of the business's bank/cash accounts.",
|
|
1738
|
+
"Creates a withdrawal transaction on the source account and a deposit on the destination.",
|
|
1739
|
+
"Use this to record moving cash to the bank, or inter-account transfers.",
|
|
1740
|
+
"Example: transfer from 'Petty Cash' to 'HDFC Current Account' to replenish cash."
|
|
1741
|
+
].join(" "),
|
|
1742
|
+
{
|
|
1743
|
+
from_account_id: z9.string().uuid().describe("UUID of the source account (funds leave this account)."),
|
|
1744
|
+
to_account_id: z9.string().uuid().describe("UUID of the destination account (funds arrive here). Must differ from from_account_id."),
|
|
1745
|
+
amount: z9.string().regex(/^\d+(\.\d{1,2})?$/).describe("Transfer amount as decimal string, e.g. '5000.00'."),
|
|
1746
|
+
description: z9.string().max(500).optional().describe("Description of the transfer, e.g. 'Monthly cash deposit to bank'."),
|
|
1747
|
+
transaction_date: z9.string().datetime().optional().describe("Date of the transfer (ISO 8601). Defaults to today.")
|
|
1748
|
+
},
|
|
1749
|
+
wrapTool(async (input) => {
|
|
1750
|
+
const result = await client2.bankAccount.transfer({
|
|
1751
|
+
fromAccountId: input.from_account_id,
|
|
1752
|
+
toAccountId: input.to_account_id,
|
|
1753
|
+
amount: input.amount,
|
|
1754
|
+
description: input.description,
|
|
1755
|
+
transactionDate: input.transaction_date
|
|
1756
|
+
});
|
|
1757
|
+
return {
|
|
1758
|
+
content: [{
|
|
1759
|
+
type: "text",
|
|
1760
|
+
text: JSON.stringify(result, null, 2)
|
|
1761
|
+
}]
|
|
1762
|
+
};
|
|
1763
|
+
})
|
|
1764
|
+
);
|
|
1765
|
+
server2.tool(
|
|
1766
|
+
"bank_account_transactions",
|
|
1767
|
+
[
|
|
1768
|
+
"List transactions for a specific bank or cash account with date and type filters.",
|
|
1769
|
+
"Each transaction includes a 'balanceAfter' field showing the running balance.",
|
|
1770
|
+
"Use type='deposit' to see only incoming funds, 'withdrawal' for outgoing, 'transfer' for inter-account moves."
|
|
1771
|
+
].join(" "),
|
|
1772
|
+
{
|
|
1773
|
+
account_id: z9.string().uuid().describe("Bank account UUID from bank_account_list."),
|
|
1774
|
+
from_date: z9.string().datetime().optional().describe("Start date (ISO 8601)."),
|
|
1775
|
+
to_date: z9.string().datetime().optional().describe("End date (ISO 8601)."),
|
|
1776
|
+
type: z9.enum(["deposit", "withdrawal", "transfer"]).optional().describe("Filter by transaction type."),
|
|
1777
|
+
page: z9.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
1778
|
+
},
|
|
1779
|
+
wrapTool(async (input) => {
|
|
1780
|
+
const result = await client2.bankAccount.listTransactions({
|
|
1781
|
+
bankAccountId: input.account_id,
|
|
1782
|
+
fromDate: input.from_date,
|
|
1783
|
+
toDate: input.to_date,
|
|
1784
|
+
type: input.type,
|
|
1785
|
+
page: input.page,
|
|
1786
|
+
limit: MAX_PAGE_SIZE
|
|
1787
|
+
});
|
|
1788
|
+
return {
|
|
1789
|
+
content: [{
|
|
1790
|
+
type: "text",
|
|
1791
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
1792
|
+
}]
|
|
1793
|
+
};
|
|
1794
|
+
})
|
|
1795
|
+
);
|
|
1796
|
+
server2.tool(
|
|
1797
|
+
"bank_account_summary",
|
|
1798
|
+
[
|
|
1799
|
+
"Get a summary of total funds across all bank and cash accounts.",
|
|
1800
|
+
"Returns totalBalance (all accounts), cashInHand (cash-type accounts only), bankBalance (non-cash accounts), and account count.",
|
|
1801
|
+
"Use this to quickly answer 'How much money do we have in total?' or 'What is our cash in hand?'"
|
|
1802
|
+
].join(" "),
|
|
1803
|
+
{},
|
|
1804
|
+
wrapTool(async (_input) => {
|
|
1805
|
+
const summary = await client2.bankAccount.summary();
|
|
1806
|
+
return {
|
|
1807
|
+
content: [{
|
|
1808
|
+
type: "text",
|
|
1809
|
+
text: JSON.stringify(summary, null, 2)
|
|
1810
|
+
}]
|
|
1811
|
+
};
|
|
1812
|
+
})
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// src/tools/reports.ts
|
|
1817
|
+
import { z as z10 } from "zod";
|
|
1818
|
+
var CURRENT_YEAR2 = (/* @__PURE__ */ new Date()).getFullYear();
|
|
1819
|
+
function registerReportTools(server2, client2) {
|
|
1820
|
+
server2.tool(
|
|
1821
|
+
"report_daybook",
|
|
1822
|
+
[
|
|
1823
|
+
"Get the daybook (daily transaction log) for a date range.",
|
|
1824
|
+
"Returns all invoices, payments, and expenses in chronological order with debit/credit columns.",
|
|
1825
|
+
"Use type_filter to narrow to only invoices, payments, or expenses.",
|
|
1826
|
+
"The summary shows totals: sales invoiced, purchases invoiced, payments received, payments made, expenses, and net cash movement.",
|
|
1827
|
+
"Dates are plain date strings (YYYY-MM-DD), not ISO datetime."
|
|
1828
|
+
].join(" "),
|
|
1829
|
+
{
|
|
1830
|
+
from_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Start date in YYYY-MM-DD format, e.g. '2024-04-01'."),
|
|
1831
|
+
to_date: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("End date in YYYY-MM-DD format, e.g. '2024-04-30'."),
|
|
1832
|
+
type_filter: z10.enum(["all", "invoices", "payments", "expenses"]).default("all").describe("'all' = everything, 'invoices' = only invoice entries, 'payments' = only payment entries, 'expenses' = only expense entries.")
|
|
1833
|
+
},
|
|
1834
|
+
wrapTool(async (input) => {
|
|
1835
|
+
const result = await client2.reports.daybook({
|
|
1836
|
+
fromDate: input.from_date,
|
|
1837
|
+
toDate: input.to_date,
|
|
1838
|
+
typeFilter: input.type_filter
|
|
1839
|
+
});
|
|
1840
|
+
return {
|
|
1841
|
+
content: [{
|
|
1842
|
+
type: "text",
|
|
1843
|
+
text: JSON.stringify(result, null, 2)
|
|
1844
|
+
}]
|
|
1845
|
+
};
|
|
1846
|
+
})
|
|
1847
|
+
);
|
|
1848
|
+
server2.tool(
|
|
1849
|
+
"report_outstanding",
|
|
1850
|
+
[
|
|
1851
|
+
"Get outstanding receivables (what customers owe you) and/or payables (what you owe suppliers).",
|
|
1852
|
+
"Results are grouped by party with aging buckets: current (0-30 days), 31-60 days, 61-90 days, 90+ days.",
|
|
1853
|
+
"Use type='receivable' for unpaid customer invoices, 'payable' for unpaid supplier bills, 'both' for all.",
|
|
1854
|
+
"as_of_date lets you see outstanding amounts as of a specific past date."
|
|
1855
|
+
].join(" "),
|
|
1856
|
+
{
|
|
1857
|
+
type: z10.enum(["receivable", "payable", "both"]).default("receivable").describe("'receivable' = unpaid customer invoices, 'payable' = unpaid supplier bills, 'both' = all outstanding."),
|
|
1858
|
+
as_of_date: z10.string().datetime().optional().describe("Calculate outstanding as of this date (ISO 8601). Defaults to today.")
|
|
1859
|
+
},
|
|
1860
|
+
wrapTool(async (input) => {
|
|
1861
|
+
const result = await client2.reports.outstanding({
|
|
1862
|
+
type: input.type,
|
|
1863
|
+
asOfDate: input.as_of_date
|
|
1864
|
+
});
|
|
1865
|
+
return {
|
|
1866
|
+
content: [{
|
|
1867
|
+
type: "text",
|
|
1868
|
+
text: JSON.stringify(result, null, 2)
|
|
1869
|
+
}]
|
|
1870
|
+
};
|
|
1871
|
+
})
|
|
1872
|
+
);
|
|
1873
|
+
server2.tool(
|
|
1874
|
+
"report_tax_summary",
|
|
1875
|
+
[
|
|
1876
|
+
"Get a tax summary (GST collected/paid) for a date range, broken down by tax rate.",
|
|
1877
|
+
"Use this to answer 'How much GST did we collect this month?' or 'What is our total ITC (input tax credit)?'",
|
|
1878
|
+
"Results are grouped by tax rate (0%, 5%, 12%, 18%, 28%) showing taxable amount and tax amount.",
|
|
1879
|
+
"type='sales' = output tax (collected from customers), 'purchases' = input tax (ITC from suppliers)."
|
|
1880
|
+
].join(" "),
|
|
1881
|
+
{
|
|
1882
|
+
from_date: z10.string().datetime().describe("Start date (ISO 8601)."),
|
|
1883
|
+
to_date: z10.string().datetime().describe("End date (ISO 8601)."),
|
|
1884
|
+
type: z10.enum(["sales", "purchases", "both"]).default("both").describe("'sales' = tax on outward supplies, 'purchases' = input tax credit, 'both' = combined.")
|
|
1885
|
+
},
|
|
1886
|
+
wrapTool(async (input) => {
|
|
1887
|
+
const result = await client2.reports.taxSummary({
|
|
1888
|
+
fromDate: input.from_date,
|
|
1889
|
+
toDate: input.to_date,
|
|
1890
|
+
type: input.type
|
|
1891
|
+
});
|
|
1892
|
+
return {
|
|
1893
|
+
content: [{
|
|
1894
|
+
type: "text",
|
|
1895
|
+
text: JSON.stringify(result, null, 2)
|
|
1896
|
+
}]
|
|
1897
|
+
};
|
|
1898
|
+
})
|
|
1899
|
+
);
|
|
1900
|
+
server2.tool(
|
|
1901
|
+
"report_item_sales",
|
|
1902
|
+
[
|
|
1903
|
+
"Get item-wise sales analysis for a date range.",
|
|
1904
|
+
"Returns revenue, quantity sold, invoice count, and margin for each item.",
|
|
1905
|
+
"Use sort_by to rank items by revenue (default), quantity, number of invoices, or profit margin.",
|
|
1906
|
+
"Use compare_to_previous=true to include comparison with the equivalent prior period.",
|
|
1907
|
+
"Use this to answer 'What were our top-selling products this quarter?' or 'Which items have the best margins?'"
|
|
1908
|
+
].join(" "),
|
|
1909
|
+
{
|
|
1910
|
+
from_date: z10.string().datetime().describe("Start date (ISO 8601)."),
|
|
1911
|
+
to_date: z10.string().datetime().describe("End date (ISO 8601)."),
|
|
1912
|
+
category: z10.string().max(100).optional().describe("Filter by item category."),
|
|
1913
|
+
item_type: z10.enum(["product", "service"]).optional().describe("'product' for physical goods, 'service' for services."),
|
|
1914
|
+
sort_by: z10.enum(["revenue", "quantity", "invoices", "margin"]).default("revenue").describe("Sort by: 'revenue' (total sales amount), 'quantity' (units sold), 'invoices' (order count), 'margin' (profit)."),
|
|
1915
|
+
compare_to_previous: z10.boolean().default(false).describe("If true, include comparison data from the previous equivalent period.")
|
|
1916
|
+
},
|
|
1917
|
+
wrapTool(async (input) => {
|
|
1918
|
+
const result = await client2.reports.itemSales({
|
|
1919
|
+
fromDate: input.from_date,
|
|
1920
|
+
toDate: input.to_date,
|
|
1921
|
+
category: input.category,
|
|
1922
|
+
itemType: input.item_type,
|
|
1923
|
+
sortBy: input.sort_by,
|
|
1924
|
+
compareToPrevious: input.compare_to_previous
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
content: [{
|
|
1928
|
+
type: "text",
|
|
1929
|
+
text: JSON.stringify(result, null, 2)
|
|
1930
|
+
}]
|
|
1931
|
+
};
|
|
1932
|
+
})
|
|
1933
|
+
);
|
|
1934
|
+
server2.tool(
|
|
1935
|
+
"report_stock_summary",
|
|
1936
|
+
[
|
|
1937
|
+
"Get current stock levels and values for all inventory items.",
|
|
1938
|
+
"Returns quantity on hand, sale value, and purchase value for each product.",
|
|
1939
|
+
"Use category to filter by product category.",
|
|
1940
|
+
"Set show_zero_stock=true to include items with zero stock (useful for identifying out-of-stock items).",
|
|
1941
|
+
"Use this to answer 'What is the total value of our inventory?' or 'Which products are out of stock?'"
|
|
1942
|
+
].join(" "),
|
|
1943
|
+
{
|
|
1944
|
+
category: z10.string().max(100).optional().describe("Filter by item category."),
|
|
1945
|
+
show_zero_stock: z10.boolean().default(false).describe("If true, include items with zero stock. Default false (only items with stock > 0).")
|
|
1946
|
+
},
|
|
1947
|
+
wrapTool(async (input) => {
|
|
1948
|
+
const result = await client2.reports.stockSummary({
|
|
1949
|
+
category: input.category,
|
|
1950
|
+
showZeroStock: input.show_zero_stock
|
|
1951
|
+
});
|
|
1952
|
+
return {
|
|
1953
|
+
content: [{
|
|
1954
|
+
type: "text",
|
|
1955
|
+
text: JSON.stringify(result, null, 2)
|
|
1956
|
+
}]
|
|
1957
|
+
};
|
|
1958
|
+
})
|
|
1959
|
+
);
|
|
1960
|
+
server2.tool(
|
|
1961
|
+
"report_party_statement",
|
|
1962
|
+
[
|
|
1963
|
+
"Get a party (customer or supplier) account statement showing all transactions and running balance.",
|
|
1964
|
+
"Returns invoices, payments, credit notes, and other entries in chronological order.",
|
|
1965
|
+
"Use this to answer 'Show me the complete transaction history with Customer X' or 'What is the account statement for this supplier?'",
|
|
1966
|
+
"Note: for a quick balance only, use party_get instead."
|
|
1967
|
+
].join(" "),
|
|
1968
|
+
{
|
|
1969
|
+
party_id: z10.string().uuid().describe("Party UUID from party_list or party_get."),
|
|
1970
|
+
from_date: z10.string().datetime().optional().describe("Start date for the statement (ISO 8601). Omit for full history."),
|
|
1971
|
+
to_date: z10.string().datetime().optional().describe("End date for the statement (ISO 8601).")
|
|
1972
|
+
},
|
|
1973
|
+
wrapTool(async (input) => {
|
|
1974
|
+
const result = await client2.reports.partyStatement({
|
|
1975
|
+
partyId: input.party_id,
|
|
1976
|
+
fromDate: input.from_date,
|
|
1977
|
+
toDate: input.to_date
|
|
1978
|
+
});
|
|
1979
|
+
return {
|
|
1980
|
+
content: [{
|
|
1981
|
+
type: "text",
|
|
1982
|
+
text: JSON.stringify(result, null, 2)
|
|
1983
|
+
}]
|
|
1984
|
+
};
|
|
1985
|
+
})
|
|
1986
|
+
);
|
|
1987
|
+
server2.tool(
|
|
1988
|
+
"report_payment_summary",
|
|
1989
|
+
[
|
|
1990
|
+
"Get a payment summary showing trends by payment mode (cash, UPI, bank, cheque) over a date range.",
|
|
1991
|
+
"Use type='received' for incoming payments from customers, 'made' for payments to suppliers, 'both' for all.",
|
|
1992
|
+
"Filter by bank_account_id to see payments for a specific account.",
|
|
1993
|
+
"Use this to answer 'How much did we receive via UPI this month?' or 'What were our total bank deposits?'"
|
|
1994
|
+
].join(" "),
|
|
1995
|
+
{
|
|
1996
|
+
from_date: z10.string().datetime().describe("Start date (ISO 8601)."),
|
|
1997
|
+
to_date: z10.string().datetime().describe("End date (ISO 8601)."),
|
|
1998
|
+
type: z10.enum(["received", "made", "both"]).default("both").describe("'received' = payments from customers, 'made' = payments to suppliers, 'both' = all."),
|
|
1999
|
+
bank_account_id: z10.string().uuid().optional().describe("Filter to payments recorded against a specific bank/cash account.")
|
|
2000
|
+
},
|
|
2001
|
+
wrapTool(async (input) => {
|
|
2002
|
+
const result = await client2.reports.paymentSummary({
|
|
2003
|
+
fromDate: input.from_date,
|
|
2004
|
+
toDate: input.to_date,
|
|
2005
|
+
type: input.type,
|
|
2006
|
+
bankAccountId: input.bank_account_id
|
|
2007
|
+
});
|
|
2008
|
+
return {
|
|
2009
|
+
content: [{
|
|
2010
|
+
type: "text",
|
|
2011
|
+
text: JSON.stringify(result, null, 2)
|
|
2012
|
+
}]
|
|
2013
|
+
};
|
|
2014
|
+
})
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// src/tools/store.ts
|
|
2019
|
+
import { z as z11 } from "zod";
|
|
2020
|
+
var ORDER_STATUSES = ["pending", "confirmed", "preparing", "ready", "delivered", "cancelled"];
|
|
2021
|
+
function registerStoreTools(server2, client2) {
|
|
2022
|
+
server2.tool(
|
|
2023
|
+
"store_settings",
|
|
2024
|
+
[
|
|
2025
|
+
"Get the current online store configuration for the active business.",
|
|
2026
|
+
"Returns whether the store is enabled, the store URL slug, tagline, accent color, minimum order amount, and order prefix.",
|
|
2027
|
+
"If storeEnabled=false, the public store is not accessible.",
|
|
2028
|
+
"The store URL is: https://<storeSlug>.hisaabo.in (when enabled)."
|
|
2029
|
+
].join(" "),
|
|
2030
|
+
{},
|
|
2031
|
+
wrapTool(async (_input) => {
|
|
2032
|
+
const settings = await client2.store.getSettings();
|
|
2033
|
+
return {
|
|
2034
|
+
content: [{
|
|
2035
|
+
type: "text",
|
|
2036
|
+
text: JSON.stringify(settings, null, 2)
|
|
2037
|
+
}]
|
|
2038
|
+
};
|
|
2039
|
+
})
|
|
2040
|
+
);
|
|
2041
|
+
server2.tool(
|
|
2042
|
+
"store_update_settings",
|
|
2043
|
+
[
|
|
2044
|
+
"Update the online store configuration.",
|
|
2045
|
+
"Enable/disable the store, change the URL slug, tagline, accent color, minimum order amount, etc.",
|
|
2046
|
+
"store_slug must be unique across all businesses and match the pattern: lowercase letters, numbers, and hyphens.",
|
|
2047
|
+
"Setting store_enabled=true activates the public storefront."
|
|
2048
|
+
].join(" "),
|
|
2049
|
+
{
|
|
2050
|
+
store_enabled: z11.boolean().optional().describe("Enable (true) or disable (false) the public storefront."),
|
|
2051
|
+
store_slug: z11.string().min(3).max(50).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/).optional().nullable().describe("URL slug for the store, e.g. 'acme-traders'. Must be lowercase alphanumeric with hyphens."),
|
|
2052
|
+
store_tagline: z11.string().max(200).optional().nullable().describe("Short tagline shown on the store homepage, e.g. 'Fresh groceries delivered daily'."),
|
|
2053
|
+
store_accent_color: z11.string().regex(/^#[0-9a-fA-F]{6}$/).optional().nullable().describe("Brand accent color as hex code, e.g. '#FF5722'."),
|
|
2054
|
+
store_min_order_amount: z11.string().regex(/^\d+(\.\d{1,2})?$/).optional().nullable().describe("Minimum order value as decimal string, e.g. '200.00'. Null to remove minimum."),
|
|
2055
|
+
store_delivery_note: z11.string().max(500).optional().nullable().describe("Note shown at checkout about delivery, e.g. 'Delivery within 2 hours in city limits'."),
|
|
2056
|
+
store_whatsapp_number: z11.string().max(15).optional().nullable().describe("WhatsApp number for order notifications (digits only)."),
|
|
2057
|
+
store_allow_negative_stock: z11.boolean().optional().describe("If true, orders can be placed even when stock is zero or negative."),
|
|
2058
|
+
store_order_prefix: z11.string().min(1).max(10).optional().describe("Prefix for store order numbers, e.g. 'ORD'. Default 'ORD'.")
|
|
2059
|
+
},
|
|
2060
|
+
wrapTool(async (input) => {
|
|
2061
|
+
const settings = await client2.store.updateSettings({
|
|
2062
|
+
storeEnabled: input.store_enabled,
|
|
2063
|
+
storeSlug: input.store_slug,
|
|
2064
|
+
storeTagline: input.store_tagline,
|
|
2065
|
+
storeAccentColor: input.store_accent_color,
|
|
2066
|
+
storeMinOrderAmount: input.store_min_order_amount,
|
|
2067
|
+
storeDeliveryNote: input.store_delivery_note,
|
|
2068
|
+
storeWhatsappNumber: input.store_whatsapp_number,
|
|
2069
|
+
storeAllowNegativeStock: input.store_allow_negative_stock,
|
|
2070
|
+
storeOrderPrefix: input.store_order_prefix
|
|
2071
|
+
});
|
|
2072
|
+
return {
|
|
2073
|
+
content: [{
|
|
2074
|
+
type: "text",
|
|
2075
|
+
text: JSON.stringify(settings, null, 2)
|
|
2076
|
+
}]
|
|
2077
|
+
};
|
|
2078
|
+
})
|
|
2079
|
+
);
|
|
2080
|
+
server2.tool(
|
|
2081
|
+
"store_orders",
|
|
2082
|
+
[
|
|
2083
|
+
"List customer orders placed through the online store.",
|
|
2084
|
+
"Orders progress through statuses: pending \u2192 confirmed \u2192 preparing \u2192 ready \u2192 delivered.",
|
|
2085
|
+
"Use status='pending' to find new orders requiring attention.",
|
|
2086
|
+
"Search by customer name, phone number, or order number."
|
|
2087
|
+
].join(" "),
|
|
2088
|
+
{
|
|
2089
|
+
status: z11.enum(ORDER_STATUSES).optional().describe("Filter by order status. 'pending' = new orders awaiting confirmation."),
|
|
2090
|
+
from_date: z11.string().datetime().optional().describe("Filter orders placed on or after this date (ISO 8601)."),
|
|
2091
|
+
to_date: z11.string().datetime().optional().describe("Filter orders placed on or before this date (ISO 8601)."),
|
|
2092
|
+
search: z11.string().max(200).optional().describe("Search by customer name, phone number, or order number."),
|
|
2093
|
+
page: z11.number().int().min(1).default(1).describe("Page number for pagination.")
|
|
2094
|
+
},
|
|
2095
|
+
wrapTool(async (input) => {
|
|
2096
|
+
const result = await client2.store.listOrders({
|
|
2097
|
+
status: input.status,
|
|
2098
|
+
fromDate: input.from_date,
|
|
2099
|
+
toDate: input.to_date,
|
|
2100
|
+
search: input.search,
|
|
2101
|
+
page: input.page,
|
|
2102
|
+
limit: MAX_PAGE_SIZE
|
|
2103
|
+
});
|
|
2104
|
+
return {
|
|
2105
|
+
content: [{
|
|
2106
|
+
type: "text",
|
|
2107
|
+
text: JSON.stringify(withPaginationMeta(result), null, 2)
|
|
2108
|
+
}]
|
|
2109
|
+
};
|
|
2110
|
+
})
|
|
2111
|
+
);
|
|
2112
|
+
server2.tool(
|
|
2113
|
+
"store_order_get",
|
|
2114
|
+
[
|
|
2115
|
+
"Get full details of a single store order, including line items and linked invoice.",
|
|
2116
|
+
"If the order has been confirmed, it will have a linked invoice (invoiceId) in the response."
|
|
2117
|
+
].join(" "),
|
|
2118
|
+
{
|
|
2119
|
+
order_id: z11.string().uuid().describe("Store order UUID from store_orders.")
|
|
2120
|
+
},
|
|
2121
|
+
wrapTool(async (input) => {
|
|
2122
|
+
const order = await client2.store.getOrder(input.order_id);
|
|
2123
|
+
return {
|
|
2124
|
+
content: [{
|
|
2125
|
+
type: "text",
|
|
2126
|
+
text: JSON.stringify(order, null, 2)
|
|
2127
|
+
}]
|
|
2128
|
+
};
|
|
2129
|
+
})
|
|
2130
|
+
);
|
|
2131
|
+
server2.tool(
|
|
2132
|
+
"store_order_update",
|
|
2133
|
+
[
|
|
2134
|
+
"Update a confirmed store order's status to 'preparing', 'ready', or 'delivered'.",
|
|
2135
|
+
"Orders must be confirmed first (status='confirmed') before they can be updated.",
|
|
2136
|
+
"Use 'preparing' when the order is being packed, 'ready' when ready for pickup/delivery, 'delivered' when handed to customer.",
|
|
2137
|
+
"To confirm a pending order or cancel an order, use the Hisaabo web app \u2014 those actions also update the linked invoice."
|
|
2138
|
+
].join(" "),
|
|
2139
|
+
{
|
|
2140
|
+
order_id: z11.string().uuid().describe("Store order UUID."),
|
|
2141
|
+
status: z11.enum(["preparing", "ready", "delivered"]).describe("New status: 'preparing' (being packed), 'ready' (ready to deliver), 'delivered' (handed to customer).")
|
|
2142
|
+
},
|
|
2143
|
+
wrapTool(async (input) => {
|
|
2144
|
+
const result = await client2.store.updateOrderStatus({
|
|
2145
|
+
orderId: input.order_id,
|
|
2146
|
+
status: input.status
|
|
2147
|
+
});
|
|
2148
|
+
return {
|
|
2149
|
+
content: [{
|
|
2150
|
+
type: "text",
|
|
2151
|
+
text: JSON.stringify(result, null, 2)
|
|
2152
|
+
}]
|
|
2153
|
+
};
|
|
2154
|
+
})
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/tools/target.ts
|
|
2159
|
+
import { z as z12 } from "zod";
|
|
2160
|
+
var TARGET_TYPES = ["order_count", "order_value", "item_quantity"];
|
|
2161
|
+
var PERIOD_TYPES = ["daily", "weekly", "monthly", "quarterly", "custom"];
|
|
2162
|
+
function registerTargetTools(server2, client2) {
|
|
2163
|
+
server2.tool(
|
|
2164
|
+
"target_list",
|
|
2165
|
+
[
|
|
2166
|
+
"List sales targets for the active business (admin/viewer view).",
|
|
2167
|
+
"Filter by user, period type, or active targets only.",
|
|
2168
|
+
"Set with_progress=true to include real-time progress (current vs target, percentage, on-track status).",
|
|
2169
|
+
"Use user_id to see targets for a specific seller."
|
|
2170
|
+
].join(" "),
|
|
2171
|
+
{
|
|
2172
|
+
user_id: z12.string().uuid().optional().describe("Filter targets assigned to a specific user UUID."),
|
|
2173
|
+
period_type: z12.enum(PERIOD_TYPES).optional().describe("Filter by period: 'daily', 'weekly', 'monthly', 'quarterly', or 'custom'."),
|
|
2174
|
+
active: z12.boolean().optional().describe("If true, return only targets whose period includes today (active targets)."),
|
|
2175
|
+
with_progress: z12.boolean().default(false).describe("If true, compute and return real-time progress for each target. Adds latency.")
|
|
2176
|
+
},
|
|
2177
|
+
wrapTool(async (input) => {
|
|
2178
|
+
const targets = await client2.target.list({
|
|
2179
|
+
userId: input.user_id,
|
|
2180
|
+
periodType: input.period_type,
|
|
2181
|
+
active: input.active,
|
|
2182
|
+
withProgress: input.with_progress
|
|
2183
|
+
});
|
|
2184
|
+
return {
|
|
2185
|
+
content: [{
|
|
2186
|
+
type: "text",
|
|
2187
|
+
text: JSON.stringify(targets, null, 2)
|
|
2188
|
+
}]
|
|
2189
|
+
};
|
|
2190
|
+
})
|
|
2191
|
+
);
|
|
2192
|
+
server2.tool(
|
|
2193
|
+
"target_create",
|
|
2194
|
+
[
|
|
2195
|
+
"Create a sales target for a seller. Requires admin role.",
|
|
2196
|
+
"target_type options: 'order_count' (number of invoices), 'order_value' (total revenue), 'item_quantity' (units sold of a specific item).",
|
|
2197
|
+
"For 'item_quantity' targets, item_id is required.",
|
|
2198
|
+
"period_start and period_end define the target window. Use period_type to categorize (daily/weekly/monthly/quarterly/custom).",
|
|
2199
|
+
"Example: set a monthly revenue target of \u20B91,00,000 for a seller."
|
|
2200
|
+
].join(" "),
|
|
2201
|
+
{
|
|
2202
|
+
user_id: z12.string().uuid().describe("UUID of the seller this target is assigned to."),
|
|
2203
|
+
target_type: z12.enum(TARGET_TYPES).describe("'order_count' = number of invoices, 'order_value' = total revenue amount, 'item_quantity' = units of a specific item sold."),
|
|
2204
|
+
target_value: z12.string().regex(/^\d+(\.\d{1,2})?$/).describe("Target value as decimal string: e.g. '100000.00' for \u20B91 lakh revenue, or '50' for 50 orders."),
|
|
2205
|
+
item_id: z12.string().uuid().optional().nullable().describe("Required for 'item_quantity' targets \u2014 the specific item UUID to track."),
|
|
2206
|
+
period_type: z12.enum(PERIOD_TYPES).describe("Categorization: 'daily', 'weekly', 'monthly', 'quarterly', or 'custom'."),
|
|
2207
|
+
period_start: z12.string().datetime().describe("Start of the target period (ISO 8601)."),
|
|
2208
|
+
period_end: z12.string().datetime().describe("End of the target period (ISO 8601). Must be after period_start."),
|
|
2209
|
+
notes: z12.string().max(500).optional().nullable().describe("Optional notes about this target, e.g. 'Q1 FY2025 target'.")
|
|
2210
|
+
},
|
|
2211
|
+
wrapTool(async (input) => {
|
|
2212
|
+
const target = await client2.target.create({
|
|
2213
|
+
userId: input.user_id,
|
|
2214
|
+
targetType: input.target_type,
|
|
2215
|
+
targetValue: input.target_value,
|
|
2216
|
+
itemId: input.item_id,
|
|
2217
|
+
periodType: input.period_type,
|
|
2218
|
+
periodStart: input.period_start,
|
|
2219
|
+
periodEnd: input.period_end,
|
|
2220
|
+
notes: input.notes
|
|
2221
|
+
});
|
|
2222
|
+
return {
|
|
2223
|
+
content: [{
|
|
2224
|
+
type: "text",
|
|
2225
|
+
text: JSON.stringify(target, null, 2)
|
|
2226
|
+
}]
|
|
2227
|
+
};
|
|
2228
|
+
})
|
|
2229
|
+
);
|
|
2230
|
+
server2.tool(
|
|
2231
|
+
"target_progress",
|
|
2232
|
+
[
|
|
2233
|
+
"Get real-time progress for a specific sales target.",
|
|
2234
|
+
"Returns current achievement, target value, percentage, remaining amount, and whether the seller is on track.",
|
|
2235
|
+
"Also shows timeline: days total, days elapsed, days remaining.",
|
|
2236
|
+
"'onTrack' is true if current progress is >= what it should be based on time elapsed."
|
|
2237
|
+
].join(" "),
|
|
2238
|
+
{
|
|
2239
|
+
target_id: z12.string().uuid().describe("Target UUID from target_list.")
|
|
2240
|
+
},
|
|
2241
|
+
wrapTool(async (input) => {
|
|
2242
|
+
const result = await client2.target.getProgress(input.target_id);
|
|
2243
|
+
return {
|
|
2244
|
+
content: [{
|
|
2245
|
+
type: "text",
|
|
2246
|
+
text: JSON.stringify(result, null, 2)
|
|
2247
|
+
}]
|
|
2248
|
+
};
|
|
2249
|
+
})
|
|
2250
|
+
);
|
|
2251
|
+
server2.tool(
|
|
2252
|
+
"target_my",
|
|
2253
|
+
[
|
|
2254
|
+
"Get the current user's active sales targets with real-time progress.",
|
|
2255
|
+
"Returns targets whose period includes today, with progress data for each.",
|
|
2256
|
+
"Sellers use this to check their own performance without needing admin access."
|
|
2257
|
+
].join(" "),
|
|
2258
|
+
{},
|
|
2259
|
+
wrapTool(async (_input) => {
|
|
2260
|
+
const targets = await client2.target.myTargets();
|
|
2261
|
+
return {
|
|
2262
|
+
content: [{
|
|
2263
|
+
type: "text",
|
|
2264
|
+
text: JSON.stringify(targets, null, 2)
|
|
2265
|
+
}]
|
|
2266
|
+
};
|
|
2267
|
+
})
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// src/tools/import.ts
|
|
2272
|
+
import { z as z13 } from "zod";
|
|
2273
|
+
function registerImportTools(server2, client2) {
|
|
2274
|
+
server2.tool(
|
|
2275
|
+
"import_parties",
|
|
2276
|
+
[
|
|
2277
|
+
"Batch import customers and/or suppliers. Requires admin role.",
|
|
2278
|
+
"Duplicates (same name, case-insensitive) are automatically skipped \u2014 never overwritten.",
|
|
2279
|
+
"Returns a summary: created (new records), skipped (duplicates), total (input count).",
|
|
2280
|
+
"Use source to tag where the data came from, e.g. 'mybillbook', 'tally', 'excel'."
|
|
2281
|
+
].join(" "),
|
|
2282
|
+
{
|
|
2283
|
+
source: z13.string().default("excel").describe("Source system name, e.g. 'mybillbook', 'tally', 'excel'. Used for audit trail."),
|
|
2284
|
+
parties: z13.array(z13.object({
|
|
2285
|
+
name: z13.string().min(1).max(200).describe("Party name. Duplicate names (case-insensitive) are skipped."),
|
|
2286
|
+
type: z13.enum(["customer", "supplier"]).default("customer").describe("'customer' for buyers, 'supplier' for sellers."),
|
|
2287
|
+
phone: z13.string().max(15).optional().describe("Phone number."),
|
|
2288
|
+
email: z13.string().optional().describe("Email address."),
|
|
2289
|
+
gstin: z13.string().optional().describe("GST Identification Number (15 chars)."),
|
|
2290
|
+
pan: z13.string().optional().describe("PAN number."),
|
|
2291
|
+
opening_balance: z13.string().optional().describe("Opening balance as decimal string. Positive = they owe you. Default '0'."),
|
|
2292
|
+
billing_address: z13.string().optional().describe("Billing address."),
|
|
2293
|
+
shipping_address: z13.string().optional().describe("Shipping address (if different from billing)."),
|
|
2294
|
+
city: z13.string().optional().describe("City."),
|
|
2295
|
+
state: z13.string().optional().describe("State name."),
|
|
2296
|
+
pincode: z13.string().optional().describe("PIN code.")
|
|
2297
|
+
})).min(1).max(5e3).describe("Array of party records to import. Max 5000 per call.")
|
|
2298
|
+
},
|
|
2299
|
+
wrapTool(async (input) => {
|
|
2300
|
+
const result = await client2.import.importParties({
|
|
2301
|
+
source: input.source,
|
|
2302
|
+
parties: input.parties.map((p) => ({
|
|
2303
|
+
name: p.name,
|
|
2304
|
+
type: p.type,
|
|
2305
|
+
phone: p.phone,
|
|
2306
|
+
email: p.email,
|
|
2307
|
+
gstin: p.gstin,
|
|
2308
|
+
pan: p.pan,
|
|
2309
|
+
openingBalance: p.opening_balance,
|
|
2310
|
+
billingAddress: p.billing_address,
|
|
2311
|
+
shippingAddress: p.shipping_address,
|
|
2312
|
+
city: p.city,
|
|
2313
|
+
state: p.state,
|
|
2314
|
+
pincode: p.pincode
|
|
2315
|
+
}))
|
|
2316
|
+
});
|
|
2317
|
+
return {
|
|
2318
|
+
content: [{
|
|
2319
|
+
type: "text",
|
|
2320
|
+
text: JSON.stringify(result, null, 2)
|
|
2321
|
+
}]
|
|
2322
|
+
};
|
|
2323
|
+
})
|
|
2324
|
+
);
|
|
2325
|
+
server2.tool(
|
|
2326
|
+
"import_items",
|
|
2327
|
+
[
|
|
2328
|
+
"Batch import inventory items or services. Requires admin role.",
|
|
2329
|
+
"Duplicates (same name, case-insensitive) are automatically skipped.",
|
|
2330
|
+
"Returns: created, skipped, total, and unmappedUnits (unit codes that were not recognized and defaulted to 'other').",
|
|
2331
|
+
"Common unit codes are auto-mapped: KGS\u2192kg, PCS\u2192pcs, LTR\u2192l, BOX\u2192box, etc.",
|
|
2332
|
+
"Stock quantity is always set to 0 on import \u2014 use import_invoices or item_adjust_stock to build stock from history."
|
|
2333
|
+
].join(" "),
|
|
2334
|
+
{
|
|
2335
|
+
source: z13.string().default("excel").describe("Source system name for audit trail."),
|
|
2336
|
+
items: z13.array(z13.object({
|
|
2337
|
+
name: z13.string().min(1).max(200).describe("Item name. Duplicates (case-insensitive) are skipped."),
|
|
2338
|
+
item_type: z13.enum(["product", "service"]).default("product").describe("'product' for physical goods, 'service' for services."),
|
|
2339
|
+
sale_price: z13.string().optional().describe("Default selling price as decimal string."),
|
|
2340
|
+
purchase_price: z13.string().optional().describe("Default purchase price as decimal string."),
|
|
2341
|
+
tax_percent: z13.string().default("0").describe("GST rate percentage, e.g. '18'. Default '0'."),
|
|
2342
|
+
hsn: z13.string().optional().describe("HSN code for GST compliance."),
|
|
2343
|
+
unit: z13.string().default("pcs").describe("Unit code. Common values: pcs, kg, l, box, m. Auto-mapped from MyBillBook/Tally format."),
|
|
2344
|
+
sku: z13.string().optional().describe("SKU / product code."),
|
|
2345
|
+
category: z13.string().optional().describe("Category name for grouping.")
|
|
2346
|
+
})).min(1).max(5e3).describe("Array of item records to import. Max 5000 per call.")
|
|
2347
|
+
},
|
|
2348
|
+
wrapTool(async (input) => {
|
|
2349
|
+
const result = await client2.import.importItems({
|
|
2350
|
+
source: input.source,
|
|
2351
|
+
items: input.items.map((item) => ({
|
|
2352
|
+
name: item.name,
|
|
2353
|
+
itemType: item.item_type,
|
|
2354
|
+
salePrice: item.sale_price,
|
|
2355
|
+
purchasePrice: item.purchase_price,
|
|
2356
|
+
taxPercent: item.tax_percent,
|
|
2357
|
+
hsn: item.hsn,
|
|
2358
|
+
unit: item.unit,
|
|
2359
|
+
sku: item.sku,
|
|
2360
|
+
category: item.category
|
|
2361
|
+
}))
|
|
2362
|
+
});
|
|
2363
|
+
return {
|
|
2364
|
+
content: [{
|
|
2365
|
+
type: "text",
|
|
2366
|
+
text: JSON.stringify(result, null, 2)
|
|
2367
|
+
}]
|
|
2368
|
+
};
|
|
2369
|
+
})
|
|
2370
|
+
);
|
|
2371
|
+
server2.tool(
|
|
2372
|
+
"import_invoices",
|
|
2373
|
+
[
|
|
2374
|
+
"Batch import historical invoices with optional line items. Requires admin role.",
|
|
2375
|
+
"Parties referenced by party_name must already exist (import them first with import_parties).",
|
|
2376
|
+
"Invoices with duplicate invoice_number are skipped.",
|
|
2377
|
+
"Set auto_create_payments=true to automatically create payment records for paid invoices.",
|
|
2378
|
+
"Use this for migrating historical data from MyBillBook, Tally, Busy, or Excel."
|
|
2379
|
+
].join(" "),
|
|
2380
|
+
{
|
|
2381
|
+
source: z13.string().default("excel").describe("Source system name for audit trail."),
|
|
2382
|
+
auto_create_payments: z13.boolean().default(false).describe("If true, automatically create payment records for invoices with amountPaid > 0."),
|
|
2383
|
+
default_payment_mode: z13.enum(["cash", "bank", "upi", "cheque", "other"]).default("cash").describe("Payment mode to use when auto-creating payments from paid invoices."),
|
|
2384
|
+
invoices: z13.array(z13.object({
|
|
2385
|
+
invoice_number: z13.string().min(1).describe("Invoice number. Duplicates are skipped."),
|
|
2386
|
+
invoice_date: z13.string().describe("Invoice date (YYYY-MM-DD or DD/MM/YYYY or DD-MM-YYYY)."),
|
|
2387
|
+
due_date: z13.string().optional().describe("Due date (same formats as invoice_date)."),
|
|
2388
|
+
party_name: z13.string().min(1).describe("Exact party name \u2014 must match an existing party (case-insensitive)."),
|
|
2389
|
+
type: z13.enum(["sale", "purchase"]).default("sale").describe("'sale' for customer invoice, 'purchase' for supplier bill."),
|
|
2390
|
+
status: z13.enum(["draft", "sent", "paid", "partial", "overdue", "cancelled"]).default("sent").describe("Invoice status. Use 'paid' for fully paid historical invoices."),
|
|
2391
|
+
total_amount: z13.string().describe("Total invoice amount including tax, as decimal string."),
|
|
2392
|
+
amount_paid: z13.string().default("0").describe("Amount already paid. '0' for unpaid, same as total_amount for fully paid."),
|
|
2393
|
+
subtotal: z13.string().default("0").describe("Pre-tax subtotal as decimal string."),
|
|
2394
|
+
tax_amount: z13.string().default("0").describe("Total tax amount as decimal string."),
|
|
2395
|
+
discount_amount: z13.string().default("0").describe("Total discount amount as decimal string."),
|
|
2396
|
+
notes: z13.string().optional().describe("Invoice notes."),
|
|
2397
|
+
line_items: z13.array(z13.object({
|
|
2398
|
+
description: z13.string().describe("Line item description or product name."),
|
|
2399
|
+
quantity: z13.string().default("1").describe("Quantity as decimal string."),
|
|
2400
|
+
unit_price: z13.string().describe("Price per unit as decimal string."),
|
|
2401
|
+
tax_percent: z13.string().default("0").describe("Tax rate percentage, e.g. '18'."),
|
|
2402
|
+
discount_percent: z13.string().default("0").describe("Discount percentage."),
|
|
2403
|
+
item_name: z13.string().optional().describe("If provided, links to an existing inventory item by name.")
|
|
2404
|
+
})).optional().describe("Line items. Optional \u2014 if omitted, a single line item using total_amount is created.")
|
|
2405
|
+
})).min(1).max(1e3).describe("Array of invoice records. Max 1000 per call.")
|
|
2406
|
+
},
|
|
2407
|
+
wrapTool(async (input) => {
|
|
2408
|
+
const result = await client2.import.importInvoices({
|
|
2409
|
+
source: input.source,
|
|
2410
|
+
autoCreatePayments: input.auto_create_payments,
|
|
2411
|
+
defaultPaymentMode: input.default_payment_mode,
|
|
2412
|
+
invoices: input.invoices.map((inv) => ({
|
|
2413
|
+
invoiceNumber: inv.invoice_number,
|
|
2414
|
+
invoiceDate: inv.invoice_date,
|
|
2415
|
+
dueDate: inv.due_date,
|
|
2416
|
+
partyName: inv.party_name,
|
|
2417
|
+
type: inv.type,
|
|
2418
|
+
status: inv.status,
|
|
2419
|
+
totalAmount: inv.total_amount,
|
|
2420
|
+
amountPaid: inv.amount_paid,
|
|
2421
|
+
subtotal: inv.subtotal,
|
|
2422
|
+
taxAmount: inv.tax_amount,
|
|
2423
|
+
discountAmount: inv.discount_amount,
|
|
2424
|
+
notes: inv.notes,
|
|
2425
|
+
lineItems: inv.line_items?.map((li) => ({
|
|
2426
|
+
description: li.description,
|
|
2427
|
+
quantity: li.quantity,
|
|
2428
|
+
unitPrice: li.unit_price,
|
|
2429
|
+
taxPercent: li.tax_percent,
|
|
2430
|
+
discountPercent: li.discount_percent,
|
|
2431
|
+
itemName: li.item_name
|
|
2432
|
+
})) ?? []
|
|
2433
|
+
}))
|
|
2434
|
+
});
|
|
2435
|
+
return {
|
|
2436
|
+
content: [{
|
|
2437
|
+
type: "text",
|
|
2438
|
+
text: JSON.stringify(result, null, 2)
|
|
2439
|
+
}]
|
|
2440
|
+
};
|
|
2441
|
+
})
|
|
2442
|
+
);
|
|
2443
|
+
server2.tool(
|
|
2444
|
+
"import_payments",
|
|
2445
|
+
[
|
|
2446
|
+
"Batch import historical payment records. Requires admin role.",
|
|
2447
|
+
"Parties referenced by party_name must already exist.",
|
|
2448
|
+
"Invoices referenced by invoice_numbers must already exist (import invoices first).",
|
|
2449
|
+
"Use this to import payment history after invoices are already imported.",
|
|
2450
|
+
"Returns: created, skipped, total, and any errors encountered."
|
|
2451
|
+
].join(" "),
|
|
2452
|
+
{
|
|
2453
|
+
source: z13.string().default("excel").describe("Source system name for audit trail."),
|
|
2454
|
+
paid_invoice_numbers: z13.array(z13.string()).default([]).describe("List of invoice numbers that were fully paid in the source system. Used for auto-allocation fallback."),
|
|
2455
|
+
payments: z13.array(z13.object({
|
|
2456
|
+
party_name: z13.string().min(1).describe("Party name \u2014 must match an existing party (case-insensitive)."),
|
|
2457
|
+
amount: z13.string().describe("Payment amount as decimal string."),
|
|
2458
|
+
mode: z13.enum(["cash", "bank", "upi", "cheque", "other"]).default("cash").describe("Payment mode."),
|
|
2459
|
+
payment_date: z13.string().optional().describe("Payment date (YYYY-MM-DD or DD/MM/YYYY)."),
|
|
2460
|
+
payment_number: z13.string().optional().describe("Original payment number from source system."),
|
|
2461
|
+
reference_number: z13.string().optional().describe("Transaction reference, cheque number, UTR, etc."),
|
|
2462
|
+
notes: z13.string().optional().describe("Notes about this payment."),
|
|
2463
|
+
invoice_numbers: z13.array(z13.string()).optional().describe("Explicit invoice numbers to allocate this payment against.")
|
|
2464
|
+
})).min(1).max(5e3).describe("Array of payment records. Max 5000 per call.")
|
|
2465
|
+
},
|
|
2466
|
+
wrapTool(async (input) => {
|
|
2467
|
+
const result = await client2.import.importPayments({
|
|
2468
|
+
source: input.source,
|
|
2469
|
+
paidInvoiceNumbers: input.paid_invoice_numbers,
|
|
2470
|
+
payments: input.payments.map((p) => ({
|
|
2471
|
+
partyName: p.party_name,
|
|
2472
|
+
amount: p.amount,
|
|
2473
|
+
mode: p.mode,
|
|
2474
|
+
paymentDate: p.payment_date,
|
|
2475
|
+
paymentNumber: p.payment_number,
|
|
2476
|
+
referenceNumber: p.reference_number,
|
|
2477
|
+
notes: p.notes,
|
|
2478
|
+
invoiceNumbers: p.invoice_numbers
|
|
2479
|
+
}))
|
|
2480
|
+
});
|
|
2481
|
+
return {
|
|
2482
|
+
content: [{
|
|
2483
|
+
type: "text",
|
|
2484
|
+
text: JSON.stringify(result, null, 2)
|
|
2485
|
+
}]
|
|
2486
|
+
};
|
|
2487
|
+
})
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// src/resources/index.ts
|
|
2492
|
+
function registerResources(server2, client2) {
|
|
2493
|
+
server2.resource(
|
|
2494
|
+
"business_current",
|
|
2495
|
+
"business://current",
|
|
2496
|
+
async (_uri) => {
|
|
2497
|
+
const biz = await client2.business.get();
|
|
2498
|
+
return {
|
|
2499
|
+
contents: [{
|
|
2500
|
+
uri: "business://current",
|
|
2501
|
+
text: JSON.stringify(biz, null, 2),
|
|
2502
|
+
mimeType: "application/json"
|
|
2503
|
+
}]
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
);
|
|
2507
|
+
server2.resource(
|
|
2508
|
+
"parties_customers",
|
|
2509
|
+
"parties://customers",
|
|
2510
|
+
async (_uri) => {
|
|
2511
|
+
const result = await client2.party.list({
|
|
2512
|
+
type: "customer",
|
|
2513
|
+
sortBy: "name",
|
|
2514
|
+
sortDir: "asc",
|
|
2515
|
+
limit: 50,
|
|
2516
|
+
page: 1
|
|
2517
|
+
});
|
|
2518
|
+
return {
|
|
2519
|
+
contents: [{
|
|
2520
|
+
uri: "parties://customers",
|
|
2521
|
+
text: JSON.stringify(result.data, null, 2),
|
|
2522
|
+
mimeType: "application/json"
|
|
2523
|
+
}]
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
);
|
|
2527
|
+
server2.resource(
|
|
2528
|
+
"parties_suppliers",
|
|
2529
|
+
"parties://suppliers",
|
|
2530
|
+
async (_uri) => {
|
|
2531
|
+
const result = await client2.party.list({
|
|
2532
|
+
type: "supplier",
|
|
2533
|
+
sortBy: "name",
|
|
2534
|
+
sortDir: "asc",
|
|
2535
|
+
limit: 50,
|
|
2536
|
+
page: 1
|
|
2537
|
+
});
|
|
2538
|
+
return {
|
|
2539
|
+
contents: [{
|
|
2540
|
+
uri: "parties://suppliers",
|
|
2541
|
+
text: JSON.stringify(result.data, null, 2),
|
|
2542
|
+
mimeType: "application/json"
|
|
2543
|
+
}]
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
);
|
|
2547
|
+
server2.resource(
|
|
2548
|
+
"items_inventory",
|
|
2549
|
+
"items://inventory",
|
|
2550
|
+
async (_uri) => {
|
|
2551
|
+
const result = await client2.item.list({ limit: 100, page: 1 });
|
|
2552
|
+
return {
|
|
2553
|
+
contents: [{
|
|
2554
|
+
uri: "items://inventory",
|
|
2555
|
+
text: JSON.stringify(result.data, null, 2),
|
|
2556
|
+
mimeType: "application/json"
|
|
2557
|
+
}]
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
);
|
|
2561
|
+
server2.resource(
|
|
2562
|
+
"invoices_recent",
|
|
2563
|
+
"invoices://recent",
|
|
2564
|
+
async (_uri) => {
|
|
2565
|
+
const result = await client2.invoice.list({
|
|
2566
|
+
type: "sale",
|
|
2567
|
+
limit: 10,
|
|
2568
|
+
page: 1
|
|
2569
|
+
});
|
|
2570
|
+
return {
|
|
2571
|
+
contents: [{
|
|
2572
|
+
uri: "invoices://recent",
|
|
2573
|
+
text: JSON.stringify(result.data, null, 2),
|
|
2574
|
+
mimeType: "application/json"
|
|
2575
|
+
}]
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
);
|
|
2579
|
+
server2.resource(
|
|
2580
|
+
"dashboard_summary",
|
|
2581
|
+
"dashboard://summary",
|
|
2582
|
+
async (_uri) => {
|
|
2583
|
+
const summary = await client2.dashboard.summary();
|
|
2584
|
+
return {
|
|
2585
|
+
contents: [{
|
|
2586
|
+
uri: "dashboard://summary",
|
|
2587
|
+
text: JSON.stringify(summary, null, 2),
|
|
2588
|
+
mimeType: "application/json"
|
|
2589
|
+
}]
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
);
|
|
2593
|
+
server2.resource(
|
|
2594
|
+
"bank_accounts",
|
|
2595
|
+
"bank://accounts",
|
|
2596
|
+
async (_uri) => {
|
|
2597
|
+
const accounts = await client2.bankAccount.list();
|
|
2598
|
+
return {
|
|
2599
|
+
contents: [{
|
|
2600
|
+
uri: "bank://accounts",
|
|
2601
|
+
text: JSON.stringify(accounts, null, 2),
|
|
2602
|
+
mimeType: "application/json"
|
|
2603
|
+
}]
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
);
|
|
2607
|
+
server2.resource(
|
|
2608
|
+
"shipments_recent",
|
|
2609
|
+
"shipments://recent",
|
|
2610
|
+
async (_uri) => {
|
|
2611
|
+
const result = await client2.shipment.list({ page: 1, limit: 10 });
|
|
2612
|
+
return {
|
|
2613
|
+
contents: [{
|
|
2614
|
+
uri: "shipments://recent",
|
|
2615
|
+
text: JSON.stringify(result.data, null, 2),
|
|
2616
|
+
mimeType: "application/json"
|
|
2617
|
+
}]
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
);
|
|
2621
|
+
server2.resource(
|
|
2622
|
+
"targets_active",
|
|
2623
|
+
"targets://active",
|
|
2624
|
+
async (_uri) => {
|
|
2625
|
+
const targets = await client2.target.list({ active: true, withProgress: true });
|
|
2626
|
+
return {
|
|
2627
|
+
contents: [{
|
|
2628
|
+
uri: "targets://active",
|
|
2629
|
+
text: JSON.stringify(targets, null, 2),
|
|
2630
|
+
mimeType: "application/json"
|
|
2631
|
+
}]
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/server.ts
|
|
2638
|
+
function registerTools(server2, client2) {
|
|
2639
|
+
registerInvoiceTools(server2, client2);
|
|
2640
|
+
registerPartyTools(server2, client2);
|
|
2641
|
+
registerItemTools(server2, client2);
|
|
2642
|
+
registerPaymentTools(server2, client2);
|
|
2643
|
+
registerExpenseTools(server2, client2);
|
|
2644
|
+
registerDashboardTools(server2, client2);
|
|
2645
|
+
registerGstTools(server2, client2);
|
|
2646
|
+
registerReportTools(server2, client2);
|
|
2647
|
+
registerShipmentTools(server2, client2);
|
|
2648
|
+
registerBankAccountTools(server2, client2);
|
|
2649
|
+
registerStoreTools(server2, client2);
|
|
2650
|
+
registerTargetTools(server2, client2);
|
|
2651
|
+
registerImportTools(server2, client2);
|
|
2652
|
+
registerResources(server2, client2);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/index.ts
|
|
2656
|
+
function requireEnv(name) {
|
|
2657
|
+
const val = process.env[name];
|
|
2658
|
+
if (!val) {
|
|
2659
|
+
process.stderr.write(
|
|
2660
|
+
`[hisaabo-mcp] Error: Required environment variable "${name}" is not set.
|
|
2661
|
+
[hisaabo-mcp] Run "hisaabo whoami --json" to get all required values.
|
|
2662
|
+
`
|
|
2663
|
+
);
|
|
2664
|
+
process.exit(1);
|
|
2665
|
+
}
|
|
2666
|
+
return val;
|
|
2667
|
+
}
|
|
2668
|
+
var config = {
|
|
2669
|
+
apiUrl: process.env.HISAABO_API_URL ?? "http://localhost:3000",
|
|
2670
|
+
token: requireEnv("HISAABO_TOKEN"),
|
|
2671
|
+
tenantId: requireEnv("HISAABO_TENANT_ID"),
|
|
2672
|
+
businessId: requireEnv("HISAABO_BUSINESS_ID")
|
|
2673
|
+
};
|
|
2674
|
+
var client = new HisaaboClient(config);
|
|
2675
|
+
var server = new McpServer({
|
|
2676
|
+
name: "hisaabo",
|
|
2677
|
+
version: "0.1.0"
|
|
2678
|
+
});
|
|
2679
|
+
registerTools(server, client);
|
|
2680
|
+
var transport = new StdioServerTransport();
|
|
2681
|
+
await server.connect(transport);
|