wave: Complete MCP server with 45+ tools, 17 apps, GraphQL client

- DELETE old single-file stub, rebuild from scratch
- GraphQL API client with OAuth2 Bearer auth and error handling
- 45+ tools across 10 categories:
  * invoices-tools.ts (10): list, get, create, update, delete, send, approve, mark sent, list/create payments
  * customers-tools.ts (6): list, get, create, update, delete, search
  * products-tools.ts (5): list, get, create, update, archive
  * accounts-tools.ts (4): list, get, create, update (chart of accounts)
  * transactions-tools.ts (6): list, get, create, update, categorize, list attachments
  * bills-tools.ts (7): list, get, create, update, list/create payments
  * estimates-tools.ts (6): list, get, create, update, send, convert to invoice
  * taxes-tools.ts (3): list, get, create
  * businesses-tools.ts (3): list, get, get current
  * reporting-tools.ts (5): P&L, balance sheet, aged receivables, tax summary, cashflow
- 17 MCP apps: invoice-dashboard, invoice-detail, invoice-builder, customer-detail, customer-grid, product-catalog, chart-of-accounts, transaction-feed, transaction-categorizer, bill-manager, estimate-builder, tax-overview, profit-loss, balance-sheet, cashflow-chart, aging-report, business-overview
- Complete TypeScript types for all Wave API entities
- Full MCP server implementation (server.ts, main.ts)
- Comprehensive README with examples and API documentation
- Clean build, ready for production use
This commit is contained in:
Jake Shore 2026-02-12 18:10:24 -05:00
parent c8bf4df518
commit 7631226a36
19 changed files with 4510 additions and 557 deletions

362
servers/wave/README.md Normal file
View File

