@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,110 @@
1
+ /**
2
+ * Dashboard tools — business-level summaries and analytics.
3
+ *
4
+ * Tools registered:
5
+ * dashboard_summary — P&L summary, receivables, payables, cash position
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import type { HisaaboClient } from "../client.js";
11
+ import { wrapTool } from "../lib/errors.js";
12
+
13
+ /** Resolve a named period to ISO 8601 date strings for the API. */
14
+ function resolvePeriod(period: string | undefined): { fromDate?: string; toDate?: string } {
15
+ const now = new Date();
16
+
17
+ if (!period || period === "this-fy") {
18
+ // Financial year logic: April-start default. API computes FY dates server-side when no date range given.
19
+ return {};
20
+ }
21
+
22
+ if (period === "this-month") {
23
+ const from = new Date(now.getFullYear(), now.getMonth(), 1);
24
+ const to = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999);
25
+ return { fromDate: from.toISOString(), toDate: to.toISOString() };
26
+ }
27
+
28
+ if (period === "last-month") {
29
+ const from = new Date(now.getFullYear(), now.getMonth() - 1, 1);
30
+ const to = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
31
+ return { fromDate: from.toISOString(), toDate: to.toISOString() };
32
+ }
33
+
34
+ if (period === "this-quarter") {
35
+ const quarter = Math.floor(now.getMonth() / 3);
36
+ const from = new Date(now.getFullYear(), quarter * 3, 1);
37
+ const to = new Date(now.getFullYear(), (quarter + 1) * 3, 0, 23, 59, 59, 999);
38
+ return { fromDate: from.toISOString(), toDate: to.toISOString() };
39
+ }
40
+
41
+ if (period === "this-year") {
42
+ const from = new Date(now.getFullYear(), 0, 1);
43
+ const to = new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999);
44
+ return { fromDate: from.toISOString(), toDate: to.toISOString() };
45
+ }
46
+
47
+ // "all" — no date filter
48
+ return {};
49
+ }
50
+
51
+ export function registerDashboardTools(server: McpServer, client: HisaaboClient) {
52
+
53
+ server.tool(
54
+ "dashboard_summary",
55
+ [
56
+ "Get a financial summary for the active business: total sales, purchases, expenses, receivables, payables, and cash position.",
57
+ "'receivable' = total amount customers owe you (outstanding invoices).",
58
+ "'payable' = total amount you owe suppliers.",
59
+ "'cashInHand' = cash balance across all cash/bank accounts.",
60
+ "Use period to select the time window. Defaults to the current financial year (April–March for Indian businesses).",
61
+ "Use this to answer questions like 'How much revenue did we make this month?' or 'What are our total outstanding receivables?'",
62
+ ].join(" "),
63
+ {
64
+ period: z.enum(["this-fy", "this-month", "last-month", "this-quarter", "this-year", "custom"]).optional()
65
+ .describe(
66
+ "'this-fy' = current financial year (default). " +
67
+ "'this-month' = current calendar month. " +
68
+ "'last-month' = previous calendar month. " +
69
+ "'this-quarter' = current calendar quarter. " +
70
+ "'this-year' = current calendar year. " +
71
+ "'custom' = use from_date and to_date."
72
+ ),
73
+ from_date: z.string().datetime().optional()
74
+ .describe("Custom start date (ISO 8601). Only used when period='custom'."),
75
+ to_date: z.string().datetime().optional()
76
+ .describe("Custom end date (ISO 8601). Only used when period='custom'."),
77
+ },
78
+ wrapTool(async (input) => {
79
+ let dateRange: { fromDate?: string; toDate?: string };
80
+
81
+ if (input.period === "custom") {
82
+ dateRange = {
83
+ fromDate: input.from_date,
84
+ toDate: input.to_date,
85
+ };
86
+ } else {
87
+ dateRange = resolvePeriod(input.period);
88
+ }
89
+
90
+ const summary = await client.dashboard.summary(
91
+ Object.keys(dateRange).length > 0 ? dateRange : undefined
92
+ );
93
+
94
+ return {
95
+ content: [{
96
+ type: "text" as const,
97
+ text: JSON.stringify(
98
+ {
99
+ ...summary,
100
+ _period: input.period ?? "this-fy",
101
+ _note: "All monetary values are decimal strings in the business's currency (default: INR).",
102
+ },
103
+ null,
104
+ 2
105
+ ),
106
+ }],
107
+ };
108
+ })
109
+ );
110
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Expense tools — track business expenses.
3
+ *
4
+ * Tools registered:
5
+ * expense_create — record a business expense
6
+ * expense_list — list expenses with filtering
7
+ * expense_update — update an existing expense record
8
+ * expense_delete — soft-delete an expense
9
+ * expense_categories — list all distinct expense categories
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 registerExpenseTools(server: McpServer, client: HisaaboClient) {
21
+
22
+ server.tool(
23
+ "expense_create",
24
+ [
25
+ "Record a business expense (operating cost, overhead, etc.).",
26
+ "Category is a free-form label — use consistent names like 'Rent', 'Utilities', 'Salaries', 'Travel'.",
27
+ "Expenses appear in the dashboard summary's 'totalExpenses' and affect the P&L.",
28
+ ].join(" "),
29
+ {
30
+ category: z.string().min(1).max(100)
31
+ .describe("Expense category, e.g. 'Rent', 'Electricity', 'Office Supplies', 'Travel'."),
32
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/)
33
+ .describe("Expense amount as decimal string, e.g. '12000.00'."),
34
+ mode: z.enum(PAYMENT_MODES)
35
+ .describe("Payment method used: 'cash', 'bank', 'upi', 'cheque', or 'other'."),
36
+ description: z.string().max(500).optional()
37
+ .describe("Additional details, e.g. 'Office rent for March 2024'."),
38
+ expense_date: z.string().datetime().optional()
39
+ .describe("Date of the expense (ISO 8601). Defaults to today."),
40
+ reference_number: z.string().max(100).optional()
41
+ .describe("Bill number, receipt number, or transaction ID."),
42
+ },
43
+ wrapTool(async (input) => {
44
+ const expense = await client.expense.create({
45
+ category: input.category,
46
+ amount: input.amount,
47
+ mode: input.mode,
48
+ description: input.description,
49
+ expenseDate: input.expense_date,
50
+ referenceNumber: input.reference_number,
51
+ });
52
+ return {
53
+ content: [{
54
+ type: "text" as const,
55
+ text: JSON.stringify(expense, null, 2),
56
+ }],
57
+ };
58
+ })
59
+ );
60
+
61
+ server.tool(
62
+ "expense_update",
63
+ [
64
+ "Update an existing expense record.",
65
+ "Only provide fields you want to change — all other fields remain unchanged.",
66
+ ].join(" "),
67
+ {
68
+ expense_id: z.string().uuid()
69
+ .describe("Expense UUID to update."),
70
+ category: z.string().min(1).max(100).optional()
71
+ .describe("Updated expense category."),
72
+ amount: z.string().regex(/^\d+(\.\d{1,2})?$/).optional()
73
+ .describe("Updated expense amount as decimal string."),
74
+ mode: z.enum(PAYMENT_MODES).optional()
75
+ .describe("Updated payment method."),
76
+ description: z.string().max(500).optional()
77
+ .describe("Updated description."),
78
+ expense_date: z.string().datetime().optional()
79
+ .describe("Updated expense date (ISO 8601)."),
80
+ reference_number: z.string().max(100).optional()
81
+ .describe("Updated bill number or receipt number."),
82
+ },
83
+ wrapTool(async (input) => {
84
+ const expense = await client.expense.update(input.expense_id, {
85
+ category: input.category,
86
+ amount: input.amount,
87
+ mode: input.mode,
88
+ description: input.description,
89
+ expenseDate: input.expense_date,
90
+ referenceNumber: input.reference_number,
91
+ });
92
+ return {
93
+ content: [{
94
+ type: "text" as const,
95
+ text: JSON.stringify(expense, null, 2),
96
+ }],
97
+ };
98
+ })
99
+ );
100
+
101
+ server.tool(
102
+ "expense_delete",
103
+ [
104
+ "Soft-delete an expense record. Requires admin role.",
105
+ "The expense is hidden from all lists and reports after deletion.",
106
+ ].join(" "),
107
+ {
108
+ expense_id: z.string().uuid()
109
+ .describe("Expense UUID to delete."),
110
+ },
111
+ wrapTool(async (input) => {
112
+ const result = await client.expense.delete(input.expense_id);
113
+ return {
114
+ content: [{
115
+ type: "text" as const,
116
+ text: JSON.stringify(result, null, 2),
117
+ }],
118
+ };
119
+ })
120
+ );
121
+
122
+ server.tool(
123
+ "expense_categories",
124
+ [
125
+ "Get a list of all distinct expense categories used in the business.",
126
+ "Use this to discover existing category names before filtering expense_list or creating new expenses with consistent categories.",
127
+ ].join(" "),
128
+ {},
129
+ wrapTool(async (_input) => {
130
+ const categories = await client.expense.categories();
131
+ return {
132
+ content: [{
133
+ type: "text" as const,
134
+ text: JSON.stringify(categories, null, 2),
135
+ }],
136
+ };
137
+ })
138
+ );
139
+
140
+ server.tool(
141
+ "expense_list",
142
+ [
143
+ "List business expenses, optionally filtered by category or date range.",
144
+ "Use this to answer 'How much did we spend on rent this year?' or 'Show me all expenses in March'.",
145
+ ].join(" "),
146
+ {
147
+ category: z.string().max(100).optional()
148
+ .describe("Filter by expense category (exact match)."),
149
+ from_date: z.string().datetime().optional()
150
+ .describe("Start date (ISO 8601)."),
151
+ to_date: z.string().datetime().optional()
152
+ .describe("End date (ISO 8601)."),
153
+ search: z.string().max(200).optional()
154
+ .describe("Search by description or reference number."),
155
+ page: z.number().int().min(1).default(1)
156
+ .describe("Page number for pagination."),
157
+ },
158
+ wrapTool(async (input) => {
159
+ const result = await client.expense.list({
160
+ category: input.category,
161
+ fromDate: input.from_date,
162
+ toDate: input.to_date,
163
+ search: input.search,
164
+ page: input.page,
165
+ limit: MAX_PAGE_SIZE,
166
+ });
167
+ return {
168
+ content: [{
169
+ type: "text" as const,
170
+ text: JSON.stringify(withPaginationMeta(result), null, 2),
171
+ }],
172
+ };
173
+ })
174
+ );
175
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * GST reporting tools.
3
+ *
4
+ * Tools registered:
5
+ * gst_report — generate GSTR1 or GSTR3B summary data for a given month/year
6
+ * gst_report_csv — get GSTR-1 data in CSV format ready for portal upload
7
+ *
8
+ * Note: PDF generation is intentionally excluded. AI agents cannot consume
9
+ * binary content in tool responses. The JSON report is designed to let agents
10
+ * summarize GST liability, answer questions, and guide filing.
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
+
18
+ const CURRENT_YEAR = new Date().getFullYear();
19
+
20
+ export function registerGstTools(server: McpServer, client: HisaaboClient) {
21
+
22
+ server.tool(
23
+ "gst_report_csv",
24
+ [
25
+ "Get GSTR-1 data as a CSV string ready for upload to the GST portal.",
26
+ "Returns the CSV content and a suggested filename (e.g. 'GSTR1_March_2024.csv').",
27
+ "Save the CSV content to a file and upload it at https://www.gst.gov.in/.",
28
+ "Month is 1–12 (1 = January, 3 = March, etc.).",
29
+ ].join(" "),
30
+ {
31
+ month: z.number().int().min(1).max(12)
32
+ .describe("Month number (1 = January, 12 = December)."),
33
+ year: z.number().int().min(2020).max(CURRENT_YEAR + 1)
34
+ .describe(`Year, e.g. ${CURRENT_YEAR}.`),
35
+ },
36
+ wrapTool(async (input) => {
37
+ const result = await client.gst.gstr1CSV({ month: input.month, year: input.year });
38
+ return {
39
+ content: [{
40
+ type: "text" as const,
41
+ text: JSON.stringify(result, null, 2),
42
+ }],
43
+ };
44
+ })
45
+ );
46
+
47
+ server.tool(
48
+ "gst_report",
49
+ [
50
+ "Generate a GST report (GSTR1 or GSTR3B) for a specific month and year.",
51
+ "GSTR1 = outward supplies summary (sales). GSTR3B = monthly return summary (sales + purchases + ITC).",
52
+ "Returns JSON data — use this to answer 'What is our GST liability for March 2024?' or 'How much ITC can we claim this month?'",
53
+ "Month is 1–12 (1 = January, 3 = March, etc.).",
54
+ "Example: { report_type: 'gstr3b', month: 3, year: 2024 } for March 2024 GSTR3B.",
55
+ ].join(" "),
56
+ {
57
+ report_type: z.enum(["gstr1", "gstr3b"])
58
+ .describe("'gstr1' for outward supplies (sales) report. 'gstr3b' for monthly summary return."),
59
+ month: z.number().int().min(1).max(12)
60
+ .describe("Month number (1 = January, 12 = December)."),
61
+ year: z.number().int().min(2020).max(CURRENT_YEAR + 1)
62
+ .describe(`Year, e.g. ${CURRENT_YEAR}.`),
63
+ },
64
+ wrapTool(async (input) => {
65
+ const report = input.report_type === "gstr1"
66
+ ? await client.gst.gstr1({ month: input.month, year: input.year })
67
+ : await client.gst.gstr3b({ month: input.month, year: input.year });
68
+
69
+ const monthName = new Date(input.year, input.month - 1, 1)
70
+ .toLocaleString("en-IN", { month: "long" });
71
+
72
+ return {
73
+ content: [{
74
+ type: "text" as const,
75
+ text: JSON.stringify(
76
+ {
77
+ ...report,
78
+ _meta: {
79
+ reportType: input.report_type.toUpperCase(),
80
+ period: `${monthName} ${input.year}`,
81
+ },
82
+ },
83
+ null,
84
+ 2
85
+ ),
86
+ }],
87
+ };
88
+ })
89
+ );
90
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Data import tools — batch import parties, items, invoices, and payments.
3
+ *
4
+ * Tools registered:
5
+ * import_parties — batch import customers and suppliers
6
+ * import_items — batch import inventory items
7
+ * import_invoices — batch import historical invoices with line items
8
+ * import_payments — batch import payment records
9
+ *
10
+ * All import operations require admin role. Duplicate records (matched by
11
+ * name or invoice number) are skipped, never overwritten.
12
+ */
13
+
14
+ import { z } from "zod";
15
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import type { HisaaboClient } from "../client.js";
17
+ import { wrapTool } from "../lib/errors.js";
18
+
19
+ export function registerImportTools(server: McpServer, client: HisaaboClient) {
20
+
21
+ server.tool(
22
+ "import_parties",
23
+ [
24
+ "Batch import customers and/or suppliers. Requires admin role.",
25
+ "Duplicates (same name, case-insensitive) are automatically skipped — never overwritten.",
26
+ "Returns a summary: created (new records), skipped (duplicates), total (input count).",
27
+ "Use source to tag where the data came from, e.g. 'mybillbook', 'tally', 'excel'.",
28
+ ].join(" "),
29
+ {
30
+ source: z.string().default("excel")
31
+ .describe("Source system name, e.g. 'mybillbook', 'tally', 'excel'. Used for audit trail."),
32
+ parties: z.array(z.object({
33
+ name: z.string().min(1).max(200)
34
+ .describe("Party name. Duplicate names (case-insensitive) are skipped."),
35
+ type: z.enum(["customer", "supplier"]).default("customer")
36
+ .describe("'customer' for buyers, 'supplier' for sellers."),
37
+ phone: z.string().max(15).optional()
38
+ .describe("Phone number."),
39
+ email: z.string().optional()
40
+ .describe("Email address."),
41
+ gstin: z.string().optional()
42
+ .describe("GST Identification Number (15 chars)."),
43
+ pan: z.string().optional()
44
+ .describe("PAN number."),
45
+ opening_balance: z.string().optional()
46
+ .describe("Opening balance as decimal string. Positive = they owe you. Default '0'."),
47
+ billing_address: z.string().optional()
48
+ .describe("Billing address."),
49
+ shipping_address: z.string().optional()
50
+ .describe("Shipping address (if different from billing)."),
51
+ city: z.string().optional()
52
+ .describe("City."),
53
+ state: z.string().optional()
54
+ .describe("State name."),
55
+ pincode: z.string().optional()
56
+ .describe("PIN code."),
57
+ })).min(1).max(5000)
58
+ .describe("Array of party records to import. Max 5000 per call."),
59
+ },
60
+ wrapTool(async (input) => {
61
+ const result = await client.import.importParties({
62
+ source: input.source,
63
+ parties: input.parties.map((p) => ({
64
+ name: p.name,
65
+ type: p.type,
66
+ phone: p.phone,
67
+ email: p.email,
68
+ gstin: p.gstin,
69
+ pan: p.pan,
70
+ openingBalance: p.opening_balance,
71
+ billingAddress: p.billing_address,
72
+ shippingAddress: p.shipping_address,
73
+ city: p.city,
74
+ state: p.state,
75
+ pincode: p.pincode,
76
+ })),
77
+ });
78
+ return {
79
+ content: [{
80
+ type: "text" as const,
81
+ text: JSON.stringify(result, null, 2),
82
+ }],
83
+ };
84
+ })
85
+ );
86
+
87
+ server.tool(
88
+ "import_items",
89
+ [
90
+ "Batch import inventory items or services. Requires admin role.",
91
+ "Duplicates (same name, case-insensitive) are automatically skipped.",
92
+ "Returns: created, skipped, total, and unmappedUnits (unit codes that were not recognized and defaulted to 'other').",
93
+ "Common unit codes are auto-mapped: KGS→kg, PCS→pcs, LTR→l, BOX→box, etc.",
94
+ "Stock quantity is always set to 0 on import — use import_invoices or item_adjust_stock to build stock from history.",
95
+ ].join(" "),
96
+ {
97
+ source: z.string().default("excel")
98
+ .describe("Source system name for audit trail."),
99
+ items: z.array(z.object({
100
+ name: z.string().min(1).max(200)
101
+ .describe("Item name. Duplicates (case-insensitive) are skipped."),
102
+ item_type: z.enum(["product", "service"]).default("product")
103
+ .describe("'product' for physical goods, 'service' for services."),
104
+ sale_price: z.string().optional()
105
+ .describe("Default selling price as decimal string."),
106
+ purchase_price: z.string().optional()
107
+ .describe("Default purchase price as decimal string."),
108
+ tax_percent: z.string().default("0")
109
+ .describe("GST rate percentage, e.g. '18'. Default '0'."),
110
+ hsn: z.string().optional()
111
+ .describe("HSN code for GST compliance."),
112
+ unit: z.string().default("pcs")
113
+ .describe("Unit code. Common values: pcs, kg, l, box, m. Auto-mapped from MyBillBook/Tally format."),
114
+ sku: z.string().optional()
115
+ .describe("SKU / product code."),
116
+ category: z.string().optional()
117
+ .describe("Category name for grouping."),
118
+ })).min(1).max(5000)
119
+ .describe("Array of item records to import. Max 5000 per call."),
120
+ },
121
+ wrapTool(async (input) => {
122
+ const result = await client.import.importItems({
123
+ source: input.source,
124
+ items: input.items.map((item) => ({
125
+ name: item.name,
126
+ itemType: item.item_type,
127
+ salePrice: item.sale_price,
128
+ purchasePrice: item.purchase_price,
129
+ taxPercent: item.tax_percent,
130
+ hsn: item.hsn,
131
+ unit: item.unit,
132
+ sku: item.sku,
133
+ category: item.category,
134
+ })),
135
+ });
136
+ return {
137
+ content: [{
138
+ type: "text" as const,
139
+ text: JSON.stringify(result, null, 2),
140
+ }],
141
+ };
142
+ })
143
+ );
144
+
145
+ server.tool(
146
+ "import_invoices",
147
+ [
148
+ "Batch import historical invoices with optional line items. Requires admin role.",
149
+ "Parties referenced by party_name must already exist (import them first with import_parties).",
150
+ "Invoices with duplicate invoice_number are skipped.",
151
+ "Set auto_create_payments=true to automatically create payment records for paid invoices.",
152
+ "Use this for migrating historical data from MyBillBook, Tally, Busy, or Excel.",
153
+ ].join(" "),
154
+ {
155
+ source: z.string().default("excel")
156
+ .describe("Source system name for audit trail."),
157
+ auto_create_payments: z.boolean().default(false)
158
+ .describe("If true, automatically create payment records for invoices with amountPaid > 0."),
159
+ default_payment_mode: z.enum(["cash", "bank", "upi", "cheque", "other"]).default("cash")
160
+ .describe("Payment mode to use when auto-creating payments from paid invoices."),
161
+ invoices: z.array(z.object({
162
+ invoice_number: z.string().min(1)
163
+ .describe("Invoice number. Duplicates are skipped."),
164
+ invoice_date: z.string()
165
+ .describe("Invoice date (YYYY-MM-DD or DD/MM/YYYY or DD-MM-YYYY)."),
166
+ due_date: z.string().optional()
167
+ .describe("Due date (same formats as invoice_date)."),
168
+ party_name: z.string().min(1)
169
+ .describe("Exact party name — must match an existing party (case-insensitive)."),
170
+ type: z.enum(["sale", "purchase"]).default("sale")
171
+ .describe("'sale' for customer invoice, 'purchase' for supplier bill."),
172
+ status: z.enum(["draft", "sent", "paid", "partial", "overdue", "cancelled"]).default("sent")
173
+ .describe("Invoice status. Use 'paid' for fully paid historical invoices."),
174
+ total_amount: z.string()
175
+ .describe("Total invoice amount including tax, as decimal string."),
176
+ amount_paid: z.string().default("0")
177
+ .describe("Amount already paid. '0' for unpaid, same as total_amount for fully paid."),
178
+ subtotal: z.string().default("0")
179
+ .describe("Pre-tax subtotal as decimal string."),
180
+ tax_amount: z.string().default("0")
181
+ .describe("Total tax amount as decimal string."),
182
+ discount_amount: z.string().default("0")
183
+ .describe("Total discount amount as decimal string."),
184
+ notes: z.string().optional()
185
+ .describe("Invoice notes."),
186
+ line_items: z.array(z.object({
187
+ description: z.string()
188
+ .describe("Line item description or product name."),
189
+ quantity: z.string().default("1")
190
+ .describe("Quantity as decimal string."),
191
+ unit_price: z.string()
192
+ .describe("Price per unit as decimal string."),
193
+ tax_percent: z.string().default("0")
194
+ .describe("Tax rate percentage, e.g. '18'."),
195
+ discount_percent: z.string().default("0")
196
+ .describe("Discount percentage."),
197
+ item_name: z.string().optional()
198
+ .describe("If provided, links to an existing inventory item by name."),
199
+ })).optional()
200
+ .describe("Line items. Optional — if omitted, a single line item using total_amount is created."),
201
+ })).min(1).max(1000)
202
+ .describe("Array of invoice records. Max 1000 per call."),
203
+ },
204
+ wrapTool(async (input) => {
205
+ const result = await client.import.importInvoices({
206
+ source: input.source,
207
+ autoCreatePayments: input.auto_create_payments,
208
+ defaultPaymentMode: input.default_payment_mode,
209
+ invoices: input.invoices.map((inv) => ({
210
+ invoiceNumber: inv.invoice_number,
211
+ invoiceDate: inv.invoice_date,
212
+ dueDate: inv.due_date,
213
+ partyName: inv.party_name,
214
+ type: inv.type,
215
+ status: inv.status,
216
+ totalAmount: inv.total_amount,
217
+ amountPaid: inv.amount_paid,
218
+ subtotal: inv.subtotal,
219
+ taxAmount: inv.tax_amount,
220
+ discountAmount: inv.discount_amount,
221
+ notes: inv.notes,
222
+ lineItems: inv.line_items?.map((li) => ({
223
+ description: li.description,
224
+ quantity: li.quantity,
225
+ unitPrice: li.unit_price,
226
+ taxPercent: li.tax_percent,
227
+ discountPercent: li.discount_percent,
228
+ itemName: li.item_name,
229
+ })) ?? [],
230
+ })),
231
+ });
232
+ return {
233
+ content: [{
234
+ type: "text" as const,
235
+ text: JSON.stringify(result, null, 2),
236
+ }],
237
+ };
238
+ })
239
+ );
240
+
241
+ server.tool(
242
+ "import_payments",
243
+ [
244
+ "Batch import historical payment records. Requires admin role.",
245
+ "Parties referenced by party_name must already exist.",
246
+ "Invoices referenced by invoice_numbers must already exist (import invoices first).",
247
+ "Use this to import payment history after invoices are already imported.",
248
+ "Returns: created, skipped, total, and any errors encountered.",
249
+ ].join(" "),
250
+ {
251
+ source: z.string().default("excel")
252
+ .describe("Source system name for audit trail."),
253
+ paid_invoice_numbers: z.array(z.string()).default([])
254
+ .describe("List of invoice numbers that were fully paid in the source system. Used for auto-allocation fallback."),
255
+ payments: z.array(z.object({
256
+ party_name: z.string().min(1)
257
+ .describe("Party name — must match an existing party (case-insensitive)."),
258
+ amount: z.string()
259
+ .describe("Payment amount as decimal string."),
260
+ mode: z.enum(["cash", "bank", "upi", "cheque", "other"]).default("cash")
261
+ .describe("Payment mode."),
262
+ payment_date: z.string().optional()
263
+ .describe("Payment date (YYYY-MM-DD or DD/MM/YYYY)."),
264
+ payment_number: z.string().optional()
265
+ .describe("Original payment number from source system."),
266
+ reference_number: z.string().optional()
267
+ .describe("Transaction reference, cheque number, UTR, etc."),
268
+ notes: z.string().optional()
269
+ .describe("Notes about this payment."),
270
+ invoice_numbers: z.array(z.string()).optional()
271
+ .describe("Explicit invoice numbers to allocate this payment against."),
272
+ })).min(1).max(5000)
273
+ .describe("Array of payment records. Max 5000 per call."),
274
+ },
275
+ wrapTool(async (input) => {
276
+ const result = await client.import.importPayments({
277
+ source: input.source,
278
+ paidInvoiceNumbers: input.paid_invoice_numbers,
279
+ payments: input.payments.map((p) => ({
280
+ partyName: p.party_name,
281
+ amount: p.amount,
282
+ mode: p.mode,
283
+ paymentDate: p.payment_date,
284
+ paymentNumber: p.payment_number,
285
+ referenceNumber: p.reference_number,
286
+ notes: p.notes,
287
+ invoiceNumbers: p.invoice_numbers,
288
+ })),
289
+ });
290
+ return {
291
+ content: [{
292
+ type: "text" as const,
293
+ text: JSON.stringify(result, null, 2),
294
+ }],
295
+ };
296
+ })
297
+ );
298
+ }