diff --git a/mcp-gateway/wave-mcp/src/tools/customers.ts b/mcp-gateway/wave-mcp/src/tools/customers.ts deleted file mode 100644 index 699ee37..0000000 --- a/mcp-gateway/wave-mcp/src/tools/customers.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import { WaveClient } from "../services/wave-client.js"; -import { WaveCustomer } from "../types.js"; -import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from "../constants.js"; - -interface CustomersData { - business: { - customers: { - pageInfo: { currentPage: number; totalPages: number; totalCount: number }; - edges: Array<{ node: WaveCustomer }>; - }; - }; -} - -interface CustomerData { - business: { customer: WaveCustomer }; -} - -interface CustomerCreateData { - customerCreate: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - customer?: WaveCustomer; - }; -} - -interface CustomerPatchData { - customerPatch: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - customer?: WaveCustomer; - }; -} - -interface CustomerDeleteData { - customerDelete: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - }; -} - -const CUSTOMER_FIELDS = ` - id name firstName lastName email - currency { code } - address { - addressLine1 city postalCode - country { name code } - } - createdAt modifiedAt -`; - -function formatCustomerMarkdown(c: WaveCustomer): string[] { - const lines = [`## ${c.name} (${c.id})`]; - if (c.email) lines.push(`- **Email**: ${c.email}`); - if (c.currency?.code) lines.push(`- **Currency**: ${c.currency.code}`); - if (c.address?.city) { - const loc = [c.address.city, c.address.country?.code].filter(Boolean).join(", "); - lines.push(`- **Location**: ${loc}`); - } - lines.push(`- **Created**: ${c.createdAt?.slice(0, 10) ?? "unknown"}`); - lines.push(""); - return lines; -} - -export function registerCustomerTools(server: McpServer, client: WaveClient): void { - // ── wave_list_customers ─────────────────────────────────────────────────── - - server.registerTool( - "wave_list_customers", - { - title: "List Wave Customers", - description: `List customers for a Wave business with optional sorting and pagination. - -Args: - - businessId (string): Wave business ID - - page (number): Page number (default: 1) - - pageSize (number): Items per page, 1-100 (default: 20) - - sort (string): Sort field — 'NAME' or 'CREATED_AT' (default: 'NAME') - - sortDirection ('ASC' | 'DESC'): Sort direction (default: 'ASC') - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Paginated list of customers with id, name, email, currency, and address.`, - 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"), - sort: z.enum(["NAME", "CREATED_AT"]).default("NAME").describe("Sort field"), - sortDirection: z.enum(["ASC", "DESC"]).default("ASC").describe("Sort direction"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, page, pageSize, sort, sortDirection, response_format }) => { - try { - const data = await client.query(` - query($businessId: ID!, $page: Int!, $pageSize: Int!, $sort: CustomerSort!, $sortDirection: SortDirection!) { - business(id: $businessId) { - customers(page: $page, pageSize: $pageSize, sort: $sort, sortDirection: $sortDirection) { - pageInfo { currentPage totalPages totalCount } - edges { node { ${CUSTOMER_FIELDS} } } - } - } - } - `, { businessId, page, pageSize, sort, sortDirection }); - - const { pageInfo, edges } = data.business.customers; - const customers = edges.map((e) => e.node); - - if (!customers.length) { - return { content: [{ type: "text", text: "No customers found." }] }; - } - - const result = { - total: pageInfo.totalCount, - count: customers.length, - page: pageInfo.currentPage, - totalPages: pageInfo.totalPages, - has_more: pageInfo.currentPage < pageInfo.totalPages, - next_page: pageInfo.currentPage < pageInfo.totalPages ? pageInfo.currentPage + 1 : undefined, - customers, - }; - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result, null, 2); - } else { - const lines = [ - `# Customers (${pageInfo.totalCount} total, page ${pageInfo.currentPage}/${pageInfo.totalPages})`, - "", - ]; - for (const c of customers) lines.push(...formatCustomerMarkdown(c)); - 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_customer ───────────────────────────────────────────────────── - - server.registerTool( - "wave_get_customer", - { - title: "Get Wave Customer", - description: `Get details for a specific customer by ID. - -Args: - - businessId (string): Wave business ID - - customerId (string): Customer ID - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Full customer details including contact info and address.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - customerId: z.string().describe("Customer ID"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, customerId, response_format }) => { - try { - const data = await client.query(` - query($businessId: ID!, $customerId: ID!) { - business(id: $businessId) { - customer(id: $customerId) { ${CUSTOMER_FIELDS} } - } - } - `, { businessId, customerId }); - - const c = data.business.customer; - if (!c) return { content: [{ type: "text", text: `No customer found with ID: ${customerId}` }] }; - - let text: string; - if (response_format === "json") { - text = JSON.stringify(c, null, 2); - } else { - text = formatCustomerMarkdown(c).join("\n"); - } - - return { content: [{ type: "text", text }], structuredContent: c }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_create_customer ────────────────────────────────────────────────── - - server.registerTool( - "wave_create_customer", - { - title: "Create Wave Customer", - description: `Create a new customer in a Wave business. - -Args: - - businessId (string): Wave business ID - - name (string): Customer display name (required) - - firstName (string): First name (optional) - - lastName (string): Last name (optional) - - email (string): Email address (optional) - - currencyCode (string): ISO 4217 currency code, e.g. 'USD' (optional) - - addressLine1 (string): Street address (optional) - - city (string): City (optional) - - postalCode (string): Postal/ZIP code (optional) - - countryCode (string): ISO 3166-1 alpha-2 country code, e.g. 'US' (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Created customer details or validation errors.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - name: z.string().min(1).describe("Customer display name"), - firstName: z.string().optional().describe("First name"), - lastName: z.string().optional().describe("Last name"), - email: z.string().email().optional().describe("Email address"), - currencyCode: z.string().length(3).optional().describe("ISO 4217 currency code (e.g. 'USD')"), - addressLine1: z.string().optional().describe("Street address"), - city: z.string().optional().describe("City"), - postalCode: z.string().optional().describe("Postal/ZIP code"), - countryCode: z.string().length(2).optional().describe("ISO 3166-1 alpha-2 country code (e.g. 'US')"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ businessId, name, firstName, lastName, email, currencyCode, addressLine1, city, postalCode, countryCode, response_format }) => { - try { - const input: Record = { businessId, name }; - if (firstName) input.firstName = firstName; - if (lastName) input.lastName = lastName; - if (email) input.email = email; - if (currencyCode) input.currency = currencyCode; - if (addressLine1 || city || postalCode || countryCode) { - const address: Record = {}; - if (addressLine1) address.addressLine1 = addressLine1; - if (city) address.city = city; - if (postalCode) address.postalCode = postalCode; - if (countryCode) address.countryCode = countryCode; - input.address = address; - } - - const data = await client.query(` - mutation($input: CustomerCreateInput!) { - customerCreate(input: $input) { - didSucceed - inputErrors { code message path } - customer { ${CUSTOMER_FIELDS} } - } - } - `, { input }); - - const result = data.customerCreate; - - 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 customer:\n${errs}` }] }; - } - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result.customer, null, 2); - } else { - text = `Customer created successfully!\n\n${formatCustomerMarkdown(result.customer!).join("\n")}`; - } - - return { content: [{ type: "text", text }], structuredContent: result.customer }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_update_customer ────────────────────────────────────────────────── - - server.registerTool( - "wave_update_customer", - { - title: "Update Wave Customer", - description: `Update an existing customer's details. Only provide fields you want to change. - -Args: - - businessId (string): Wave business ID - - customerId (string): Customer ID to update - - name (string): New display name (optional) - - firstName (string): New first name (optional) - - lastName (string): New last name (optional) - - email (string): New email address (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Updated customer details or validation errors.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - customerId: z.string().describe("Customer ID to update"), - name: z.string().min(1).optional().describe("New display name"), - firstName: z.string().optional().describe("New first name"), - lastName: z.string().optional().describe("New last name"), - email: z.string().email().optional().describe("New email address"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, customerId, name, firstName, lastName, email, response_format }) => { - try { - const input: Record = { businessId, customerId }; - if (name) input.name = name; - if (firstName !== undefined) input.firstName = firstName; - if (lastName !== undefined) input.lastName = lastName; - if (email !== undefined) input.email = email; - - const data = await client.query(` - mutation($input: CustomerPatchInput!) { - customerPatch(input: $input) { - didSucceed - inputErrors { code message path } - customer { ${CUSTOMER_FIELDS} } - } - } - `, { input }); - - const result = data.customerPatch; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.path?.join(".") ?? "field"}: ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to update customer:\n${errs}` }] }; - } - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result.customer, null, 2); - } else { - text = `Customer updated successfully!\n\n${formatCustomerMarkdown(result.customer!).join("\n")}`; - } - - return { content: [{ type: "text", text }], structuredContent: result.customer }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_delete_customer ────────────────────────────────────────────────── - - server.registerTool( - "wave_delete_customer", - { - title: "Delete Wave Customer", - description: `Permanently delete a customer from a Wave business. - -⚠️ This action is irreversible. Customers with existing invoices may not be deletable. - -Args: - - businessId (string): Wave business ID - - customerId (string): Customer ID to delete - -Returns: - Success confirmation or error details.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - customerId: z.string().describe("Customer ID to delete"), - }), - annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }, - }, - async ({ businessId, customerId }) => { - try { - const data = await client.query(` - mutation($input: CustomerDeleteInput!) { - customerDelete(input: $input) { - didSucceed - inputErrors { code message path } - } - } - `, { input: { businessId, customerId } }); - - const result = data.customerDelete; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to delete customer:\n${errs}` }] }; - } - - return { content: [{ type: "text", text: `Customer ${customerId} deleted successfully.` }] }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); -}