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:
parent
c8bf4df518
commit
7631226a36
362
servers/wave/README.md
Normal file
362
servers/wave/README.md
Normal 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)
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
267
servers/wave/src/apps/index.ts
Normal file
267
servers/wave/src/apps/index.ts
Normal 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'] },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
72
servers/wave/src/client.ts
Normal file
72
servers/wave/src/client.ts
Normal 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);
|
||||
}
|
||||
@ -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
43
servers/wave/src/main.ts
Normal 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
194
servers/wave/src/server.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
253
servers/wave/src/tools/accounts-tools.ts
Normal file
253
servers/wave/src/tools/accounts-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
371
servers/wave/src/tools/bills-tools.ts
Normal file
371
servers/wave/src/tools/bills-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
129
servers/wave/src/tools/businesses-tools.ts
Normal file
129
servers/wave/src/tools/businesses-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
394
servers/wave/src/tools/customers-tools.ts
Normal file
394
servers/wave/src/tools/customers-tools.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
379
servers/wave/src/tools/estimates-tools.ts
Normal file
379
servers/wave/src/tools/estimates-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
572
servers/wave/src/tools/invoices-tools.ts
Normal file
572
servers/wave/src/tools/invoices-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
297
servers/wave/src/tools/products-tools.ts
Normal file
297
servers/wave/src/tools/products-tools.ts
Normal 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' };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
289
servers/wave/src/tools/reporting-tools.ts
Normal file
289
servers/wave/src/tools/reporting-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
142
servers/wave/src/tools/taxes-tools.ts
Normal file
142
servers/wave/src/tools/taxes-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
347
servers/wave/src/tools/transactions-tools.ts
Normal file
347
servers/wave/src/tools/transactions-tools.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
372
servers/wave/src/types/index.ts
Normal file
372
servers/wave/src/types/index.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user