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)}` }] }; } } ); }