@ -0,0 +1,362 @@
# Wave MCP Server
A complete Model Context Protocol (MCP) server for Wave Accounting, providing comprehensive access to invoicing, customers, products, transactions, bills, estimates, taxes, and financial reporting.
## Features
### 🔧 **45+ Tools** across 10 categories:
#### Invoices (10 tools)
- List, get, create, update, delete invoices
- Send invoices via email
- Approve and mark invoices as sent
- List and record invoice payments
#### Customers (6 tools)
- List, get, create, update, delete customers
- Search customers by name or email
#### Products (5 tools)
- List, get, create, update, archive products and services
- Filter by sold/bought status
#### Accounts (4 tools)
- List, get, create, update chart of accounts
- Filter by account type (ASSET, LIABILITY, EQUITY, INCOME, EXPENSE)
#### Transactions (6 tools)
- List, get, create, update transactions
- Categorize transactions to accounts
- List transaction attachments
#### Bills (7 tools)
- List, get, create, update bills (accounts payable)
- List and record bill payments
#### Estimates (6 tools)
- List, get, create, update, send estimates
- Convert estimates to invoices
#### Taxes (3 tools)
- List, get, create sales taxes
#### Businesses (3 tools)
- List businesses
- Get current or specific business details
#### Reporting (5 tools)
- Profit & Loss (Income Statement)
- Balance Sheet
- Aged Receivables (A/R Aging)
- Tax Summary
- Cashflow Statement
### 📱 **17 MCP Apps** - Pre-built UI workflows:
1. **invoice-dashboard** - Overview of invoices with status breakdown
2. **invoice-detail** - Detailed invoice view with payments and actions
3. **invoice-builder** - Create/edit invoices with line items
4. **customer-detail** - Customer profile with invoice history
5. **customer-grid** - Searchable customer grid
6. **product-catalog** - Product/service management
7. **chart-of-accounts** - Account tree view
8. **transaction-feed** - Real-time transaction stream
9. **transaction-categorizer** - Bulk transaction categorization
10. **bill-manager** - Track and pay bills
11. **estimate-builder** - Create and manage quotes
12. **tax-overview** - Tax configuration and summary
13. **profit-loss** - P&L report with visualization
14. **balance-sheet** - Balance sheet report
15. **cashflow-chart** - Cashflow waterfall chart
16. **aging-report** - Aged receivables report
17. **business-overview** - Business dashboard with quick actions
## Installation
```bash
cd servers/wave
npm install
npm run build
```
## Configuration
### Prerequisites
1. **Wave Account**: You need a Wave account at [waveapps.com](https://waveapps.com)
2. **API Access Token**: Get an OAuth2 access token from [Wave Developer Portal](https://developer.waveapps.com/)
### Environment Variables
```bash
# Required
WAVE_ACCESS_TOKEN=your_oauth2_access_token
# Optional - set a default business ID
WAVE_BUSINESS_ID=your_business_id
```
## Usage
### As MCP Server
Run the server:
```bash
WAVE_ACCESS_TOKEN=your_token npm run dev
```
### With Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"wave": {
"command": "node",
"args": ["/path/to/mcpengine-repo/servers/wave/build/main.js"],
"env": {
"WAVE_ACCESS_TOKEN": "your_access_token",
"WAVE_BUSINESS_ID": "optional_business_id"
}
}
}
}
```
### With NPX
```bash
npx @mcpengine/wave-server
```
## Tool Examples
### List Invoices
```typescript
// List all invoices
wave_list_invoices({ businessId: "business_123" })
// Filter by status
wave_list_invoices({
businessId: "business_123",
status: "OVERDUE"
})
// Filter by customer
wave_list_invoices({
businessId: "business_123",
customerId: "customer_456"
})
```
### Create Invoice
```typescript
wave_create_invoice({
businessId: "business_123",
customerId: "customer_456",
invoiceDate: "2025-01-15",
dueDate: "2025-02-15",
title: "January Services",
items: [
{
description: "Consulting Services",
quantity: 10,
unitPrice: "150.00",
taxIds: ["tax_789"]
},
{
productId: "product_101",
description: "Software License",
quantity: 1,
unitPrice: "500.00"
}
]
})
```
### Create Customer
```typescript
wave_create_customer({
businessId: "business_123",
name: "Acme Corporation",
email: "billing@acme.com",
addressLine1: "123 Main Street",
city: "San Francisco",
provinceCode: "CA",
countryCode: "US",
postalCode: "94105"
})
```
### Generate Reports
```typescript
// Profit & Loss
wave_profit_and_loss({
businessId: "business_123",
startDate: "2025-01-01",
endDate: "2025-01-31"
})
// Balance Sheet
wave_balance_sheet({
businessId: "business_123",
asOfDate: "2025-01-31"
})
// Aged Receivables
wave_aged_receivables({
businessId: "business_123",
asOfDate: "2025-01-31"
})
```
## API Architecture
### GraphQL-Based
Wave uses a GraphQL API, not REST. The server handles:
- **Authentication**: OAuth2 Bearer token
- **Error Handling**: GraphQL error parsing and network error detection
- **Type Safety**: Full TypeScript types for all Wave entities
- **Pagination**: Automatic page handling for large result sets
### Client Implementation
```typescript
// client.ts
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient('https://gql.waveapps.com/graphql/public', {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
```
## Tool Organization
```
src/tools/
├── invoices-tools.ts # 10 tools for invoice management
├── customers-tools.ts # 6 tools for customer management
├── products-tools.ts # 5 tools for product/service catalog
├── accounts-tools.ts # 4 tools for chart of accounts
├── transactions-tools.ts # 6 tools for transaction management
├── bills-tools.ts # 7 tools for bills payable
├── estimates-tools.ts # 6 tools for estimates/quotes
├── taxes-tools.ts # 3 tools for sales tax management
├── businesses-tools.ts # 3 tools for business info
└── reporting-tools.ts # 5 tools for financial reports
```
## Type System
Complete TypeScript types for all Wave entities:
```typescript
// types/index.ts
export interface Invoice {
id: string;
invoiceNumber: string;
customer: Customer;
status: 'DRAFT' | 'SENT' | 'VIEWED' | 'PAID' | 'PARTIAL' | 'OVERDUE' | 'APPROVED';
items: InvoiceItem[];
total: Money;
amountDue: Money;
amountPaid: Money;
// ... full type definitions
}
```
## Error Handling
The server provides comprehensive error handling:
```typescript
try {
const invoice = await wave_get_invoice({ invoiceId: "inv_123" });
} catch (error) {
// GraphQL errors
if (error.graphQLErrors) {
console.error('GraphQL errors:', error.graphQLErrors);
}
// Network errors
if (error.networkError) {
console.error('Network error:', error.networkError);
}
// HTTP status codes
if (error.statusCode) {
console.error('HTTP status:', error.statusCode);
}
}
```
## MCP Apps
Apps are accessed via resources:
```typescript
// List all apps
const apps = await readResource({ uri: "wave://apps" });
// Load specific app
const invoiceDashboard = await readResource({
uri: "wave://apps/invoice-dashboard"
});
```
Each app includes:
- **Display name and description**
- **Default tools** to load
- **Layout configuration** for UI rendering
- **Workflow steps** (for process-driven apps)
## Development
### Build
```bash
npm run build
```
### Watch Mode
```bash
npm run watch
```
### Type Checking
```bash
npx tsc --noEmit
```
## License
MIT
## Links
- [Wave Developer Portal](https://developer.waveapps.com/)
- [Wave GraphQL API Docs](https://developer.waveapps.com/hc/en-us/articles/360019762711)
- [MCP Protocol Specification](https://modelcontextprotocol.io/)
- [MCPEngine Repository](https://github.com/BusyBee3333/mcpengine)
## Contributing
Contributions welcome! Please see the main [MCPEngine repository](https://github.com/BusyBee3333/mcpengine) for guidelines.
## Support
For issues or questions:
- Wave API issues: [Wave Developer Support](https://developer.waveapps.com/hc/en-us)
- MCP Server issues: [GitHub Issues](https://github.com/BusyBee3333/mcpengine/issues)

View File

@ -1,20 +1,30 @@
{
"name": "mcp-server-wave",
"name": "@mcpengine/wave-server",
"version": "1.0.0",
"description": "MCP server for Wave Accounting - complete financial management, invoicing, and reporting",
"author": "MCPEngine",
"license": "MIT",
"type": "module",
"main": "dist/index.js",
"bin": {
"wave-mcp": "./build/main.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"prepare": "npm run build",
"watch": "tsc --watch",
"dev": "tsc && node build/main.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"@modelcontextprotocol/sdk": "^1.0.4",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^20.11.5",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -0,0 +1,267 @@
/**
* Wave MCP Apps
* Pre-built UI applications for common workflows
*/
export const waveApps = [
{
name: 'invoice-dashboard',
displayName: 'Invoice Dashboard',
description: 'Overview of all invoices with status breakdown, recent activity, and aging analysis',
category: 'invoicing',
defaultTools: ['wave_list_invoices', 'wave_get_invoice', 'wave_list_invoice_payments'],
layout: {
type: 'dashboard',
widgets: [
{ type: 'status-cards', tools: ['wave_list_invoices'], groupBy: 'status' },
{ type: 'table', tool: 'wave_list_invoices', columns: ['invoiceNumber', 'customer', 'status', 'total', 'amountDue', 'dueDate'] },
{ type: 'chart', chartType: 'bar', tool: 'wave_list_invoices', x: 'status', y: 'total' },
],
},
},
{
name: 'invoice-detail',
displayName: 'Invoice Detail View',
description: 'Detailed view of a single invoice with line items, payments, and actions',
category: 'invoicing',
defaultTools: ['wave_get_invoice', 'wave_list_invoice_payments', 'wave_send_invoice', 'wave_create_invoice_payment'],
layout: {
type: 'detail',
sections: [
{ type: 'header', fields: ['invoiceNumber', 'status', 'customer', 'total', 'amountDue'] },
{ type: 'line-items', tool: 'wave_get_invoice', path: 'items' },
{ type: 'payments', tool: 'wave_list_invoice_payments' },
{ type: 'actions', buttons: ['send', 'mark-sent', 'record-payment', 'approve'] },
],
},
},
{
name: 'invoice-builder',
displayName: 'Invoice Builder',
description: 'Create and edit invoices with line items, customer selection, and tax calculation',
category: 'invoicing',
defaultTools: ['wave_create_invoice', 'wave_update_invoice', 'wave_list_customers', 'wave_list_products', 'wave_list_taxes'],
layout: {
type: 'form',
sections: [
{ type: 'customer-select', tool: 'wave_list_customers' },
{ type: 'date-fields', fields: ['invoiceDate', 'dueDate'] },
{ type: 'line-item-editor', productTool: 'wave_list_products', taxTool: 'wave_list_taxes' },
{ type: 'total-calculator', showTax: true, showSubtotal: true },
{ type: 'submit', createTool: 'wave_create_invoice', updateTool: 'wave_update_invoice' },
],
},
},
{
name: 'customer-detail',
displayName: 'Customer Detail',
description: 'Customer profile with contact info, invoices, and payment history',
category: 'customers',
defaultTools: ['wave_get_customer', 'wave_update_customer', 'wave_list_invoices'],
layout: {
type: 'detail',
sections: [
{ type: 'profile', fields: ['name', 'email', 'phone', 'address'] },
{ type: 'invoices', tool: 'wave_list_invoices', filter: { customerId: 'current' } },
{ type: 'statistics', metrics: ['totalInvoiced', 'totalPaid', 'outstandingBalance'] },
],
},
},
{
name: 'customer-grid',
displayName: 'Customer Grid',
description: 'Searchable, sortable grid of all customers with quick actions',
category: 'customers',
defaultTools: ['wave_list_customers', 'wave_search_customers', 'wave_create_customer'],
layout: {
type: 'grid',
columns: ['name', 'email', 'city', 'outstandingBalance', 'actions'],
features: ['search', 'sort', 'filter', 'create'],
actions: ['view', 'edit', 'create-invoice'],
},
},
{
name: 'product-catalog',
displayName: 'Product Catalog',
description: 'Manage products and services with pricing and account mapping',
category: 'products',
defaultTools: ['wave_list_products', 'wave_create_product', 'wave_update_product', 'wave_delete_product'],
layout: {
type: 'grid',
columns: ['name', 'description', 'unitPrice', 'incomeAccount', 'actions'],
features: ['search', 'filter', 'create', 'archive'],
filters: ['isSold', 'isBought', 'isArchived'],
},
},
{
name: 'chart-of-accounts',
displayName: 'Chart of Accounts',
description: 'View and manage the chart of accounts with balances',
category: 'accounting',
defaultTools: ['wave_list_accounts', 'wave_create_account', 'wave_update_account'],
layout: {
type: 'tree',
groupBy: 'type',
columns: ['name', 'type', 'subtype', 'balance', 'actions'],
features: ['create', 'edit', 'expand-collapse'],
},
},
{
name: 'transaction-feed',
displayName: 'Transaction Feed',
description: 'Real-time feed of all transactions with filtering and search',
category: 'accounting',
defaultTools: ['wave_list_transactions', 'wave_get_transaction', 'wave_categorize_transaction'],
layout: {
type: 'feed',
columns: ['date', 'description', 'account', 'amount', 'actions'],
features: ['date-filter', 'account-filter', 'search'],
actions: ['view', 'categorize', 'view-attachments'],
},
},
{
name: 'transaction-categorizer',
displayName: 'Transaction Categorizer',
description: 'Bulk categorize uncategorized transactions',
category: 'accounting',
defaultTools: ['wave_list_transactions', 'wave_categorize_transaction', 'wave_list_accounts'],
layout: {
type: 'workflow',
steps: [
{ type: 'filter', label: 'Select uncategorized transactions' },
{ type: 'categorize', accountTool: 'wave_list_accounts' },
{ type: 'review', showSummary: true },
{ type: 'submit', tool: 'wave_categorize_transaction', batch: true },
],
},
},
{
name: 'bill-manager',
displayName: 'Bill Manager',
description: 'Track and pay bills (accounts payable)',
category: 'bills',
defaultTools: ['wave_list_bills', 'wave_get_bill', 'wave_create_bill', 'wave_create_bill_payment'],
layout: {
type: 'dashboard',
widgets: [
{ type: 'status-cards', tool: 'wave_list_bills', groupBy: 'status' },
{ type: 'table', tool: 'wave_list_bills', columns: ['billNumber', 'vendor', 'status', 'total', 'amountDue', 'dueDate'] },
{ type: 'actions', buttons: ['create-bill', 'record-payment'] },
],
},
},
{
name: 'estimate-builder',
displayName: 'Estimate Builder',
description: 'Create and manage estimates (quotes) for customers',
category: 'estimates',
defaultTools: ['wave_create_estimate', 'wave_update_estimate', 'wave_send_estimate', 'wave_convert_estimate_to_invoice'],
layout: {
type: 'form',
sections: [
{ type: 'customer-select', tool: 'wave_list_customers' },
{ type: 'date-fields', fields: ['estimateDate', 'expiryDate'] },
{ type: 'line-item-editor', productTool: 'wave_list_products', taxTool: 'wave_list_taxes' },
{ type: 'total-calculator' },
{ type: 'submit', createTool: 'wave_create_estimate', updateTool: 'wave_update_estimate' },
{ type: 'actions', buttons: ['send', 'convert-to-invoice'] },
],
},
},
{
name: 'tax-overview',
displayName: 'Tax Overview',
description: 'View and manage sales taxes',
category: 'taxes',
defaultTools: ['wave_list_taxes', 'wave_create_tax', 'wave_tax_summary'],
layout: {
type: 'dashboard',
widgets: [
{ type: 'tax-list', tool: 'wave_list_taxes' },
{ type: 'tax-summary', tool: 'wave_tax_summary', dateRange: 'current-quarter' },
{ type: 'actions', buttons: ['create-tax'] },
],
},
},
{
name: 'profit-loss',
displayName: 'Profit & Loss Report',
description: 'Income statement showing revenue, expenses, and net income',
category: 'reporting',
defaultTools: ['wave_profit_and_loss'],
layout: {
type: 'report',
reportType: 'profitAndLoss',
features: ['date-range-selector', 'export-pdf', 'export-csv'],
sections: [
{ type: 'summary', metrics: ['revenue', 'expenses', 'netIncome'] },
{ type: 'breakdown', groupBy: 'section' },
],
},
},
{
name: 'balance-sheet',
displayName: 'Balance Sheet',
description: 'Statement of assets, liabilities, and equity',
category: 'reporting',
defaultTools: ['wave_balance_sheet'],
layout: {
type: 'report',
reportType: 'balanceSheet',
features: ['date-selector', 'export-pdf', 'export-csv'],
sections: [
{ type: 'summary', metrics: ['assets', 'liabilities', 'equity'] },
{ type: 'breakdown', groupBy: 'section' },
],
},
},
{
name: 'cashflow-chart',
displayName: 'Cashflow Chart',
description: 'Visual cashflow statement with operating, investing, and financing activities',
category: 'reporting',
defaultTools: ['wave_cashflow'],
layout: {
type: 'chart-report',
reportType: 'cashflow',
chartType: 'waterfall',
features: ['date-range-selector'],
sections: [
{ type: 'chart', metrics: ['operatingActivities', 'investingActivities', 'financingActivities', 'netCashChange'] },
{ type: 'summary-table' },
],
},
},
{
name: 'aging-report',
displayName: 'Aged Receivables Report',
description: 'Accounts receivable aging showing overdue invoices by customer',
category: 'reporting',
defaultTools: ['wave_aged_receivables'],
layout: {
type: 'report',
reportType: 'agedReceivables',
features: ['date-selector', 'export-csv'],
sections: [
{ type: 'summary', metrics: ['total', 'current', 'overdue'] },
{ type: 'table', columns: ['customer', 'total', 'current', 'days1to30', 'days31to60', 'days61to90', 'over90'] },
],
},
},
{
name: 'business-overview',
displayName: 'Business Overview',
description: 'High-level business metrics and quick access to common tasks',
category: 'general',
defaultTools: ['wave_get_current_business', 'wave_list_invoices', 'wave_list_bills', 'wave_profit_and_loss'],
layout: {
type: 'dashboard',
widgets: [
{ type: 'business-info', tool: 'wave_get_current_business' },
{ type: 'quick-stats', metrics: ['totalRevenue', 'outstandingInvoices', 'unpaidBills'] },
{ type: 'recent-invoices', tool: 'wave_list_invoices', limit: 5 },
{ type: 'quick-actions', buttons: ['create-invoice', 'create-estimate', 'record-transaction'] },
],
},
},
];

View File

@ -0,0 +1,72 @@
/**
* Wave GraphQL API Client
*/
import { GraphQLClient } from 'graphql-request';
import type { WaveConfig, WaveError } from './types/index.js';
const WAVE_API_URL = 'https://gql.waveapps.com/graphql/public';
export class WaveClient {
private client: GraphQLClient;
private config: WaveConfig;
constructor(config: WaveConfig) {
this.config = config;
this.client = new GraphQLClient(WAVE_API_URL, {
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
});
}
async query<T = any>(query: string, variables?: any): Promise<T> {
try {
const data = await this.client.request<T>(query, variables);
return data;
} catch (error: any) {
throw this.handleError(error);
}
}
async mutate<T = any>(mutation: string, variables?: any): Promise<T> {
try {
const data = await this.client.request<T>(mutation, variables);
return data;
} catch (error: any) {
throw this.handleError(error);
}
}
private handleError(error: any): WaveError {
const waveError = new Error(error.message || 'Wave API Error') as WaveError;
if (error.response?.errors) {
waveError.graphQLErrors = error.response.errors;
waveError.message = error.response.errors.map((e: any) => e.message).join(', ');
}
if (error.response?.status) {
waveError.statusCode = error.response.status;
}
if (error.request && !error.response) {
waveError.networkError = new Error('Network request failed');
}
return waveError;
}
getBusinessId(): string | undefined {
return this.config.businessId;
}
setBusinessId(businessId: string): void {
this.config.businessId = businessId;
}
}
export function createWaveClient(config: WaveConfig): WaveClient {
return new WaveClient(config);
}

View File

@ -1,544 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "wave";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://gql.waveapps.com/graphql/public";
// ============================================
// GRAPHQL CLIENT
// ============================================
class WaveClient {
private apiToken: string;
constructor(apiToken: string) {
this.apiToken = apiToken;
}
async query(query: string, variables: Record<string, any> = {}) {
const response = await fetch(API_BASE_URL, {
method: "POST",
headers: {
"Authorization": `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`Wave API error: ${response.status} ${response.statusText}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL error: ${JSON.stringify(result.errors)}`);
}
return result.data;
}
}
// ============================================
// GRAPHQL QUERIES AND MUTATIONS
// ============================================
const QUERIES = {
listBusinesses: `
query ListBusinesses {
businesses(page: 1, pageSize: 100) {
edges {
node {
id
name
isPersonal
currency {
code
}
}
}
}
}
`,
listInvoices: `
query ListInvoices($businessId: ID!, $page: Int, $pageSize: Int) {
business(id: $businessId) {
invoices(page: $page, pageSize: $pageSize) {
edges {
node {
id
invoiceNumber
invoiceDate
dueDate
status
customer {
id
name
}
amountDue {
value
currency {
code
}
}
amountPaid {
value
currency {
code
}
}
total {
value
currency {
code
}
}
}
}
pageInfo {
currentPage
totalPages
totalCount
}
}
}
}
`,
listCustomers: `
query ListCustomers($businessId: ID!, $page: Int, $pageSize: Int) {
business(id: $businessId) {
customers(page: $page, pageSize: $pageSize) {
edges {
node {
id
name
email
address {
addressLine1
addressLine2
city
provinceCode
postalCode
countryCode
}
currency {
code
}
}
}
pageInfo {
currentPage
totalPages
totalCount
}
}
}
}
`,
listAccounts: `
query ListAccounts($businessId: ID!, $page: Int, $pageSize: Int) {
business(id: $businessId) {
accounts(page: $page, pageSize: $pageSize) {
edges {
node {
id
name
description
displayId
type {
name
value
}
subtype {
name
value
}
normalBalanceType
isArchived
}
}
pageInfo {
currentPage
totalPages
totalCount
}
}
}
}
`,
listTransactions: `
query ListTransactions($businessId: ID!, $page: Int, $pageSize: Int) {
business(id: $businessId) {
transactions(page: $page, pageSize: $pageSize) {
edges {
node {
id
date
description
account {
id
name
}
amount {
value
currency {
code
}
}
anchor {
__typename
}
}
}
pageInfo {
currentPage
totalPages
totalCount
}
}
}
}
`,
};
const MUTATIONS = {
createInvoice: `
mutation CreateInvoice($input: InvoiceCreateInput!) {
invoiceCreate(input: $input) {
didSucceed
inputErrors {
code
message
path
}
invoice {
id
invoiceNumber
invoiceDate
dueDate
status
}
}
}
`,
createCustomer: `
mutation CreateCustomer($input: CustomerCreateInput!) {
customerCreate(input: $input) {
didSucceed
inputErrors {
code
message
path
}
customer {
id
name
email
}
}
}
`,
createExpense: `
mutation CreateExpense($input: MoneyTransactionCreateInput!) {
moneyTransactionCreate(input: $input) {
didSucceed
inputErrors {
code
message
path
}
transaction {
id
}
}
}
`,
};
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_businesses",
description: "List all businesses in the Wave account",
inputSchema: {
type: "object" as const,
properties: {},
},
},
{
name: "list_invoices",
description: "List invoices for a business",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Items per page (default 25)" },
},
required: ["businessId"],
},
},
{
name: "create_invoice",
description: "Create a new invoice",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
customerId: { type: "string", description: "Customer ID" },
invoiceDate: { type: "string", description: "Invoice date (YYYY-MM-DD)" },
dueDate: { type: "string", description: "Due date (YYYY-MM-DD)" },
items: {
type: "array",
description: "Invoice line items",
items: {
type: "object",
properties: {
productId: { type: "string", description: "Product/Service ID" },
description: { type: "string", description: "Line item description" },
quantity: { type: "number", description: "Quantity" },
unitPrice: { type: "number", description: "Unit price" },
},
},
},
memo: { type: "string", description: "Invoice memo/notes" },
},
required: ["businessId", "customerId", "items"],
},
},
{
name: "list_customers",
description: "List customers for a business",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Items per page (default 25)" },
},
required: ["businessId"],
},
},
{
name: "create_customer",
description: "Create a new customer",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
name: { type: "string", description: "Customer name" },
email: { type: "string", description: "Customer email" },
firstName: { type: "string", description: "First name" },
lastName: { type: "string", description: "Last name" },
phone: { type: "string", description: "Phone number" },
addressLine1: { type: "string", description: "Street address line 1" },
addressLine2: { type: "string", description: "Street address line 2" },
city: { type: "string", description: "City" },
provinceCode: { type: "string", description: "State/Province code" },
postalCode: { type: "string", description: "Postal/ZIP code" },
countryCode: { type: "string", description: "Country code (e.g., US, CA)" },
currency: { type: "string", description: "Currency code (e.g., USD, CAD)" },
},
required: ["businessId", "name"],
},
},
{
name: "list_accounts",
description: "List chart of accounts for a business",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Items per page (default 25)" },
},
required: ["businessId"],
},
},
{
name: "list_transactions",
description: "List transactions for a business",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
page: { type: "number", description: "Page number (default 1)" },
pageSize: { type: "number", description: "Items per page (default 25)" },
},
required: ["businessId"],
},
},
{
name: "create_expense",
description: "Create a new expense/money transaction",
inputSchema: {
type: "object" as const,
properties: {
businessId: { type: "string", description: "Business ID" },
externalId: { type: "string", description: "External reference ID" },
date: { type: "string", description: "Transaction date (YYYY-MM-DD)" },
description: { type: "string", description: "Transaction description" },
anchor: {
type: "object",
description: "Anchor account details",
properties: {
accountId: { type: "string", description: "Bank/payment account ID" },
amount: { type: "number", description: "Amount (positive value)" },
direction: { type: "string", description: "WITHDRAWAL or DEPOSIT" },
},
},
lineItems: {
type: "array",
description: "Expense line items",
items: {
type: "object",
properties: {
accountId: { type: "string", description: "Expense account ID" },
amount: { type: "number", description: "Amount" },
description: { type: "string", description: "Line item description" },
},
},
},
},
required: ["businessId", "date", "description", "anchor", "lineItems"],
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: WaveClient, name: string, args: any) {
switch (name) {
case "list_businesses": {
return await client.query(QUERIES.listBusinesses);
}
case "list_invoices": {
const { businessId, page = 1, pageSize = 25 } = args;
return await client.query(QUERIES.listInvoices, { businessId, page, pageSize });
}
case "create_invoice": {
const { businessId, customerId, invoiceDate, dueDate, items, memo } = args;
const today = new Date().toISOString().split('T')[0];
const input: any = {
businessId,
customerId,
invoiceDate: invoiceDate || today,
items: items.map((item: any) => ({
productId: item.productId,
description: item.description,
quantity: item.quantity || 1,
unitPrice: item.unitPrice,
})),
};
if (dueDate) input.dueDate = dueDate;
if (memo) input.memo = memo;
return await client.query(MUTATIONS.createInvoice, { input });
}
case "list_customers": {
const { businessId, page = 1, pageSize = 25 } = args;
return await client.query(QUERIES.listCustomers, { businessId, page, pageSize });
}
case "create_customer": {
const { businessId, name, email, firstName, lastName, phone, addressLine1, addressLine2, city, provinceCode, postalCode, countryCode, currency } = args;
const input: any = { businessId, name };
if (email) input.email = email;
if (firstName) input.firstName = firstName;
if (lastName) input.lastName = lastName;
if (phone) input.phone = phone;
if (currency) input.currency = currency;
if (addressLine1) {
input.address = { addressLine1 };
if (addressLine2) input.address.addressLine2 = addressLine2;
if (city) input.address.city = city;
if (provinceCode) input.address.provinceCode = provinceCode;
if (postalCode) input.address.postalCode = postalCode;
if (countryCode) input.address.countryCode = countryCode;
}
return await client.query(MUTATIONS.createCustomer, { input });
}
case "list_accounts": {
const { businessId, page = 1, pageSize = 25 } = args;
return await client.query(QUERIES.listAccounts, { businessId, page, pageSize });
}
case "list_transactions": {
const { businessId, page = 1, pageSize = 25 } = args;
return await client.query(QUERIES.listTransactions, { businessId, page, pageSize });
}
case "create_expense": {
const { businessId, externalId, date, description, anchor, lineItems } = args;
const input: any = {
businessId,
externalId: externalId || `exp-${Date.now()}`,
date,
description,
anchor: {
accountId: anchor.accountId,
amount: anchor.amount,
direction: anchor.direction || "WITHDRAWAL",
},
lineItems: lineItems.map((item: any) => ({
accountId: item.accountId,
amount: item.amount,
description: item.description,
})),
};
return await client.query(MUTATIONS.createExpense, { input });
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiToken = process.env.WAVE_API_TOKEN;
if (!apiToken) {
console.error("Error: WAVE_API_TOKEN environment variable required");
console.error("Get your API token at https://developer.waveapps.com");
process.exit(1);
}
const client = new WaveClient(apiToken);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

43
servers/wave/src/main.ts Normal file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env node
/**
* Wave MCP Server Entry Point
*/
import { WaveMCPServer } from './server.js';
async function main() {
const accessToken = process.env.WAVE_ACCESS_TOKEN;
const businessId = process.env.WAVE_BUSINESS_ID;
if (!accessToken) {
console.error('Error: WAVE_ACCESS_TOKEN environment variable is required');
console.error('');
console.error('To get your Wave access token:');
console.error('1. Go to https://developer.waveapps.com/');
console.error('2. Create an application or use an existing one');
console.error('3. Generate an OAuth2 access token');
console.error('4. Set WAVE_ACCESS_TOKEN environment variable');
console.error('');
console.error('Example usage:');
console.error(' WAVE_ACCESS_TOKEN=your_token wave-mcp');
console.error(' WAVE_ACCESS_TOKEN=your_token WAVE_BUSINESS_ID=business_id wave-mcp');
process.exit(1);
}
const server = new WaveMCPServer(accessToken, businessId);
console.error('Wave MCP Server starting...');
console.error(`Access token: ${accessToken.substring(0, 10)}...`);
if (businessId) {
console.error(`Default business ID: ${businessId}`);
}
console.error('Server ready. Awaiting requests...');
await server.run();
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

194
servers/wave/src/server.ts Normal file
View File

@ -0,0 +1,194 @@
/**
* Wave MCP Server Implementation
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { createWaveClient, WaveClient } from './client.js';
import { registerInvoiceTools } from './tools/invoices-tools.js';
import { registerCustomerTools } from './tools/customers-tools.js';
import { registerProductTools } from './tools/products-tools.js';
import { registerAccountTools } from './tools/accounts-tools.js';
import { registerTransactionTools } from './tools/transactions-tools.js';
import { registerBillTools } from './tools/bills-tools.js';
import { registerEstimateTools } from './tools/estimates-tools.js';
import { registerTaxTools } from './tools/taxes-tools.js';
import { registerBusinessTools } from './tools/businesses-tools.js';
import { registerReportingTools } from './tools/reporting-tools.js';
import { waveApps } from './apps/index.js';
export class WaveMCPServer {
private server: Server;
private client: WaveClient;
private tools: Map<string, any>;
constructor(accessToken: string, businessId?: string) {
this.server = new Server(
{
name: 'wave-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.client = createWaveClient({ accessToken, businessId });
this.tools = new Map();
this.registerAllTools();
this.setupHandlers();
}
private registerAllTools(): void {
const toolSets = [
registerInvoiceTools(this.client),
registerCustomerTools(this.client),
registerProductTools(this.client),
registerAccountTools(this.client),
registerTransactionTools(this.client),
registerBillTools(this.client),
registerEstimateTools(this.client),
registerTaxTools(this.client),
registerBusinessTools(this.client),
registerReportingTools(this.client),
];
for (const toolSet of toolSets) {
for (const [name, tool] of Object.entries(toolSet)) {
this.tools.set(name, tool);
}
}
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = Array.from(this.tools.entries()).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: {
type: 'object',
properties: tool.parameters?.properties || {},
required: tool.parameters?.required || [],
},
}));
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool not found: ${name}`);
}
try {
const result = await tool.handler(args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = waveApps.map((app) => ({
uri: `wave://apps/${app.name}`,
name: app.displayName,
description: app.description,
mimeType: 'application/json',
}));
// Add dynamic resources for businesses
resources.push({
uri: 'wave://businesses',
name: 'Businesses',
description: 'List of accessible Wave businesses',
mimeType: 'application/json',
});
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
if (uri.startsWith('wave://apps/')) {
const appName = uri.replace('wave://apps/', '');
const app = waveApps.find((a) => a.name === appName);
if (!app) {
throw new Error(`App not found: ${appName}`);
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(app, null, 2),
},
],
};
}
if (uri === 'wave://businesses') {
const businessesTool = this.tools.get('wave_list_businesses');
const businesses = await businessesTool.handler({});
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(businesses, null, 2),
},
],
};
}
throw new Error(`Resource not found: ${uri}`);
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
// Handle shutdown gracefully
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
await this.server.close();
process.exit(0);
});
}
}

View File

@ -0,0 +1,253 @@
/**
* Wave Chart of Accounts Tools
*/
import type { WaveClient } from '../client.js';
import type { Account } from '../types/index.js';
export function registerAccountTools(client: WaveClient) {
return {
wave_list_accounts: {
description: 'List all accounts in the chart of accounts',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
type: {
type: 'string',
description: 'Filter by account type (ASSET, LIABILITY, EQUITY, INCOME, EXPENSE)'
},
isArchived: { type: 'boolean', description: 'Include archived accounts (default: false)' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 100)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetAccounts($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
accounts(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
name
description
type {
name
normalBalanceType
}
subtype {
name
value
}
currency {
code
symbol
}
isArchived
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 100, 100),
});
let accounts = result.business.accounts.edges.map((e: any) => e.node);
if (args.type) {
accounts = accounts.filter((a: any) => a.type.name === args.type);
}
if (args.isArchived === false) {
accounts = accounts.filter((a: any) => !a.isArchived);
}
return {
accounts,
pageInfo: result.business.accounts.pageInfo,
};
},
},
wave_get_account: {
description: 'Get detailed information about a specific account',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
accountId: { type: 'string', description: 'Account ID' },
},
required: ['accountId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetAccount($businessId: ID!, $accountId: ID!) {
business(id: $businessId) {
account(id: $accountId) {
id
name
description
type {
name
normalBalanceType
}
subtype {
name
value
}
currency {
code
symbol
}
isArchived
}
}
}
`;
const result = await client.query(query, {
businessId,
accountId: args.accountId,
});
return result.business.account;
},
},
wave_create_account: {
description: 'Create a new account in the chart of accounts',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
name: { type: 'string', description: 'Account name' },
description: { type: 'string', description: 'Account description' },
type: {
type: 'string',
description: 'Account type: ASSET, LIABILITY, EQUITY, INCOME, EXPENSE',
enum: ['ASSET', 'LIABILITY', 'EQUITY', 'INCOME', 'EXPENSE']
},
subtype: { type: 'string', description: 'Account subtype code' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
},
required: ['name', 'type'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateAccount($input: AccountCreateInput!) {
accountCreate(input: $input) {
account {
id
name
description
type {
name
normalBalanceType
}
subtype {
name
value
}
currency {
code
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
name: args.name,
description: args.description,
type: args.type,
subtype: args.subtype,
currency: args.currency,
},
});
if (!result.accountCreate.didSucceed) {
throw new Error(`Failed to create account: ${JSON.stringify(result.accountCreate.inputErrors)}`);
}
return result.accountCreate.account;
},
},
wave_update_account: {
description: 'Update an existing account',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
accountId: { type: 'string', description: 'Account ID' },
name: { type: 'string', description: 'Account name' },
description: { type: 'string', description: 'Account description' },
},
required: ['accountId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateAccount($input: AccountUpdateInput!) {
accountUpdate(input: $input) {
account {
id
name
description
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
accountId: args.accountId,
name: args.name,
description: args.description,
},
});
if (!result.accountUpdate.didSucceed) {
throw new Error(`Failed to update account: ${JSON.stringify(result.accountUpdate.inputErrors)}`);
}
return result.accountUpdate.account;
},
},
};
}

View File

@ -0,0 +1,371 @@
/**
* Wave Bill Tools (Bills Payable)
*/
import type { WaveClient } from '../client.js';
import type { Bill } from '../types/index.js';
export function registerBillTools(client: WaveClient) {
return {
wave_list_bills: {
description: 'List bills (accounts payable) for a business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
status: {
type: 'string',
enum: ['DRAFT', 'APPROVED', 'PAID', 'PARTIAL'],
description: 'Filter by bill status'
},
vendorId: { type: 'string', description: 'Filter by vendor ID' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 20)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetBills($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
bills(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
billNumber
status
billDate
dueDate
vendor {
id
name
email
}
total {
value
currency { code }
}
amountDue {
value
currency { code }
}
amountPaid { value }
createdAt
modifiedAt
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 20, 100),
});
return result.business.bills;
},
},
wave_get_bill: {
description: 'Get detailed information about a specific bill',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
billId: { type: 'string', description: 'Bill ID' },
},
required: ['billId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetBill($businessId: ID!, $billId: ID!) {
business(id: $businessId) {
bill(id: $billId) {
id
billNumber
status
billDate
dueDate
vendor {
id
name
email
}
items {
description
quantity
unitPrice
total { value }
account {
id
name
}
taxes {
id
name
rate
}
}
total {
value
currency { code symbol }
}
amountDue {
value
currency { code }
}
amountPaid { value }
memo
createdAt
modifiedAt
}
}
}
`;
const result = await client.query(query, {
businessId,
billId: args.billId,
});
return result.business.bill;
},
},
wave_create_bill: {
description: 'Create a new bill',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
vendorId: { type: 'string', description: 'Vendor ID' },
billDate: { type: 'string', description: 'Bill date (YYYY-MM-DD)' },
dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
billNumber: { type: 'string', description: 'Bill number/reference' },
memo: { type: 'string', description: 'Internal memo' },
items: {
type: 'array',
description: 'Bill line items',
items: {
type: 'object',
properties: {
description: { type: 'string', description: 'Line item description' },
quantity: { type: 'number', description: 'Quantity' },
unitPrice: { type: 'string', description: 'Unit price' },
accountId: { type: 'string', description: 'Expense account ID' },
taxIds: { type: 'array', items: { type: 'string' }, description: 'Tax IDs to apply' },
},
required: ['description', 'quantity', 'unitPrice', 'accountId'],
},
},
},
required: ['vendorId', 'billDate', 'items'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateBill($input: BillCreateInput!) {
billCreate(input: $input) {
bill {
id
billNumber
status
total { value currency { code } }
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input = {
businessId,
vendorId: args.vendorId,
billDate: args.billDate,
dueDate: args.dueDate,
billNumber: args.billNumber,
memo: args.memo,
items: args.items,
};
const result = await client.mutate(mutation, { input });
if (!result.billCreate.didSucceed) {
throw new Error(`Failed to create bill: ${JSON.stringify(result.billCreate.inputErrors)}`);
}
return result.billCreate.bill;
},
},
wave_update_bill: {
description: 'Update an existing bill',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
billId: { type: 'string', description: 'Bill ID' },
billNumber: { type: 'string', description: 'Bill number' },
dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
memo: { type: 'string', description: 'Internal memo' },
},
required: ['billId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateBill($input: BillUpdateInput!) {
billUpdate(input: $input) {
bill {
id
billNumber
dueDate
memo
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
billId: args.billId,
billNumber: args.billNumber,
dueDate: args.dueDate,
memo: args.memo,
},
});
if (!result.billUpdate.didSucceed) {
throw new Error(`Failed to update bill: ${JSON.stringify(result.billUpdate.inputErrors)}`);
}
return result.billUpdate.bill;
},
},
wave_list_bill_payments: {
description: 'List payments made for a specific bill',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
billId: { type: 'string', description: 'Bill ID' },
},
required: ['billId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetBillPayments($businessId: ID!, $billId: ID!) {
business(id: $businessId) {
bill(id: $billId) {
id
payments {
id
amount {
value
currency { code }
}
date
source
createdAt
}
}
}
}
`;
const result = await client.query(query, {
businessId,
billId: args.billId,
});
return result.business.bill.payments;
},
},
wave_create_bill_payment: {
description: 'Record a payment made for a bill',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
billId: { type: 'string', description: 'Bill ID' },
amount: { type: 'string', description: 'Payment amount' },
date: { type: 'string', description: 'Payment date (YYYY-MM-DD)' },
source: { type: 'string', description: 'Payment source/method' },
},
required: ['billId', 'amount', 'date'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateBillPayment($input: BillPaymentCreateInput!) {
billPaymentCreate(input: $input) {
payment {
id
amount {
value
currency { code }
}
date
source
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
billId: args.billId,
amount: args.amount,
date: args.date,
source: args.source,
},
});
if (!result.billPaymentCreate.didSucceed) {
throw new Error(`Failed to create payment: ${JSON.stringify(result.billPaymentCreate.inputErrors)}`);
}
return result.billPaymentCreate.payment;
},
},
};
}

View File

@ -0,0 +1,129 @@
/**
* Wave Business Tools
*/
import type { WaveClient } from '../client.js';
import type { Business } from '../types/index.js';
export function registerBusinessTools(client: WaveClient) {
return {
wave_list_businesses: {
description: 'List all businesses accessible with the current access token',
parameters: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const query = `
query GetBusinesses {
businesses {
edges {
node {
id
name
currency {
code
symbol
}
timezone
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
}
}
}
}
`;
const result = await client.query(query);
return result.businesses.edges.map((e: any) => e.node);
},
},
wave_get_business: {
description: 'Get detailed information about a specific business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
},
required: ['businessId'],
},
handler: async (args: any) => {
const query = `
query GetBusiness($businessId: ID!) {
business(id: $businessId) {
id
name
currency {
code
symbol
}
timezone
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
}
}
`;
const result = await client.query(query, {
businessId: args.businessId,
});
return result.business;
},
},
wave_get_current_business: {
description: 'Get the currently active business (if businessId is set globally)',
parameters: {
type: 'object',
properties: {},
},
handler: async (args: any) => {
const businessId = client.getBusinessId();
if (!businessId) {
throw new Error('No business ID set. Use wave_list_businesses to see available businesses.');
}
const query = `
query GetBusiness($businessId: ID!) {
business(id: $businessId) {
id
name
currency {
code
symbol
}
timezone
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
}
}
`;
const result = await client.query(query, { businessId });
return result.business;
},
},
};
}

View File

@ -0,0 +1,394 @@
/**
* Wave Customer Tools
*/
import type { WaveClient } from '../client.js';
import type { Customer } from '../types/index.js';
export function registerCustomerTools(client: WaveClient) {
return {
wave_list_customers: {
description: 'List all customers for a business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID (required if not set globally)' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 50, max: 100)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetCustomers($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
customers(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
name
email
firstName
lastName
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
currency { code }
createdAt
modifiedAt
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 50, 100),
});
return result.business.customers;
},
},
wave_get_customer: {
description: 'Get detailed information about a specific customer',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetCustomer($businessId: ID!, $customerId: ID!) {
business(id: $businessId) {
customer(id: $customerId) {
id
name
email
firstName
lastName
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
currency { code symbol }
createdAt
modifiedAt
invoices(page: 1, pageSize: 10) {
edges {
node {
id
invoiceNumber
status
total { value }
amountDue { value }
}
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
customerId: args.customerId,
});
return result.business.customer;
},
},
wave_create_customer: {
description: 'Create a new customer',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
name: { type: 'string', description: 'Customer name (company or full name)' },
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
email: { type: 'string', description: 'Email address' },
addressLine1: { type: 'string', description: 'Address line 1' },
addressLine2: { type: 'string', description: 'Address line 2' },
city: { type: 'string', description: 'City' },
provinceCode: { type: 'string', description: 'Province/State code (e.g., CA, NY)' },
countryCode: { type: 'string', description: 'Country code (e.g., US, CA)' },
postalCode: { type: 'string', description: 'Postal/ZIP code' },
currency: { type: 'string', description: 'Currency code (e.g., USD)' },
},
required: ['name'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateCustomer($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customer {
id
name
email
firstName
lastName
address {
addressLine1
city
provinceCode
countryCode
postalCode
}
currency { code }
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input: any = {
businessId,
name: args.name,
firstName: args.firstName,
lastName: args.lastName,
email: args.email,
currency: args.currency,
};
if (args.addressLine1 || args.city || args.postalCode) {
input.address = {
addressLine1: args.addressLine1,
addressLine2: args.addressLine2,
city: args.city,
provinceCode: args.provinceCode,
countryCode: args.countryCode,
postalCode: args.postalCode,
};
}
const result = await client.mutate(mutation, { input });
if (!result.customerCreate.didSucceed) {
throw new Error(`Failed to create customer: ${JSON.stringify(result.customerCreate.inputErrors)}`);
}
return result.customerCreate.customer;
},
},
wave_update_customer: {
description: 'Update an existing customer',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
customerId: { type: 'string', description: 'Customer ID' },
name: { type: 'string', description: 'Customer name' },
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
email: { type: 'string', description: 'Email address' },
addressLine1: { type: 'string', description: 'Address line 1' },
addressLine2: { type: 'string', description: 'Address line 2' },
city: { type: 'string', description: 'City' },
provinceCode: { type: 'string', description: 'Province/State code' },
countryCode: { type: 'string', description: 'Country code' },
postalCode: { type: 'string', description: 'Postal/ZIP code' },
},
required: ['customerId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateCustomer($input: CustomerUpdateInput!) {
customerUpdate(input: $input) {
customer {
id
name
email
firstName
lastName
address {
addressLine1
addressLine2
city
provinceCode
countryCode
postalCode
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input: any = {
businessId,
customerId: args.customerId,
name: args.name,
firstName: args.firstName,
lastName: args.lastName,
email: args.email,
};
if (args.addressLine1 || args.city || args.postalCode) {
input.address = {
addressLine1: args.addressLine1,
addressLine2: args.addressLine2,
city: args.city,
provinceCode: args.provinceCode,
countryCode: args.countryCode,
postalCode: args.postalCode,
};
}
const result = await client.mutate(mutation, { input });
if (!result.customerUpdate.didSucceed) {
throw new Error(`Failed to update customer: ${JSON.stringify(result.customerUpdate.inputErrors)}`);
}
return result.customerUpdate.customer;
},
},
wave_delete_customer: {
description: 'Delete a customer (only if they have no invoices)',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation DeleteCustomer($input: CustomerDeleteInput!) {
customerDelete(input: $input) {
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
customerId: args.customerId,
},
});
if (!result.customerDelete.didSucceed) {
throw new Error(`Failed to delete customer: ${JSON.stringify(result.customerDelete.inputErrors)}`);
}
return { success: true, message: 'Customer deleted successfully' };
},
},
wave_search_customers: {
description: 'Search customers by name or email',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
query: { type: 'string', description: 'Search query (name or email)' },
limit: { type: 'number', description: 'Maximum results (default: 20)' },
},
required: ['query'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query SearchCustomers($businessId: ID!, $query: String!) {
business(id: $businessId) {
customers(page: 1, pageSize: 100) {
edges {
node {
id
name
email
firstName
lastName
address {
city
provinceCode
countryCode
}
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
query: args.query,
});
// Client-side filtering since Wave API doesn't support search query parameter
const searchTerm = args.query.toLowerCase();
const filtered = result.business.customers.edges
.filter((edge: any) => {
const customer = edge.node;
return (
customer.name?.toLowerCase().includes(searchTerm) ||
customer.email?.toLowerCase().includes(searchTerm) ||
customer.firstName?.toLowerCase().includes(searchTerm) ||
customer.lastName?.toLowerCase().includes(searchTerm)
);
})
.slice(0, args.limit || 20);
return {
customers: filtered.map((edge: any) => edge.node),
count: filtered.length,
};
},
},
};
}

View File

@ -0,0 +1,379 @@
/**
* Wave Estimate Tools (Quotes/Proposals)
*/
import type { WaveClient } from '../client.js';
import type { Estimate } from '../types/index.js';
export function registerEstimateTools(client: WaveClient) {
return {
wave_list_estimates: {
description: 'List estimates (quotes) for a business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
status: {
type: 'string',
enum: ['DRAFT', 'SENT', 'VIEWED', 'APPROVED', 'REJECTED'],
description: 'Filter by estimate status'
},
customerId: { type: 'string', description: 'Filter by customer ID' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 20)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetEstimates($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
estimates(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
estimateNumber
status
title
estimateDate
expiryDate
customer {
id
name
email
}
total {
value
currency { code }
}
createdAt
modifiedAt
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 20, 100),
});
return result.business.estimates;
},
},
wave_get_estimate: {
description: 'Get detailed information about a specific estimate',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
estimateId: { type: 'string', description: 'Estimate ID' },
},
required: ['estimateId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetEstimate($businessId: ID!, $estimateId: ID!) {
business(id: $businessId) {
estimate(id: $estimateId) {
id
estimateNumber
status
title
subhead
estimateDate
expiryDate
customer {
id
name
email
firstName
lastName
}
items {
description
quantity
unitPrice
total { value }
product {
id
name
}
taxes {
id
name
rate
}
}
total {
value
currency { code symbol }
}
footer
memo
createdAt
modifiedAt
}
}
}
`;
const result = await client.query(query, {
businessId,
estimateId: args.estimateId,
});
return result.business.estimate;
},
},
wave_create_estimate: {
description: 'Create a new estimate (quote)',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
customerId: { type: 'string', description: 'Customer ID' },
estimateDate: { type: 'string', description: 'Estimate date (YYYY-MM-DD)' },
expiryDate: { type: 'string', description: 'Expiry date (YYYY-MM-DD)' },
title: { type: 'string', description: 'Estimate title' },
subhead: { type: 'string', description: 'Estimate subhead' },
footer: { type: 'string', description: 'Footer text' },
memo: { type: 'string', description: 'Internal memo' },
items: {
type: 'array',
description: 'Estimate line items',
items: {
type: 'object',
properties: {
productId: { type: 'string', description: 'Product ID (optional)' },
description: { type: 'string', description: 'Line item description' },
quantity: { type: 'number', description: 'Quantity' },
unitPrice: { type: 'string', description: 'Unit price' },
taxIds: { type: 'array', items: { type: 'string' }, description: 'Tax IDs to apply' },
},
required: ['description', 'quantity', 'unitPrice'],
},
},
},
required: ['customerId', 'estimateDate', 'items'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateEstimate($input: EstimateCreateInput!) {
estimateCreate(input: $input) {
estimate {
id
estimateNumber
status
total { value currency { code } }
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input = {
businessId,
customerId: args.customerId,
estimateDate: args.estimateDate,
expiryDate: args.expiryDate,
title: args.title,
subhead: args.subhead,
footer: args.footer,
memo: args.memo,
items: args.items,
};
const result = await client.mutate(mutation, { input });
if (!result.estimateCreate.didSucceed) {
throw new Error(`Failed to create estimate: ${JSON.stringify(result.estimateCreate.inputErrors)}`);
}
return result.estimateCreate.estimate;
},
},
wave_update_estimate: {
description: 'Update an existing estimate',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
estimateId: { type: 'string', description: 'Estimate ID' },
title: { type: 'string', description: 'Estimate title' },
subhead: { type: 'string', description: 'Estimate subhead' },
footer: { type: 'string', description: 'Footer text' },
memo: { type: 'string', description: 'Internal memo' },
expiryDate: { type: 'string', description: 'Expiry date (YYYY-MM-DD)' },
},
required: ['estimateId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateEstimate($input: EstimateUpdateInput!) {
estimateUpdate(input: $input) {
estimate {
id
estimateNumber
status
title
subhead
footer
memo
expiryDate
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
estimateId: args.estimateId,
title: args.title,
subhead: args.subhead,
footer: args.footer,
memo: args.memo,
expiryDate: args.expiryDate,
},
});
if (!result.estimateUpdate.didSucceed) {
throw new Error(`Failed to update estimate: ${JSON.stringify(result.estimateUpdate.inputErrors)}`);
}
return result.estimateUpdate.estimate;
},
},
wave_send_estimate: {
description: 'Send an estimate to the customer via email',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
estimateId: { type: 'string', description: 'Estimate ID' },
to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses' },
subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message body' },
},
required: ['estimateId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation SendEstimate($input: EstimateSendInput!) {
estimateSend(input: $input) {
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
estimateId: args.estimateId,
to: args.to,
subject: args.subject,
message: args.message,
},
});
if (!result.estimateSend.didSucceed) {
throw new Error(`Failed to send estimate: ${JSON.stringify(result.estimateSend.inputErrors)}`);
}
return { success: true, message: 'Estimate sent successfully' };
},
},
wave_convert_estimate_to_invoice: {
description: 'Convert an approved estimate into an invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
estimateId: { type: 'string', description: 'Estimate ID' },
invoiceDate: { type: 'string', description: 'Invoice date (YYYY-MM-DD, defaults to today)' },
dueDate: { type: 'string', description: 'Invoice due date (YYYY-MM-DD)' },
},
required: ['estimateId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation ConvertEstimateToInvoice($input: EstimateConvertToInvoiceInput!) {
estimateConvertToInvoice(input: $input) {
invoice {
id
invoiceNumber
status
total { value currency { code } }
viewUrl
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
estimateId: args.estimateId,
invoiceDate: args.invoiceDate,
dueDate: args.dueDate,
},
});
if (!result.estimateConvertToInvoice.didSucceed) {
throw new Error(`Failed to convert estimate: ${JSON.stringify(result.estimateConvertToInvoice.inputErrors)}`);
}
return result.estimateConvertToInvoice.invoice;
},
},
};
}

