diff --git a/servers/freshbooks/README.md b/servers/freshbooks/README.md new file mode 100644 index 0000000..8958a61 --- /dev/null +++ b/servers/freshbooks/README.md @@ -0,0 +1,241 @@ +# FreshBooks MCP Server + +Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting. + +## Features + +### 🎯 55+ Tools + +**Invoices** (10 tools) +- List, get, create, update, delete invoices +- Send invoices via email +- Mark paid/unpaid, create payments +- Get payment history + +**Clients** (6 tools) +- List, get, create, update, delete clients +- List client contacts + +**Expenses** (6 tools) +- List, get, create, update, delete expenses +- List expense categories + +**Estimates** (7 tools) +- List, get, create, update, delete estimates +- Send estimates, convert to invoices + +**Time Tracking** (5 tools) +- List, get, create, update, delete time entries + +**Projects** (6 tools) +- List, get, create, update, delete projects +- List project services + +**Payments** (5 tools) +- List, get, create, update, delete payments + +**Items** (5 tools) +- List, get, create, update, delete items (products/services) + +**Taxes** (5 tools) +- List, get, create, update, delete taxes + +**Reports** (5 tools) +- Profit & Loss report +- Tax summary +- Accounts aging +- Expense report +- Revenue by client + +**Recurring** (5 tools) +- List, get, create, update, delete recurring profiles + +**Accounts** (3 tools) +- Get account details +- List staff members +- Get current user + +### 🎨 22 React MCP Apps + +Dark-themed, client-side state React apps (inline HTML): + +1. **invoice-dashboard** - Overview of all invoices with stats +2. **invoice-detail** - Single invoice view +3. **invoice-builder** - Create/edit invoices +4. **invoice-grid** - Grid view of invoices +5. **client-dashboard** - Client overview with metrics +6. **client-detail** - Single client view +7. **client-grid** - Grid view of clients +8. **expense-dashboard** - Expense overview +9. **expense-tracker** - Add and track expenses +10. **estimate-builder** - Create/edit estimates +11. **estimate-grid** - Grid view of estimates +12. **time-tracker** - Real-time timer for tracking hours +13. **time-entries** - List of time entries +14. **project-dashboard** - Project overview with progress +15. **project-detail** - Single project view +16. **payment-history** - List of all payments +17. **reports-dashboard** - Reports menu +18. **profit-loss** - Profit & loss report +19. **tax-summary** - Tax summary report +20. **aging-report** - Accounts aging report +21. **recurring-invoices** - Recurring invoice profiles +22. **revenue-chart** - Revenue visualization + +## Installation + +```bash +npm install +npm run build +``` + +## Configuration + +Set environment variables: + +```bash +export FRESHBOOKS_ACCOUNT_ID="your_account_id" +export FRESHBOOKS_BEARER_TOKEN="your_bearer_token" +``` + +## Usage + +### As MCP Server + +Add to your MCP settings: + +```json +{ + "mcpServers": { + "freshbooks": { + "command": "node", + "args": ["/path/to/freshbooks/dist/main.js"], + "env": { + "FRESHBOOKS_ACCOUNT_ID": "your_account_id", + "FRESHBOOKS_BEARER_TOKEN": "your_bearer_token" + } + } + } +} +``` + +### Direct Usage + +```bash +npm start +``` + +## Architecture + +``` +src/ +├── clients/ +│ └── freshbooks.ts # API client with OAuth2, pagination, error handling +├── tools/ +│ ├── invoices-tools.ts # 10 invoice tools +│ ├── clients-tools.ts # 6 client tools +│ ├── expenses-tools.ts # 6 expense tools +│ ├── estimates-tools.ts # 7 estimate tools +│ ├── time-entries-tools.ts # 5 time tracking tools +│ ├── projects-tools.ts # 6 project tools +│ ├── payments-tools.ts # 5 payment tools +│ ├── items-tools.ts # 5 item tools +│ ├── taxes-tools.ts # 5 tax tools +│ ├── reports-tools.ts # 5 report tools +│ ├── recurring-tools.ts # 5 recurring tools +│ └── accounts-tools.ts # 3 account tools +├── types/ +│ └── index.ts # TypeScript types for FreshBooks API +├── ui/ +│ └── react-app/ # 22 standalone React apps +├── server.ts # MCP server implementation +└── main.ts # Entry point +``` + +## API Client Features + +- **OAuth2 Bearer Authentication** +- **Automatic Pagination** - Fetch all pages or paginated results +- **Error Handling** - Structured error responses +- **Rate Limiting** - Respects FreshBooks API limits +- **Type Safety** - Full TypeScript support + +## Example Tool Calls + +### Create Invoice + +```typescript +{ + "name": "freshbooks_create_invoice", + "arguments": { + "clientid": 12345, + "lines": [ + { "name": "Website Design", "qty": 1, "unit_cost": "2500.00" }, + { "name": "Hosting Setup", "qty": 1, "unit_cost": "150.00" } + ], + "currency_code": "USD", + "notes": "Thank you for your business!" + } +} +``` + +### List Overdue Invoices + +```typescript +{ + "name": "freshbooks_list_invoices", + "arguments": { + "status": "overdue", + "per_page": 50 + } +} +``` + +### Track Time + +```typescript +{ + "name": "freshbooks_create_time_entry", + "arguments": { + "duration": 7200, + "note": "Website development", + "started_at": "2024-01-15T09:00:00Z", + "projectid": 456 + } +} +``` + +### Generate Profit/Loss Report + +```typescript +{ + "name": "freshbooks_profit_loss_report", + "arguments": { + "start_date": "2024-01-01", + "end_date": "2024-01-31", + "currency_code": "USD" + } +} +``` + +## Development + +### Build + +```bash +npm run build +``` + +### Watch Mode + +```bash +npm run watch +``` + +## License + +MIT + +## Author + +MCPEngine - Complete MCP implementations for modern platforms diff --git a/servers/freshbooks/package.json b/servers/freshbooks/package.json index dddb7d2..c8ee48d 100644 --- a/servers/freshbooks/package.json +++ b/servers/freshbooks/package.json @@ -1,20 +1,34 @@ { - "name": "mcp-server-freshbooks", + "name": "@mcpengine/freshbooks", "version": "1.0.0", + "description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management", + "main": "dist/main.js", "type": "module", - "main": "dist/index.js", "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "watch": "tsc --watch", + "prepare": "npm run build", + "start": "node dist/main.js" }, + "keywords": [ + "mcp", + "freshbooks", + "accounting", + "invoicing", + "time-tracking", + "expenses", + "estimates", + "payments" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "zod": "^3.24.1" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.2", + "typescript": "^5.7.2" } } diff --git a/servers/freshbooks/src/clients/freshbooks.ts b/servers/freshbooks/src/clients/freshbooks.ts new file mode 100644 index 0000000..ecad7ab --- /dev/null +++ b/servers/freshbooks/src/clients/freshbooks.ts @@ -0,0 +1,144 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + FreshBooksConfig, + PaginatedResponse, + FreshBooksError, +} from '../types/index.js'; + +export class FreshBooksClient { + private client: AxiosInstance; + private accountId: string; + + constructor(config: FreshBooksConfig) { + this.accountId = config.accountId; + const baseURL = config.baseUrl || `https://api.freshbooks.com/accounting/account/${config.accountId}`; + + this.client = axios.create({ + baseURL, + headers: { + 'Authorization': `Bearer ${config.bearerToken}`, + 'Content-Type': 'application/json', + 'Api-Version': 'alpha', + }, + timeout: 30000, + }); + + // Request interceptor for logging + this.client.interceptors.request.use( + (config) => { + console.error(`[FreshBooks] ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => Promise.reject(error) + ); + + // Response interceptor for error handling + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + return Promise.reject(this.handleError(error)); + } + ); + } + + private handleError(error: AxiosError): FreshBooksError { + if (error.response) { + const data = error.response.data as any; + return { + message: data.message || data.error || `HTTP ${error.response.status}: ${error.response.statusText}`, + code: data.code || `${error.response.status}`, + errors: data.errors || data.response?.errors, + }; + } else if (error.request) { + return { + message: 'No response from FreshBooks API', + code: 'NETWORK_ERROR', + }; + } else { + return { + message: error.message || 'Unknown error', + code: 'UNKNOWN_ERROR', + }; + } + } + + // Generic GET with pagination support + async get(endpoint: string, params?: Record): Promise { + const response = await this.client.get(endpoint, { params }); + return response.data; + } + + // Generic GET with automatic pagination (fetch all pages) + async getAll( + endpoint: string, + params?: Record, + resultKey: string = 'result' + ): Promise { + let page = 1; + let allResults: T[] = []; + let hasMore = true; + + while (hasMore) { + const response = await this.client.get>(endpoint, { + params: { ...params, page, per_page: 100 }, + }); + + const result = response.data.response.result; + const items = Array.isArray(result[resultKey]) ? result[resultKey] : []; + allResults = allResults.concat(items); + + const { page: currentPage, pages } = response.data.response; + hasMore = currentPage < pages; + page++; + } + + return allResults; + } + + // Paginated GET (single page) + async getPaginated( + endpoint: string, + page: number = 1, + perPage: number = 30, + params?: Record + ): Promise> { + const response = await this.client.get>(endpoint, { + params: { ...params, page, per_page: perPage }, + }); + return response.data; + } + + // POST + async post(endpoint: string, data: any): Promise { + const response = await this.client.post(endpoint, data); + return response.data; + } + + // PUT + async put(endpoint: string, data: any): Promise { + const response = await this.client.put(endpoint, data); + return response.data; + } + + // DELETE + async delete(endpoint: string): Promise { + const response = await this.client.delete(endpoint); + return response.data; + } + + // Convenience method: search with filters + async search( + endpoint: string, + searchFields: Record, + page: number = 1, + perPage: number = 30 + ): Promise> { + return this.getPaginated(endpoint, page, perPage, { + search: searchFields, + }); + } + + getAccountId(): string { + return this.accountId; + } +} diff --git a/servers/freshbooks/src/index.ts b/servers/freshbooks/src/index.ts deleted file mode 100644 index 01928b3..0000000 --- a/servers/freshbooks/src/index.ts +++ /dev/null @@ -1,445 +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 = "freshbooks"; -const MCP_VERSION = "1.0.0"; -const API_BASE_URL = "https://api.freshbooks.com"; - -// ============================================ -// API CLIENT -// ============================================ -class FreshBooksClient { - private accessToken: string; - private accountId: string; - private baseUrl: string; - - constructor(accessToken: string, accountId: string) { - this.accessToken = accessToken; - this.accountId = accountId; - this.baseUrl = `${API_BASE_URL}/accounting/account/${accountId}`; - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - "Api-Version": "alpha", - ...options.headers, - }, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`FreshBooks API error: ${response.status} ${response.statusText} - ${text}`); - } - - return response.json(); - } - - async get(endpoint: string) { - return this.request(endpoint, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } - - // Invoice methods - async listInvoices(options?: { page?: number; perPage?: number; status?: string }) { - const params = new URLSearchParams(); - if (options?.page) params.append("page", options.page.toString()); - if (options?.perPage) params.append("per_page", options.perPage.toString()); - if (options?.status) params.append("search[v3_status]", options.status); - const query = params.toString(); - return this.get(`/invoices/invoices${query ? `?${query}` : ""}`); - } - - async getInvoice(invoiceId: string) { - return this.get(`/invoices/invoices/${invoiceId}`); - } - - async createInvoice(data: { - customerid: number; - create_date: string; - due_offset_days?: number; - currency_code?: string; - language?: string; - notes?: string; - terms?: string; - lines?: Array<{ - name: string; - description?: string; - qty: number; - unit_cost: { amount: string; code?: string }; - }>; - }) { - return this.post("/invoices/invoices", { invoice: data }); - } - - async sendInvoice(invoiceId: string, emailData: { - email_recipients?: string[]; - email_subject?: string; - email_body?: string; - action_email?: boolean; - }) { - // To send an invoice, update status to "sent" - return this.put(`/invoices/invoices/${invoiceId}`, { - invoice: { - action_email: emailData.action_email ?? true, - email_recipients: emailData.email_recipients, - email_subject: emailData.email_subject, - email_body: emailData.email_body, - status: 2, // 2 = sent - }, - }); - } - - // Client methods - async listClients(options?: { page?: number; perPage?: number }) { - const params = new URLSearchParams(); - if (options?.page) params.append("page", options.page.toString()); - if (options?.perPage) params.append("per_page", options.perPage.toString()); - const query = params.toString(); - return this.get(`/users/clients${query ? `?${query}` : ""}`); - } - - async getClient(clientId: string) { - return this.get(`/users/clients/${clientId}`); - } - - async createClient(data: { - email?: string; - fname?: string; - lname?: string; - organization?: string; - p_street?: string; - p_street2?: string; - p_city?: string; - p_province?: string; - p_code?: string; - p_country?: string; - currency_code?: string; - language?: string; - bus_phone?: string; - mob_phone?: string; - note?: string; - }) { - return this.post("/users/clients", { client: data }); - } - - // Expense methods - async listExpenses(options?: { page?: number; perPage?: number }) { - const params = new URLSearchParams(); - if (options?.page) params.append("page", options.page.toString()); - if (options?.perPage) params.append("per_page", options.perPage.toString()); - const query = params.toString(); - return this.get(`/expenses/expenses${query ? `?${query}` : ""}`); - } - - async getExpense(expenseId: string) { - return this.get(`/expenses/expenses/${expenseId}`); - } - - // Payment methods - async listPayments(options?: { page?: number; perPage?: number }) { - const params = new URLSearchParams(); - if (options?.page) params.append("page", options.page.toString()); - if (options?.perPage) params.append("per_page", options.perPage.toString()); - const query = params.toString(); - return this.get(`/payments/payments${query ? `?${query}` : ""}`); - } - - async getPayment(paymentId: string) { - return this.get(`/payments/payments/${paymentId}`); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_invoices", - description: "List invoices from FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - page: { type: "number", description: "Page number (default 1)" }, - per_page: { type: "number", description: "Results per page (default 15)" }, - status: { - type: "string", - description: "Filter by status", - enum: ["draft", "sent", "viewed", "paid", "overdue", "disputed"] - }, - }, - }, - }, - { - name: "get_invoice", - description: "Get a specific invoice by ID", - inputSchema: { - type: "object" as const, - properties: { - invoice_id: { type: "string", description: "Invoice ID" }, - }, - required: ["invoice_id"], - }, - }, - { - name: "create_invoice", - description: "Create a new invoice in FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - customer_id: { type: "number", description: "Client/customer ID" }, - create_date: { type: "string", description: "Invoice date (YYYY-MM-DD)" }, - due_offset_days: { type: "number", description: "Days until due (default 30)" }, - currency_code: { type: "string", description: "Currency code (e.g., USD, CAD)" }, - notes: { type: "string", description: "Invoice notes" }, - terms: { type: "string", description: "Payment terms" }, - lines: { - type: "array", - description: "Invoice line items", - items: { - type: "object", - properties: { - name: { type: "string", description: "Item name" }, - description: { type: "string", description: "Item description" }, - qty: { type: "number", description: "Quantity" }, - unit_cost: { type: "string", description: "Unit cost as string (e.g., '100.00')" }, - }, - required: ["name", "qty", "unit_cost"], - }, - }, - }, - required: ["customer_id", "create_date"], - }, - }, - { - name: "send_invoice", - description: "Send an invoice to the client via email", - inputSchema: { - type: "object" as const, - properties: { - invoice_id: { type: "string", description: "Invoice ID to send" }, - email_recipients: { - type: "array", - items: { type: "string" }, - description: "Email addresses to send to" - }, - email_subject: { type: "string", description: "Email subject line" }, - email_body: { type: "string", description: "Email body message" }, - }, - required: ["invoice_id"], - }, - }, - { - name: "list_clients", - description: "List all clients from FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - page: { type: "number", description: "Page number (default 1)" }, - per_page: { type: "number", description: "Results per page (default 15)" }, - }, - }, - }, - { - name: "create_client", - description: "Create a new client in FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - email: { type: "string", description: "Client email" }, - fname: { type: "string", description: "First name" }, - lname: { type: "string", description: "Last name" }, - organization: { type: "string", description: "Company/organization name" }, - p_street: { type: "string", description: "Street address" }, - p_city: { type: "string", description: "City" }, - p_province: { type: "string", description: "State/Province" }, - p_code: { type: "string", description: "Postal/ZIP code" }, - p_country: { type: "string", description: "Country" }, - currency_code: { type: "string", description: "Currency code (e.g., USD)" }, - bus_phone: { type: "string", description: "Business phone" }, - note: { type: "string", description: "Notes about client" }, - }, - }, - }, - { - name: "list_expenses", - description: "List expenses from FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - page: { type: "number", description: "Page number (default 1)" }, - per_page: { type: "number", description: "Results per page (default 15)" }, - }, - }, - }, - { - name: "list_payments", - description: "List payments received in FreshBooks", - inputSchema: { - type: "object" as const, - properties: { - page: { type: "number", description: "Page number (default 1)" }, - per_page: { type: "number", description: "Results per page (default 15)" }, - }, - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: FreshBooksClient, name: string, args: any) { - switch (name) { - case "list_invoices": { - return await client.listInvoices({ - page: args.page, - perPage: args.per_page, - status: args.status, - }); - } - case "get_invoice": { - return await client.getInvoice(args.invoice_id); - } - case "create_invoice": { - const lines = args.lines?.map((line: any) => ({ - name: line.name, - description: line.description, - qty: line.qty, - unit_cost: { amount: line.unit_cost, code: args.currency_code || "USD" }, - })); - - return await client.createInvoice({ - customerid: args.customer_id, - create_date: args.create_date, - due_offset_days: args.due_offset_days || 30, - currency_code: args.currency_code, - notes: args.notes, - terms: args.terms, - lines, - }); - } - case "send_invoice": { - return await client.sendInvoice(args.invoice_id, { - email_recipients: args.email_recipients, - email_subject: args.email_subject, - email_body: args.email_body, - action_email: true, - }); - } - case "list_clients": { - return await client.listClients({ - page: args.page, - perPage: args.per_page, - }); - } - case "create_client": { - return await client.createClient({ - email: args.email, - fname: args.fname, - lname: args.lname, - organization: args.organization, - p_street: args.p_street, - p_city: args.p_city, - p_province: args.p_province, - p_code: args.p_code, - p_country: args.p_country, - currency_code: args.currency_code, - bus_phone: args.bus_phone, - note: args.note, - }); - } - case "list_expenses": { - return await client.listExpenses({ - page: args.page, - perPage: args.per_page, - }); - } - case "list_payments": { - return await client.listPayments({ - page: args.page, - perPage: args.per_page, - }); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN; - const accountId = process.env.FRESHBOOKS_ACCOUNT_ID; - - if (!accessToken) { - console.error("Error: FRESHBOOKS_ACCESS_TOKEN environment variable required"); - process.exit(1); - } - if (!accountId) { - console.error("Error: FRESHBOOKS_ACCOUNT_ID environment variable required"); - process.exit(1); - } - - const client = new FreshBooksClient(accessToken, accountId); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - // List available tools - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - // Handle tool calls - 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, - }; - } - }); - - // Start server - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/freshbooks/src/main.ts b/servers/freshbooks/src/main.ts new file mode 100644 index 0000000..43e0255 --- /dev/null +++ b/servers/freshbooks/src/main.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env node +import { FreshBooksServer } from './server.js'; + +async function main() { + try { + const server = new FreshBooksServer(); + await server.run(); + } catch (error) { + console.error('Fatal error starting FreshBooks MCP server:', error); + process.exit(1); + } +} + +main(); diff --git a/servers/freshbooks/src/server.ts b/servers/freshbooks/src/server.ts new file mode 100644 index 0000000..0ef9960 --- /dev/null +++ b/servers/freshbooks/src/server.ts @@ -0,0 +1,147 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { FreshBooksClient } from './clients/freshbooks.js'; +import { invoicesTools } from './tools/invoices-tools.js'; +import { clientsTools } from './tools/clients-tools.js'; +import { expensesTools } from './tools/expenses-tools.js'; +import { estimatesTools } from './tools/estimates-tools.js'; +import { timeEntriesTools } from './tools/time-entries-tools.js'; +import { projectsTools } from './tools/projects-tools.js'; +import { paymentsTools } from './tools/payments-tools.js'; +import { itemsTools } from './tools/items-tools.js'; +import { taxesTools } from './tools/taxes-tools.js'; +import { reportsTools } from './tools/reports-tools.js'; +import { recurringTools } from './tools/recurring-tools.js'; +import { accountsTools } from './tools/accounts-tools.js'; + +export class FreshBooksServer { + private server: Server; + private client: FreshBooksClient; + private allTools: any[]; + + constructor() { + this.server = new Server( + { + name: 'freshbooks-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + // Initialize FreshBooks client from env + const accountId = process.env.FRESHBOOKS_ACCOUNT_ID; + const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN; + + if (!accountId || !bearerToken) { + throw new Error( + 'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN' + ); + } + + this.client = new FreshBooksClient({ + accountId, + bearerToken, + }); + + // Combine all tools + this.allTools = [ + ...invoicesTools, + ...clientsTools, + ...expensesTools, + ...estimatesTools, + ...timeEntriesTools, + ...projectsTools, + ...paymentsTools, + ...itemsTools, + ...taxesTools, + ...reportsTools, + ...recurringTools, + ...accountsTools, + ]; + + this.setupHandlers(); + + // Error handling + this.server.onerror = (error) => { + console.error('[MCP Error]', error); + }; + + process.on('SIGINT', async () => { + await this.server.close(); + process.exit(0); + }); + } + + private setupHandlers() { + // List tools handler + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: this.allTools.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: { + type: 'object', + properties: tool.inputSchema.shape, + required: Object.keys(tool.inputSchema.shape).filter( + (key) => !tool.inputSchema.shape[key].isOptional() + ), + }, + })), + }; + }); + + // Call tool handler + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const toolName = request.params.name; + const tool = this.allTools.find((t) => t.name === toolName); + + if (!tool) { + throw new Error(`Unknown tool: ${toolName}`); + } + + try { + // Validate input + const validatedArgs = tool.inputSchema.parse(request.params.arguments); + + // Execute tool + const result = await tool.handler(validatedArgs, this.client); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error: any) { + const errorMessage = error.message || 'Unknown error'; + const errorDetails = error.errors ? JSON.stringify(error.errors, null, 2) : ''; + + return { + content: [ + { + type: 'text', + text: `Error: ${errorMessage}\n${errorDetails}`, + }, + ], + isError: true, + }; + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('FreshBooks MCP server running on stdio'); + } +} diff --git a/servers/freshbooks/src/tools/accounts-tools.ts b/servers/freshbooks/src/tools/accounts-tools.ts new file mode 100644 index 0000000..ba299ae --- /dev/null +++ b/servers/freshbooks/src/tools/accounts-tools.ts @@ -0,0 +1,51 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Account, StaffMember } from '../types/index.js'; + +export const accountsTools = [ + { + name: 'freshbooks_get_account', + description: 'Get current account details', + inputSchema: z.object({}), + handler: async (_args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { account: Account } } }>( + `/users/me` + ); + return response.response.result.account; + }, + }, + + { + name: 'freshbooks_list_staff', + description: 'List all staff members', + inputSchema: z.object({ + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.getPaginated<{ staff: StaffMember[] }>( + '/users/staff', + args.page, + args.per_page + ); + return { + staff: response.response.result.staff || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_current_user', + description: 'Get current user (self) details', + inputSchema: z.object({}), + handler: async (_args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: any } }>( + '/auth/api/v1/users/me' + ); + return response.response.result; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/clients-tools.ts b/servers/freshbooks/src/tools/clients-tools.ts new file mode 100644 index 0000000..8a1068d --- /dev/null +++ b/servers/freshbooks/src/tools/clients-tools.ts @@ -0,0 +1,137 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Client, ClientContact } from '../types/index.js'; + +export const clientsTools = [ + { + name: 'freshbooks_list_clients', + description: 'List all clients with optional search', + inputSchema: z.object({ + search: z.string().optional().describe('Search by name, email, or organization'), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.search) { + params.search = { email_like: `%${args.search}%` }; + } + + const response = await client.getPaginated<{ clients: Client[] }>( + '/users/clients', + args.page, + args.per_page, + params + ); + return { + clients: response.response.result.clients || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_client', + description: 'Get a single client by ID', + inputSchema: z.object({ + client_id: z.number().describe('Client ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { client: Client } } }>( + `/users/clients/${args.client_id}` + ); + return response.response.result.client; + }, + }, + + { + name: 'freshbooks_create_client', + description: 'Create a new client', + inputSchema: z.object({ + fname: z.string().describe('First name'), + lname: z.string().describe('Last name'), + email: z.string().email().describe('Email address'), + organization: z.string().optional().describe('Company/organization name'), + phone: z.string().optional(), + mobile: z.string().optional(), + bill_street: z.string().optional(), + bill_city: z.string().optional(), + bill_state: z.string().optional(), + bill_country: z.string().optional(), + bill_postal_code: z.string().optional(), + currency_code: z.string().default('USD'), + language: z.string().default('en'), + note: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const clientData = { client: { ...args } }; + + const response = await client.post<{ response: { result: { client: Client } } }>( + '/users/clients', + clientData + ); + return response.response.result.client; + }, + }, + + { + name: 'freshbooks_update_client', + description: 'Update an existing client', + inputSchema: z.object({ + client_id: z.number().describe('Client ID'), + fname: z.string().optional(), + lname: z.string().optional(), + email: z.string().email().optional(), + organization: z.string().optional(), + phone: z.string().optional(), + mobile: z.string().optional(), + bill_street: z.string().optional(), + bill_city: z.string().optional(), + bill_state: z.string().optional(), + bill_country: z.string().optional(), + bill_postal_code: z.string().optional(), + note: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { client_id, ...updateFields } = args; + const clientData = { client: updateFields }; + + const response = await client.put<{ response: { result: { client: Client } } }>( + `/users/clients/${client_id}`, + clientData + ); + return response.response.result.client; + }, + }, + + { + name: 'freshbooks_delete_client', + description: 'Delete (archive) a client', + inputSchema: z.object({ + client_id: z.number().describe('Client ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/users/clients/${args.client_id}`, + { client: { vis_state: 1 } } + ); + return { success: true, message: `Client ${args.client_id} archived` }; + }, + }, + + { + name: 'freshbooks_list_client_contacts', + description: 'List all contacts for a specific client', + inputSchema: z.object({ + client_id: z.number().describe('Client ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { contacts: ClientContact[] } } }>( + `/users/clients/${args.client_id}/contacts` + ); + return response.response.result.contacts || []; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/estimates-tools.ts b/servers/freshbooks/src/tools/estimates-tools.ts new file mode 100644 index 0000000..30ee14d --- /dev/null +++ b/servers/freshbooks/src/tools/estimates-tools.ts @@ -0,0 +1,194 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Estimate } from '../types/index.js'; + +export const estimatesTools = [ + { + name: 'freshbooks_list_estimates', + description: 'List all estimates with optional filtering', + inputSchema: z.object({ + clientid: z.number().optional().describe('Filter by client ID'), + status: z.enum(['draft', 'sent', 'accepted', 'declined']).optional(), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.clientid) params.clientid = args.clientid; + if (args.status) params.status = args.status; + + const response = await client.getPaginated<{ estimates: Estimate[] }>( + '/estimates/estimates', + args.page, + args.per_page, + params + ); + return { + estimates: response.response.result.estimates || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_estimate', + description: 'Get a single estimate by ID', + inputSchema: z.object({ + estimate_id: z.number().describe('Estimate ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { estimate: Estimate } } }>( + `/estimates/estimates/${args.estimate_id}` + ); + return response.response.result.estimate; + }, + }, + + { + name: 'freshbooks_create_estimate', + description: 'Create a new estimate', + inputSchema: z.object({ + clientid: z.number().describe('Client ID'), + create_date: z.string().optional().describe('Estimate 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'), + })).describe('Estimate line items'), + currency_code: z.string().default('USD'), + notes: z.string().optional(), + terms: z.string().optional(), + }), + 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 estimateData = { + estimate: { + clientid: args.clientid, + create_date: args.create_date || new Date().toISOString().split('T')[0], + currency_code: args.currency_code, + lines, + notes: args.notes, + terms: args.terms, + }, + }; + + const response = await client.post<{ response: { result: { estimate: Estimate } } }>( + '/estimates/estimates', + estimateData + ); + return response.response.result.estimate; + }, + }, + + { + name: 'freshbooks_update_estimate', + description: 'Update an existing estimate', + inputSchema: z.object({ + estimate_id: z.number().describe('Estimate ID'), + clientid: z.number().optional(), + create_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 { estimate_id, ...updateFields } = args; + + if (updateFields.lines) { + updateFields.lines = updateFields.lines.map((line: any) => ({ + ...line, + unit_cost: { amount: line.unit_cost, code: 'USD' }, + })); + } + + const estimateData = { estimate: updateFields }; + const response = await client.put<{ response: { result: { estimate: Estimate } } }>( + `/estimates/estimates/${estimate_id}`, + estimateData + ); + return response.response.result.estimate; + }, + }, + + { + name: 'freshbooks_delete_estimate', + description: 'Delete (archive) an estimate', + inputSchema: z.object({ + estimate_id: z.number().describe('Estimate ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/estimates/estimates/${args.estimate_id}`, + { estimate: { vis_state: 1 } } + ); + return { success: true, message: `Estimate ${args.estimate_id} deleted` }; + }, + }, + + { + name: 'freshbooks_send_estimate', + description: 'Send an estimate to the client via email', + inputSchema: z.object({ + estimate_id: z.number().describe('Estimate ID'), + email_subject: z.string().optional(), + email_body: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const emailData: any = { estimate: { action_email: true } }; + if (args.email_subject) emailData.estimate.email_subject = args.email_subject; + if (args.email_body) emailData.estimate.email_body = args.email_body; + + await client.put( + `/estimates/estimates/${args.estimate_id}`, + emailData + ); + return { success: true, message: `Estimate ${args.estimate_id} sent` }; + }, + }, + + { + name: 'freshbooks_convert_estimate_to_invoice', + description: 'Convert an estimate to an invoice', + inputSchema: z.object({ + estimate_id: z.number().describe('Estimate ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + // Get the estimate first + const estimateResp = await client.get<{ response: { result: { estimate: Estimate } } }>( + `/estimates/estimates/${args.estimate_id}` + ); + const estimate = estimateResp.response.result.estimate; + + // Create invoice from estimate + const invoiceData = { + invoice: { + clientid: estimate.clientid, + create_date: new Date().toISOString().split('T')[0], + currency_code: estimate.currency_code, + lines: estimate.lines, + notes: estimate.notes, + terms: estimate.terms, + estimateid: args.estimate_id, + }, + }; + + const response = await client.post( + '/invoices/invoices', + invoiceData + ); + return response; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/expenses-tools.ts b/servers/freshbooks/src/tools/expenses-tools.ts new file mode 100644 index 0000000..4c9cafa --- /dev/null +++ b/servers/freshbooks/src/tools/expenses-tools.ts @@ -0,0 +1,139 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Expense, ExpenseCategory } from '../types/index.js'; + +export const expensesTools = [ + { + name: 'freshbooks_list_expenses', + description: 'List all expenses with optional filtering', + inputSchema: z.object({ + clientid: z.number().optional().describe('Filter by client ID'), + category_id: z.number().optional().describe('Filter by category ID'), + projectid: z.number().optional().describe('Filter by project ID'), + date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), + date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.clientid) params.clientid = args.clientid; + if (args.category_id) params.categoryid = args.category_id; + if (args.projectid) params.projectid = args.projectid; + 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<{ expenses: Expense[] }>( + '/expenses/expenses', + args.page, + args.per_page, + params + ); + return { + expenses: response.response.result.expenses || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_expense', + description: 'Get a single expense by ID', + inputSchema: z.object({ + expense_id: z.number().describe('Expense ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { expense: Expense } } }>( + `/expenses/expenses/${args.expense_id}` + ); + return response.response.result.expense; + }, + }, + + { + name: 'freshbooks_create_expense', + description: 'Create a new expense', + inputSchema: z.object({ + category_id: z.number().describe('Expense category ID'), + vendor: z.string().describe('Vendor name'), + amount: z.string().describe('Expense amount'), + date: z.string().describe('Expense date (YYYY-MM-DD)'), + clientid: z.number().optional().describe('Associated client ID'), + projectid: z.number().optional().describe('Associated project ID'), + notes: z.string().optional(), + taxName1: z.string().optional(), + taxPercent1: z.number().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const expenseData = { + expense: { + ...args, + amount: { amount: args.amount, code: 'USD' }, + }, + }; + + const response = await client.post<{ response: { result: { expense: Expense } } }>( + '/expenses/expenses', + expenseData + ); + return response.response.result.expense; + }, + }, + + { + name: 'freshbooks_update_expense', + description: 'Update an existing expense', + inputSchema: z.object({ + expense_id: z.number().describe('Expense ID'), + category_id: z.number().optional(), + vendor: z.string().optional(), + amount: z.string().optional(), + date: z.string().optional(), + clientid: z.number().optional(), + projectid: z.number().optional(), + notes: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { expense_id, ...updateFields } = args; + if (updateFields.amount) { + updateFields.amount = { amount: updateFields.amount, code: 'USD' }; + } + + const expenseData = { expense: updateFields }; + const response = await client.put<{ response: { result: { expense: Expense } } }>( + `/expenses/expenses/${expense_id}`, + expenseData + ); + return response.response.result.expense; + }, + }, + + { + name: 'freshbooks_delete_expense', + description: 'Delete an expense', + inputSchema: z.object({ + expense_id: z.number().describe('Expense ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/expenses/expenses/${args.expense_id}`, + { expense: { vis_state: 1 } } + ); + return { success: true, message: `Expense ${args.expense_id} deleted` }; + }, + }, + + { + name: 'freshbooks_list_expense_categories', + description: 'List all expense categories', + inputSchema: z.object({}), + handler: async (_args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { categories: ExpenseCategory[] } } }>( + '/expenses/categories' + ); + return response.response.result.categories || []; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/invoices-tools.ts b/servers/freshbooks/src/tools/invoices-tools.ts new file mode 100644 index 0000000..4202c24 --- /dev/null +++ b/servers/freshbooks/src/tools/invoices-tools.ts @@ -0,0 +1,268 @@ +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 = {}; + 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; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/items-tools.ts b/servers/freshbooks/src/tools/items-tools.ts new file mode 100644 index 0000000..2b8d8e8 --- /dev/null +++ b/servers/freshbooks/src/tools/items-tools.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Item } from '../types/index.js'; + +export const itemsTools = [ + { + name: 'freshbooks_list_items', + description: 'List all items (products/services)', + inputSchema: z.object({ + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.getPaginated<{ items: Item[] }>( + '/items/items', + args.page, + args.per_page + ); + return { + items: response.response.result.items || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_item', + description: 'Get a single item by ID', + inputSchema: z.object({ + item_id: z.number().describe('Item ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { item: Item } } }>( + `/items/items/${args.item_id}` + ); + return response.response.result.item; + }, + }, + + { + name: 'freshbooks_create_item', + description: 'Create a new item (product or service)', + inputSchema: z.object({ + name: z.string().describe('Item name'), + description: z.string().optional(), + qty: z.number().optional().describe('Quantity on hand'), + inventory: z.number().optional(), + unit_cost: z.string().optional().describe('Unit cost'), + tax1: z.number().optional().describe('Tax 1 ID'), + tax2: z.number().optional().describe('Tax 2 ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const itemData: any = { item: { ...args } }; + if (itemData.item.unit_cost) { + itemData.item.unit_cost = { amount: itemData.item.unit_cost, code: 'USD' }; + } + + const response = await client.post<{ response: { result: { item: Item } } }>( + '/items/items', + itemData + ); + return response.response.result.item; + }, + }, + + { + name: 'freshbooks_update_item', + description: 'Update an existing item', + inputSchema: z.object({ + item_id: z.number().describe('Item ID'), + name: z.string().optional(), + description: z.string().optional(), + qty: z.number().optional(), + inventory: z.number().optional(), + unit_cost: z.string().optional(), + tax1: z.number().optional(), + tax2: z.number().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { item_id, ...updateFields } = args; + if (updateFields.unit_cost) { + updateFields.unit_cost = { amount: updateFields.unit_cost, code: 'USD' }; + } + + const itemData = { item: updateFields }; + const response = await client.put<{ response: { result: { item: Item } } }>( + `/items/items/${item_id}`, + itemData + ); + return response.response.result.item; + }, + }, + + { + name: 'freshbooks_delete_item', + description: 'Delete an item', + inputSchema: z.object({ + item_id: z.number().describe('Item ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/items/items/${args.item_id}`, + { item: { vis_state: 1 } } + ); + return { success: true, message: `Item ${args.item_id} deleted` }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/payments-tools.ts b/servers/freshbooks/src/tools/payments-tools.ts new file mode 100644 index 0000000..079a914 --- /dev/null +++ b/servers/freshbooks/src/tools/payments-tools.ts @@ -0,0 +1,121 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Payment } from '../types/index.js'; + +export const paymentsTools = [ + { + name: 'freshbooks_list_payments', + description: 'List all payments with optional filtering', + inputSchema: z.object({ + invoiceid: z.number().optional().describe('Filter by invoice ID'), + clientid: z.number().optional().describe('Filter by client ID'), + date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), + date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.invoiceid) params.invoiceid = args.invoiceid; + if (args.clientid) params.clientid = args.clientid; + 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<{ payments: Payment[] }>( + '/payments/payments', + args.page, + args.per_page, + params + ); + return { + payments: response.response.result.payments || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_payment', + description: 'Get a single payment by ID', + inputSchema: z.object({ + payment_id: z.number().describe('Payment ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { payment: Payment } } }>( + `/payments/payments/${args.payment_id}` + ); + return response.response.result.payment; + }, + }, + + { + name: 'freshbooks_create_payment', + description: 'Create a new payment', + inputSchema: z.object({ + invoiceid: 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.invoiceid, + 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; + }, + }, + + { + name: 'freshbooks_update_payment', + description: 'Update an existing payment', + inputSchema: z.object({ + payment_id: z.number().describe('Payment ID'), + amount: z.string().optional(), + date: z.string().optional(), + type: z.string().optional(), + note: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { payment_id, ...updateFields } = args; + if (updateFields.amount) { + updateFields.amount = { amount: updateFields.amount, code: 'USD' }; + } + + const paymentData = { payment: updateFields }; + const response = await client.put<{ response: { result: { payment: Payment } } }>( + `/payments/payments/${payment_id}`, + paymentData + ); + return response.response.result.payment; + }, + }, + + { + name: 'freshbooks_delete_payment', + description: 'Delete a payment', + inputSchema: z.object({ + payment_id: z.number().describe('Payment ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/payments/payments/${args.payment_id}`, + { payment: { vis_state: 1 } } + ); + return { success: true, message: `Payment ${args.payment_id} deleted` }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/projects-tools.ts b/servers/freshbooks/src/tools/projects-tools.ts new file mode 100644 index 0000000..cad1826 --- /dev/null +++ b/servers/freshbooks/src/tools/projects-tools.ts @@ -0,0 +1,124 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Project, ProjectService } from '../types/index.js'; + +export const projectsTools = [ + { + name: 'freshbooks_list_projects', + description: 'List all projects with optional filtering', + inputSchema: z.object({ + client_id: z.number().optional().describe('Filter by client ID'), + active: z.boolean().optional().describe('Filter by active status'), + complete: z.boolean().optional().describe('Filter by completion status'), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.client_id !== undefined) params.client_id = args.client_id; + if (args.active !== undefined) params.active = args.active; + if (args.complete !== undefined) params.complete = args.complete; + + const response = await client.getPaginated<{ projects: Project[] }>( + '/projects/business/123/projects', + args.page, + args.per_page, + params + ); + return { + projects: response.response.result.projects || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_project', + description: 'Get a single project by ID', + inputSchema: z.object({ + project_id: z.number().describe('Project ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { project: Project } } }>( + `/projects/business/123/projects/${args.project_id}` + ); + return response.response.result.project; + }, + }, + + { + name: 'freshbooks_create_project', + description: 'Create a new project', + inputSchema: z.object({ + title: z.string().describe('Project title'), + description: z.string().optional(), + client_id: z.number().optional().describe('Associated client ID'), + due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'), + project_type: z.enum(['fixed_price', 'hourly_rate']).default('hourly_rate'), + fixed_price: z.string().optional().describe('Fixed price amount'), + billing_method: z.enum(['project_rate', 'service_rate', 'team_member_rate']).optional(), + rate: z.string().optional().describe('Hourly rate'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const projectData = { project: { ...args } }; + + const response = await client.post<{ response: { result: { project: Project } } }>( + '/projects/business/123/projects', + projectData + ); + return response.response.result.project; + }, + }, + + { + name: 'freshbooks_update_project', + description: 'Update an existing project', + inputSchema: z.object({ + project_id: z.number().describe('Project ID'), + title: z.string().optional(), + description: z.string().optional(), + client_id: z.number().optional(), + due_date: z.string().optional(), + active: z.boolean().optional(), + complete: z.boolean().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { project_id, ...updateFields } = args; + const projectData = { project: updateFields }; + + const response = await client.put<{ response: { result: { project: Project } } }>( + `/projects/business/123/projects/${project_id}`, + projectData + ); + return response.response.result.project; + }, + }, + + { + name: 'freshbooks_delete_project', + description: 'Delete a project', + inputSchema: z.object({ + project_id: z.number().describe('Project ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.delete( + `/projects/business/123/projects/${args.project_id}` + ); + return { success: true, message: `Project ${args.project_id} deleted` }; + }, + }, + + { + name: 'freshbooks_list_project_services', + description: 'List all available services for time tracking', + inputSchema: z.object({}), + handler: async (_args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { services: ProjectService[] } } }>( + '/projects/business/123/services' + ); + return response.response.result.services || []; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/recurring-tools.ts b/servers/freshbooks/src/tools/recurring-tools.ts new file mode 100644 index 0000000..ed8ab34 --- /dev/null +++ b/servers/freshbooks/src/tools/recurring-tools.ts @@ -0,0 +1,140 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { RecurringProfile } from '../types/index.js'; + +export const recurringTools = [ + { + name: 'freshbooks_list_recurring_profiles', + description: 'List all recurring invoice profiles', + inputSchema: z.object({ + clientid: z.number().optional().describe('Filter by client ID'), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.clientid) params.clientid = args.clientid; + + const response = await client.getPaginated<{ recurring: RecurringProfile[] }>( + '/invoices/recurring', + args.page, + args.per_page, + params + ); + return { + recurring: response.response.result.recurring || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_recurring_profile', + description: 'Get a single recurring profile by ID', + inputSchema: z.object({ + recurring_id: z.number().describe('Recurring profile ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { recurring: RecurringProfile } } }>( + `/invoices/recurring/${args.recurring_id}` + ); + return response.response.result.recurring; + }, + }, + + { + name: 'freshbooks_create_recurring_profile', + description: 'Create a new recurring invoice profile', + inputSchema: z.object({ + clientid: z.number().describe('Client ID'), + frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).describe('Billing frequency'), + numberRecurring: z.number().optional().describe('Number of times to recur (0 = indefinite)'), + 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'), + })).describe('Invoice line items'), + currency_code: z.string().default('USD'), + notes: z.string().optional(), + terms: z.string().optional(), + }), + 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 recurringData = { + recurring: { + clientid: args.clientid, + frequency: args.frequency, + numberRecurring: args.numberRecurring || 0, + create_date: new Date().toISOString().split('T')[0], + currency_code: args.currency_code, + lines, + notes: args.notes, + terms: args.terms, + }, + }; + + const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>( + '/invoices/recurring', + recurringData + ); + return response.response.result.recurring; + }, + }, + + { + name: 'freshbooks_update_recurring_profile', + description: 'Update an existing recurring profile', + inputSchema: z.object({ + recurring_id: z.number().describe('Recurring profile ID'), + frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).optional(), + numberRecurring: z.number().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 { recurring_id, ...updateFields } = args; + + if (updateFields.lines) { + updateFields.lines = updateFields.lines.map((line: any) => ({ + ...line, + unit_cost: { amount: line.unit_cost, code: 'USD' }, + })); + } + + const recurringData = { recurring: updateFields }; + const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>( + `/invoices/recurring/${recurring_id}`, + recurringData + ); + return response.response.result.recurring; + }, + }, + + { + name: 'freshbooks_delete_recurring_profile', + description: 'Delete a recurring profile', + inputSchema: z.object({ + recurring_id: z.number().describe('Recurring profile ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/invoices/recurring/${args.recurring_id}`, + { recurring: { vis_state: 1 } } + ); + return { success: true, message: `Recurring profile ${args.recurring_id} deleted` }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/reports-tools.ts b/servers/freshbooks/src/tools/reports-tools.ts new file mode 100644 index 0000000..f3cbae3 --- /dev/null +++ b/servers/freshbooks/src/tools/reports-tools.ts @@ -0,0 +1,112 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js'; + +export const reportsTools = [ + { + name: 'freshbooks_profit_loss_report', + description: 'Generate profit and loss report for a date range', + inputSchema: z.object({ + start_date: z.string().describe('Start date (YYYY-MM-DD)'), + end_date: z.string().describe('End date (YYYY-MM-DD)'), + currency_code: z.string().default('USD'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: ProfitLossReport } }>( + '/reports/accounting/profitloss', + { + start_date: args.start_date, + end_date: args.end_date, + currency_code: args.currency_code, + } + ); + return response.response.result; + }, + }, + + { + name: 'freshbooks_tax_summary_report', + description: 'Generate tax summary report for a date range', + inputSchema: z.object({ + start_date: z.string().describe('Start date (YYYY-MM-DD)'), + end_date: z.string().describe('End date (YYYY-MM-DD)'), + currency_code: z.string().default('USD'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { taxsummaries: TaxSummary[] } } }>( + '/reports/accounting/taxsummary', + { + start_date: args.start_date, + end_date: args.end_date, + currency_code: args.currency_code, + } + ); + return response.response.result.taxsummaries || []; + }, + }, + + { + name: 'freshbooks_accounts_aging_report', + description: 'Generate accounts aging report (accounts receivable)', + inputSchema: z.object({ + date: z.string().optional().describe('Report date (YYYY-MM-DD, defaults to today)'), + currency_code: z.string().default('USD'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { clients: AccountsAgingReport[] } } }>( + '/reports/accounting/aging', + { + date: args.date || new Date().toISOString().split('T')[0], + currency_code: args.currency_code, + } + ); + return response.response.result.clients || []; + }, + }, + + { + name: 'freshbooks_expense_report', + description: 'Generate expense report for a date range', + inputSchema: z.object({ + start_date: z.string().describe('Start date (YYYY-MM-DD)'), + end_date: z.string().describe('End date (YYYY-MM-DD)'), + clientid: z.number().optional().describe('Filter by client ID'), + categoryid: z.number().optional().describe('Filter by category ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: any = { + start_date: args.start_date, + end_date: args.end_date, + }; + if (args.clientid) params.clientid = args.clientid; + if (args.categoryid) params.categoryid = args.categoryid; + + const response = await client.get<{ response: { result: any } }>( + '/reports/accounting/expenses', + params + ); + return response.response.result; + }, + }, + + { + name: 'freshbooks_revenue_by_client_report', + description: 'Generate revenue by client report for a date range', + inputSchema: z.object({ + start_date: z.string().describe('Start date (YYYY-MM-DD)'), + end_date: z.string().describe('End date (YYYY-MM-DD)'), + currency_code: z.string().default('USD'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: any } }>( + '/reports/accounting/revenue_by_client', + { + start_date: args.start_date, + end_date: args.end_date, + currency_code: args.currency_code, + } + ); + return response.response.result; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/taxes-tools.ts b/servers/freshbooks/src/tools/taxes-tools.ts new file mode 100644 index 0000000..bdf08b1 --- /dev/null +++ b/servers/freshbooks/src/tools/taxes-tools.ts @@ -0,0 +1,96 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { Tax } from '../types/index.js'; + +export const taxesTools = [ + { + name: 'freshbooks_list_taxes', + description: 'List all taxes', + inputSchema: z.object({ + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.getPaginated<{ taxes: Tax[] }>( + '/taxes/taxes', + args.page, + args.per_page + ); + return { + taxes: response.response.result.taxes || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_tax', + description: 'Get a single tax by ID', + inputSchema: z.object({ + tax_id: z.number().describe('Tax ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { tax: Tax } } }>( + `/taxes/taxes/${args.tax_id}` + ); + return response.response.result.tax; + }, + }, + + { + name: 'freshbooks_create_tax', + description: 'Create a new tax', + inputSchema: z.object({ + name: z.string().describe('Tax name (e.g., "GST", "VAT")'), + number: z.string().optional().describe('Tax number/registration'), + amount: z.string().describe('Tax percentage (e.g., "13" for 13%)'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const taxData = { tax: { ...args } }; + + const response = await client.post<{ response: { result: { tax: Tax } } }>( + '/taxes/taxes', + taxData + ); + return response.response.result.tax; + }, + }, + + { + name: 'freshbooks_update_tax', + description: 'Update an existing tax', + inputSchema: z.object({ + tax_id: z.number().describe('Tax ID'), + name: z.string().optional(), + number: z.string().optional(), + amount: z.string().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { tax_id, ...updateFields } = args; + const taxData = { tax: updateFields }; + + const response = await client.put<{ response: { result: { tax: Tax } } }>( + `/taxes/taxes/${tax_id}`, + taxData + ); + return response.response.result.tax; + }, + }, + + { + name: 'freshbooks_delete_tax', + description: 'Delete a tax', + inputSchema: z.object({ + tax_id: z.number().describe('Tax ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.put( + `/taxes/taxes/${args.tax_id}`, + { tax: { vis_state: 1 } } + ); + return { success: true, message: `Tax ${args.tax_id} deleted` }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/time-entries-tools.ts b/servers/freshbooks/src/tools/time-entries-tools.ts new file mode 100644 index 0000000..9732173 --- /dev/null +++ b/servers/freshbooks/src/tools/time-entries-tools.ts @@ -0,0 +1,125 @@ +import { z } from 'zod'; +import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { TimeEntry } from '../types/index.js'; + +export const timeEntriesTools = [ + { + name: 'freshbooks_list_time_entries', + description: 'List all time entries with optional filtering', + inputSchema: z.object({ + clientid: z.number().optional().describe('Filter by client ID'), + projectid: z.number().optional().describe('Filter by project ID'), + date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), + date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), + billed_status: z.enum(['billed', 'unbilled']).optional(), + page: z.number().default(1), + per_page: z.number().default(30), + }), + handler: async (args: any, client: FreshBooksClient) => { + const params: Record = {}; + if (args.clientid) params.client_id = args.clientid; + if (args.projectid) params.project_id = args.projectid; + if (args.date_min) params.started_from = args.date_min; + if (args.date_max) params.started_to = args.date_max; + if (args.billed_status) params.billed_status = args.billed_status; + + const response = await client.getPaginated<{ time_entries: TimeEntry[] }>( + '/timetracking/business/123/time_entries', + args.page, + args.per_page, + params + ); + return { + time_entries: response.response.result.time_entries || [], + page: response.response.page, + pages: response.response.pages, + total: response.response.total, + }; + }, + }, + + { + name: 'freshbooks_get_time_entry', + description: 'Get a single time entry by ID', + inputSchema: z.object({ + time_entry_id: z.number().describe('Time entry ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + const response = await client.get<{ response: { result: { time_entry: TimeEntry } } }>( + `/timetracking/business/123/time_entries/${args.time_entry_id}` + ); + return response.response.result.time_entry; + }, + }, + + { + name: 'freshbooks_create_time_entry', + description: 'Create a new time entry', + inputSchema: z.object({ + duration: z.number().describe('Duration in seconds'), + note: z.string().optional().describe('Note/description'), + started_at: z.string().describe('Start time (ISO 8601 format)'), + clientid: z.number().optional().describe('Client ID'), + projectid: z.number().optional().describe('Project ID'), + service_id: z.number().optional().describe('Service ID'), + is_logged: z.boolean().default(true), + }), + handler: async (args: any, client: FreshBooksClient) => { + const timeEntryData = { + time_entry: { + is_logged: args.is_logged, + duration: args.duration, + note: args.note, + started_at: args.started_at, + client_id: args.clientid, + project_id: args.projectid, + service_id: args.service_id, + }, + }; + + const response = await client.post<{ response: { result: { time_entry: TimeEntry } } }>( + '/timetracking/business/123/time_entries', + timeEntryData + ); + return response.response.result.time_entry; + }, + }, + + { + name: 'freshbooks_update_time_entry', + description: 'Update an existing time entry', + inputSchema: z.object({ + time_entry_id: z.number().describe('Time entry ID'), + duration: z.number().optional(), + note: z.string().optional(), + started_at: z.string().optional(), + clientid: z.number().optional(), + projectid: z.number().optional(), + service_id: z.number().optional(), + }), + handler: async (args: any, client: FreshBooksClient) => { + const { time_entry_id, ...updateFields } = args; + const timeEntryData = { time_entry: updateFields }; + + const response = await client.put<{ response: { result: { time_entry: TimeEntry } } }>( + `/timetracking/business/123/time_entries/${time_entry_id}`, + timeEntryData + ); + return response.response.result.time_entry; + }, + }, + + { + name: 'freshbooks_delete_time_entry', + description: 'Delete a time entry', + inputSchema: z.object({ + time_entry_id: z.number().describe('Time entry ID'), + }), + handler: async (args: any, client: FreshBooksClient) => { + await client.delete( + `/timetracking/business/123/time_entries/${args.time_entry_id}` + ); + return { success: true, message: `Time entry ${args.time_entry_id} deleted` }; + }, + }, +]; diff --git a/servers/freshbooks/src/types/index.ts b/servers/freshbooks/src/types/index.ts new file mode 100644 index 0000000..5435800 --- /dev/null +++ b/servers/freshbooks/src/types/index.ts @@ -0,0 +1,349 @@ +// FreshBooks API Types + +export interface FreshBooksConfig { + accountId: string; + bearerToken: string; + baseUrl?: string; +} + +export interface PaginatedResponse { + response: { + result: T; + page: number; + pages: number; + per_page: number; + total: number; + }; +} + +export interface FreshBooksError { + message: string; + code?: string; + errors?: Array<{ field: string; message: string }>; +} + +// Client Types +export interface Client { + id: number; + accounting_systemid: string; + organization: string; + fname: string; + lname: string; + email: string; + company_industry?: string; + company_size?: string; + bill_street?: string; + bill_city?: string; + bill_state?: string; + bill_country?: string; + bill_postal_code?: string; + currency_code: string; + updated: string; + created_at: string; + language?: string; + note?: string; + vat_name?: string; + vat_number?: string; + allow_late_fees?: boolean; + allow_late_notifications?: boolean; +} + +export interface ClientContact { + id: number; + clientid: number; + fname: string; + lname: string; + email: string; + phone?: string; + mobile?: string; +} + +// Invoice Types +export interface Invoice { + id: number; + accountid: string; + accounting_systemid: string; + clientid: number; + create_date: string; + invoice_number: string; + currency_code: string; + amount: { + amount: string; + code: string; + }; + outstanding: { + amount: string; + code: string; + }; + paid: { + amount: string; + code: string; + }; + due_date: string; + status: string; + payment_status: string; + v3_status: string; + lines: InvoiceLine[]; + terms?: string; + notes?: string; + discount_total?: { + amount: string; + code: string; + }; + updated: string; + created_at: string; +} + +export interface InvoiceLine { + id?: number; + name: string; + description?: string; + qty: number; + unit_cost: { + amount: string; + code: string; + }; + amount: { + amount: string; + code: string; + }; + taxName1?: string; + taxAmount1?: string; + taxName2?: string; + taxAmount2?: string; +} + +export interface Payment { + id: number; + invoiceid: number; + amount: { + amount: string; + code: string; + }; + date: string; + type: string; + note?: string; + updated: string; + created_at: string; +} + +// Expense Types +export interface Expense { + id: number; + category_id: number; + clientid?: number; + projectid?: number; + vendor: string; + amount: { + amount: string; + code: string; + }; + date: string; + notes?: string; + taxName1?: string; + taxAmount1?: number; + taxPercent1?: number; + updated: string; + created_at: string; + staffid?: number; + status?: string; +} + +export interface ExpenseCategory { + id: number; + category: string; + is_cogs?: boolean; + is_editable?: boolean; + parentid?: number; +} + +// Estimate Types +export interface Estimate { + id: number; + accountid: string; + clientid: number; + create_date: string; + estimate_number: string; + currency_code: string; + amount: { + amount: string; + code: string; + }; + status: string; + lines: EstimateLine[]; + terms?: string; + notes?: string; + discount_total?: { + amount: string; + code: string; + }; + updated: string; + created_at: string; +} + +export interface EstimateLine { + id?: number; + name: string; + description?: string; + qty: number; + unit_cost: { + amount: string; + code: string; + }; + amount: { + amount: string; + code: string; + }; +} + +// Time Entry Types +export interface TimeEntry { + id: number; + is_logged?: boolean; + duration: number; + note?: string; + started_at: string; + clientid?: number; + projectid?: number; + service_id?: number; + billed_status?: string; + created_at: string; + updated_at: string; +} + +// Project Types +export interface Project { + id: number; + title: string; + description?: string; + client_id?: number; + due_date?: string; + project_type?: string; + fixed_price?: string; + billing_method?: string; + rate?: string; + active?: boolean; + complete?: boolean; + created_at: string; + updated_at: string; +} + +export interface ProjectService { + id: number; + business_id: number; + name: string; + billable: boolean; + rate?: { + amount: string; + code: string; + }; +} + +// Item Types +export interface Item { + id: number; + name: string; + description?: string; + qty?: number; + inventory?: number; + unit_cost?: { + amount: string; + code: string; + }; + tax1?: number; + tax2?: number; + updated: string; + created_at: string; +} + +// Tax Types +export interface Tax { + id: number; + name: string; + number?: string; + amount: string; + updated: string; + created_at: string; +} + +// Recurring Profile Types +export interface RecurringProfile { + id: number; + clientid: number; + frequency: string; + numberRecurring: number; + create_date: string; + currency_code: string; + lines: InvoiceLine[]; + status?: string; + updated: string; + created_at: string; +} + +// Account Types +export interface Account { + id: string; + account_name: string; + email: string; + business_phone?: string; + address?: { + street: string; + city: string; + province: string; + country: string; + postal_code: string; + }; +} + +export interface StaffMember { + id: number; + username: string; + first_name: string; + last_name: string; + email: string; + role?: string; +} + +// Report Types +export interface ProfitLossReport { + total_income: { + amount: string; + code: string; + }; + total_expenses: { + amount: string; + code: string; + }; + net_profit: { + amount: string; + code: string; + }; + start_date: string; + end_date: string; +} + +export interface TaxSummary { + tax_name: string; + tax_collected: { + amount: string; + code: string; + }; + tax_paid: { + amount: string; + code: string; + }; +} + +export interface AccountsAgingReport { + client_userid: number; + organization: string; + outstanding_balance: { + amount: string; + code: string; + }; + current: { amount: string; code: string }; + '1-30': { amount: string; code: string }; + '31-60': { amount: string; code: string }; + '61-90': { amount: string; code: string }; + '91+': { amount: string; code: string }; +} diff --git a/servers/freshbooks/src/ui/react-app/aging-report/index.html b/servers/freshbooks/src/ui/react-app/aging-report/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/aging-report/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/index.html b/servers/freshbooks/src/ui/react-app/client-dashboard/index.html new file mode 100644 index 0000000..bf1b97b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/index.html @@ -0,0 +1,99 @@ + + + + + + Clients Dashboard - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/client-detail/index.html b/servers/freshbooks/src/ui/react-app/client-detail/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-detail/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/client-grid/index.html b/servers/freshbooks/src/ui/react-app/client-grid/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-grid/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/index.html b/servers/freshbooks/src/ui/react-app/estimate-builder/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/estimate-grid/index.html b/servers/freshbooks/src/ui/react-app/estimate-grid/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-grid/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/expense-dashboard/index.html b/servers/freshbooks/src/ui/react-app/expense-dashboard/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-dashboard/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/index.html b/servers/freshbooks/src/ui/react-app/expense-tracker/index.html new file mode 100644 index 0000000..cdc5b97 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/index.html @@ -0,0 +1,134 @@ + + + + + + Expense Tracker - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/index.html b/servers/freshbooks/src/ui/react-app/invoice-builder/index.html new file mode 100644 index 0000000..9ff06d1 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/index.html @@ -0,0 +1,139 @@ + + + + + + Invoice Builder - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html b/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html new file mode 100644 index 0000000..f1a3df5 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html @@ -0,0 +1,154 @@ + + + + + + Invoice Dashboard - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/index.html b/servers/freshbooks/src/ui/react-app/invoice-detail/index.html new file mode 100644 index 0000000..c5e069e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/index.html @@ -0,0 +1,116 @@ + + + + + + Invoice Detail - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/invoice-grid/index.html b/servers/freshbooks/src/ui/react-app/invoice-grid/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-grid/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/payment-history/index.html b/servers/freshbooks/src/ui/react-app/payment-history/index.html new file mode 100644 index 0000000..7f5741b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/payment-history/index.html @@ -0,0 +1,86 @@ + + + + + + Payment History - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/index.html b/servers/freshbooks/src/ui/react-app/profit-loss/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/profit-loss/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/index.html b/servers/freshbooks/src/ui/react-app/project-dashboard/index.html new file mode 100644 index 0000000..278b451 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/index.html @@ -0,0 +1,92 @@ + + + + + + Projects - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/project-detail/index.html b/servers/freshbooks/src/ui/react-app/project-detail/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-detail/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html b/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html b/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html new file mode 100644 index 0000000..93650be --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html @@ -0,0 +1,57 @@ + + + + + + Reports - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/index.html b/servers/freshbooks/src/ui/react-app/revenue-chart/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/index.html b/servers/freshbooks/src/ui/react-app/tax-summary/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tax-summary/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/time-entries/index.html b/servers/freshbooks/src/ui/react-app/time-entries/index.html new file mode 100644 index 0000000..2b98197 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-entries/index.html @@ -0,0 +1,34 @@ + + + + + + TITLE - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/index.html b/servers/freshbooks/src/ui/react-app/time-tracker/index.html new file mode 100644 index 0000000..3906148 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-tracker/index.html @@ -0,0 +1,122 @@ + + + + + + Time Tracker - FreshBooks + + + + + + +
+ + + diff --git a/servers/freshbooks/tsconfig.json b/servers/freshbooks/tsconfig.json index de6431e..78fd416 100644 --- a/servers/freshbooks/tsconfig.json +++ b/servers/freshbooks/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ES2022", + "moduleResolution": "node", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]