Remove mcp-gateway/wave-mcp/src/tools/accounting.ts
This commit is contained in:
parent
c28d53262b
commit
199fc34684
1 changed files with 0 additions and 406 deletions
|
|
@ -1,406 +0,0 @@
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { WaveClient } from "../services/wave-client.js";
|
|
||||||
import { WaveAccount, WaveTransaction } from "../types.js";
|
|
||||||
import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from "../constants.js";
|
|
||||||
|
|
||||||
interface AccountsData {
|
|
||||||
business: {
|
|
||||||
accounts: {
|
|
||||||
pageInfo: { currentPage: number; totalPages: number; totalCount: number };
|
|
||||||
edges: Array<{ node: WaveAccount }>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransactionCreateData {
|
|
||||||
moneyTransactionCreate: {
|
|
||||||
didSucceed: boolean;
|
|
||||||
inputErrors: Array<{ code: string; message: string; path: string[] }>;
|
|
||||||
transaction?: WaveTransaction;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserData {
|
|
||||||
user: { id: string; defaultEmail: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
const ACCOUNT_FIELDS = `
|
|
||||||
id name description displayId
|
|
||||||
type { name value normalBalanceType }
|
|
||||||
subtype { name value }
|
|
||||||
currency { code }
|
|
||||||
isArchived sequence normalBalanceType
|
|
||||||
`;
|
|
||||||
|
|
||||||
function formatAccountMarkdown(a: WaveAccount): string[] {
|
|
||||||
const lines = [
|
|
||||||
`## ${a.name} (${a.displayId})`,
|
|
||||||
`- **ID**: ${a.id}`,
|
|
||||||
`- **Type**: ${a.type.name} / ${a.subtype.name}`,
|
|
||||||
`- **Currency**: ${a.currency.code}`,
|
|
||||||
];
|
|
||||||
if (a.isArchived) lines.push("- **Status**: Archived");
|
|
||||||
if (a.description) lines.push(`- **Description**: ${a.description}`);
|
|
||||||
lines.push("");
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerAccountingTools(server: McpServer, client: WaveClient): void {
|
|
||||||
// ── wave_get_user ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"wave_get_user",
|
|
||||||
{
|
|
||||||
title: "Get Wave User",
|
|
||||||
description: `Get the authenticated Wave user's ID and email. Useful for verifying token validity and identifying the logged-in user.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
User ID and default email address.`,
|
|
||||||
inputSchema: z.object({
|
|
||||||
response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
||||||
}),
|
|
||||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
||||||
},
|
|
||||||
async ({ response_format }) => {
|
|
||||||
try {
|
|
||||||
const data = await client.query<UserData>(`query { user { id defaultEmail } }`);
|
|
||||||
const user = data.user;
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
if (response_format === "json") {
|
|
||||||
text = JSON.stringify(user, null, 2);
|
|
||||||
} else {
|
|
||||||
text = `# Wave User\n\n- **ID**: ${user.id}\n- **Email**: ${user.defaultEmail}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: [{ type: "text", text }], structuredContent: user };
|
|
||||||
} catch (error) {
|
|
||||||
return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── wave_list_accounts ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"wave_list_accounts",
|
|
||||||
{
|
|
||||||
title: "List Wave Accounts",
|
|
||||||
description: `List chart of accounts for a Wave business, optionally filtered by type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- businessId (string): Wave business ID
|
|
||||||
- page (number): Page number (default: 1)
|
|
||||||
- pageSize (number): Items per page, 1-100 (default: 20)
|
|
||||||
- type ('ASSET' | 'LIABILITY' | 'EQUITY' | 'INCOME' | 'EXPENSE'): Filter by account type (optional)
|
|
||||||
- includeArchived (boolean): Include archived accounts (default: false)
|
|
||||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Paginated chart of accounts with type, subtype, currency, and display ID.`,
|
|
||||||
inputSchema: z.object({
|
|
||||||
businessId: z.string().describe("Wave business ID"),
|
|
||||||
page: z.number().int().min(1).default(1).describe("Page number"),
|
|
||||||
pageSize: z.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE).describe("Items per page"),
|
|
||||||
type: z.enum(["ASSET", "LIABILITY", "EQUITY", "INCOME", "EXPENSE"]).optional().describe("Filter by account type"),
|
|
||||||
includeArchived: z.boolean().default(false).describe("Include archived accounts"),
|
|
||||||
response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
||||||
}),
|
|
||||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
||||||
},
|
|
||||||
async ({ businessId, page, pageSize, type, includeArchived, response_format }) => {
|
|
||||||
try {
|
|
||||||
const data = await client.query<AccountsData>(`
|
|
||||||
query($businessId: ID!, $page: Int!, $pageSize: Int!) {
|
|
||||||
business(id: $businessId) {
|
|
||||||
accounts(page: $page, pageSize: $pageSize) {
|
|
||||||
pageInfo { currentPage totalPages totalCount }
|
|
||||||
edges { node { ${ACCOUNT_FIELDS} } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, { businessId, page, pageSize });
|
|
||||||
|
|
||||||
const { pageInfo, edges } = data.business.accounts;
|
|
||||||
let accounts = edges.map((e) => e.node);
|
|
||||||
|
|
||||||
// Client-side filtering
|
|
||||||
if (type) accounts = accounts.filter((a) => a.type.value === type);
|
|
||||||
if (!includeArchived) accounts = accounts.filter((a) => !a.isArchived);
|
|
||||||
|
|
||||||
if (!accounts.length) {
|
|
||||||
return { content: [{ type: "text", text: "No accounts found matching the criteria." }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
total: pageInfo.totalCount,
|
|
||||||
count: accounts.length,
|
|
||||||
page: pageInfo.currentPage,
|
|
||||||
totalPages: pageInfo.totalPages,
|
|
||||||
has_more: pageInfo.currentPage < pageInfo.totalPages,
|
|
||||||
next_page: pageInfo.currentPage < pageInfo.totalPages ? pageInfo.currentPage + 1 : undefined,
|
|
||||||
accounts,
|
|
||||||
};
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
if (response_format === "json") {
|
|
||||||
text = JSON.stringify(result, null, 2);
|
|
||||||
} else {
|
|
||||||
const lines = [`# Chart of Accounts (${pageInfo.totalCount} total, page ${pageInfo.currentPage}/${pageInfo.totalPages})`, ""];
|
|
||||||
// Group by type
|
|
||||||
const grouped: Record<string, WaveAccount[]> = {};
|
|
||||||
for (const a of accounts) {
|
|
||||||
const t = a.type.name;
|
|
||||||
if (!grouped[t]) grouped[t] = [];
|
|
||||||
grouped[t].push(a);
|
|
||||||
}
|
|
||||||
for (const [typeName, typeAccounts] of Object.entries(grouped)) {
|
|
||||||
lines.push(`### ${typeName}`, "");
|
|
||||||
for (const a of typeAccounts) lines.push(...formatAccountMarkdown(a));
|
|
||||||
}
|
|
||||||
text = lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: WaveClient.truncate(text) }],
|
|
||||||
structuredContent: result,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── wave_create_transaction ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"wave_create_transaction",
|
|
||||||
{
|
|
||||||
title: "Create Wave Transaction",
|
|
||||||
description: `Create a money transaction (deposit or withdrawal) in Wave accounting.
|
|
||||||
|
|
||||||
A transaction requires:
|
|
||||||
1. An anchor account (typically a bank or credit card account) with a direction
|
|
||||||
2. One or more line items that categorize the transaction
|
|
||||||
|
|
||||||
The anchor is the primary account (e.g., your bank account). Line items describe what the money was for (e.g., an expense account).
|
|
||||||
|
|
||||||
Direction values:
|
|
||||||
- For anchor: 'DEPOSIT' (money in) or 'WITHDRAWAL' (money out)
|
|
||||||
- For line items: 'INCREASE' or 'DECREASE' relative to the account's normal balance
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- businessId (string): Wave business ID
|
|
||||||
- description (string): Transaction description/memo
|
|
||||||
- anchorAccountId (string): Bank or credit card account ID
|
|
||||||
- anchorAmount (number): Transaction amount (positive)
|
|
||||||
- anchorDirection ('DEPOSIT' | 'WITHDRAWAL'): Money in or out of anchor account
|
|
||||||
- anchorDate (string): Date in YYYY-MM-DD format
|
|
||||||
- lineItems (array): Categorization items
|
|
||||||
- accountId (string): Account to categorize against
|
|
||||||
- amount (number): Amount for this line item
|
|
||||||
- balance ('INCREASE' | 'DECREASE'): Effect on the line item account
|
|
||||||
- description (string): Line item description (optional)
|
|
||||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Created transaction details or validation errors.`,
|
|
||||||
inputSchema: z.object({
|
|
||||||
businessId: z.string().describe("Wave business ID"),
|
|
||||||
description: z.string().optional().describe("Transaction description/memo"),
|
|
||||||
anchorAccountId: z.string().describe("Bank or credit card account ID"),
|
|
||||||
anchorAmount: z.number().positive().describe("Transaction amount (positive number)"),
|
|
||||||
anchorDirection: z.enum(["DEPOSIT", "WITHDRAWAL"]).describe("DEPOSIT = money in, WITHDRAWAL = money out"),
|
|
||||||
anchorDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("Transaction date (YYYY-MM-DD)"),
|
|
||||||
lineItems: z.array(z.object({
|
|
||||||
accountId: z.string().describe("Account ID for categorization"),
|
|
||||||
amount: z.number().positive().describe("Amount for this line item"),
|
|
||||||
balance: z.enum(["INCREASE", "DECREASE"]).describe("Effect on this account's balance"),
|
|
||||||
description: z.string().optional().describe("Line item description"),
|
|
||||||
})).min(1).describe("Line items categorizing the transaction"),
|
|
||||||
response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
||||||
}),
|
|
||||||
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
||||||
},
|
|
||||||
async ({ businessId, description, anchorAccountId, anchorAmount, anchorDirection, anchorDate, lineItems, response_format }) => {
|
|
||||||
try {
|
|
||||||
const input: Record<string, unknown> = {
|
|
||||||
businessId,
|
|
||||||
externalId: `mcp-${Date.now()}`,
|
|
||||||
date: anchorDate,
|
|
||||||
income: anchorDirection === "DEPOSIT" ? {
|
|
||||||
accountId: anchorAccountId,
|
|
||||||
amount: anchorAmount,
|
|
||||||
description,
|
|
||||||
} : undefined,
|
|
||||||
expense: anchorDirection === "WITHDRAWAL" ? {
|
|
||||||
accountId: anchorAccountId,
|
|
||||||
amount: anchorAmount,
|
|
||||||
description,
|
|
||||||
} : undefined,
|
|
||||||
lineItems: lineItems.map((li) => ({
|
|
||||||
accountId: li.accountId,
|
|
||||||
amount: li.amount,
|
|
||||||
balance: li.balance,
|
|
||||||
description: li.description,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove undefined keys
|
|
||||||
if (!input.income) delete input.income;
|
|
||||||
if (!input.expense) delete input.expense;
|
|
||||||
|
|
||||||
const data = await client.query<TransactionCreateData>(`
|
|
||||||
mutation($input: MoneyTransactionCreateInput!) {
|
|
||||||
moneyTransactionCreate(input: $input) {
|
|
||||||
didSucceed
|
|
||||||
inputErrors { code message path }
|
|
||||||
transaction {
|
|
||||||
id description notes
|
|
||||||
createdAt modifiedAt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`, { input });
|
|
||||||
|
|
||||||
const result = data.moneyTransactionCreate;
|
|
||||||
if (!result.didSucceed) {
|
|
||||||
const errs = result.inputErrors.map((e) => ` - ${e.path?.join(".") ?? "field"}: ${e.message}`).join("\n");
|
|
||||||
return { content: [{ type: "text", text: `Failed to create transaction:\n${errs}` }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
if (response_format === "json") {
|
|
||||||
text = JSON.stringify(result.transaction, null, 2);
|
|
||||||
} else {
|
|
||||||
text = [
|
|
||||||
`Transaction created successfully!`,
|
|
||||||
`- **ID**: ${result.transaction?.id}`,
|
|
||||||
`- **Description**: ${result.transaction?.description ?? description ?? "N/A"}`,
|
|
||||||
`- **Amount**: ${WaveClient.formatCurrency(anchorAmount, "")} (${anchorDirection})`,
|
|
||||||
`- **Date**: ${anchorDate}`,
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: [{ type: "text", text }], structuredContent: result.transaction };
|
|
||||||
} catch (error) {
|
|
||||||
return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── wave_list_currencies ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"wave_list_currencies",
|
|
||||||
{
|
|
||||||
title: "List Wave Currencies",
|
|
||||||
description: `List all currencies supported by Wave (ISO 4217). Useful for finding valid currency codes to use when creating customers or invoices.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of all supported currencies with code, symbol, and name.`,
|
|
||||||
inputSchema: z.object({
|
|
||||||
response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
||||||
}),
|
|
||||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
||||||
},
|
|
||||||
async ({ response_format }) => {
|
|
||||||
try {
|
|
||||||
const data = await client.query<{
|
|
||||||
currencies: Array<{ code: string; symbol: string; name: string; plural: string }>;
|
|
||||||
}>(`
|
|
||||||
query {
|
|
||||||
currencies {
|
|
||||||
code symbol name plural
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const currencies = data.currencies;
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
if (response_format === "json") {
|
|
||||||
text = JSON.stringify({ count: currencies.length, currencies }, null, 2);
|
|
||||||
} else {
|
|
||||||
const lines = [`# Supported Currencies (${currencies.length})`, ""];
|
|
||||||
for (const c of currencies) {
|
|
||||||
lines.push(`- **${c.code}** ${c.symbol} — ${c.name}`);
|
|
||||||
}
|
|
||||||
text = lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: WaveClient.truncate(text) }],
|
|
||||||
structuredContent: { count: currencies.length, currencies },
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── wave_list_countries ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"wave_list_countries",
|
|
||||||
{
|
|
||||||
title: "List Wave Countries",
|
|
||||||
description: `List all countries and their provinces supported by Wave. Use this to find valid country and province codes for addresses.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of countries with ISO codes and their provinces/states.`,
|
|
||||||
inputSchema: z.object({
|
|
||||||
response_format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
||||||
}),
|
|
||||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
||||||
},
|
|
||||||
async ({ response_format }) => {
|
|
||||||
try {
|
|
||||||
const data = await client.query<{
|
|
||||||
countries: Array<{
|
|
||||||
code: string;
|
|
||||||
name: string;
|
|
||||||
provinces: Array<{ code: string; name: string }>;
|
|
||||||
}>;
|
|
||||||
}>(`
|
|
||||||
query {
|
|
||||||
countries {
|
|
||||||
code name
|
|
||||||
provinces { code name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
const countries = data.countries;
|
|
||||||
|
|
||||||
let text: string;
|
|
||||||
if (response_format === "json") {
|
|
||||||
text = JSON.stringify({ count: countries.length, countries }, null, 2);
|
|
||||||
} else {
|
|
||||||
const lines = [`# Supported Countries (${countries.length})`, ""];
|
|
||||||
for (const c of countries) {
|
|
||||||
lines.push(`- **${c.code}** — ${c.name} (${c.provinces.length} provinces/states)`);
|
|
||||||
}
|
|
||||||
text = lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: WaveClient.truncate(text) }],
|
|
||||||
structuredContent: { count: countries.length, countries },
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue