@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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Party tools — manage customers and suppliers.
3
+ *
4
+ * Tools registered:
5
+ * party_list — search parties with filter/sort options
6
+ * party_create — create a new customer or supplier
7
+ * party_get — get party details including outstanding balance
8
+ * party_ledger — get full transaction ledger for a party
9
+ * party_update — update an existing party's details
10
+ * party_delete — delete a party record
11
+ */
12
+
13
+ import { z } from "zod";
14
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import type { HisaaboClient } from "../client.js";
16
+ import { wrapTool } from "../lib/errors.js";
17
+ import { MAX_PAGE_SIZE, withPaginationMeta } from "../lib/pagination.js";
18
+
19
+ export function registerPartyTools(server: McpServer, client: HisaaboClient) {
20
+
21
+ server.tool(
22
+ "party_list",
23
+ [
24
+ "List customers and suppliers (parties) for the active business.",
25
+ "The 'balance' field in each result shows the outstanding amount: positive = they owe you (receivable), negative = you owe them (payable).",
26
+ "Use filter='outstanding' to find parties with unpaid balances. Use filter='overdue' for parties with overdue invoices.",
27
+ "Use this tool to find party UUIDs before calling invoice_create or payment_create.",
28
+ ].join(" "),
29
+ {
30
+ type: z.enum(["customer", "supplier"]).optional()
31
+ .describe("'customer' for buyers, 'supplier' for vendors. Omit to return both."),
32
+ filter: z.enum(["all", "customer", "supplier", "outstanding", "overdue"]).optional()
33
+ .describe("'outstanding' = parties with unpaid balance, 'overdue' = parties with overdue invoices."),
34
+ search: z.string().max(200).optional()
35
+ .describe("Search by party name, phone, email, or GSTIN (partial match)."),
36
+ category: z.string().max(100).optional()
37
+ .describe("Filter by party category tag."),
38
+ sort_by: z.enum(["name", "balance"]).optional()
39
+ .describe("Sort by name (alphabetical) or balance (largest first)."),
40
+ sort_dir: z.enum(["asc", "desc"]).optional()
41
+ .describe("Sort direction."),
42
+ page: z.number().int().min(1).default(1)
43
+ .describe("Page number for pagination."),
44
+ },
45
+ wrapTool(async (input) => {
46
+ const result = await client.party.list({
47
+ type: input.type,
48
+ filter: input.filter,
49
+ search: input.search,
50
+ category: input.category,
51
+ sortBy: input.sort_by,
52
+ sortDir: input.sort_dir,
53
+ page: input.page,
54
+ limit: MAX_PAGE_SIZE,
55
+ });
56
+ return {
57
+ content: [{
58
+ type: "text" as const,
59
+ text: JSON.stringify(withPaginationMeta(result), null, 2),
60
+ }],
61
+ };
62
+ })
63
+ );
64
+
65
+ server.tool(
66
+ "party_create",
67
+ [
68
+ "Create a new customer or supplier party.",
69
+ "Minimum required: type ('customer' or 'supplier') and name.",
70
+ "For GST-registered parties, provide gstin (format: 22AAAAA0000A1Z5). For unregistered, omit it.",
71
+ "opening_balance sets their starting account balance: positive = they owe you, negative = you owe them.",
72
+ ].join(" "),
73
+ {
74
+ type: z.enum(["customer", "supplier"])
75
+ .describe("'customer' for buyers/clients, 'supplier' for vendors/sellers."),
76
+ name: z.string().min(1).max(200)
77
+ .describe("Full name of the customer or business."),
78
+ phone: z.string().max(15).optional()
79
+ .describe("Phone number (digits only, no country code prefix required)."),
80
+ email: z.string().email().optional()
81
+ .describe("Email address for sending invoices."),
82
+ gstin: z.string().regex(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/).optional()
83
+ .describe("GST Identification Number (15 characters), e.g. '22AAAAA0000A1Z5'. Omit if unregistered."),
84
+ pan: z.string().optional()
85
+ .describe("PAN (Permanent Account Number), 10 characters."),
86
+ billing_address: z.string().max(500).optional()
87
+ .describe("Full billing address."),
88
+ city: z.string().max(100).optional(),
89
+ state: z.string().max(100).optional(),
90
+ pincode: z.string().max(10).optional(),
91
+ opening_balance: z.string().regex(/^-?\d+(\.\d{1,2})?$/).optional()
92
+ .describe("Starting balance as decimal string. Positive = they owe you, negative = you owe them. Default '0'."),
93
+ category: z.string().max(100).optional()
94
+ .describe("Category tag for grouping parties, e.g. 'retail', 'wholesale', 'government'."),
95
+ credit_period_days: z.number().int().min(0).max(365).optional()
96
+ .describe("Number of days before payment is due (e.g. 30 for net-30 terms)."),
97
+ credit_limit: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
98
+ .describe("Maximum credit amount as decimal string."),
99
+ },
100
+ wrapTool(async (input) => {
101
+ const party = await client.party.create({
102
+ type: input.type,
103
+ name: input.name,
104
+ phone: input.phone,
105
+ email: input.email,
106
+ gstin: input.gstin,
107
+ pan: input.pan,
108
+ billingAddress: input.billing_address,
109
+ city: input.city,
110
+ state: input.state,
111
+ pincode: input.pincode,
112
+ openingBalance: input.opening_balance,
113
+ category: input.category,
114
+ creditPeriodDays: input.credit_period_days,
115
+ creditLimit: input.credit_limit,
116
+ });
117
+ return {
118
+ content: [{
119
+ type: "text" as const,
120
+ text: JSON.stringify(party, null, 2),
121
+ }],
122
+ };
123
+ })
124
+ );
125
+
126
+ server.tool(
127
+ "party_get",
128
+ [
129
+ "Get full details of a single customer or supplier, including their current outstanding balance.",
130
+ "'balance' shows net amount: positive = they owe you, negative = you owe them.",
131
+ "Use this to answer questions like 'What does Acme Corp owe us?' or 'What is our balance with Supplier X?'",
132
+ ].join(" "),
133
+ {
134
+ party_id: z.string().uuid()
135
+ .describe("Party UUID from party_list."),
136
+ },
137
+ wrapTool(async (input) => {
138
+ const party = await client.party.get(input.party_id);
139
+ return {
140
+ content: [{
141
+ type: "text" as const,
142
+ text: JSON.stringify(party, null, 2),
143
+ }],
144
+ };
145
+ })
146
+ );
147
+
148
+ server.tool(
149
+ "party_update",
150
+ [
151
+ "Update an existing customer or supplier's details.",
152
+ "Only provide fields you want to change — all other fields remain unchanged.",
153
+ "Note: 'type' (customer/supplier) cannot be changed after creation.",
154
+ ].join(" "),
155
+ {
156
+ party_id: z.string().uuid()
157
+ .describe("Party UUID to update."),
158
+ name: z.string().min(1).max(200).optional()
159
+ .describe("Updated name."),
160
+ phone: z.string().max(15).optional()
161
+ .describe("Updated phone number."),
162
+ email: z.string().email().optional()
163
+ .describe("Updated email address."),
164
+ gstin: z.string().regex(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/).optional()
165
+ .describe("Updated GSTIN (15 characters)."),
166
+ billing_address: z.string().max(500).optional()
167
+ .describe("Updated billing address."),
168
+ shipping_address: z.string().max(500).optional()
169
+ .describe("Updated shipping address."),
170
+ city: z.string().max(100).optional()
171
+ .describe("Updated city."),
172
+ state: z.string().max(100).optional()
173
+ .describe("Updated state."),
174
+ pincode: z.string().max(10).optional()
175
+ .describe("Updated PIN code."),
176
+ category: z.string().max(100).optional()
177
+ .describe("Updated category tag."),
178
+ credit_period_days: z.number().int().min(0).max(365).optional()
179
+ .describe("Updated credit period in days."),
180
+ credit_limit: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
181
+ .describe("Updated credit limit as decimal string."),
182
+ contact_person_name: z.string().max(200).optional()
183
+ .describe("Updated contact person name."),
184
+ },
185
+ wrapTool(async (input) => {
186
+ const { party_id, ...fields } = input;
187
+ const party = await client.party.update(party_id, {
188
+ name: fields.name,
189
+ phone: fields.phone,
190
+ email: fields.email,
191
+ gstin: fields.gstin,
192
+ billingAddress: fields.billing_address,
193
+ shippingAddress: fields.shipping_address,
194
+ city: fields.city,
195
+ state: fields.state,
196
+ pincode: fields.pincode,
197
+ category: fields.category,
198
+ creditPeriodDays: fields.credit_period_days,
199
+ creditLimit: fields.credit_limit,
200
+ contactPersonName: fields.contact_person_name,
201
+ });
202
+ return {
203
+ content: [{
204
+ type: "text" as const,
205
+ text: JSON.stringify(party, null, 2),
206
+ }],
207
+ };
208
+ })
209
+ );
210
+
211
+ server.tool(
212
+ "party_delete",
213
+ [
214
+ "Permanently delete a customer or supplier. Requires admin role.",
215
+ "Warning: this is a hard delete. Associated invoices and payments are not deleted, but the party reference will be broken.",
216
+ "Consider deactivating or archiving instead — only delete if the party was created in error.",
217
+ ].join(" "),
218
+ {
219
+ party_id: z.string().uuid()
220
+ .describe("Party UUID to delete."),
221
+ },
222
+ wrapTool(async (input) => {
223
+ const result = await client.party.delete(input.party_id);
224
+ return {
225
+ content: [{
226
+ type: "text" as const,
227
+ text: JSON.stringify(result, null, 2),
228
+ }],
229
+ };
230
+ })
231
+ );
232
+
233
+ server.tool(
234
+ "party_ledger",
235
+ [
236
+ "Get the full transaction ledger (account statement) for a customer or supplier.",
237
+ "Shows all invoices, payments, and credit notes in chronological order with running balance.",
238
+ "Use this to answer 'Show me all transactions with Customer X' or 'What is the payment history for this supplier?'",
239
+ "The closing_balance field is the current net balance for the party.",
240
+ ].join(" "),
241
+ {
242
+ party_id: z.string().uuid()
243
+ .describe("Party UUID."),
244
+ from_date: z.string().datetime().optional()
245
+ .describe("Start date for ledger entries (ISO 8601). Omit for all-time."),
246
+ to_date: z.string().datetime().optional()
247
+ .describe("End date for ledger entries (ISO 8601)."),
248
+ page: z.number().int().min(1).default(1)
249
+ .describe("Page number for pagination."),
250
+ },
251
+ wrapTool(async (input) => {
252
+ const ledger = await client.party.ledger(input.party_id, {
253
+ fromDate: input.from_date,
254
+ toDate: input.to_date,
255
+ page: input.page,
256
+ limit: MAX_PAGE_SIZE,
257
+ });
258
+ return {
259
+ content: [{
260
+ type: "text" as const,
261
+ text: JSON.stringify(ledger, null, 2),
262
+ }],
263
+ };
264
+ })
265
+ );
266
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Payment tools — record and query payments.
3
+ *
4
+ * Tools registered:
5
+ * payment_create — record a payment received from a customer or made to a supplier
6
+ * payment_list — list payments with filtering
7
+ * payment_get — get full payment details including linked invoices
8
+ * payment_update — update an existing payment record
9
+ * payment_delete — soft-delete a payment
10
+ */
11
+
12
+ import { z } from "zod";
13
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import type { HisaaboClient } from "../client.js";
15
+ import { wrapTool } from "../lib/errors.js";
16
+ import { MAX_PAGE_SIZE, withPaginationMeta } from "../lib/pagination.js";
17
+
18
+ const PAYMENT_MODES = ["cash", "bank", "upi", "cheque", "other"] as const;
19
+
20
+ export function registerPaymentTools(server: McpServer, client: HisaaboClient) {
21
+
22
+ server.tool(
23
+ "payment_create",
24
+ [
25
+ "Record a payment received from a customer or made to a supplier.",
26
+ "If the payment is for a specific invoice, set invoice_id — the invoice status will update automatically to 'paid' or 'partial'.",
27
+ "If the payment is not tied to a specific invoice (advance payment or bulk payment), omit invoice_id — it applies to the party's account balance.",
28
+ "To split a payment across multiple invoices, use the allocations array instead of invoice_id.",
29
+ "Example: customer paid invoice INV-001 by UPI: { party_id: '<uuid>', amount: '5000.00', mode: 'upi', invoice_id: '<uuid>' }",
30
+ ].join(" "),
31
+ {
32
+ party_id: z.string().uuid()
33
+ .describe("UUID of the customer (payment received) or supplier (payment made)."),
34
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/)
35
+ .describe("Payment amount as decimal string, e.g. '5000.00'."),
36
+ mode: z.enum(PAYMENT_MODES)
37
+ .describe("Payment method: 'cash', 'bank' (bank transfer/NEFT/RTGS), 'upi', 'cheque', or 'other'."),
38
+ invoice_id: z.string().uuid().optional()
39
+ .describe("Link this payment to a specific invoice UUID. The invoice status updates automatically. Omit for advance payments."),
40
+ discount: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
41
+ .describe("Discount or write-off amount applied alongside this payment, e.g. '100.00'. Default '0'."),
42
+ reference_number: z.string().max(100).optional()
43
+ .describe("Transaction reference, UTR number, cheque number, or any payment identifier."),
44
+ payment_date: z.string().datetime().optional()
45
+ .describe("Date the payment was received/made (ISO 8601). Defaults to today."),
46
+ notes: z.string().max(500).optional()
47
+ .describe("Internal notes about this payment."),
48
+ bank_account_id: z.string().uuid().optional()
49
+ .describe("Bank/cash account UUID to record this transaction against (for cash flow tracking)."),
50
+ allocations: z.array(z.object({
51
+ invoice_id: z.string().uuid()
52
+ .describe("Invoice UUID to allocate part of this payment to."),
53
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/)
54
+ .describe("Amount to allocate to this invoice."),
55
+ })).optional()
56
+ .describe("Allocate a single payment across multiple invoices. Use instead of invoice_id when splitting a payment."),
57
+ },
58
+ wrapTool(async (input) => {
59
+ const payment = await client.payment.create({
60
+ partyId: input.party_id,
61
+ amount: input.amount,
62
+ mode: input.mode,
63
+ invoiceId: input.invoice_id,
64
+ discount: input.discount,
65
+ referenceNumber: input.reference_number,
66
+ paymentDate: input.payment_date,
67
+ notes: input.notes,
68
+ bankAccountId: input.bank_account_id,
69
+ allocations: input.allocations?.map((a) => ({
70
+ invoiceId: a.invoice_id,
71
+ amount: a.amount,
72
+ })),
73
+ });
74
+ return {
75
+ content: [{
76
+ type: "text" as const,
77
+ text: JSON.stringify(payment, null, 2),
78
+ }],
79
+ };
80
+ })
81
+ );
82
+
83
+ server.tool(
84
+ "payment_get",
85
+ [
86
+ "Get full details of a single payment, including the invoices it was applied to.",
87
+ "The 'linkedInvoices' field shows which invoices received allocations from this payment.",
88
+ ].join(" "),
89
+ {
90
+ payment_id: z.string().uuid()
91
+ .describe("Payment UUID from payment_list."),
92
+ },
93
+ wrapTool(async (input) => {
94
+ const payment = await client.payment.getById(input.payment_id);
95
+ return {
96
+ content: [{
97
+ type: "text" as const,
98
+ text: JSON.stringify(payment, null, 2),
99
+ }],
100
+ };
101
+ })
102
+ );
103
+
104
+ server.tool(
105
+ "payment_update",
106
+ [
107
+ "Update an existing payment record. This reverses the old allocation and re-applies the new one.",
108
+ "The linked invoice's amountPaid and status are recalculated automatically.",
109
+ "Only provide fields you want to change.",
110
+ "Warning: updating a payment recalculates invoice statuses — ensure the new amount/allocation is correct.",
111
+ ].join(" "),
112
+ {
113
+ payment_id: z.string().uuid()
114
+ .describe("Payment UUID to update."),
115
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
116
+ .describe("New payment amount as decimal string."),
117
+ mode: z.enum(PAYMENT_MODES).optional()
118
+ .describe("Updated payment mode."),
119
+ discount: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
120
+ .describe("Updated discount/write-off amount."),
121
+ reference_number: z.string().max(100).optional().nullable()
122
+ .describe("Updated transaction reference number."),
123
+ payment_date: z.string().datetime().optional()
124
+ .describe("Updated payment date (ISO 8601)."),
125
+ notes: z.string().max(500).optional().nullable()
126
+ .describe("Updated internal notes."),
127
+ bank_account_id: z.string().uuid().optional().nullable()
128
+ .describe("Updated bank account UUID. Null to unlink from any account."),
129
+ allocations: z.array(z.object({
130
+ invoice_id: z.string().uuid()
131
+ .describe("Invoice UUID to allocate part of this payment to."),
132
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/)
133
+ .describe("Amount to allocate."),
134
+ })).optional()
135
+ .describe("Updated invoice allocations. Replaces all existing allocations."),
136
+ },
137
+ wrapTool(async (input) => {
138
+ const payment = await client.payment.update({
139
+ id: input.payment_id,
140
+ amount: input.amount,
141
+ mode: input.mode,
142
+ discount: input.discount,
143
+ referenceNumber: input.reference_number,
144
+ paymentDate: input.payment_date,
145
+ notes: input.notes,
146
+ bankAccountId: input.bank_account_id,
147
+ allocations: input.allocations?.map((a) => ({
148
+ invoiceId: a.invoice_id,
149
+ amount: a.amount,
150
+ })),
151
+ });
152
+ return {
153
+ content: [{
154
+ type: "text" as const,
155
+ text: JSON.stringify(payment, null, 2),
156
+ }],
157
+ };
158
+ })
159
+ );
160
+
161
+ server.tool(
162
+ "payment_delete",
163
+ [
164
+ "Soft-delete a payment record. Requires admin role.",
165
+ "Deleting a payment reverses the invoice allocation: the linked invoice's amountPaid is reduced and its status is recalculated.",
166
+ "The bank account balance is also reversed if the payment was recorded against an account.",
167
+ ].join(" "),
168
+ {
169
+ payment_id: z.string().uuid()
170
+ .describe("Payment UUID to delete."),
171
+ },
172
+ wrapTool(async (input) => {
173
+ const result = await client.payment.delete(input.payment_id);
174
+ return {
175
+ content: [{
176
+ type: "text" as const,
177
+ text: JSON.stringify(result, null, 2),
178
+ }],
179
+ };
180
+ })
181
+ );
182
+
183
+ server.tool(
184
+ "payment_list",
185
+ [
186
+ "List payments for the active business, filtered by party, invoice, or date range.",
187
+ "Use party_id to see all payments from/to a specific customer or supplier.",
188
+ "Use invoice_id to see all payments applied to a specific invoice.",
189
+ ].join(" "),
190
+ {
191
+ party_id: z.string().uuid().optional()
192
+ .describe("Filter by customer or supplier UUID."),
193
+ invoice_id: z.string().uuid().optional()
194
+ .describe("Filter payments linked to a specific invoice."),
195
+ from_date: z.string().datetime().optional()
196
+ .describe("Start date (ISO 8601)."),
197
+ to_date: z.string().datetime().optional()
198
+ .describe("End date (ISO 8601)."),
199
+ search: z.string().max(200).optional()
200
+ .describe("Search by payment number, party name, or reference number."),
201
+ page: z.number().int().min(1).default(1)
202
+ .describe("Page number for pagination."),
203
+ },
204
+ wrapTool(async (input) => {
205
+ const result = await client.payment.list({
206
+ partyId: input.party_id,
207
+ invoiceId: input.invoice_id,
208
+ fromDate: input.from_date,
209
+ toDate: input.to_date,
210
+ search: input.search,
211
+ page: input.page,
212
+ limit: MAX_PAGE_SIZE,
213
+ });
214
+ return {
215
+ content: [{
216
+ type: "text" as const,
217
+ text: JSON.stringify(withPaginationMeta(result), null, 2),
218
+ }],
219
+ };
220
+ })
221
+ );
222
+ }