From 498abcc9376cb1ea5a58ac8de472730516fe8b55 Mon Sep 17 00:00:00 2001 From: zgaetano Date: Tue, 31 Mar 2026 15:33:50 -0400 Subject: [PATCH] Add wave-mcp/src/tools/businesses.ts --- wave-mcp/src/tools/businesses.ts | 191 +++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 wave-mcp/src/tools/businesses.ts diff --git a/wave-mcp/src/tools/businesses.ts b/wave-mcp/src/tools/businesses.ts new file mode 100644 index 0000000..7496311 --- /dev/null +++ b/wave-mcp/src/tools/businesses.ts @@ -0,0 +1,191 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { WaveClient } from "../services/wave-client.js"; +import { WaveBusiness } from "../types.js"; + +const ResponseFormat = { MARKDOWN: "markdown", JSON: "json" } as const; +type ResponseFormat = (typeof ResponseFormat)[keyof typeof ResponseFormat]; + +export function registerBusinessTools( + server: McpServer, + client: WaveClient +): void { + // ── wave_list_businesses ────────────────────────────────────────────────── + + server.registerTool( + "wave_list_businesses", + { + title: "List Wave Businesses", + description: `List all Wave businesses accessible to the authenticated user. + +Returns all business accounts the API token has access to. Most other Wave operations require a businessId, so call this first to discover available businesses. + +Args: + - response_format ('markdown' | 'json'): Output format (default: 'markdown') + +Returns: + List of businesses with id, name, currency, address, and subscription details.`, + inputSchema: z.object({ + response_format: z + .enum(["markdown", "json"]) + .default("markdown") + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"), + }), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async ({ response_format }) => { + try { + const data = await client.query<{ businesses: { edges: Array<{ node: WaveBusiness }> } }>(` + query { + businesses(page: 1, pageSize: 100) { + edges { + node { + id + name + isPersonal + organizationalType + isClassicAccounting + isClassicInvoicing + currency { code symbol name } + address { + addressLine1 + addressLine2 + city + province { name code } + country { name code } + postalCode + } + } + } + } + } + `); + + const businesses = data.businesses.edges.map((e) => e.node); + + if (!businesses.length) { + return { content: [{ type: "text", text: "No businesses found for this account." }] }; + } + + let text: string; + if (response_format === "json") { + text = JSON.stringify({ count: businesses.length, businesses }, null, 2); + } else { + const lines = [`# Wave Businesses (${businesses.length})`, ""]; + for (const b of businesses) { + lines.push(`## ${b.name}`); + lines.push(`- **ID**: ${b.id}`); + lines.push(`- **Currency**: ${b.currency.code} (${b.currency.name})`); + if (b.address?.city) { + const loc = [b.address.city, b.address.province?.code, b.address.country?.code] + .filter(Boolean) + .join(", "); + lines.push(`- **Location**: ${loc}`); + } + lines.push(""); + } + text = lines.join("\n"); + } + + return { + content: [{ type: "text", text: WaveClient.truncate(text) }], + structuredContent: { count: businesses.length, businesses }, + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + }; + } + } + ); + + // ── wave_get_business ───────────────────────────────────────────────────── + + server.registerTool( + "wave_get_business", + { + title: "Get Wave Business", + description: `Get details for a specific Wave business by ID. + +Args: + - businessId (string): The Wave business ID (obtain from wave_list_businesses) + - response_format ('markdown' | 'json'): Output format (default: 'markdown') + +Returns: + Full business details including name, currency, address, and account settings.`, + inputSchema: z.object({ + businessId: z.string().describe("Wave business ID"), + response_format: z + .enum(["markdown", "json"]) + .default("markdown") + .describe("Output format"), + }), + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + }, + async ({ businessId, response_format }) => { + try { + const data = await client.query<{ business: WaveBusiness }>(` + query($businessId: ID!) { + business(id: $businessId) { + id name isPersonal organizationalType + isClassicAccounting isClassicInvoicing + currency { code symbol name } + address { + addressLine1 addressLine2 city postalCode + province { name code } + country { name code } + } + } + } + `, { businessId }); + + const b = data.business; + if (!b) { + return { content: [{ type: "text", text: `No business found with ID: ${businessId}` }] }; + } + + let text: string; + if (response_format === "json") { + text = JSON.stringify(b, null, 2); + } else { + const lines = [ + `# Business: ${b.name}`, + "", + `- **ID**: ${b.id}`, + `- **Currency**: ${b.currency.code} (${b.currency.name})`, + `- **Type**: ${b.isPersonal ? "Personal" : (b.organizationalType ?? "Business")}`, + ]; + if (b.address) { + const addr = b.address; + if (addr.addressLine1) lines.push(`- **Address**: ${addr.addressLine1}`); + if (addr.addressLine2) lines.push(` ${addr.addressLine2}`); + const cityLine = [addr.city, addr.province?.code, addr.postalCode, addr.country?.code] + .filter(Boolean) + .join(", "); + if (cityLine) lines.push(` ${cityLine}`); + } + text = lines.join("\n"); + } + + return { + content: [{ type: "text", text }], + structuredContent: b, + }; + } catch (error) { + return { + content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }], + }; + } + } + ); +}