@kirjapro/mcp-server 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +73 -9
  3. package/dist/index.js +1472 -657
  4. package/package.json +10 -4
  5. package/server.json +6 -2
  6. package/tool-fingerprints.json +55 -51
  7. package/dist/accounts.d.ts +0 -40
  8. package/dist/accounts.js +0 -45
  9. package/dist/auth.d.ts +0 -17
  10. package/dist/auth.js +0 -245
  11. package/dist/compliance/rates.d.ts +0 -53
  12. package/dist/compliance/rates.js +0 -57
  13. package/dist/database.types.d.ts +0 -6525
  14. package/dist/database.types.js +0 -20
  15. package/dist/index.d.ts +0 -2
  16. package/dist/lib/incomeStatementSynthesis.d.ts +0 -47
  17. package/dist/lib/incomeStatementSynthesis.js +0 -140
  18. package/dist/resources/company.d.ts +0 -2
  19. package/dist/resources/company.js +0 -75
  20. package/dist/resources/compliance.d.ts +0 -2
  21. package/dist/resources/compliance.js +0 -338
  22. package/dist/resources/index.d.ts +0 -2
  23. package/dist/resources/index.js +0 -10
  24. package/dist/resources/schema.d.ts +0 -2
  25. package/dist/resources/schema.js +0 -63
  26. package/dist/resources/security.d.ts +0 -2
  27. package/dist/resources/security.js +0 -33
  28. package/dist/schemas.d.ts +0 -12
  29. package/dist/schemas.js +0 -12
  30. package/dist/tools/accounts.d.ts +0 -2
  31. package/dist/tools/accounts.js +0 -48
  32. package/dist/tools/banking.d.ts +0 -2
  33. package/dist/tools/banking.js +0 -196
  34. package/dist/tools/bookings.d.ts +0 -2
  35. package/dist/tools/bookings.js +0 -294
  36. package/dist/tools/customers.d.ts +0 -2
  37. package/dist/tools/customers.js +0 -320
  38. package/dist/tools/financialNotes.d.ts +0 -2
  39. package/dist/tools/financialNotes.js +0 -255
  40. package/dist/tools/fingerprints.d.ts +0 -6
  41. package/dist/tools/fingerprints.js +0 -28
  42. package/dist/tools/fiscal-years.d.ts +0 -2
  43. package/dist/tools/fiscal-years.js +0 -35
  44. package/dist/tools/index.d.ts +0 -9
  45. package/dist/tools/index.js +0 -105
  46. package/dist/tools/invoices.d.ts +0 -2
  47. package/dist/tools/invoices.js +0 -489
  48. package/dist/tools/offers.d.ts +0 -2
  49. package/dist/tools/offers.js +0 -443
  50. package/dist/tools/received-invoices.d.ts +0 -2
  51. package/dist/tools/received-invoices.js +0 -134
  52. package/dist/tools/recurring-invoices.d.ts +0 -2
  53. package/dist/tools/recurring-invoices.js +0 -348
  54. package/dist/tools/reports.d.ts +0 -2
  55. package/dist/tools/reports.js +0 -869
  56. package/dist/tools/templates.d.ts +0 -89
  57. package/dist/tools/templates.js +0 -636
  58. package/dist/tools/transactions.d.ts +0 -2
  59. package/dist/tools/transactions.js +0 -597
  60. package/dist/tools/validation.d.ts +0 -26
  61. package/dist/tools/validation.js +0 -127
  62. package/dist/types.d.ts +0 -388
  63. package/dist/types.js +0 -34
  64. package/dist/utils.d.ts +0 -276
  65. package/dist/utils.js +0 -637
  66. package/dist/version.d.ts +0 -1
  67. package/dist/version.js +0 -3