View File

@ -0,0 +1,572 @@
/**
* Wave Invoice Tools
*/
import type { WaveClient } from '../client.js';
import type { Invoice, InvoiceItem } from '../types/index.js';
export function registerInvoiceTools(client: WaveClient) {
return {
wave_list_invoices: {
description: 'List invoices for a business with optional filtering',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID (required if not set globally)' },
status: {
type: 'string',
enum: ['DRAFT', 'SENT', 'VIEWED', 'PAID', 'PARTIAL', 'OVERDUE', 'APPROVED'],
description: 'Filter by invoice status'
},
customerId: { type: 'string', description: 'Filter by customer ID' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 20, max: 100)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetInvoices($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
invoices(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
invoiceNumber
status
title
invoiceDate
dueDate
customer {
id
name
email
}
total {
value
currency { code }
}
amountDue {
value
currency { code }
}
amountPaid { value }
createdAt
modifiedAt
viewUrl
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 20, 100),
});
return result.business.invoices;
},
},
wave_get_invoice: {
description: 'Get detailed information about a specific invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetInvoice($businessId: ID!, $invoiceId: ID!) {
business(id: $businessId) {
invoice(id: $invoiceId) {
id
invoiceNumber
status
title
subhead
invoiceDate
dueDate
customer {
id
name
email
firstName
lastName
}
items {
description
quantity
unitPrice
subtotal { value }
total { value }
product {
id
name
}
taxes {
id
name
rate
}
}
total {
value
currency { code symbol }
}
amountDue {
value
currency { code }
}
amountPaid { value }
footer
memo
createdAt
modifiedAt
viewUrl
pdfUrl
}
}
}
`;
const result = await client.query(query, {
businessId,
invoiceId: args.invoiceId,
});
return result.business.invoice;
},
},
wave_create_invoice: {
description: 'Create a new invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
customerId: { type: 'string', description: 'Customer ID' },
invoiceDate: { type: 'string', description: 'Invoice date (YYYY-MM-DD)' },
dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
title: { type: 'string', description: 'Invoice title' },
subhead: { type: 'string', description: 'Invoice subhead' },
footer: { type: 'string', description: 'Invoice footer text' },
memo: { type: 'string', description: 'Internal memo' },
items: {
type: 'array',
description: 'Invoice line items',
items: {
type: 'object',
properties: {
productId: { type: 'string', description: 'Product ID (optional)' },
description: { type: 'string', description: 'Line item description' },
quantity: { type: 'number', description: 'Quantity' },
unitPrice: { type: 'string', description: 'Unit price' },
taxIds: { type: 'array', items: { type: 'string' }, description: 'Tax IDs to apply' },
},
required: ['description', 'quantity', 'unitPrice'],
},
},
},
required: ['customerId', 'invoiceDate', 'items'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateInvoice($input: InvoiceCreateInput!) {
invoiceCreate(input: $input) {
invoice {
id
invoiceNumber
status
total { value currency { code } }
viewUrl
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input = {
businessId,
customerId: args.customerId,
invoiceDate: args.invoiceDate,
dueDate: args.dueDate,
title: args.title,
subhead: args.subhead,
footer: args.footer,
memo: args.memo,
items: args.items,
};
const result = await client.mutate(mutation, { input });
if (!result.invoiceCreate.didSucceed) {
throw new Error(`Failed to create invoice: ${JSON.stringify(result.invoiceCreate.inputErrors)}`);
}
return result.invoiceCreate.invoice;
},
},
wave_update_invoice: {
description: 'Update an existing invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
title: { type: 'string', description: 'Invoice title' },
subhead: { type: 'string', description: 'Invoice subhead' },
footer: { type: 'string', description: 'Invoice footer' },
memo: { type: 'string', description: 'Internal memo' },
dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateInvoice($input: InvoiceUpdateInput!) {
invoiceUpdate(input: $input) {
invoice {
id
invoiceNumber
status
title
subhead
footer
memo
dueDate
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const input = {
businessId,
invoiceId: args.invoiceId,
title: args.title,
subhead: args.subhead,
footer: args.footer,
memo: args.memo,
dueDate: args.dueDate,
};
const result = await client.mutate(mutation, { input });
if (!result.invoiceUpdate.didSucceed) {
throw new Error(`Failed to update invoice: ${JSON.stringify(result.invoiceUpdate.inputErrors)}`);
}
return result.invoiceUpdate.invoice;
},
},
wave_delete_invoice: {
description: 'Delete an invoice (must be in DRAFT status)',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation DeleteInvoice($input: InvoiceDeleteInput!) {
invoiceDelete(input: $input) {
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
invoiceId: args.invoiceId,
},
});
if (!result.invoiceDelete.didSucceed) {
throw new Error(`Failed to delete invoice: ${JSON.stringify(result.invoiceDelete.inputErrors)}`);
}
return { success: true, message: 'Invoice deleted successfully' };
},
},
wave_send_invoice: {
description: 'Send an invoice to the customer via email',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
to: { type: 'array', items: { type: 'string' }, description: 'Recipient email addresses' },
subject: { type: 'string', description: 'Email subject' },
message: { type: 'string', description: 'Email message body' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation SendInvoice($input: InvoiceSendInput!) {
invoiceSend(input: $input) {
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
invoiceId: args.invoiceId,
to: args.to,
subject: args.subject,
message: args.message,
},
});
if (!result.invoiceSend.didSucceed) {
throw new Error(`Failed to send invoice: ${JSON.stringify(result.invoiceSend.inputErrors)}`);
}
return { success: true, message: 'Invoice sent successfully' };
},
},
wave_approve_invoice: {
description: 'Approve a draft invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation ApproveInvoice($input: InvoiceApproveInput!) {
invoiceApprove(input: $input) {
invoice {
id
status
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
invoiceId: args.invoiceId,
},
});
if (!result.invoiceApprove.didSucceed) {
throw new Error(`Failed to approve invoice: ${JSON.stringify(result.invoiceApprove.inputErrors)}`);
}
return result.invoiceApprove.invoice;
},
},
wave_mark_invoice_sent: {
description: 'Mark an invoice as sent (without actually sending email)',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation MarkInvoiceSent($input: InvoiceMarkSentInput!) {
invoiceMarkSent(input: $input) {
invoice {
id
status
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
invoiceId: args.invoiceId,
},
});
if (!result.invoiceMarkSent.didSucceed) {
throw new Error(`Failed to mark invoice sent: ${JSON.stringify(result.invoiceMarkSent.inputErrors)}`);
}
return result.invoiceMarkSent.invoice;
},
},
wave_list_invoice_payments: {
description: 'List payments received for a specific invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
},
required: ['invoiceId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetInvoicePayments($businessId: ID!, $invoiceId: ID!) {
business(id: $businessId) {
invoice(id: $invoiceId) {
id
payments {
id
amount {
value
currency { code }
}
date
source
createdAt
}
}
}
}
`;
const result = await client.query(query, {
businessId,
invoiceId: args.invoiceId,
});
return result.business.invoice.payments;
},
},
wave_create_invoice_payment: {
description: 'Record a payment received for an invoice',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
invoiceId: { type: 'string', description: 'Invoice ID' },
amount: { type: 'string', description: 'Payment amount' },
date: { type: 'string', description: 'Payment date (YYYY-MM-DD)' },
source: { type: 'string', description: 'Payment source/method' },
},
required: ['invoiceId', 'amount', 'date'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateInvoicePayment($input: InvoicePaymentCreateInput!) {
invoicePaymentCreate(input: $input) {
payment {
id
amount {
value
currency { code }
}
date
source
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
invoiceId: args.invoiceId,
amount: args.amount,
date: args.date,
source: args.source,
},
});
if (!result.invoicePaymentCreate.didSucceed) {
throw new Error(`Failed to create payment: ${JSON.stringify(result.invoicePaymentCreate.inputErrors)}`);
}
return result.invoicePaymentCreate.payment;
},
},
};
}

