- 55+ tools across 12 categories (invoices, clients, expenses, estimates, time, projects, payments, items, taxes, reports, recurring, accounts) - FreshBooks API client with OAuth2, pagination, error handling - 22 dark-themed React MCP apps (invoice dashboard, builder, client dashboard, expense tracker, time tracker, project dashboard, reports, etc.) - Full TypeScript types for all FreshBooks entities - Comprehensive README with examples and architecture docs
269 lines
9.3 KiB
TypeScript
269 lines
9.3 KiB
TypeScript
import { z } from 'zod';
|
|
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
|
import type { Invoice, Payment } from '../types/index.js';
|
|
|
|
export const invoicesTools = [
|
|
{
|
|
name: 'freshbooks_list_invoices',
|
|
description: 'List all invoices with optional filtering (client, status, date range)',
|
|
inputSchema: z.object({
|
|
clientid: z.number().optional().describe('Filter by client ID'),
|
|
status: z.enum(['draft', 'sent', 'viewed', 'paid', 'partial', 'overdue', 'disputed']).optional(),
|
|
date_min: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
|
|
date_max: z.string().optional().describe('Maximum date (YYYY-MM-DD)'),
|
|
page: z.number().default(1),
|
|
per_page: z.number().default(30),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const params: Record<string, any> = {};
|
|
if (args.clientid) params.clientid = args.clientid;
|
|
if (args.status) params.status = args.status;
|
|
if (args.date_min) params.date_min = args.date_min;
|
|
if (args.date_max) params.date_max = args.date_max;
|
|
|
|
const response = await client.getPaginated<{ invoices: Invoice[] }>(
|
|
'/invoices/invoices',
|
|
args.page,
|
|
args.per_page,
|
|
params
|
|
);
|
|
return {
|
|
invoices: response.response.result.invoices || [],
|
|
page: response.response.page,
|
|
pages: response.response.pages,
|
|
total: response.response.total,
|
|
};
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_get_invoice',
|
|
description: 'Get a single invoice by ID',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const response = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
|
`/invoices/invoices/${args.invoice_id}`
|
|
);
|
|
return response.response.result.invoice;
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_create_invoice',
|
|
description: 'Create a new invoice',
|
|
inputSchema: z.object({
|
|
clientid: z.number().describe('Client ID'),
|
|
create_date: z.string().optional().describe('Invoice date (YYYY-MM-DD, defaults to today)'),
|
|
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
|
lines: z.array(z.object({
|
|
name: z.string().describe('Line item name'),
|
|
description: z.string().optional(),
|
|
qty: z.number().default(1),
|
|
unit_cost: z.string().describe('Unit cost as string (e.g., "100.00")'),
|
|
})).describe('Invoice line items'),
|
|
currency_code: z.string().default('USD'),
|
|
notes: z.string().optional(),
|
|
terms: z.string().optional(),
|
|
status: z.enum(['draft', 'sent']).default('draft'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const lines = args.lines.map((line: any) => ({
|
|
...line,
|
|
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
|
}));
|
|
|
|
const invoiceData = {
|
|
invoice: {
|
|
clientid: args.clientid,
|
|
create_date: args.create_date || new Date().toISOString().split('T')[0],
|
|
due_date: args.due_date,
|
|
currency_code: args.currency_code,
|
|
lines,
|
|
notes: args.notes,
|
|
terms: args.terms,
|
|
status: args.status === 'sent' ? 2 : 1,
|
|
},
|
|
};
|
|
|
|
const response = await client.post<{ response: { result: { invoice: Invoice } } }>(
|
|
'/invoices/invoices',
|
|
invoiceData
|
|
);
|
|
return response.response.result.invoice;
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_update_invoice',
|
|
description: 'Update an existing invoice',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
clientid: z.number().optional(),
|
|
create_date: z.string().optional(),
|
|
due_date: z.string().optional(),
|
|
lines: z.array(z.object({
|
|
name: z.string(),
|
|
description: z.string().optional(),
|
|
qty: z.number(),
|
|
unit_cost: z.string(),
|
|
})).optional(),
|
|
notes: z.string().optional(),
|
|
terms: z.string().optional(),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const updateData: any = { invoice: {} };
|
|
|
|
if (args.clientid) updateData.invoice.clientid = args.clientid;
|
|
if (args.create_date) updateData.invoice.create_date = args.create_date;
|
|
if (args.due_date) updateData.invoice.due_date = args.due_date;
|
|
if (args.notes) updateData.invoice.notes = args.notes;
|
|
if (args.terms) updateData.invoice.terms = args.terms;
|
|
if (args.lines) {
|
|
updateData.invoice.lines = args.lines.map((line: any) => ({
|
|
...line,
|
|
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
|
}));
|
|
}
|
|
|
|
const response = await client.put<{ response: { result: { invoice: Invoice } } }>(
|
|
`/invoices/invoices/${args.invoice_id}`,
|
|
updateData
|
|
);
|
|
return response.response.result.invoice;
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_delete_invoice',
|
|
description: 'Delete an invoice (moves to archived)',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
await client.put(
|
|
`/invoices/invoices/${args.invoice_id}`,
|
|
{ invoice: { vis_state: 1 } }
|
|
);
|
|
return { success: true, message: `Invoice ${args.invoice_id} archived` };
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_send_invoice',
|
|
description: 'Send an invoice to the client via email',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
email_subject: z.string().optional(),
|
|
email_body: z.string().optional(),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const emailData: any = { invoice: {} };
|
|
if (args.email_subject) emailData.invoice.email_subject = args.email_subject;
|
|
if (args.email_body) emailData.invoice.email_body = args.email_body;
|
|
|
|
await client.put(
|
|
`/invoices/invoices/${args.invoice_id}`,
|
|
{ invoice: { action_email: true, ...emailData.invoice } }
|
|
);
|
|
return { success: true, message: `Invoice ${args.invoice_id} sent` };
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_mark_invoice_paid',
|
|
description: 'Mark an invoice as paid',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
payment_type: z.string().default('Cash').describe('Payment method'),
|
|
payment_date: z.string().optional().describe('Payment date (YYYY-MM-DD, defaults to today)'),
|
|
amount: z.string().optional().describe('Payment amount (defaults to outstanding amount)'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
// First get the invoice to know the outstanding amount
|
|
const invoiceResp = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
|
`/invoices/invoices/${args.invoice_id}`
|
|
);
|
|
const invoice = invoiceResp.response.result.invoice;
|
|
|
|
const paymentData = {
|
|
payment: {
|
|
invoiceid: args.invoice_id,
|
|
amount: {
|
|
amount: args.amount || invoice.outstanding.amount,
|
|
code: invoice.currency_code,
|
|
},
|
|
date: args.payment_date || new Date().toISOString().split('T')[0],
|
|
type: args.payment_type,
|
|
},
|
|
};
|
|
|
|
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
|
'/payments/payments',
|
|
paymentData
|
|
);
|
|
return response.response.result.payment;
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_mark_invoice_unpaid',
|
|
description: 'Mark an invoice as unpaid (reopen it)',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
await client.put(
|
|
`/invoices/invoices/${args.invoice_id}`,
|
|
{ invoice: { v3_status: 'unpaid' } }
|
|
);
|
|
return { success: true, message: `Invoice ${args.invoice_id} marked unpaid` };
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_get_invoice_payment',
|
|
description: 'Get payment details for an invoice',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const response = await client.get<{ response: { result: { payments: Payment[] } } }>(
|
|
'/payments/payments',
|
|
{ invoiceid: args.invoice_id }
|
|
);
|
|
return response.response.result.payments || [];
|
|
},
|
|
},
|
|
|
|
{
|
|
name: 'freshbooks_create_payment',
|
|
description: 'Create a payment record for an invoice',
|
|
inputSchema: z.object({
|
|
invoice_id: z.number().describe('Invoice ID'),
|
|
amount: z.string().describe('Payment amount'),
|
|
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
|
|
type: z.string().default('Cash').describe('Payment method'),
|
|
note: z.string().optional(),
|
|
}),
|
|
handler: async (args: any, client: FreshBooksClient) => {
|
|
const paymentData = {
|
|
payment: {
|
|
invoiceid: args.invoice_id,
|
|
amount: { amount: args.amount, code: 'USD' },
|
|
date: args.date || new Date().toISOString().split('T')[0],
|
|
type: args.type,
|
|
note: args.note,
|
|
},
|
|
};
|
|
|
|
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
|
'/payments/payments',
|
|
paymentData
|
|
);
|
|
return response.response.result.payment;
|
|
},
|
|
},
|
|
];
|