diff --git a/mcp-gateway/wave-mcp/src/tools/invoices.ts b/mcp-gateway/wave-mcp/src/tools/invoices.ts deleted file mode 100644 index 856d74d..0000000 --- a/mcp-gateway/wave-mcp/src/tools/invoices.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import { WaveClient } from "../services/wave-client.js"; -import { WaveInvoice } from "../types.js"; -import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from "../constants.js"; - -interface InvoicesData { - business: { - invoices: { - pageInfo: { currentPage: number; totalPages: number; totalCount: number }; - edges: Array<{ node: WaveInvoice }>; - }; - }; -} - -interface InvoiceData { - business: { invoice: WaveInvoice }; -} - -interface InvoiceCreateData { - invoiceCreate: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - invoice?: WaveInvoice; - }; -} - -interface InvoicePatchData { - invoicePatch: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - invoice?: WaveInvoice; - }; -} - -interface InvoiceActionData { - invoiceApprove?: { didSucceed: boolean; inputErrors: Array<{ message: string }> }; - invoiceSend?: { didSucceed: boolean; inputErrors: Array<{ message: string }>; invoice?: { id: string } }; - invoiceMarkSent?: { didSucceed: boolean; inputErrors: Array<{ message: string }> }; - invoiceDelete?: { didSucceed: boolean; inputErrors: Array<{ message: string }> }; -} - -const INVOICE_FIELDS = ` - id pdfUrl viewUrl status title subhead invoiceNumber - invoiceDate dueDate memo - customer { id name email } - currency { code } - amountDue { value currency { code } } - amountPaid { value currency { code } } - taxTotal { value currency { code } } - subtotal { value currency { code } } - total { value currency { code } } - items { - product { id name } - description quantity unitPrice subtotal total - account { id name } - } - createdAt modifiedAt -`; - -function formatInvoiceMarkdown(inv: WaveInvoice): string[] { - const lines = [ - `## Invoice #${inv.invoiceNumber} — ${inv.customer?.name ?? "Unknown"} (${inv.id})`, - `- **Status**: ${inv.status}`, - `- **Date**: ${inv.invoiceDate ?? "N/A"}`, - `- **Due**: ${inv.dueDate ?? "N/A"}`, - `- **Total**: ${WaveClient.formatCurrency(inv.total?.value ?? 0, inv.currency?.code ?? "")}`, - `- **Amount Due**: ${WaveClient.formatCurrency(inv.amountDue?.value ?? 0, inv.currency?.code ?? "")}`, - `- **Amount Paid**: ${WaveClient.formatCurrency(inv.amountPaid?.value ?? 0, inv.currency?.code ?? "")}`, - ]; - if (inv.pdfUrl) lines.push(`- **PDF**: ${inv.pdfUrl}`); - if (inv.viewUrl) lines.push(`- **View**: ${inv.viewUrl}`); - lines.push(""); - return lines; -} - -export function registerInvoiceTools(server: McpServer, client: WaveClient): void { - // ── wave_list_invoices ──────────────────────────────────────────────────── - - server.registerTool( - "wave_list_invoices", - { - title: "List Wave Invoices", - description: `List invoices for a Wave business with optional filtering by status or customer. - -Args: - - businessId (string): Wave business ID - - page (number): Page number (default: 1) - - pageSize (number): Items per page, 1-100 (default: 20) - - customerId (string): Filter by customer ID (optional) - - invoiceNumber (string): Filter by invoice number (optional) - - status ('DRAFT' | 'APPROVED' | 'SENT' | 'VIEWED' | 'PARTIALLY_PAID' | 'PAID' | 'OVERDUE'): Filter by status (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Paginated list of invoices with totals, status, and customer info.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - page: z.number().int().min(1).default(1).describe("Page number"), - pageSize: z.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Items per page"), - customerId: z.string().optional().describe("Filter by customer ID"), - invoiceNumber: z.string().optional().describe("Filter by invoice number"), - status: z.enum(["DRAFT", "APPROVED", "SENT", "VIEWED", "PARTIALLY_PAID", "PAID", "OVERDUE"]).optional().describe("Filter by status"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, page, pageSize, customerId, invoiceNumber, status, response_format }) => { - try { - // Build optional filter variables - const vars: Record = { businessId, page, pageSize }; - if (customerId) vars.customerId = customerId; - if (invoiceNumber) vars.invoiceNumber = invoiceNumber; - if (status) vars.status = status; - - const data = await client.query(` - query($businessId: ID!, $page: Int!, $pageSize: Int!) { - business(id: $businessId) { - invoices(page: $page, pageSize: $pageSize) { - pageInfo { currentPage totalPages totalCount } - edges { node { ${INVOICE_FIELDS} } } - } - } - } - `, vars); - - let { pageInfo, edges } = data.business.invoices; - let invoices = edges.map((e) => e.node); - - // Client-side filter since Wave API may not support all filter params at query level - if (customerId) invoices = invoices.filter((i) => i.customer?.id === customerId); - if (status) invoices = invoices.filter((i) => i.status === status); - if (invoiceNumber) invoices = invoices.filter((i) => i.invoiceNumber?.includes(invoiceNumber)); - - if (!invoices.length) { - return { content: [{ type: "text", text: "No invoices found matching the criteria." }] }; - } - - const result = { - total: pageInfo.totalCount, - count: invoices.length, - page: pageInfo.currentPage, - totalPages: pageInfo.totalPages, - has_more: pageInfo.currentPage < pageInfo.totalPages, - next_page: pageInfo.currentPage < pageInfo.totalPages ? pageInfo.currentPage + 1 : undefined, - invoices, - }; - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result, null, 2); - } else { - const lines = [`# Invoices (${pageInfo.totalCount} total, page ${pageInfo.currentPage}/${pageInfo.totalPages})`, ""]; - for (const inv of invoices) lines.push(...formatInvoiceMarkdown(inv)); - text = lines.join("\n"); - } - - return { - content: [{ type: "text", text: WaveClient.truncate(text) }], - structuredContent: result, - }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_get_invoice ────────────────────────────────────────────────────── - - server.registerTool( - "wave_get_invoice", - { - title: "Get Wave Invoice", - description: `Get full details for a specific invoice including line items. - -Args: - - businessId (string): Wave business ID - - invoiceId (string): Invoice ID - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Full invoice with all line items, totals, dates, and PDF/view links.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - invoiceId: z.string().describe("Invoice ID"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, invoiceId, response_format }) => { - try { - const data = await client.query(` - query($businessId: ID!, $invoiceId: ID!) { - business(id: $businessId) { - invoice(id: $invoiceId) { ${INVOICE_FIELDS} } - } - } - `, { businessId, invoiceId }); - - const inv = data.business.invoice; - if (!inv) return { content: [{ type: "text", text: `No invoice found with ID: ${invoiceId}` }] }; - - let text: string; - if (response_format === "json") { - text = JSON.stringify(inv, null, 2); - } else { - const lines = [...formatInvoiceMarkdown(inv)]; - if (inv.items?.length) { - lines.push("### Line Items", ""); - for (const item of inv.items) { - const desc = item.description ?? item.product?.name ?? "Item"; - lines.push(`- ${desc}: ${item.quantity} × ${WaveClient.formatCurrency(item.unitPrice, inv.currency?.code ?? "")} = ${WaveClient.formatCurrency(item.total, inv.currency?.code ?? "")}`); - } - } - text = lines.join("\n"); - } - - return { content: [{ type: "text", text }], structuredContent: inv }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_create_invoice ─────────────────────────────────────────────────── - - server.registerTool( - "wave_create_invoice", - { - title: "Create Wave Invoice", - description: `Create a new draft invoice in Wave. - -Args: - - businessId (string): Wave business ID - - customerId (string): Customer ID for the invoice - - invoiceDate (string): Invoice date in YYYY-MM-DD format (optional, defaults to today) - - dueDate (string): Due date in YYYY-MM-DD format (optional) - - invoiceNumber (string): Custom invoice number (optional, auto-assigned if omitted) - - memo (string): Note to appear on invoice (optional) - - items (array): Line items (required, at least 1) - - productId (string): Product ID (optional) - - description (string): Item description - - quantity (number): Quantity - - unitPrice (number): Unit price - - accountId (string): Revenue account ID (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Created invoice details or validation errors.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - customerId: z.string().describe("Customer ID"), - invoiceDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Invoice date (YYYY-MM-DD)"), - dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Due date (YYYY-MM-DD)"), - invoiceNumber: z.string().optional().describe("Custom invoice number"), - memo: z.string().optional().describe("Memo/note on invoice"), - items: z.array(z.object({ - productId: z.string().optional().describe("Product ID"), - description: z.string().optional().describe("Line item description"), - quantity: z.number().min(0.001).describe("Quantity"), - unitPrice: z.number().describe("Unit price"), - accountId: z.string().optional().describe("Revenue account ID"), - })).min(1).describe("Line items (at least 1 required)"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ businessId, customerId, invoiceDate, dueDate, invoiceNumber, memo, items, response_format }) => { - try { - const input: Record = { - businessId, - customerId, - items: items.map((item) => { - const li: Record = { - quantity: item.quantity, - unitPrice: item.unitPrice, - }; - if (item.productId) li.productId = item.productId; - if (item.description) li.description = item.description; - if (item.accountId) li.accountId = item.accountId; - return li; - }), - }; - if (invoiceDate) input.invoiceDate = invoiceDate; - if (dueDate) input.dueDate = dueDate; - if (invoiceNumber) input.invoiceNumber = invoiceNumber; - if (memo) input.memo = memo; - - const data = await client.query(` - mutation($input: InvoiceCreateInput!) { - invoiceCreate(input: $input) { - didSucceed - inputErrors { code message path } - invoice { ${INVOICE_FIELDS} } - } - } - `, { input }); - - const result = data.invoiceCreate; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.path?.join(".") ?? "field"}: ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to create invoice:\n${errs}` }] }; - } - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result.invoice, null, 2); - } else { - text = `Invoice created successfully!\n\n${formatInvoiceMarkdown(result.invoice!).join("\n")}`; - } - - return { content: [{ type: "text", text }], structuredContent: result.invoice }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_approve_invoice ────────────────────────────────────────────────── - - server.registerTool( - "wave_approve_invoice", - { - title: "Approve Wave Invoice", - description: `Approve a draft invoice, moving it to APPROVED status so it can be sent. - -Args: - - invoiceId (string): Invoice ID to approve - -Returns: - Success confirmation or error details.`, - inputSchema: z.object({ - invoiceId: z.string().describe("Invoice ID to approve"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ invoiceId }) => { - try { - const data = await client.query(` - mutation($input: InvoiceApproveInput!) { - invoiceApprove(input: $input) { - didSucceed - inputErrors { message } - } - } - `, { input: { invoiceId } }); - - const result = data.invoiceApprove!; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to approve invoice:\n${errs}` }] }; - } - - return { content: [{ type: "text", text: `Invoice ${invoiceId} approved successfully.` }] }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_send_invoice ───────────────────────────────────────────────────── - - server.registerTool( - "wave_send_invoice", - { - title: "Send Wave Invoice", - description: `Send an approved invoice to the customer via email. - -Args: - - invoiceId (string): Invoice ID to send - - to (array of strings): Recipient email addresses - - subject (string): Email subject (optional) - - message (string): Email message body (optional) - -Returns: - Success confirmation or error details.`, - inputSchema: z.object({ - invoiceId: z.string().describe("Invoice ID to send"), - to: z.array(z.string().email()).min(1).describe("Recipient email addresses"), - subject: z.string().optional().describe("Email subject line"), - message: z.string().optional().describe("Email message body"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ invoiceId, to, subject, message }) => { - try { - const input: Record = { invoiceId, to }; - if (subject) input.subject = subject; - if (message) input.message = message; - - const data = await client.query(` - mutation($input: InvoiceSendInput!) { - invoiceSend(input: $input) { - didSucceed - inputErrors { message } - } - } - `, { input }); - - const result = data.invoiceSend!; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to send invoice:\n${errs}` }] }; - } - - return { content: [{ type: "text", text: `Invoice ${invoiceId} sent to ${to.join(", ")}.` }] }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_delete_invoice ─────────────────────────────────────────────────── - - server.registerTool( - "wave_delete_invoice", - { - title: "Delete Wave Invoice", - description: `Delete an invoice. Only DRAFT invoices can be deleted; paid invoices cannot be deleted. - -⚠️ This action is irreversible. - -Args: - - businessId (string): Wave business ID - - invoiceId (string): Invoice ID to delete - -Returns: - Success confirmation or error details.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - invoiceId: z.string().describe("Invoice ID to delete"), - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, - }, - async ({ businessId, invoiceId }) => { - try { - const data = await client.query(` - mutation($input: InvoiceDeleteInput!) { - invoiceDelete(input: $input) { - didSucceed - inputErrors { message } - } - } - `, { input: { businessId, invoiceId } }); - - const result = data.invoiceDelete!; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to delete invoice:\n${errs}` }] }; - } - - return { content: [{ type: "text", text: `Invoice ${invoiceId} deleted successfully.` }] }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); -}