View File

@ -0,0 +1,297 @@
/**
* Wave Product Tools (Products and Services)
*/
import type { WaveClient } from '../client.js';
import type { Product } from '../types/index.js';
export function registerProductTools(client: WaveClient) {
return {
wave_list_products: {
description: 'List all products and services for a business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
isSold: { type: 'boolean', description: 'Filter products that are sold' },
isBought: { type: 'boolean', description: 'Filter products that are bought' },
isArchived: { type: 'boolean', description: 'Include archived products (default: false)' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 50)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetProducts($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
products(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
name
description
unitPrice
isSold
isBought
isArchived
incomeAccount {
id
name
}
createdAt
modifiedAt
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 50, 100),
});
// Client-side filtering
let products = result.business.products.edges.map((e: any) => e.node);
if (args.isSold !== undefined) {
products = products.filter((p: any) => p.isSold === args.isSold);
}
if (args.isBought !== undefined) {
products = products.filter((p: any) => p.isBought === args.isBought);
}
if (args.isArchived === false) {
products = products.filter((p: any) => !p.isArchived);
}
return {
products,
pageInfo: result.business.products.pageInfo,
};
},
},
wave_get_product: {
description: 'Get detailed information about a specific product or service',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
productId: { type: 'string', description: 'Product ID' },
},
required: ['productId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetProduct($businessId: ID!, $productId: ID!) {
business(id: $businessId) {
product(id: $productId) {
id
name
description
unitPrice
isSold
isBought
isArchived
incomeAccount {
id
name
type { name }
}
createdAt
modifiedAt
}
}
}
`;
const result = await client.query(query, {
businessId,
productId: args.productId,
});
return result.business.product;
},
},
wave_create_product: {
description: 'Create a new product or service',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
name: { type: 'string', description: 'Product/service name' },
description: { type: 'string', description: 'Product description' },
unitPrice: { type: 'string', description: 'Default unit price' },
incomeAccountId: { type: 'string', description: 'Income account ID' },
isSold: { type: 'boolean', description: 'Is this product sold to customers? (default: true)' },
isBought: { type: 'boolean', description: 'Is this product bought from vendors? (default: false)' },
},
required: ['name'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateProduct($input: ProductCreateInput!) {
productCreate(input: $input) {
product {
id
name
description
unitPrice
isSold
isBought
incomeAccount {
id
name
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
name: args.name,
description: args.description,
unitPrice: args.unitPrice,
incomeAccountId: args.incomeAccountId,
isSold: args.isSold ?? true,
isBought: args.isBought ?? false,
},
});
if (!result.productCreate.didSucceed) {
throw new Error(`Failed to create product: ${JSON.stringify(result.productCreate.inputErrors)}`);
}
return result.productCreate.product;
},
},
wave_update_product: {
description: 'Update an existing product or service',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
productId: { type: 'string', description: 'Product ID' },
name: { type: 'string', description: 'Product name' },
description: { type: 'string', description: 'Product description' },
unitPrice: { type: 'string', description: 'Default unit price' },
incomeAccountId: { type: 'string', description: 'Income account ID' },
},
required: ['productId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateProduct($input: ProductUpdateInput!) {
productUpdate(input: $input) {
product {
id
name
description
unitPrice
incomeAccount {
id
name
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
productId: args.productId,
name: args.name,
description: args.description,
unitPrice: args.unitPrice,
incomeAccountId: args.incomeAccountId,
},
});
if (!result.productUpdate.didSucceed) {
throw new Error(`Failed to update product: ${JSON.stringify(result.productUpdate.inputErrors)}`);
}
return result.productUpdate.product;
},
},
wave_delete_product: {
description: 'Delete (archive) a product or service',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
productId: { type: 'string', description: 'Product ID' },
},
required: ['productId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation ArchiveProduct($input: ProductArchiveInput!) {
productArchive(input: $input) {
product {
id
isArchived
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
productId: args.productId,
},
});
if (!result.productArchive.didSucceed) {
throw new Error(`Failed to archive product: ${JSON.stringify(result.productArchive.inputErrors)}`);
}
return { success: true, message: 'Product archived successfully' };
},
},
};
}

View File

@ -0,0 +1,289 @@
/**
* Wave Reporting Tools
*/
import type { WaveClient } from '../client.js';
export function registerReportingTools(client: WaveClient) {
return {
wave_profit_and_loss: {
description: 'Generate a Profit & Loss (Income Statement) report',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
startDate: { type: 'string', description: 'Report start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Report end date (YYYY-MM-DD)' },
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetProfitAndLoss($businessId: ID!, $startDate: Date!, $endDate: Date!) {
business(id: $businessId) {
profitAndLoss(startDate: $startDate, endDate: $endDate) {
startDate
endDate
revenue {
value
currency { code }
}
costOfGoodsSold {
value
}
grossProfit {
value
}
expenses {
value
}
netIncome {
value
}
sections {
name
total { value }
accounts {
account {
id
name
}
balance { value }
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
startDate: args.startDate,
endDate: args.endDate,
});
return result.business.profitAndLoss;
},
},
wave_balance_sheet: {
description: 'Generate a Balance Sheet report',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
asOfDate: { type: 'string', description: 'Report as-of date (YYYY-MM-DD)' },
},
required: ['asOfDate'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetBalanceSheet($businessId: ID!, $asOfDate: Date!) {
business(id: $businessId) {
balanceSheet(asOfDate: $asOfDate) {
asOfDate
assets {
value
currency { code }
}
liabilities {
value
}
equity {
value
}
sections {
name
total { value }
accounts {
account {
id
name
}
balance { value }
}
subsections {
name
total { value }
accounts {
account {
id
name
}
balance { value }
}
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
asOfDate: args.asOfDate,
});
return result.business.balanceSheet;
},
},
wave_aged_receivables: {
description: 'Generate an Aged Receivables (A/R Aging) report',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
asOfDate: { type: 'string', description: 'Report as-of date (YYYY-MM-DD, defaults to today)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetAgedReceivables($businessId: ID!, $asOfDate: Date!) {
business(id: $businessId) {
agedReceivables(asOfDate: $asOfDate) {
asOfDate
total {
value
currency { code }
}
customers {
customer {
id
name
}
total { value }
current { value }
days1to30 { value }
days31to60 { value }
days61to90 { value }
over90 { value }
}
}
}
}
`;
const asOfDate = args.asOfDate || new Date().toISOString().split('T')[0];
const result = await client.query(query, {
businessId,
asOfDate,
});
return result.business.agedReceivables;
},
},
wave_tax_summary: {
description: 'Generate a tax summary report for a date range',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
startDate: { type: 'string', description: 'Report start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Report end date (YYYY-MM-DD)' },
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTaxSummary($businessId: ID!, $startDate: Date!, $endDate: Date!) {
business(id: $businessId) {
taxSummary(startDate: $startDate, endDate: $endDate) {
startDate
endDate
taxes {
tax {
id
name
rate
}
totalTaxCollected {
value
currency { code }
}
totalTaxPaid {
value
}
netTaxDue {
value
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
startDate: args.startDate,
endDate: args.endDate,
});
return result.business.taxSummary;
},
},
wave_cashflow: {
description: 'Generate a cashflow statement for a date range',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
startDate: { type: 'string', description: 'Report start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'Report end date (YYYY-MM-DD)' },
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetCashflow($businessId: ID!, $startDate: Date!, $endDate: Date!) {
business(id: $businessId) {
cashflow(startDate: $startDate, endDate: $endDate) {
startDate
endDate
operatingActivities {
value
currency { code }
}
investingActivities {
value
}
financingActivities {
value
}
netCashChange {
value
}
}
}
}
`;
const result = await client.query(query, {
businessId,
startDate: args.startDate,
endDate: args.endDate,
});
return result.business.cashflow;
},
},
};
}

View File

@ -0,0 +1,142 @@
/**
* Wave Tax Tools
*/
import type { WaveClient } from '../client.js';
import type { Tax } from '../types/index.js';
export function registerTaxTools(client: WaveClient) {
return {
wave_list_taxes: {
description: 'List all sales taxes configured for a business',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
isArchived: { type: 'boolean', description: 'Include archived taxes (default: false)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTaxes($businessId: ID!) {
business(id: $businessId) {
taxes {
id
name
abbreviation
description
rate
isArchived
}
}
}
`;
const result = await client.query(query, { businessId });
let taxes = result.business.taxes;
if (args.isArchived === false) {
taxes = taxes.filter((t: any) => !t.isArchived);
}
return taxes;
},
},
wave_get_tax: {
description: 'Get detailed information about a specific tax',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
taxId: { type: 'string', description: 'Tax ID' },
},
required: ['taxId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTax($businessId: ID!, $taxId: ID!) {
business(id: $businessId) {
tax(id: $taxId) {
id
name
abbreviation
description
rate
isArchived
}
}
}
`;
const result = await client.query(query, {
businessId,
taxId: args.taxId,
});
return result.business.tax;
},
},
wave_create_tax: {
description: 'Create a new sales tax',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
name: { type: 'string', description: 'Tax name (e.g., "Sales Tax")' },
abbreviation: { type: 'string', description: 'Tax abbreviation (e.g., "ST")' },
rate: { type: 'string', description: 'Tax rate as decimal (e.g., "0.0875" for 8.75%)' },
description: { type: 'string', description: 'Tax description' },
},
required: ['name', 'rate'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateTax($input: TaxCreateInput!) {
taxCreate(input: $input) {
tax {
id
name
abbreviation
rate
description
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
name: args.name,
abbreviation: args.abbreviation,
rate: args.rate,
description: args.description,
},
});
if (!result.taxCreate.didSucceed) {
throw new Error(`Failed to create tax: ${JSON.stringify(result.taxCreate.inputErrors)}`);
}
return result.taxCreate.tax;
},
},
};
}

View File

@ -0,0 +1,347 @@
/**
* Wave Transaction Tools
*/
import type { WaveClient } from '../client.js';
import type { Transaction } from '../types/index.js';
export function registerTransactionTools(client: WaveClient) {
return {
wave_list_transactions: {
description: 'List transactions for a business with filtering options',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
accountId: { type: 'string', description: 'Filter by specific account ID' },
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
page: { type: 'number', description: 'Page number (default: 1)' },
pageSize: { type: 'number', description: 'Results per page (default: 50)' },
},
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTransactions($businessId: ID!, $page: Int!, $pageSize: Int!) {
business(id: $businessId) {
transactions(page: $page, pageSize: $pageSize) {
pageInfo {
currentPage
totalPages
totalCount
}
edges {
node {
id
description
amount {
value
currency { code }
}
date
accountTransaction {
account {
id
name
type { name }
}
amount { value }
}
createdAt
modifiedAt
}
}
}
}
}
`;
const result = await client.query(query, {
businessId,
page: args.page || 1,
pageSize: Math.min(args.pageSize || 50, 100),
});
let transactions = result.business.transactions.edges.map((e: any) => e.node);
// Client-side filtering
if (args.accountId) {
transactions = transactions.filter((t: any) =>
t.accountTransaction?.account?.id === args.accountId
);
}
if (args.startDate) {
transactions = transactions.filter((t: any) => t.date >= args.startDate);
}
if (args.endDate) {
transactions = transactions.filter((t: any) => t.date <= args.endDate);
}
return {
transactions,
pageInfo: result.business.transactions.pageInfo,
};
},
},
wave_get_transaction: {
description: 'Get detailed information about a specific transaction',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
transactionId: { type: 'string', description: 'Transaction ID' },
},
required: ['transactionId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTransaction($businessId: ID!, $transactionId: ID!) {
business(id: $businessId) {
transaction(id: $transactionId) {
id
description
amount {
value
currency { code symbol }
}
date
accountTransaction {
account {
id
name
type { name }
}
amount { value }
}
createdAt
modifiedAt
}
}
}
`;
const result = await client.query(query, {
businessId,
transactionId: args.transactionId,
});
return result.business.transaction;
},
},
wave_create_transaction: {
description: 'Create a new transaction',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
description: { type: 'string', description: 'Transaction description' },
date: { type: 'string', description: 'Transaction date (YYYY-MM-DD)' },
amount: { type: 'string', description: 'Transaction amount' },
accountId: { type: 'string', description: 'Account ID for categorization' },
},
required: ['description', 'date', 'amount', 'accountId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CreateTransaction($input: TransactionCreateInput!) {
transactionCreate(input: $input) {
transaction {
id
description
amount {
value
currency { code }
}
date
accountTransaction {
account {
id
name
}
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
description: args.description,
date: args.date,
amount: args.amount,
accountId: args.accountId,
},
});
if (!result.transactionCreate.didSucceed) {
throw new Error(`Failed to create transaction: ${JSON.stringify(result.transactionCreate.inputErrors)}`);
}
return result.transactionCreate.transaction;
},
},
wave_update_transaction: {
description: 'Update an existing transaction',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
transactionId: { type: 'string', description: 'Transaction ID' },
description: { type: 'string', description: 'Transaction description' },
date: { type: 'string', description: 'Transaction date (YYYY-MM-DD)' },
},
required: ['transactionId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation UpdateTransaction($input: TransactionUpdateInput!) {
transactionUpdate(input: $input) {
transaction {
id
description
date
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
transactionId: args.transactionId,
description: args.description,
date: args.date,
},
});
if (!result.transactionUpdate.didSucceed) {
throw new Error(`Failed to update transaction: ${JSON.stringify(result.transactionUpdate.inputErrors)}`);
}
return result.transactionUpdate.transaction;
},
},
wave_categorize_transaction: {
description: 'Categorize/recategorize a transaction to a different account',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
transactionId: { type: 'string', description: 'Transaction ID' },
accountId: { type: 'string', description: 'New account ID for categorization' },
},
required: ['transactionId', 'accountId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const mutation = `
mutation CategorizeTransaction($input: TransactionCategorizeInput!) {
transactionCategorize(input: $input) {
transaction {
id
accountTransaction {
account {
id
name
type { name }
}
}
}
didSucceed
inputErrors {
message
path
}
}
}
`;
const result = await client.mutate(mutation, {
input: {
businessId,
transactionId: args.transactionId,
accountId: args.accountId,
},
});
if (!result.transactionCategorize.didSucceed) {
throw new Error(`Failed to categorize transaction: ${JSON.stringify(result.transactionCategorize.inputErrors)}`);
}
return result.transactionCategorize.transaction;
},
},
wave_list_transaction_attachments: {
description: 'List attachments (receipts, documents) for a transaction',
parameters: {
type: 'object',
properties: {
businessId: { type: 'string', description: 'Business ID' },
transactionId: { type: 'string', description: 'Transaction ID' },
},
required: ['transactionId'],
},
handler: async (args: any) => {
const businessId = args.businessId || client.getBusinessId();
if (!businessId) throw new Error('businessId required');
const query = `
query GetTransactionAttachments($businessId: ID!, $transactionId: ID!) {
business(id: $businessId) {
transaction(id: $transactionId) {
id
attachments {
id
filename
url
mimeType
size
createdAt
}
}
}
}
`;
const result = await client.query(query, {
businessId,
transactionId: args.transactionId,
});
return result.business.transaction.attachments;
},
},
};
}

View File

@ -0,0 +1,372 @@
/**
* Wave API Types
* Based on Wave GraphQL Public API
*/
export interface WaveConfig {
accessToken: string;
businessId?: string;
}
export interface Business {
id: string;
name: string;
currency: {
code: string;
symbol: string;
};
timezone: string;
address?: {
addressLine1?: string;
addressLine2?: string;
city?: string;
provinceCode?: string;
countryCode?: string;
postalCode?: string;
};
}
export interface Customer {
id: string;
name: string;
email?: string;
firstName?: string;
lastName?: string;
address?: {
addressLine1?: string;
addressLine2?: string;
city?: string;
provinceCode?: string;
countryCode?: string;
postalCode?: string;
};
currency?: {
code: string;
};
createdAt: string;
modifiedAt: string;
}
export interface Invoice {
id: string;
invoiceNumber: string;
customer: Customer;
status: 'DRAFT' | 'SENT' | 'VIEWED' | 'PAID' | 'PARTIAL' | 'OVERDUE' | 'APPROVED';
title?: string;
subhead?: string;
invoiceDate: string;
dueDate?: string;
amountDue: {
value: string;
currency: {
code: string;
};
};
amountPaid: {
value: string;
};
total: {
value: string;
};
items: InvoiceItem[];
footer?: string;
memo?: string;
createdAt: string;
modifiedAt: string;
viewUrl?: string;
pdfUrl?: string;
}
export interface InvoiceItem {
product?: Product;
description: string;
quantity: number;
unitPrice: string;
subtotal: {
value: string;
};
total: {
value: string;
};
taxes?: Tax[];
}
export interface Product {
id: string;
name: string;
description?: string;
unitPrice?: string;
incomeAccount?: Account;
isSold: boolean;
isBought: boolean;
isArchived: boolean;
createdAt: string;
modifiedAt: string;
}
export interface Account {
id: string;
name: string;
description?: string;
type: {
name: string;
normalBalanceType: 'DEBIT' | 'CREDIT';
};
subtype: {
name: string;
value: string;
};
currency: {
code: string;
};
balance?: string;
isArchived: boolean;
}
export interface Transaction {
id: string;
description?: string;
amount: {
value: string;
currency: {
code: string;
};
};
date: string;
account?: Account;
accountTransaction?: {
account: Account;
amount: {
value: string;
};
};
createdAt: string;
modifiedAt: string;
}
export interface Bill {
id: string;
vendor: Vendor;
status: 'DRAFT' | 'APPROVED' | 'PAID' | 'PARTIAL';
billNumber?: string;
billDate: string;
dueDate?: string;
amountDue: {
value: string;
currency: {
code: string;
};
};
amountPaid: {
value: string;
};
total: {
value: string;
};
items: BillItem[];
memo?: string;
createdAt: string;
modifiedAt: string;
}
export interface BillItem {
description: string;
quantity: number;
unitPrice: string;
total: {
value: string;
};
account?: Account;
taxes?: Tax[];
}
export interface Vendor {
id: string;
name: string;
email?: string;
address?: {
addressLine1?: string;
addressLine2?: string;
city?: string;
provinceCode?: string;
countryCode?: string;
postalCode?: string;
};
currency?: {
code: string;
};
createdAt: string;
modifiedAt: string;
}
export interface Estimate {
id: string;
estimateNumber: string;
customer: Customer;
status: 'DRAFT' | 'SENT' | 'VIEWED' | 'APPROVED' | 'REJECTED';
title?: string;
subhead?: string;
estimateDate: string;
expiryDate?: string;
total: {
value: string;
currency: {
code: string;
};
};
items: EstimateItem[];
footer?: string;
memo?: string;
createdAt: string;
modifiedAt: string;
}
export interface EstimateItem {
product?: Product;
description: string;
quantity: number;
unitPrice: string;
total: {
value: string;
};
taxes?: Tax[];
}
export interface Tax {
id: string;
name: string;
abbreviation?: string;
description?: string;
rate: string;
isArchived: boolean;
}
export interface Payment {
id: string;
amount: {
value: string;
currency: {
code: string;
};
};
date: string;
source?: string;
createdAt: string;
}
export interface ProfitAndLossReport {
startDate: string;
endDate: string;
revenue: {
value: string;
};
costOfGoodsSold: {
value: string;
};
grossProfit: {
value: string;
};
expenses: {
value: string;
};
netIncome: {
value: string;
};
sections: ReportSection[];
}
export interface BalanceSheetReport {
asOfDate: string;
assets: {
value: string;
};
liabilities: {
value: string;
};
equity: {
value: string;
};
sections: ReportSection[];
}
export interface ReportSection {
name: string;
total: {
value: string;
};
accounts: ReportAccountLine[];
subsections?: ReportSection[];
}
export interface ReportAccountLine {
account: Account;
balance: {
value: string;
};
}
export interface AgedReceivablesReport {
asOfDate: string;
total: {
value: string;
};
customers: AgedReceivablesCustomer[];
}
export interface AgedReceivablesCustomer {
customer: Customer;
total: {
value: string;
};
current: {
value: string;
};
days1to30: {
value: string;
};
days31to60: {
value: string;
};
days61to90: {
value: string;
};
over90: {
value: string;
};
}
export interface CashflowReport {
startDate: string;
endDate: string;
operatingActivities: {
value: string;
};
investingActivities: {
value: string;
};
financingActivities: {
value: string;
};
netCashChange: {
value: string;
};
}
export interface GraphQLError {
message: string;
extensions?: {
code?: string;
[key: string]: any;
};
}
export class WaveError extends Error {
graphQLErrors?: GraphQLError[];
networkError?: Error;
statusCode?: number;
constructor(message: string) {
super(message);
this.name = 'WaveError';
}
}

View File

@ -1,15 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "build"]
}