@@ -1,489 +0,0 @@
1
- import { z } from "zod";
2
- import { getSupabase, getCompanyId } from "../auth.js";
3
- import { addDaysToIsoDate, clampPagination, helsinkiDateString, paginate, toErrorContent, toDryRunResponse, toResult, sanitizeDbError, roundCents, tagCustomer, withIdempotency, IDEMPOTENCY_KEY_SCHEMA, ISO_DATE_SCHEMA, DRY_RUN_SCHEMA, READ_ONLY_ANNOTATIONS, WRITE_ANNOTATIONS, DESTRUCTIVE_ANNOTATIONS, } from "../utils.js";
4
- import { VALID_TRANSITIONS } from "../types.js";
5
- /**
6
- * Generate Finnish reference number (viitenumero) with 7-3-1 check digit.
7
- */
8
- function generateReferenceNumber(baseNumber) {
9
- const digits = baseNumber.replace(/\D/g, "");
10
- if (digits.length < 1 || digits.length > 19) {
11
- throw new Error("Base number must be 1-19 digits");
12
- }
13
- const weights = [7, 3, 1];
14
- let sum = 0;
15
- for (let i = digits.length - 1, w = 0; i >= 0; i--, w++) {
16
- sum += parseInt(digits[i]) * weights[w % 3];
17
- }
18
- const checkDigit = (10 - (sum % 10)) % 10;
19
- return digits + checkDigit;
20
- }
21
- const LIST_INVOICES_ROW_SHAPE = {
22
- id: z.string().uuid(),
23
- invoice_number: z.string(),
24
- reference_number: z.string().nullable(),
25
- invoice_date: z.string(),
26
- due_date: z.string().nullable(),
27
- status: z.string(),
28
- subtotal: z.number(),
29
- vat_total: z.number(),
30
- total: z.number(),
31
- notes: z.string().nullable(),
32
- customers: z
33
- .object({
34
- name: z.string().nullable(),
35
- business_id: z.string().nullable(),
36
- })
37
- .nullable(),
38
- };
39
- const GET_INVOICE_OUTPUT_SHAPE = {
40
- id: z.string().uuid(),
41
- invoice_number: z.string(),
42
- reference_number: z.string().nullable(),
43
- invoice_date: z.string(),
44
- due_date: z.string().nullable(),
45
- status: z.string(),
46
- subtotal: z.number(),
47
- vat_total: z.number(),
48
- total: z.number(),
49
- notes: z.string().nullable(),
50
- created_at: z.string().nullable(),
51
- // `.optional()` is explicit since Zod 4 — bare `z.unknown()` was implicitly
52
- // key-optional in Zod 3 but is now required. These three are joined relations
53
- // that may be absent when the row is fetched without the corresponding
54
- // .select() include.
55
- customers: z.unknown().optional(),
56
- invoice_lines: z.unknown().optional(),
57
- invoice_payments: z.unknown().optional(),
58
- vat_breakdown: z.array(z.object({
59
- vat_rate: z.number(),
60
- subtotal: z.number(),
61
- vat_amount: z.number(),
62
- total: z.number(),
63
- })),
64
- paid_total: z.number(),
65
- remaining_balance: z.number(),
66
- };
67
- export function registerInvoiceTools(server) {
68
- // ---- READ: List Invoices ----
69
- server.registerTool("kirjapro_list_invoices", {
70
- description: "List invoices with customer names. Filter by status (draft, sent, paid, overdue, cancelled).",
71
- inputSchema: {
72
- status: z
73
- .enum(["draft", "sent", "paid", "partial", "overdue", "cancelled"])
74
- .optional()
75
- .describe("Filter by invoice status"),
76
- limit: z
77
- .number()
78
- .min(1)
79
- .max(200)
80
- .default(50)
81
- .describe("Max results (default 50)"),
82
- offset: z.number().min(0).default(0).describe("Skip first N results"),
83
- },
84
- outputSchema: {
85
- results: z.array(z.object(LIST_INVOICES_ROW_SHAPE).passthrough()),
86
- total_count: z.number(),
87
- has_more: z.boolean(),
88
- next_offset: z.number(),
89
- },
90
- annotations: READ_ONLY_ANNOTATIONS,
91
- }, async (params) => {
92
- const supabase = getSupabase();
93
- const companyId = getCompanyId();
94
- const { limit, offset } = clampPagination(params.limit, params.offset);
95
- let query = supabase
96
- .from("invoices")
97
- .select(`
98
- id, invoice_number, reference_number, invoice_date, due_date,
99
- status, subtotal, vat_total, total, notes,
100
- customers ( name, business_id )
101
- `, { count: "exact" })
102
- .eq("company_id", companyId)
103
- .order("invoice_date", { ascending: false })
104
- .order("id", { ascending: false });
105
- if (params.status) {
106
- query = query.eq("status", params.status);
107
- }
108
- query = query.range(offset, offset + limit - 1);
109
- const { data, count, error } = await query;
110
- if (error) {
111
- return toErrorContent(sanitizeDbError("Failed to list invoices", error));
112
- }
113
- const tagged = (data || []).map((row) => ({
114
- ...row,
115
- customers: tagCustomer(row.customers),
116
- }));
117
- return toResult(paginate(tagged, count || 0, offset, limit));
118
- });
119
- // ---- WRITE: Create Invoice ----
120
- server.registerTool("kirjapro_create_invoice", {
121
- description: "Create a new sales invoice with line items. Automatically generates invoice number, reference number (viitenumero), and calculates VAT. Returns the complete invoice.",
122
- inputSchema: {
123
- customerId: z.string().uuid().describe("Customer UUID"),
124
- invoiceDate: ISO_DATE_SCHEMA
125
- .describe("Invoice date (YYYY-MM-DD)"),
126
- paymentTermDays: z
127
- .number()
128
- .min(0)
129
- .max(365)
130
- .default(14)
131
- .describe("Payment term in days (default 14)"),
132
- notes: z.string().optional().describe("Notes printed on the invoice"),
133
- lines: z
134
- .array(z.object({
135
- description: z.string().min(1).describe("Line item description"),
136
- quantity: z.number().min(0).describe("Quantity"),
137
- unit: z.string().default("kpl").describe("Unit (kpl, h, kk, etc.)"),
138
- unitPrice: z
139
- .number()
140
- .finite()
141
- .min(-1_000_000_000)
142
- .max(1_000_000_000)
143
- .describe("Unit price in EUR (excl. VAT). Can be negative for credit lines."),
144
- vatRate: z
145
- .enum(["25.5", "13.5", "10", "0"])
146
- .default("25.5")
147
- .describe("VAT rate"),
148
- }))
149
- .min(1)
150
- .describe("Invoice line items"),
151
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
152
- },
153
- annotations: WRITE_ANNOTATIONS,
154
- }, async (params) => {
155
- const supabase = getSupabase();
156
- const companyId = getCompanyId();
157
- return withIdempotency(supabase, companyId, "kirjapro_create_invoice", params.idempotencyKey, params, async () => {
158
- // Verify customer exists
159
- const { data: customer, error: custError } = await supabase
160
- .from("customers")
161
- .select("id, name")
162
- .eq("id", params.customerId)
163
- .eq("company_id", companyId)
164
- .single();
165
- if (custError || !customer) {
166
- return toErrorContent(`Customer ${params.customerId} not found. ` +
167
- `Use kirjapro_list_customers to find customer IDs.`);
168
- }
169
- // Get next invoice number atomically (advisory-locked)
170
- const { data: invoiceNumber, error: numError } = await supabase.rpc("get_next_invoice_number", {
171
- p_company_id: companyId,
172
- });
173
- if (numError || !invoiceNumber) {
174
- return toErrorContent(numError ? sanitizeDbError("Failed to generate invoice number", numError) : "Failed to generate invoice number.");
175
- }
176
- const referenceNumber = generateReferenceNumber(invoiceNumber.replace(/\D/g, ""));
177
- const dueDateStr = addDaysToIsoDate(params.invoiceDate, params.paymentTermDays);
178
- // Calculate line totals
179
- const calculatedLines = params.lines.map((line, i) => {
180
- const vatRate = parseFloat(line.vatRate);
181
- const subtotal = roundCents(line.quantity * line.unitPrice);
182
- const vatAmount = roundCents(subtotal * (vatRate / 100));
183
- const total = roundCents(subtotal + vatAmount);
184
- return {
185
- line_number: i + 1,
186
- description: line.description,
187
- quantity: line.quantity,
188
- unit: line.unit,
189
- unit_price: line.unitPrice,
190
- vat_rate: vatRate,
191
- subtotal,
192
- vat_amount: vatAmount,
193
- total,
194
- };
195
- });
196
- const subtotal = roundCents(calculatedLines.reduce((s, l) => s + l.subtotal, 0));
197
- const vatTotal = roundCents(calculatedLines.reduce((s, l) => s + l.vat_amount, 0));
198
- const total = roundCents(subtotal + vatTotal);
199
- const { data: invoiceId, error: invError } = await supabase.rpc("create_invoice_with_lines", {
200
- p_company_id: companyId,
201
- p_customer_id: params.customerId,
202
- p_invoice_number: invoiceNumber,
203
- p_reference_number: referenceNumber,
204
- p_invoice_date: params.invoiceDate,
205
- p_due_date: dueDateStr,
206
- p_status: "draft",
207
- p_subtotal: subtotal,
208
- p_vat_total: vatTotal,
209
- p_total: total,
210
- p_notes: params.notes || "",
211
- p_lines: calculatedLines,
212
- });
213
- if (invError || !invoiceId) {
214
- return toErrorContent(invError ? sanitizeDbError("Failed to create invoice", invError) : "Failed to create invoice.");
215
- }
216
- const { data: invoice, error: fetchError } = await supabase
217
- .from("invoices")
218
- .select("id, invoice_number, reference_number, invoice_date, due_date, status, subtotal, vat_total, total, notes, created_at")
219
- .eq("id", invoiceId)
220
- .eq("company_id", companyId)
221
- .single();
222
- if (fetchError || !invoice) {
223
- return toErrorContent(fetchError ? sanitizeDbError("Invoice created but fetch failed", fetchError) : "Invoice created but could not be retrieved.");
224
- }
225
- return toResult({
226
- message: `Invoice ${invoiceNumber} created for ${customer.name}`,
227
- invoice: {
228
- ...invoice,
229
- customer_name: customer.name,
230
- lines: calculatedLines,
231
- },
232
- });
233
- });
234
- });
235
- // ---- WRITE: Record Payment ----
236
- server.registerTool("kirjapro_record_payment", {
237
- description: 'Record a payment against an invoice. Automatically updates invoice status to "paid" when fully paid, or keeps "sent" if partially paid. Returns payment details and updated invoice status.',
238
- inputSchema: {
239
- invoiceId: z.string().uuid().describe("Invoice UUID"),
240
- paymentDate: ISO_DATE_SCHEMA
241
- .describe("Payment date (YYYY-MM-DD)"),
242
- amount: z
243
- .number()
244
- .finite()
245
- .min(0.01)
246
- .max(1_000_000_000)
247
- .describe("Payment amount in EUR"),
248
- paymentMethod: z
249
- .string()
250
- .default("bank_transfer")
251
- .describe("Payment method (bank_transfer, cash, card)"),
252
- reference: z.string().optional().describe("Payment reference"),
253
- notes: z.string().optional().describe("Payment notes"),
254
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
255
- },
256
- annotations: WRITE_ANNOTATIONS,
257
- }, async (params) => {
258
- const supabase = getSupabase();
259
- const companyId = getCompanyId();
260
- return withIdempotency(supabase, companyId, "kirjapro_record_payment", params.idempotencyKey, params, async () => {
261
- // Fetch invoice with existing payments
262
- const { data: invoice, error: invError } = await supabase
263
- .from("invoices")
264
- .select(`
265
- id, invoice_number, total, status,
266
- invoice_payments ( amount )
267
- `)
268
- .eq("id", params.invoiceId)
269
- .eq("company_id", companyId)
270
- .single();
271
- if (invError || !invoice) {
272
- return toErrorContent(`Invoice ${params.invoiceId} not found. Use kirjapro_list_invoices to find invoice IDs.`);
273
- }
274
- if (invoice.status === "paid") {
275
- return toErrorContent(`Invoice ${invoice.invoice_number} is already fully paid.`);
276
- }
277
- if (invoice.status === "cancelled") {
278
- return toErrorContent(`Invoice ${invoice.invoice_number} is cancelled and cannot accept payments.`);
279
- }
280
- const paidSoFar = roundCents((invoice.invoice_payments || []).reduce((s, p) => s + (p.amount || 0), 0));
281
- const remaining = roundCents(invoice.total - paidSoFar);
282
- if (params.amount > remaining + 0.01) {
283
- return toErrorContent(`Payment amount ${params.amount.toFixed(2)} exceeds remaining balance ${remaining.toFixed(2)} EUR ` +
284
- `on invoice ${invoice.invoice_number} (total: ${invoice.total.toFixed(2)}, paid: ${paidSoFar.toFixed(2)}). ` +
285
- `Reduce the payment amount to ${remaining.toFixed(2)} or less.`);
286
- }
287
- const { data: paymentResult, error: payError } = await supabase.rpc("record_invoice_payment", {
288
- p_company_id: companyId,
289
- p_invoice_id: params.invoiceId,
290
- p_payment_date: params.paymentDate,
291
- p_amount: params.amount,
292
- p_payment_method: params.paymentMethod,
293
- p_reference: params.reference || undefined,
294
- p_notes: params.notes || undefined,
295
- });
296
- if (payError || !paymentResult) {
297
- return toErrorContent(payError ? sanitizeDbError("Failed to record payment", payError) : "Failed to record payment.");
298
- }
299
- const result = paymentResult;
300
- return toResult({
301
- message: `Payment of ${params.amount.toFixed(2)} EUR recorded on invoice ${invoice.invoice_number}`,
302
- payment_id: result.payment_id,
303
- invoice_status: result.invoice_status,
304
- remaining_balance: result.remaining_balance,
305
- });
306
- });
307
- });
308
- // ---- READ: Get Invoice Detail ----
309
- server.registerTool("kirjapro_get_invoice", {
310
- description: "Get a single invoice with full detail: customer info, line items, payments, VAT breakdown, and remaining balance.",
311
- inputSchema: {
312
- invoiceId: z.string().uuid().describe("Invoice UUID"),
313
- },
314
- outputSchema: GET_INVOICE_OUTPUT_SHAPE,
315
- annotations: READ_ONLY_ANNOTATIONS,
316
- }, async (params) => {
317
- const supabase = getSupabase();
318
- const companyId = getCompanyId();
319
- const { data: invoice, error } = await supabase
320
- .from("invoices")
321
- .select(`
322
- id, invoice_number, reference_number, invoice_date, due_date,
323
- status, subtotal, vat_total, total, notes, created_at,
324
- customers ( id, name, business_id, email, address, postal_code, city ),
325
- invoice_lines ( id, line_number, description, quantity, unit, unit_price, vat_rate, subtotal, vat_amount, total ),
326
- invoice_payments ( id, payment_date, amount, payment_method, reference )
327
- `)
328
- .eq("id", params.invoiceId)
329
- .eq("company_id", companyId)
330
- .single();
331
- if (error || !invoice) {
332
- return toErrorContent(`Invoice ${params.invoiceId} not found. Use kirjapro_list_invoices to find invoice IDs.`);
333
- }
334
- // Calculate VAT breakdown by rate
335
- const vatMap = new Map();
336
- for (const line of invoice
337
- .invoice_lines || []) {
338
- const existing = vatMap.get(line.vat_rate) || {
339
- vat_rate: line.vat_rate,
340
- subtotal: 0,
341
- vat_amount: 0,
342
- total: 0,
343
- };
344
- existing.subtotal += line.subtotal || 0;
345
- existing.vat_amount += line.vat_amount || 0;
346
- existing.total += line.total || 0;
347
- vatMap.set(line.vat_rate, existing);
348
- }
349
- const vatBreakdown = [...vatMap.values()].map((v) => ({
350
- ...v,
351
- subtotal: roundCents(v.subtotal),
352
- vat_amount: roundCents(v.vat_amount),
353
- total: roundCents(v.total),
354
- }));
355
- // Calculate payment totals
356
- const payments = invoice.invoice_payments || [];
357
- const paidTotal = roundCents(payments.reduce((s, p) => s + (p.amount || 0), 0));
358
- const remainingBalance = roundCents(invoice.total - paidTotal);
359
- return toResult({
360
- ...invoice,
361
- customers: tagCustomer(invoice
362
- .customers),
363
- vat_breakdown: vatBreakdown,
364
- paid_total: paidTotal,
365
- remaining_balance: remainingBalance,
366
- });
367
- });
368
- // ---- WRITE: Update Invoice Status ----
369
- server.registerTool("kirjapro_update_invoice_status", {
370
- description: 'Update invoice status with state machine validation. Valid transitions: draft→sent/cancelled, sent→paid/partial/overdue/cancelled/draft, partial→paid/overdue/cancelled, overdue→paid/partial/cancelled/sent, cancelled→draft. "paid" is terminal.',
371
- inputSchema: {
372
- invoiceId: z.string().uuid().describe("Invoice UUID"),
373
- status: z
374
- .enum(["draft", "sent", "paid", "partial", "overdue", "cancelled"])
375
- .describe("New status"),
376
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
377
- dryRun: DRY_RUN_SCHEMA,
378
- },
379
- annotations: WRITE_ANNOTATIONS,
380
- }, async (params) => {
381
- const supabase = getSupabase();
382
- const companyId = getCompanyId();
383
- const { data: invoice, error: fetchError } = await supabase
384
- .from("invoices")
385
- .select("id, invoice_number, status")
386
- .eq("id", params.invoiceId)
387
- .eq("company_id", companyId)
388
- .single();
389
- if (fetchError || !invoice) {
390
- return toErrorContent(`Invoice ${params.invoiceId} not found. Use kirjapro_list_invoices to find invoice IDs.`);
391
- }
392
- const currentStatus = invoice.status;
393
- const newStatus = params.status;
394
- if (currentStatus === newStatus) {
395
- return toErrorContent(`Invoice ${invoice.invoice_number} is already in status "${currentStatus}".`);
396
- }
397
- const validNext = VALID_TRANSITIONS[currentStatus] || [];
398
- if (!validNext.includes(newStatus)) {
399
- return toErrorContent(`Cannot transition invoice ${invoice.invoice_number} from "${currentStatus}" to "${newStatus}". ` +
400
- `Valid next statuses: ${validNext.length > 0 ? validNext.join(", ") : "(none — terminal status)"}.`);
401
- }
402
- if (params.dryRun) {
403
- return toDryRunResponse("kirjapro_update_invoice_status", {
404
- action: "update_invoice_status",
405
- invoice_number: invoice.invoice_number,
406
- previous_status: currentStatus,
407
- new_status: newStatus,
408
- });
409
- }
410
- return withIdempotency(supabase, companyId, "kirjapro_update_invoice_status", params.idempotencyKey, params, async () => {
411
- const updateFields = { status: newStatus };
412
- if (newStatus === "paid") {
413
- updateFields.paid_date = helsinkiDateString();
414
- }
415
- // TOCTOU guard: include current status in the WHERE clause so a
416
- // concurrent webhook/cron/MCP call that flipped the status between
417
- // our SELECT and this UPDATE causes the UPDATE to match zero rows
418
- // instead of silently overwriting the concurrent change. `.select()`
419
- // makes PostgREST return the updated row(s) so we can detect this.
420
- const { data: updated, error: updateError } = await supabase
421
- .from("invoices")
422
- .update(updateFields)
423
- .eq("id", params.invoiceId)
424
- .eq("company_id", companyId)
425
- .eq("status", currentStatus)
426
- .select("id")
427
- .maybeSingle();
428
- if (updateError) {
429
- return toErrorContent(sanitizeDbError("Failed to update invoice status", updateError));
430
- }
431
- if (!updated) {
432
- return toErrorContent(`Invoice ${invoice.invoice_number} status changed concurrently (was "${currentStatus}" when read). ` +
433
- `Re-read with kirjapro_get_invoice and decide whether to retry.`);
434
- }
435
- return toResult({
436
- message: `Invoice ${invoice.invoice_number} status changed from "${currentStatus}" to "${newStatus}"`,
437
- invoice_number: invoice.invoice_number,
438
- previous_status: currentStatus,
439
- new_status: newStatus,
440
- });
441
- });
442
- });
443
- // ---- WRITE: Delete Draft Invoice ----
444
- server.registerTool("kirjapro_delete_invoice", {
445
- description: 'Delete a draft invoice. Only invoices with status "draft" can be deleted — sent or paid invoices must be cancelled first (Finnish audit trail requirement).',
446
- inputSchema: {
447
- invoiceId: z.string().uuid().describe("Invoice UUID"),
448
- idempotencyKey: IDEMPOTENCY_KEY_SCHEMA,
449
- dryRun: DRY_RUN_SCHEMA,
450
- },
451
- annotations: DESTRUCTIVE_ANNOTATIONS,
452
- }, async (params) => {
453
- const supabase = getSupabase();
454
- const companyId = getCompanyId();
455
- const { data: invoice, error: fetchError } = await supabase
456
- .from("invoices")
457
- .select("id, invoice_number, status")
458
- .eq("id", params.invoiceId)
459
- .eq("company_id", companyId)
460
- .single();
461
- if (fetchError || !invoice) {
462
- return toErrorContent(`Invoice ${params.invoiceId} not found. Use kirjapro_list_invoices to find invoice IDs.`);
463
- }
464
- if (invoice.status !== "draft") {
465
- return toErrorContent(`Cannot delete invoice ${invoice.invoice_number} — status is "${invoice.status}". ` +
466
- `Only draft invoices can be deleted. Use kirjapro_update_invoice_status to cancel it first.`);
467
- }
468
- if (params.dryRun) {
469
- return toDryRunResponse("kirjapro_delete_invoice", {
470
- action: "delete_invoice",
471
- invoice_number: invoice.invoice_number,
472
- note: "Will cascade-delete invoice_payments and invoice_lines rows for this invoice.",
473
- });
474
- }
475
- return withIdempotency(supabase, companyId, "kirjapro_delete_invoice", params.idempotencyKey, params, async () => {
476
- const { error: rpcError } = await supabase.rpc("delete_draft_invoice_atomic", {
477
- p_company_id: companyId,
478
- p_invoice_id: params.invoiceId,
479
- });
480
- if (rpcError) {
481
- return toErrorContent(sanitizeDbError("Failed to delete invoice", rpcError));
482
- }
483
- return toResult({
484
- message: `Draft invoice ${invoice.invoice_number} deleted`,
485
- invoice_number: invoice.invoice_number,
486
- });
487
- });
488
- });
489
- }
@@ -1,2 +0,0 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function registerOfferTools(server: McpServer): void;