diff --git a/mcp-gateway/wave-mcp/src/tools/products.ts b/mcp-gateway/wave-mcp/src/tools/products.ts deleted file mode 100644 index 0d9c7db..0000000 --- a/mcp-gateway/wave-mcp/src/tools/products.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { z } from "zod"; -import { WaveClient } from "../services/wave-client.js"; -import { WaveProduct } from "../types.js"; -import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from "../constants.js"; - -interface ProductsData { - business: { - products: { - pageInfo: { currentPage: number; totalPages: number; totalCount: number }; - edges: Array<{ node: WaveProduct }>; - }; - }; -} - -interface ProductCreateData { - productCreate: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - product?: WaveProduct; - }; -} - -interface ProductPatchData { - productPatch: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - product?: WaveProduct; - }; -} - -interface ProductArchiveData { - productArchive: { - didSucceed: boolean; - inputErrors: Array<{ code: string; message: string; path: string[] }>; - product?: WaveProduct; - }; -} - -const PRODUCT_FIELDS = ` - id name description unitPrice isSold isBought isArchived - currency { code } - incomeAccount { id name } - expenseAccount { id name } - createdAt modifiedAt -`; - -function formatProductMarkdown(p: WaveProduct): string[] { - const lines = [`## ${p.name} (${p.id})`]; - if (p.description) lines.push(` ${p.description}`); - if (p.unitPrice !== undefined && p.unitPrice !== null) { - lines.push(`- **Unit Price**: ${WaveClient.formatCurrency(p.unitPrice, p.currency?.code ?? "")}`); - } - const flags = [p.isSold && "sold", p.isBought && "bought", p.isArchived && "archived"] - .filter(Boolean) - .join(", "); - if (flags) lines.push(`- **Flags**: ${flags}`); - if (p.incomeAccount) lines.push(`- **Income Account**: ${p.incomeAccount.name}`); - if (p.expenseAccount) lines.push(`- **Expense Account**: ${p.expenseAccount.name}`); - lines.push(""); - return lines; -} - -export function registerProductTools(server: McpServer, client: WaveClient): void { - // ── wave_list_products ──────────────────────────────────────────────────── - - server.registerTool( - "wave_list_products", - { - title: "List Wave Products", - description: `List products/services for a Wave business. - -Args: - - businessId (string): Wave business ID - - page (number): Page number (default: 1) - - pageSize (number): Items per page, 1-100 (default: 20) - - includeArchived (boolean): Include archived products (default: false) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Paginated list of products with pricing, accounts, and status.`, - 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"), - includeArchived: z.boolean().default(false).describe("Include archived products"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, page, pageSize, response_format }) => { - try { - const data = await client.query(` - query($businessId: ID!, $page: Int!, $pageSize: Int!) { - business(id: $businessId) { - products(page: $page, pageSize: $pageSize) { - pageInfo { currentPage totalPages totalCount } - edges { node { ${PRODUCT_FIELDS} } } - } - } - } - `, { businessId, page, pageSize }); - - const { pageInfo, edges } = data.business.products; - const products = edges.map((e) => e.node); - - if (!products.length) { - return { content: [{ type: "text", text: "No products found." }] }; - } - - const result = { - total: pageInfo.totalCount, - count: products.length, - page: pageInfo.currentPage, - totalPages: pageInfo.totalPages, - has_more: pageInfo.currentPage < pageInfo.totalPages, - next_page: pageInfo.currentPage < pageInfo.totalPages ? pageInfo.currentPage + 1 : undefined, - products, - }; - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result, null, 2); - } else { - const lines = [`# Products (${pageInfo.totalCount} total, page ${pageInfo.currentPage}/${pageInfo.totalPages})`, ""]; - for (const p of products) lines.push(...formatProductMarkdown(p)); - 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_create_product ─────────────────────────────────────────────────── - - server.registerTool( - "wave_create_product", - { - title: "Create Wave Product", - description: `Create a new product or service in a Wave business. - -Args: - - businessId (string): Wave business ID - - name (string): Product name (required) - - description (string): Product description (optional) - - unitPrice (number): Unit price amount (optional) - - isSold (boolean): Whether this product is sold to customers (default: true) - - isBought (boolean): Whether this product is purchased from suppliers (default: false) - - incomeAccountId (string): Account ID for income (optional) - - expenseAccountId (string): Account ID for expenses (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Created product details or validation errors.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - name: z.string().min(1).describe("Product name"), - description: z.string().optional().describe("Product description"), - unitPrice: z.number().min(0).optional().describe("Unit price"), - isSold: z.boolean().default(true).describe("Whether sold to customers"), - isBought: z.boolean().default(false).describe("Whether bought from suppliers"), - incomeAccountId: z.string().optional().describe("Income account ID"), - expenseAccountId: z.string().optional().describe("Expense account ID"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, - }, - async ({ businessId, name, description, unitPrice, isSold, isBought, incomeAccountId, expenseAccountId, response_format }) => { - try { - const input: Record = { businessId, name, isSold, isBought }; - if (description) input.description = description; - if (unitPrice !== undefined) input.unitPrice = unitPrice; - if (incomeAccountId) input.incomeAccountId = incomeAccountId; - if (expenseAccountId) input.expenseAccountId = expenseAccountId; - - const data = await client.query(` - mutation($input: ProductCreateInput!) { - productCreate(input: $input) { - didSucceed - inputErrors { code message path } - product { ${PRODUCT_FIELDS} } - } - } - `, { input }); - - const result = data.productCreate; - 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 product:\n${errs}` }] }; - } - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result.product, null, 2); - } else { - text = `Product created successfully!\n\n${formatProductMarkdown(result.product!).join("\n")}`; - } - - return { content: [{ type: "text", text }], structuredContent: result.product }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_update_product ─────────────────────────────────────────────────── - - server.registerTool( - "wave_update_product", - { - title: "Update Wave Product", - description: `Update an existing product's details. Only provide fields you want to change. - -Args: - - businessId (string): Wave business ID - - productId (string): Product ID to update - - name (string): New name (optional) - - description (string): New description (optional) - - unitPrice (number): New unit price (optional) - - response_format ('markdown' | 'json'): Output format (default: 'markdown') - -Returns: - Updated product details or validation errors.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - productId: z.string().describe("Product ID to update"), - name: z.string().min(1).optional().describe("New name"), - description: z.string().optional().describe("New description"), - unitPrice: z.number().min(0).optional().describe("New unit price"), - response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, productId, name, description, unitPrice, response_format }) => { - try { - const input: Record = { businessId, productId }; - if (name) input.name = name; - if (description !== undefined) input.description = description; - if (unitPrice !== undefined) input.unitPrice = unitPrice; - - const data = await client.query(` - mutation($input: ProductPatchInput!) { - productPatch(input: $input) { - didSucceed - inputErrors { code message path } - product { ${PRODUCT_FIELDS} } - } - } - `, { input }); - - const result = data.productPatch; - 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 product:\n${errs}` }] }; - } - - let text: string; - if (response_format === "json") { - text = JSON.stringify(result.product, null, 2); - } else { - text = `Product updated successfully!\n\n${formatProductMarkdown(result.product!).join("\n")}`; - } - - return { content: [{ type: "text", text }], structuredContent: result.product }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); - - // ── wave_archive_product ────────────────────────────────────────────────── - - server.registerTool( - "wave_archive_product", - { - title: "Archive Wave Product", - description: `Archive a product so it no longer appears in active lists. This is reversible via the Wave UI. - -Args: - - businessId (string): Wave business ID - - productId (string): Product ID to archive - -Returns: - Confirmation or error details.`, - inputSchema: z.object({ - businessId: z.string().describe("Wave business ID"), - productId: z.string().describe("Product ID to archive"), - }), - annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, - }, - async ({ businessId, productId }) => { - try { - const data = await client.query(` - mutation($input: ProductArchiveInput!) { - productArchive(input: $input) { - didSucceed - inputErrors { code message path } - product { id name isArchived } - } - } - `, { input: { businessId, productId } }); - - const result = data.productArchive; - if (!result.didSucceed) { - const errs = result.inputErrors.map((e) => ` - ${e.message}`).join("\n"); - return { content: [{ type: "text", text: `Failed to archive product:\n${errs}` }] }; - } - - return { content: [{ type: "text", text: `Product "${result.product?.name ?? productId}" archived successfully.` }] }; - } catch (error) { - return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] }; - } - } - ); -}