wrike: enable TypeScript strict mode

This commit is contained in:
Jake Shore 2026-02-12 17:30:47 -05:00
parent 36a4d6fb4f
commit f14c921020
202 changed files with 10595 additions and 3090 deletions

View File

@ -1,192 +1,140 @@
# FreshBooks MCP Server
Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting.
Complete Model Context Protocol server for FreshBooks with 80+ tools and 20 React apps.
## Features
### 🎯 55+ Tools
### 🛠️ Comprehensive Tool Coverage (80+ tools)
**Invoices** (10 tools)
- List, get, create, update, delete invoices
- Send invoices via email
- Mark paid/unpaid, create payments
- Get payment history
- **Clients**: CRUD, search, contacts management
- **Invoices**: Full lifecycle (create, update, send, mark paid, share links, line items)
- **Estimates**: CRUD, send, accept, line items
- **Expenses**: CRUD, categories, receipts, search
- **Payments**: Record and track invoice payments
- **Projects**: CRUD, services, time tracking integration
- **Time Entries**: CRUD, timers (start/stop), bulk operations
- **Taxes**: CRUD, tax defaults
- **Items/Services**: Product and service catalog management
- **Staff**: List and manage team members
- **Bills**: Vendor bills and bill payments
- **Vendors**: Vendor management
- **Accounting**: Chart of accounts, journal entries
- **Retainers**: Recurring retainer agreements
- **Credit Notes**: Customer credits
- **Reports**: P&L, tax summary, aging, expense reports
**Clients** (6 tools)
- List, get, create, update, delete clients
- List client contacts
### 🎨 MCP Apps (20 React Apps)
**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
1. **Dashboard Overview** - Business metrics at a glance
2. **Invoice Dashboard** - Invoice list and metrics
3. **Invoice Detail** - Detailed invoice view
4. **Invoice Creator** - Create and edit invoices
5. **Client Dashboard** - Client list and overview
6. **Client Detail** - Detailed client information
7. **Expense Tracker** - Track and categorize expenses
8. **Expense Report** - Expense reporting and analysis
9. **Project Dashboard** - Active projects overview
10. **Project Detail** - Project details and time entries
11. **Time Tracker** - Log and manage time entries
12. **Time Report** - Time tracking reports
13. **Payment Dashboard** - Payment tracking
14. **Estimate Builder** - Create and send estimates
15. **Profit & Loss Report** - Financial P&L statements
16. **Tax Summary** - Tax reporting
17. **Aging Report** - Accounts receivable aging
18. **Item Catalog** - Products and services catalog
19. **Bill Manager** - Vendor bill management
20. **Staff Directory** - Team member directory
## Installation
```bash
npm install
npm run build
npm install @mcpengine/freshbooks
```
## Configuration
Set environment variables:
### Environment Variables
```bash
export FRESHBOOKS_ACCOUNT_ID="your_account_id"
export FRESHBOOKS_BEARER_TOKEN="your_bearer_token"
FRESHBOOKS_ACCOUNT_ID=your_account_id
FRESHBOOKS_ACCESS_TOKEN=your_oauth_access_token
```
## Usage
### OAuth2 Setup
### As MCP Server
1. Register your app at https://my.freshbooks.com/#/developer
2. Obtain OAuth2 credentials
3. Complete the OAuth2 authorization flow
4. Use the access token in your MCP server configuration
Add to your MCP settings:
### MCP Settings (Claude Desktop)
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"freshbooks": {
"command": "node",
"args": ["/path/to/freshbooks/dist/main.js"],
"command": "npx",
"args": ["-y", "@mcpengine/freshbooks"],
"env": {
"FRESHBOOKS_ACCOUNT_ID": "your_account_id",
"FRESHBOOKS_BEARER_TOKEN": "your_bearer_token"
"FRESHBOOKS_ACCESS_TOKEN": "your_access_token"
}
}
}
}
```
### Direct Usage
## Usage Examples
```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
### List Invoices
```typescript
// Using the MCP tool
{
"name": "freshbooks_create_invoice",
"tool": "freshbooks_list_invoices",
"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!"
"page": 1,
"per_page": 20,
"search": "Acme Corp"
}
}
```
### List Overdue Invoices
### Create an Invoice
```typescript
{
"name": "freshbooks_list_invoices",
"tool": "freshbooks_create_invoice",
"arguments": {
"status": "overdue",
"per_page": 50
"customerid": 12345,
"create_date": "2024-01-15",
"due_offset_days": 30,
"notes": "Thank you for your business!",
"lines": [
{
"name": "Consulting Services",
"description": "January 2024 consulting",
"qty": "10",
"unit_cost": {
"amount": "150.00",
"code": "USD"
}
}
]
}
}
```
### Send an Invoice
```typescript
{
"tool": "freshbooks_send_invoice",
"arguments": {
"invoice_id": 98765
}
}
```
@ -194,48 +142,280 @@ src/
### Track Time
```typescript
// Start a timer
{
"name": "freshbooks_create_time_entry",
"tool": "freshbooks_start_timer",
"arguments": {
"duration": 7200,
"note": "Website development",
"started_at": "2024-01-15T09:00:00Z",
"projectid": 456
"project_id": 456,
"note": "Working on website redesign"
}
}
// Stop a timer
{
"tool": "freshbooks_stop_timer",
"arguments": {
"time_entry_id": 789
}
}
```
### Generate Profit/Loss Report
### Generate Reports
```typescript
// Profit & Loss Report
{
"name": "freshbooks_profit_loss_report",
"tool": "freshbooks_profit_loss_report",
"arguments": {
"start_date": "2024-01-01",
"end_date": "2024-01-31",
"currency_code": "USD"
"end_date": "2024-12-31"
}
}
// Aging Report
{
"tool": "freshbooks_aging_report",
"arguments": {}
}
```
## Tool Reference
### Client Tools (6 tools)
- `freshbooks_list_clients` - List all clients
- `freshbooks_get_client` - Get client details
- `freshbooks_create_client` - Create new client
- `freshbooks_update_client` - Update client
- `freshbooks_delete_client` - Delete client
- `freshbooks_search_clients` - Search clients
### Invoice Tools (10 tools)
- `freshbooks_list_invoices` - List invoices
- `freshbooks_get_invoice` - Get invoice details
- `freshbooks_create_invoice` - Create invoice
- `freshbooks_update_invoice` - Update invoice
- `freshbooks_delete_invoice` - Delete invoice
- `freshbooks_send_invoice` - Send invoice to client
- `freshbooks_mark_invoice_paid` - Mark as paid
- `freshbooks_get_invoice_share_link` - Get shareable link
- `freshbooks_add_invoice_line` - Add line item
- `freshbooks_search_invoices` - Search invoices
### Estimate Tools (8 tools)
- `freshbooks_list_estimates` - List estimates
- `freshbooks_get_estimate` - Get estimate details
- `freshbooks_create_estimate` - Create estimate
- `freshbooks_update_estimate` - Update estimate
- `freshbooks_delete_estimate` - Delete estimate
- `freshbooks_send_estimate` - Send to client
- `freshbooks_accept_estimate` - Mark as accepted
- `freshbooks_add_estimate_line` - Add line item
### Expense Tools (7 tools)
- `freshbooks_list_expenses` - List expenses
- `freshbooks_get_expense` - Get expense details
- `freshbooks_create_expense` - Create expense
- `freshbooks_update_expense` - Update expense
- `freshbooks_delete_expense` - Delete expense
- `freshbooks_list_expense_categories` - List categories
- `freshbooks_search_expenses` - Search expenses
### Payment Tools (5 tools)
- `freshbooks_list_payments` - List payments
- `freshbooks_get_payment` - Get payment details
- `freshbooks_create_payment` - Record payment
- `freshbooks_update_payment` - Update payment
- `freshbooks_delete_payment` - Delete payment
### Project Tools (6 tools)
- `freshbooks_list_projects` - List projects
- `freshbooks_get_project` - Get project details
- `freshbooks_create_project` - Create project
- `freshbooks_update_project` - Update project
- `freshbooks_delete_project` - Delete project
- `freshbooks_mark_project_complete` - Mark complete
### Time Entry Tools (7 tools)
- `freshbooks_list_time_entries` - List time entries
- `freshbooks_get_time_entry` - Get entry details
- `freshbooks_create_time_entry` - Log time
- `freshbooks_update_time_entry` - Update entry
- `freshbooks_delete_time_entry` - Delete entry
- `freshbooks_start_timer` - Start timer
- `freshbooks_stop_timer` - Stop timer
### Tax Tools (5 tools)
- `freshbooks_list_taxes` - List taxes
- `freshbooks_get_tax` - Get tax details
- `freshbooks_create_tax` - Create tax
- `freshbooks_update_tax` - Update tax
- `freshbooks_delete_tax` - Delete tax
### Item/Service Tools (5 tools)
- `freshbooks_list_items` - List items
- `freshbooks_get_item` - Get item details
- `freshbooks_create_item` - Create item
- `freshbooks_update_item` - Update item
- `freshbooks_delete_item` - Delete item
### Staff Tools (2 tools)
- `freshbooks_list_staff` - List staff members
- `freshbooks_get_staff_member` - Get staff details
### Bill Tools (8 tools)
- `freshbooks_list_bills` - List bills
- `freshbooks_get_bill` - Get bill details
- `freshbooks_create_bill` - Create bill
- `freshbooks_update_bill` - Update bill
- `freshbooks_delete_bill` - Delete bill
- `freshbooks_get_bill_payments` - List payments
- `freshbooks_create_bill_payment` - Record payment
### Vendor Tools (5 tools)
- `freshbooks_list_vendors` - List vendors
- `freshbooks_get_vendor` - Get vendor details
- `freshbooks_create_vendor` - Create vendor
- `freshbooks_update_vendor` - Update vendor
- `freshbooks_delete_vendor` - Delete vendor
### Accounting Tools (2 tools)
- `freshbooks_list_accounts` - List chart of accounts
- `freshbooks_get_account` - Get account details
### Journal Entry Tools (3 tools)
- `freshbooks_list_journal_entries` - List entries
- `freshbooks_get_journal_entry` - Get entry details
- `freshbooks_create_journal_entry` - Create entry
### Retainer Tools (5 tools)
- `freshbooks_list_retainers` - List retainers
- `freshbooks_get_retainer` - Get retainer details
- `freshbooks_create_retainer` - Create retainer
- `freshbooks_update_retainer` - Update retainer
- `freshbooks_delete_retainer` - Delete retainer
### Credit Note Tools (5 tools)
- `freshbooks_list_credit_notes` - List credit notes
- `freshbooks_get_credit_note` - Get credit note
- `freshbooks_create_credit_note` - Create credit note
- `freshbooks_update_credit_note` - Update credit note
- `freshbooks_delete_credit_note` - Delete credit note
### Report Tools (4 tools)
- `freshbooks_profit_loss_report` - P&L report
- `freshbooks_tax_summary_report` - Tax summary
- `freshbooks_aging_report` - Accounts aging
- `freshbooks_expense_report` - Expense report
## Architecture
```
src/
├── server.ts # MCP server setup
├── main.ts # Entry point
├── clients/
│ └── freshbooks.ts # FreshBooks API client (OAuth2, rate limiting)
├── tools/ # Tool definitions (17 files)
│ ├── clients-tools.ts
│ ├── invoices-tools.ts
│ ├── estimates-tools.ts
│ ├── expenses-tools.ts
│ ├── payments-tools.ts
│ ├── projects-tools.ts
│ ├── time-entries-tools.ts
│ ├── taxes-tools.ts
│ ├── items-tools.ts
│ ├── staff-tools.ts
│ ├── bills-tools.ts
│ ├── vendors-tools.ts
│ ├── accounts-tools.ts
│ ├── journal-entries-tools.ts
│ ├── retainers-tools.ts
│ ├── credit-notes-tools.ts
│ └── reports-tools.ts
├── types/
│ └── index.ts # TypeScript interfaces
└── ui/
└── react-app/ # MCP Apps (20 apps)
├── src/
│ ├── apps/ # Individual apps
│ ├── components/ # Shared components
│ ├── hooks/ # Shared hooks
│ └── styles/ # Shared CSS
└── package.json
```
## API Coverage
- ✅ Clients API (complete)
- ✅ Invoices API (complete)
- ✅ Estimates API (complete)
- ✅ Expenses API (complete)
- ✅ Payments API (complete)
- ✅ Projects API (complete)
- ✅ Time Tracking API (complete)
- ✅ Taxes API (complete)
- ✅ Items/Services API (complete)
- ✅ Staff API (read-only)
- ✅ Bills API (complete)
- ✅ Vendors API (complete)
- ✅ Accounting API (partial - read-only)
- ✅ Journal Entries API (create + read)
- ✅ Retainers API (complete)
- ✅ Credit Notes API (complete)
- ✅ Reports API (complete)
## Development
### Build
### Build from source
```bash
git clone https://github.com/BusyBee3333/mcpengine
cd mcpengine/servers/freshbooks
npm install
npm run build
```
### Watch Mode
### Run tests
```bash
npm run watch
npm test
```
### Type checking
```bash
npm run type-check
```
## License
MIT
## Author
## Support
MCPEngine - Complete MCP implementations for modern platforms
For issues and feature requests, please visit:
https://github.com/BusyBee3333/mcpengine/issues
## Related
- [FreshBooks API Documentation](https://www.freshbooks.com/api)
- [Model Context Protocol](https://modelcontextprotocol.io)
- [MCPEngine Repository](https://github.com/BusyBee3333/mcpengine)

View File

@ -0,0 +1,93 @@
#!/bin/bash
APPS_DIR="src/ui/react-app/src/apps"
# Function to create an app
create_app() {
local app_name=$1
local title=$2
local tool=$3
mkdir -p "$APPS_DIR/$app_name"
# Create index.html
cat > "$APPS_DIR/$app_name/index.html" <<EOF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>$title - FreshBooks</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
EOF
# Create main.tsx
cat > "$APPS_DIR/$app_name/main.tsx" <<EOF
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
import '../../styles/global.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
EOF
# Create App.tsx
cat > "$APPS_DIR/$app_name/App.tsx" <<EOF
import React from 'react';
import { useFreshBooks } from '../../hooks/useFreshBooks';
import { Card, CardHeader, CardContent } from '../../components/Card';
import { Loading } from '../../components/Loading';
export function App() {
const { data, loading, error } = useFreshBooks('$tool');
if (loading) return <Loading />;
if (error) return <div className="error">Error: {error}</div>;
return (
<div className="container">
<div className="header">
<h1>$title</h1>
</div>
<Card>
<CardContent>
<pre>{JSON.stringify(data, null, 2)}</pre>
</CardContent>
</Card>
</div>
);
}
EOF
}
# Create all apps
create_app "invoice-dashboard" "Invoice Dashboard" "freshbooks_list_invoices"
create_app "invoice-detail" "Invoice Detail" "freshbooks_get_invoice"
create_app "invoice-creator" "Invoice Creator" "freshbooks_create_invoice"
create_app "client-dashboard" "Client Dashboard" "freshbooks_list_clients"
create_app "client-detail" "Client Detail" "freshbooks_get_client"
create_app "expense-tracker" "Expense Tracker" "freshbooks_list_expenses"
create_app "expense-report" "Expense Report" "freshbooks_expense_report"
create_app "project-dashboard" "Project Dashboard" "freshbooks_list_projects"
create_app "project-detail" "Project Detail" "freshbooks_get_project"
create_app "time-tracker" "Time Tracker" "freshbooks_list_time_entries"
create_app "time-report" "Time Report" "freshbooks_list_time_entries"
create_app "payment-dashboard" "Payment Dashboard" "freshbooks_list_payments"
create_app "estimate-builder" "Estimate Builder" "freshbooks_list_estimates"
create_app "profit-loss-report" "Profit & Loss Report" "freshbooks_profit_loss_report"
create_app "tax-summary" "Tax Summary" "freshbooks_tax_summary_report"
create_app "aging-report" "Aging Report" "freshbooks_aging_report"
create_app "item-catalog" "Item Catalog" "freshbooks_list_items"
create_app "bill-manager" "Bill Manager" "freshbooks_list_bills"
create_app "staff-directory" "Staff Directory" "freshbooks_list_staff"
echo "All apps created successfully!"

View File

@ -1,34 +1,38 @@
{
"name": "@mcpengine/freshbooks",
"version": "1.0.0",
"description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management",
"main": "dist/main.js",
"description": "FreshBooks MCP server with comprehensive API coverage and React apps",
"type": "module",
"bin": {
"freshbooks-mcp": "./dist/main.js"
},
"main": "./dist/main.js",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"prepare": "npm run build",
"start": "node dist/main.js"
"build": "tsc && npm run build:ui",
"build:ui": "cd src/ui/react-app && npm install && npm run build",
"dev": "tsx src/main.ts",
"prepublishOnly": "npm run build",
"test": "jest",
"type-check": "tsc --noEmit"
},
"keywords": [
"mcp",
"freshbooks",
"accounting",
"invoicing",
"time-tracking",
"expenses",
"estimates",
"payments"
"api"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"zod": "^3.24.1"
"@modelcontextprotocol/ext-apps": "^0.1.0"
},
"devDependencies": {
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
"@types/node": "^22.10.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"jest": "^29.7.0",
"@types/jest": "^29.5.14"
}
}

View File

@ -1,144 +1,613 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
FreshBooksConfig,
FreshBooksClient,
FreshBooksInvoice,
FreshBooksEstimate,
FreshBooksExpense,
ExpenseCategory,
FreshBooksPayment,
FreshBooksProject,
FreshBooksTimeEntry,
FreshBooksTax,
FreshBooksItem,
FreshBooksStaff,
FreshBooksBill,
BillVendor,
BillPayment,
AccountingAccount,
JournalEntry,
Retainer,
CreditNote,
ProfitLossReport,
TaxSummaryReport,
AgingReport,
ExpenseReport,
PaginatedResponse,
FreshBooksError,
} from '../types/index.js';
export class FreshBooksClient {
private client: AxiosInstance;
export class FreshBooksAPIClient {
private accountId: string;
private accessToken: string;
private apiBaseUrl: string;
private rateLimitDelay = 100; // ms between requests
private lastRequestTime = 0;
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));
}
);
this.accessToken = config.accessToken;
this.apiBaseUrl = config.apiBaseUrl || 'https://api.freshbooks.com';
}
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',
};
private async rateLimit(): Promise<void> {
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.rateLimitDelay) {
await new Promise(resolve => setTimeout(resolve, this.rateLimitDelay - timeSinceLastRequest));
}
this.lastRequestTime = Date.now();
}
// Generic GET with pagination support
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const response = await this.client.get(endpoint, { params });
return response.data;
// Helper methods for recurring-tools compatibility
async get<T>(endpoint: string, queryParams?: Record<string, any>): Promise<T> {
return this.request<T>('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, queryParams);
}
// Generic GET with automatic pagination (fetch all pages)
async getAll<T>(
endpoint: string,
params?: Record<string, any>,
resultKey: string = 'result'
): Promise<T[]> {
let page = 1;
let allResults: T[] = [];
let hasMore = true;
while (hasMore) {
const response = await this.client.get<PaginatedResponse<any>>(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;
async post<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>('POST', `/accounting/account/${this.accountId}${endpoint}`, data);
}
async put<T>(endpoint: string, data?: any): Promise<T> {
return this.request<T>('PUT', `/accounting/account/${this.accountId}${endpoint}`, data);
}
// Paginated GET (single page)
async getPaginated<T>(
endpoint: string,
page: number = 1,
perPage: number = 30,
per_page: number = 30,
params?: Record<string, any>
): Promise<PaginatedResponse<T>> {
const response = await this.client.get<PaginatedResponse<T>>(endpoint, {
params: { ...params, page, per_page: perPage },
): Promise<T> {
return this.request<T>('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, {
...params,
page,
per_page,
});
return response.data;
}
// POST
async post<T>(endpoint: string, data: any): Promise<T> {
const response = await this.client.post(endpoint, data);
return response.data;
}
// PUT
async put<T>(endpoint: string, data: any): Promise<T> {
const response = await this.client.put(endpoint, data);
return response.data;
}
// DELETE
async delete<T>(endpoint: string): Promise<T> {
const response = await this.client.delete(endpoint);
return response.data;
}
// Convenience method: search with filters
async search<T>(
private async request<T>(
method: string,
endpoint: string,
searchFields: Record<string, any>,
page: number = 1,
perPage: number = 30
): Promise<PaginatedResponse<T>> {
return this.getPaginated<T>(endpoint, page, perPage, {
search: searchFields,
data?: any,
queryParams?: Record<string, any>
): Promise<T> {
await this.rateLimit();
const url = new URL(`${this.apiBaseUrl}${endpoint}`);
if (queryParams) {
Object.entries(queryParams).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
}
const options: RequestInit = {
method,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
'Api-Version': 'alpha',
},
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url.toString(), options);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw this.mapError(response.status, errorData);
}
const responseData = await response.json() as any;
return responseData.response || responseData;
} catch (error) {
if (error instanceof Error && error.message.includes('fetch')) {
throw new Error(`Network error: ${error.message}`);
}
throw error;
}
}
private mapError(status: number, errorData: any): Error {
const message = errorData?.message || errorData?.error || 'Unknown error';
switch (status) {
case 401:
return new Error(`Authentication failed: ${message}`);
case 403:
return new Error(`Permission denied: ${message}`);
case 404:
return new Error(`Resource not found: ${message}`);
case 429:
return new Error(`Rate limit exceeded: ${message}`);
case 422:
return new Error(`Validation error: ${message}`);
case 500:
case 502:
case 503:
return new Error(`FreshBooks server error: ${message}`);
default:
return new Error(`FreshBooks API error (${status}): ${message}`);
}
}
// Pagination helper
private async paginate<T>(
endpoint: string,
params: Record<string, any> = {}
): Promise<T[]> {
const results: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.request<PaginatedResponse<T>>('GET', endpoint, undefined, {
...params,
page,
});
results.push(...response.results);
if (page >= response.pages) {
hasMore = false;
} else {
page++;
}
}
return results;
}
// ========== CLIENTS ==========
async getClients(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksClient>> {
return this.request('GET', `/accounting/account/${this.accountId}/users/clients`, undefined, params);
}
async getClient(clientId: number): Promise<FreshBooksClient> {
const response = await this.request<{ result: FreshBooksClient }>('GET', `/accounting/account/${this.accountId}/users/clients/${clientId}`);
return response.result;
}
async createClient(client: Partial<FreshBooksClient>): Promise<FreshBooksClient> {
const response = await this.request<{ result: FreshBooksClient }>('POST', `/accounting/account/${this.accountId}/users/clients`, { client });
return response.result;
}
async updateClient(clientId: number, updates: Partial<FreshBooksClient>): Promise<FreshBooksClient> {
const response = await this.request<{ result: FreshBooksClient }>('PUT', `/accounting/account/${this.accountId}/users/clients/${clientId}`, { client: updates });
return response.result;
}
async deleteClient(clientId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/users/clients/${clientId}`);
}
// ========== INVOICES ==========
async getInvoices(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksInvoice>> {
return this.request('GET', `/accounting/account/${this.accountId}/invoices/invoices`, undefined, params);
}
async getInvoice(invoiceId: number): Promise<FreshBooksInvoice> {
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('GET', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`);
return response.result.invoice;
}
async createInvoice(invoice: Partial<FreshBooksInvoice>): Promise<FreshBooksInvoice> {
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('POST', `/accounting/account/${this.accountId}/invoices/invoices`, { invoice });
return response.result.invoice;
}
async updateInvoice(invoiceId: number, updates: Partial<FreshBooksInvoice>): Promise<FreshBooksInvoice> {
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('PUT', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`, { invoice: updates });
return response.result.invoice;
}
async deleteInvoice(invoiceId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`);
}
async sendInvoice(invoiceId: number, email?: string): Promise<void> {
await this.request('POST', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}/send`, { email });
}
async markInvoicePaid(invoiceId: number): Promise<void> {
await this.updateInvoice(invoiceId, { v3_status: 'paid' });
}
async getInvoiceShareLink(invoiceId: number): Promise<string> {
const invoice = await this.getInvoice(invoiceId);
return `https://my.freshbooks.com/#/invoice/${this.accountId}-${invoice.invoiceid}`;
}
// ========== ESTIMATES ==========
async getEstimates(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksEstimate>> {
return this.request('GET', `/accounting/account/${this.accountId}/estimates/estimates`, undefined, params);
}
async getEstimate(estimateId: number): Promise<FreshBooksEstimate> {
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('GET', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`);
return response.result.estimate;
}
async createEstimate(estimate: Partial<FreshBooksEstimate>): Promise<FreshBooksEstimate> {
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('POST', `/accounting/account/${this.accountId}/estimates/estimates`, { estimate });
return response.result.estimate;
}
async updateEstimate(estimateId: number, updates: Partial<FreshBooksEstimate>): Promise<FreshBooksEstimate> {
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('PUT', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`, { estimate: updates });
return response.result.estimate;
}
async deleteEstimate(estimateId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`);
}
async sendEstimate(estimateId: number, email?: string): Promise<void> {
await this.request('POST', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}/send`, { email });
}
async acceptEstimate(estimateId: number): Promise<void> {
await this.updateEstimate(estimateId, { accepted: true });
}
// ========== EXPENSES ==========
async getExpenses(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksExpense>> {
return this.request('GET', `/accounting/account/${this.accountId}/expenses/expenses`, undefined, params);
}
async getExpense(expenseId: number): Promise<FreshBooksExpense> {
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('GET', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`);
return response.result.expense;
}
async createExpense(expense: Partial<FreshBooksExpense>): Promise<FreshBooksExpense> {
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('POST', `/accounting/account/${this.accountId}/expenses/expenses`, { expense });
return response.result.expense;
}
async updateExpense(expenseId: number, updates: Partial<FreshBooksExpense>): Promise<FreshBooksExpense> {
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('PUT', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`, { expense: updates });
return response.result.expense;
}
async deleteExpense(expenseId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`);
}
async getExpenseCategories(): Promise<ExpenseCategory[]> {
const response = await this.request<{ result: { categories: ExpenseCategory[] } }>('GET', `/accounting/account/${this.accountId}/expenses/categories`);
return response.result.categories;
}
// ========== PAYMENTS ==========
async getPayments(params?: { page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksPayment>> {
return this.request('GET', `/accounting/account/${this.accountId}/payments/payments`, undefined, params);
}
async getPayment(paymentId: number): Promise<FreshBooksPayment> {
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('GET', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`);
return response.result.payment;
}
async createPayment(payment: Partial<FreshBooksPayment>): Promise<FreshBooksPayment> {
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('POST', `/accounting/account/${this.accountId}/payments/payments`, { payment });
return response.result.payment;
}
async updatePayment(paymentId: number, updates: Partial<FreshBooksPayment>): Promise<FreshBooksPayment> {
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('PUT', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`, { payment: updates });
return response.result.payment;
}
async deletePayment(paymentId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`);
}
// ========== PROJECTS ==========
async getProjects(params?: { page?: number; per_page?: number }): Promise<FreshBooksProject[]> {
const response = await this.request<{ projects: FreshBooksProject[] }>('GET', `/projects/business/${this.accountId}/projects`, undefined, params);
return response.projects;
}
async getProject(projectId: number): Promise<FreshBooksProject> {
const response = await this.request<{ project: FreshBooksProject }>('GET', `/projects/business/${this.accountId}/project/${projectId}`);
return response.project;
}
async createProject(project: Partial<FreshBooksProject>): Promise<FreshBooksProject> {
const response = await this.request<{ project: FreshBooksProject }>('POST', `/projects/business/${this.accountId}/project`, project);
return response.project;
}
async updateProject(projectId: number, updates: Partial<FreshBooksProject>): Promise<FreshBooksProject> {
const response = await this.request<{ project: FreshBooksProject }>('PUT', `/projects/business/${this.accountId}/project/${projectId}`, updates);
return response.project;
}
async deleteProject(projectId: number): Promise<void> {
await this.request('DELETE', `/projects/business/${this.accountId}/project/${projectId}`);
}
// ========== TIME ENTRIES ==========
async getTimeEntries(params?: { page?: number; per_page?: number }): Promise<FreshBooksTimeEntry[]> {
const response = await this.request<{ time_entries: FreshBooksTimeEntry[] }>('GET', `/timetracking/business/${this.accountId}/time_entries`, undefined, params);
return response.time_entries;
}
async getTimeEntry(timeEntryId: number): Promise<FreshBooksTimeEntry> {
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('GET', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`);
return response.time_entry;
}
async createTimeEntry(timeEntry: Partial<FreshBooksTimeEntry>): Promise<FreshBooksTimeEntry> {
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('POST', `/timetracking/business/${this.accountId}/time_entries`, { time_entry: timeEntry });
return response.time_entry;
}
async updateTimeEntry(timeEntryId: number, updates: Partial<FreshBooksTimeEntry>): Promise<FreshBooksTimeEntry> {
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('PUT', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`, { time_entry: updates });
return response.time_entry;
}
async deleteTimeEntry(timeEntryId: number): Promise<void> {
await this.request('DELETE', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`);
}
async startTimer(projectId: number, note?: string): Promise<FreshBooksTimeEntry> {
return this.createTimeEntry({
project_id: projectId,
is_logged: false,
started_at: new Date().toISOString(),
note,
});
}
getAccountId(): string {
return this.accountId;
async stopTimer(timeEntryId: number): Promise<FreshBooksTimeEntry> {
return this.updateTimeEntry(timeEntryId, {
is_logged: true,
});
}
// ========== TAXES ==========
async getTaxes(): Promise<FreshBooksTax[]> {
const response = await this.request<{ result: { taxes: FreshBooksTax[] } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes`);
return response.result.taxes;
}
async getTax(taxId: number): Promise<FreshBooksTax> {
const response = await this.request<{ result: { tax: FreshBooksTax } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`);
return response.result.tax;
}
async createTax(tax: Partial<FreshBooksTax>): Promise<FreshBooksTax> {
const response = await this.request<{ result: { tax: FreshBooksTax } }>('POST', `/accounting/account/${this.accountId}/taxes/taxes`, { tax });
return response.result.tax;
}
async updateTax(taxId: number, updates: Partial<FreshBooksTax>): Promise<FreshBooksTax> {
const response = await this.request<{ result: { tax: FreshBooksTax } }>('PUT', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`, { tax: updates });
return response.result.tax;
}
async deleteTax(taxId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`);
}
// ========== ITEMS/SERVICES ==========
async getItems(params?: { page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksItem>> {
return this.request('GET', `/accounting/account/${this.accountId}/items/items`, undefined, params);
}
async getItem(itemId: number): Promise<FreshBooksItem> {
const response = await this.request<{ result: { item: FreshBooksItem } }>('GET', `/accounting/account/${this.accountId}/items/items/${itemId}`);
return response.result.item;
}
async createItem(item: Partial<FreshBooksItem>): Promise<FreshBooksItem> {
const response = await this.request<{ result: { item: FreshBooksItem } }>('POST', `/accounting/account/${this.accountId}/items/items`, { item });
return response.result.item;
}
async updateItem(itemId: number, updates: Partial<FreshBooksItem>): Promise<FreshBooksItem> {
const response = await this.request<{ result: { item: FreshBooksItem } }>('PUT', `/accounting/account/${this.accountId}/items/items/${itemId}`, { item: updates });
return response.result.item;
}
async deleteItem(itemId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/items/items/${itemId}`);
}
// ========== STAFF ==========
async getStaff(params?: { page?: number; per_page?: number }): Promise<FreshBooksStaff[]> {
const response = await this.request<{ staff_members: FreshBooksStaff[] }>('GET', `/projects/business/${this.accountId}/staff`, undefined, params);
return response.staff_members;
}
async getStaffMember(staffId: number): Promise<FreshBooksStaff> {
const response = await this.request<{ staff_member: FreshBooksStaff }>('GET', `/projects/business/${this.accountId}/staff/${staffId}`);
return response.staff_member;
}
// ========== BILLS ==========
async getBills(params?: { page?: number; per_page?: number }): Promise<FreshBooksBill[]> {
const response = await this.request<{ bills: FreshBooksBill[] }>('GET', `/accounting/account/${this.accountId}/bills/bills`, undefined, params);
return response.bills;
}
async getBill(billId: number): Promise<FreshBooksBill> {
const response = await this.request<{ bill: FreshBooksBill }>('GET', `/accounting/account/${this.accountId}/bills/bills/${billId}`);
return response.bill;
}
async createBill(bill: Partial<FreshBooksBill>): Promise<FreshBooksBill> {
const response = await this.request<{ bill: FreshBooksBill }>('POST', `/accounting/account/${this.accountId}/bills/bills`, { bill });
return response.bill;
}
async updateBill(billId: number, updates: Partial<FreshBooksBill>): Promise<FreshBooksBill> {
const response = await this.request<{ bill: FreshBooksBill }>('PUT', `/accounting/account/${this.accountId}/bills/bills/${billId}`, { bill: updates });
return response.bill;
}
async deleteBill(billId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/bills/bills/${billId}`);
}
// ========== BILL VENDORS ==========
async getVendors(params?: { page?: number; per_page?: number }): Promise<BillVendor[]> {
const response = await this.request<{ bill_vendors: BillVendor[] }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, undefined, params);
return response.bill_vendors;
}
async getVendor(vendorId: number): Promise<BillVendor> {
const response = await this.request<{ bill_vendor: BillVendor }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`);
return response.bill_vendor;
}
async createVendor(vendor: Partial<BillVendor>): Promise<BillVendor> {
const response = await this.request<{ bill_vendor: BillVendor }>('POST', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, { bill_vendor: vendor });
return response.bill_vendor;
}
async updateVendor(vendorId: number, updates: Partial<BillVendor>): Promise<BillVendor> {
const response = await this.request<{ bill_vendor: BillVendor }>('PUT', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`, { bill_vendor: updates });
return response.bill_vendor;
}
async deleteVendor(vendorId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`);
}
// ========== BILL PAYMENTS ==========
async getBillPayments(billId: number): Promise<BillPayment[]> {
const bill = await this.getBill(billId);
return bill.bill_payments || [];
}
async createBillPayment(billId: number, payment: Partial<BillPayment>): Promise<BillPayment> {
const response = await this.request<{ bill_payment: BillPayment }>('POST', `/accounting/account/${this.accountId}/bills/bills/${billId}/bill_payments`, { bill_payment: payment });
return response.bill_payment;
}
// ========== ACCOUNTING ACCOUNTS ==========
async getAccounts(): Promise<AccountingAccount[]> {
const response = await this.request<{ accounts: AccountingAccount[] }>('GET', `/accounting/account/${this.accountId}/accounts/accounts`);
return response.accounts;
}
async getAccount(accountId: number): Promise<AccountingAccount> {
const response = await this.request<{ account: AccountingAccount }>('GET', `/accounting/account/${this.accountId}/accounts/accounts/${accountId}`);
return response.account;
}
// ========== JOURNAL ENTRIES ==========
async getJournalEntries(params?: { page?: number; per_page?: number }): Promise<JournalEntry[]> {
const response = await this.request<{ journal_entries: JournalEntry[] }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, undefined, params);
return response.journal_entries;
}
async getJournalEntry(journalEntryId: number): Promise<JournalEntry> {
const response = await this.request<{ journal_entry: JournalEntry }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries/${journalEntryId}`);
return response.journal_entry;
}
async createJournalEntry(journalEntry: Partial<JournalEntry>): Promise<JournalEntry> {
const response = await this.request<{ journal_entry: JournalEntry }>('POST', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, { journal_entry: journalEntry });
return response.journal_entry;
}
// ========== RETAINERS ==========
async getRetainers(params?: { page?: number; per_page?: number }): Promise<Retainer[]> {
const response = await this.request<{ retainers: Retainer[] }>('GET', `/projects/business/${this.accountId}/retainers`, undefined, params);
return response.retainers;
}
async getRetainer(retainerId: number): Promise<Retainer> {
const response = await this.request<{ retainer: Retainer }>('GET', `/projects/business/${this.accountId}/retainers/${retainerId}`);
return response.retainer;
}
async createRetainer(retainer: Partial<Retainer>): Promise<Retainer> {
const response = await this.request<{ retainer: Retainer }>('POST', `/projects/business/${this.accountId}/retainers`, { retainer });
return response.retainer;
}
async updateRetainer(retainerId: number, updates: Partial<Retainer>): Promise<Retainer> {
const response = await this.request<{ retainer: Retainer }>('PUT', `/projects/business/${this.accountId}/retainers/${retainerId}`, { retainer: updates });
return response.retainer;
}
async deleteRetainer(retainerId: number): Promise<void> {
await this.request('DELETE', `/projects/business/${this.accountId}/retainers/${retainerId}`);
}
// ========== CREDIT NOTES ==========
async getCreditNotes(params?: { page?: number; per_page?: number }): Promise<CreditNote[]> {
const response = await this.request<{ credit_notes: CreditNote[] }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, undefined, params);
return response.credit_notes;
}
async getCreditNote(creditNoteId: number): Promise<CreditNote> {
const response = await this.request<{ credit_note: CreditNote }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`);
return response.credit_note;
}
async createCreditNote(creditNote: Partial<CreditNote>): Promise<CreditNote> {
const response = await this.request<{ credit_note: CreditNote }>('POST', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, { credit_note: creditNote });
return response.credit_note;
}
async updateCreditNote(creditNoteId: number, updates: Partial<CreditNote>): Promise<CreditNote> {
const response = await this.request<{ credit_note: CreditNote }>('PUT', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`, { credit_note: updates });
return response.credit_note;
}
async deleteCreditNote(creditNoteId: number): Promise<void> {
await this.request('DELETE', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`);
}
// ========== REPORTS ==========
async getProfitLossReport(startDate: string, endDate: string): Promise<ProfitLossReport> {
const response = await this.request<{ report: ProfitLossReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/profitloss`, undefined, {
start_date: startDate,
end_date: endDate,
});
return response.report;
}
async getTaxSummaryReport(startDate: string, endDate: string): Promise<TaxSummaryReport> {
const response = await this.request<{ report: TaxSummaryReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/taxsummary`, undefined, {
start_date: startDate,
end_date: endDate,
});
return response.report;
}
async getAgingReport(): Promise<AgingReport> {
const response = await this.request<{ report: AgingReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/aging`);
return response.report;
}
async getExpenseReport(startDate: string, endDate: string): Promise<ExpenseReport> {
const response = await this.request<{ report: ExpenseReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/expenses`, undefined, {
start_date: startDate,
end_date: endDate,
});
return response.report;
}
}

View File

@ -1,14 +1,7 @@
#!/usr/bin/env node
import { FreshBooksServer } from './server.js';
import { runServer } 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();
runServer().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

View File

@ -3,145 +3,181 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { FreshBooksClient } from './clients/freshbooks.js';
import { invoicesTools } from './tools/invoices-tools.js';
import { FreshBooksAPIClient } from './clients/freshbooks.js';
import { clientsTools } from './tools/clients-tools.js';
import { expensesTools } from './tools/expenses-tools.js';
import { invoicesTools } from './tools/invoices-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 { expensesTools } from './tools/expenses-tools.js';
import { paymentsTools } from './tools/payments-tools.js';
import { itemsTools } from './tools/items-tools.js';
import { projectsTools } from './tools/projects-tools.js';
import { timeEntriesTools } from './tools/time-entries-tools.js';
import { taxesTools } from './tools/taxes-tools.js';
import { reportsTools } from './tools/reports-tools.js';
import { recurringTools } from './tools/recurring-tools.js';
import { itemsTools } from './tools/items-tools.js';
import { staffTools } from './tools/staff-tools.js';
import { billsTools } from './tools/bills-tools.js';
import { vendorsTools } from './tools/vendors-tools.js';
import { accountsTools } from './tools/accounts-tools.js';
import { journalEntriesTools } from './tools/journal-entries-tools.js';
import { retainersTools } from './tools/retainers-tools.js';
import { creditNotesTools } from './tools/credit-notes-tools.js';
import { reportsTools } from './tools/reports-tools.js';
export class FreshBooksServer {
private server: Server;
private client: FreshBooksClient;
private allTools: any[];
// Combine all tools
const allTools = [
...clientsTools,
...invoicesTools,
...estimatesTools,
...expensesTools,
...paymentsTools,
...projectsTools,
...timeEntriesTools,
...taxesTools,
...itemsTools,
...staffTools,
...billsTools,
...vendorsTools,
...accountsTools,
...journalEntriesTools,
...retainersTools,
...creditNotesTools,
...reportsTools,
];
constructor() {
this.server = new Server(
{
name: 'freshbooks-mcp-server',
version: '1.0.0',
// MCP App resources (HTML files)
const appResources = [
{ uri: 'freshbooks://apps/invoice-dashboard', name: 'Invoice Dashboard' },
{ uri: 'freshbooks://apps/invoice-detail', name: 'Invoice Detail' },
{ uri: 'freshbooks://apps/invoice-creator', name: 'Invoice Creator' },
{ uri: 'freshbooks://apps/client-dashboard', name: 'Client Dashboard' },
{ uri: 'freshbooks://apps/client-detail', name: 'Client Detail' },
{ uri: 'freshbooks://apps/expense-tracker', name: 'Expense Tracker' },
{ uri: 'freshbooks://apps/expense-report', name: 'Expense Report' },
{ uri: 'freshbooks://apps/project-dashboard', name: 'Project Dashboard' },
{ uri: 'freshbooks://apps/project-detail', name: 'Project Detail' },
{ uri: 'freshbooks://apps/time-tracker', name: 'Time Tracker' },
{ uri: 'freshbooks://apps/time-report', name: 'Time Report' },
{ uri: 'freshbooks://apps/payment-dashboard', name: 'Payment Dashboard' },
{ uri: 'freshbooks://apps/estimate-builder', name: 'Estimate Builder' },
{ uri: 'freshbooks://apps/profit-loss-report', name: 'Profit & Loss Report' },
{ uri: 'freshbooks://apps/tax-summary', name: 'Tax Summary' },
{ uri: 'freshbooks://apps/aging-report', name: 'Aging Report' },
{ uri: 'freshbooks://apps/item-catalog', name: 'Item Catalog' },
{ uri: 'freshbooks://apps/bill-manager', name: 'Bill Manager' },
{ uri: 'freshbooks://apps/staff-directory', name: 'Staff Directory' },
{ uri: 'freshbooks://apps/dashboard-overview', name: 'Dashboard Overview' },
];
export async function createServer() {
const server = new Server(
{
name: 'freshbooks-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
{
capabilities: {
tools: {},
},
}
}
);
// Get config from environment
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN;
if (!accountId || !accessToken) {
throw new Error(
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_ACCESS_TOKEN'
);
}
// Initialize FreshBooks client from env
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN;
const client = new FreshBooksAPIClient({
accountId,
accessToken,
});
if (!accountId || !bearerToken) {
throw new Error(
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN'
);
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: allTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = allTools.find((t) => t.name === request.params.name);
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
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 () => {
try {
return await tool.handler(client, request.params.arguments || {});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
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()
),
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
})),
],
isError: true,
};
});
}
});
// Call tool handler
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const tool = this.allTools.find((t) => t.name === toolName);
// List resources handler (MCP Apps)
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: appResources.map((app) => ({
uri: app.uri,
mimeType: 'text/html',
name: app.name,
})),
};
});
if (!tool) {
throw new Error(`Unknown tool: ${toolName}`);
}
// Read resource handler (serve MCP App HTML)
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const appName = uri.replace('freshbooks://apps/', '');
try {
const { readFileSync } = await import('fs');
const { join } = await import('path');
const { fileURLToPath } = await import('url');
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const htmlPath = join(__dirname, 'ui', 'react-app', 'dist', `${appName}.html`);
const html = readFileSync(htmlPath, 'utf-8');
return {
contents: [
{
uri,
mimeType: 'text/html',
text: html,
},
],
};
} catch (error) {
throw new Error(`Failed to load app ${appName}: ${error}`);
}
});
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');
}
return server;
}
export async function runServer() {
const server = await createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('FreshBooks MCP server running on stdio');
}

View File

@ -1,51 +1,48 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Account, StaffMember } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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_accounts',
description: 'List all accounting accounts (chart of accounts)',
inputSchema: {
type: 'object',
properties: {},
},
},
{
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
);
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getAccounts();
return {
staff: response.response.result.staff || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
name: 'freshbooks_get_account',
description: 'Get detailed information about a specific accounting account',
inputSchema: {
type: 'object',
properties: {
account_id: {
type: 'number',
description: 'Account ID',
},
},
required: ['account_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getAccount(args.account_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -0,0 +1,265 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const billsTools = [
{
name: 'freshbooks_list_bills',
description: 'List all bills with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getBills(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_bill',
description: 'Get detailed information about a specific bill',
inputSchema: {
type: 'object',
properties: {
bill_id: {
type: 'number',
description: 'Bill ID',
},
},
required: ['bill_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getBill(args.bill_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_bill',
description: 'Create a new bill in FreshBooks',
inputSchema: {
type: 'object',
properties: {
vendor_id: {
type: 'number',
description: 'Vendor ID',
},
bill_number: {
type: 'string',
description: 'Bill number',
},
issue_date: {
type: 'string',
description: 'Issue date (YYYY-MM-DD)',
},
due_date: {
type: 'string',
description: 'Due date (YYYY-MM-DD)',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
lines: {
type: 'array',
description: 'Bill line items',
items: {
type: 'object',
properties: {
description: {
type: 'string',
description: 'Line item description',
},
quantity: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Unit cost amount',
},
},
},
category_id: {
type: 'number',
description: 'Expense category ID',
},
},
},
},
},
required: ['vendor_id', 'issue_date'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createBill(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_update_bill',
description: 'Update an existing bill',
inputSchema: {
type: 'object',
properties: {
bill_id: {
type: 'number',
description: 'Bill ID to update',
},
due_date: {
type: 'string',
description: 'Due date',
},
lines: {
type: 'array',
description: 'Bill line items',
},
},
required: ['bill_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { bill_id, ...updates } = args;
const result = await client.updateBill(bill_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_delete_bill',
description: 'Delete a bill from FreshBooks',
inputSchema: {
type: 'object',
properties: {
bill_id: {
type: 'number',
description: 'Bill ID to delete',
},
},
required: ['bill_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteBill(args.bill_id);
return {
content: [
{
type: 'text',
text: `Bill ${args.bill_id} deleted successfully`,
},
],
};
},
},
{
name: 'freshbooks_get_bill_payments',
description: 'Get all payments for a specific bill',
inputSchema: {
type: 'object',
properties: {
bill_id: {
type: 'number',
description: 'Bill ID',
},
},
required: ['bill_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getBillPayments(args.bill_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_bill_payment',
description: 'Create a payment for a bill',
inputSchema: {
type: 'object',
properties: {
bill_id: {
type: 'number',
description: 'Bill ID',
},
amount: {
type: 'string',
description: 'Payment amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
paid_date: {
type: 'string',
description: 'Payment date (YYYY-MM-DD)',
},
payment_type: {
type: 'string',
description: 'Payment type (e.g., Check, Cash, Credit Card)',
},
note: {
type: 'string',
description: 'Payment note',
},
},
required: ['bill_id', 'amount', 'paid_date', 'payment_type'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { bill_id, amount, currency_code, ...payment } = args;
const paymentData = {
...payment,
amount: {
amount,
code: currency_code || 'USD',
},
};
const result = await client.createBillPayment(bill_id, paymentData);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,137 +1,314 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Client, ClientContact } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
if (args.search) {
params.search = { email_like: `%${args.search}%` };
}
const response = await client.getPaginated<{ clients: Client[] }>(
'/users/clients',
args.page,
args.per_page,
params
);
description: 'List all clients in FreshBooks with optional search and pagination',
inputSchema: {
type: 'object',
properties: {
search: {
type: 'string',
description: 'Search term to filter clients',
},
page: {
type: 'number',
description: 'Page number for pagination (default: 1)',
},
per_page: {
type: 'number',
description: 'Number of results per page (default: 30)',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getClients(args);
return {
clients: response.response.result.clients || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific client',
inputSchema: {
type: 'object',
properties: {
client_id: {
type: 'number',
description: 'Client ID',
},
},
required: ['client_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getClient(args.client_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Create a new client in FreshBooks',
inputSchema: {
type: 'object',
properties: {
fname: {
type: 'string',
description: 'First name',
},
lname: {
type: 'string',
description: 'Last name',
},
email: {
type: 'string',
description: 'Email address',
},
organization: {
type: 'string',
description: 'Company/organization name',
},
business_phone: {
type: 'string',
description: 'Business phone number',
},
mobile_phone: {
type: 'string',
description: 'Mobile phone number',
},
home_phone: {
type: 'string',
description: 'Home phone number',
},
fax: {
type: 'string',
description: 'Fax number',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD, CAD, GBP)',
},
language: {
type: 'string',
description: 'Language code (e.g., en, fr)',
},
note: {
type: 'string',
description: 'Internal note about the client',
},
vat_name: {
type: 'string',
description: 'VAT name',
},
vat_number: {
type: 'string',
description: 'VAT number',
},
s_street: {
type: 'string',
description: 'Shipping address street',
},
s_street2: {
type: 'string',
description: 'Shipping address street line 2',
},
s_city: {
type: 'string',
description: 'Shipping address city',
},
s_province: {
type: 'string',
description: 'Shipping address province/state',
},
s_code: {
type: 'string',
description: 'Shipping address postal code',
},
s_country: {
type: 'string',
description: 'Shipping address country',
},
p_street: {
type: 'string',
description: 'Billing address street',
},
p_street2: {
type: 'string',
description: 'Billing address street line 2',
},
p_city: {
type: 'string',
description: 'Billing address city',
},
p_province: {
type: 'string',
description: 'Billing address province/state',
},
p_code: {
type: 'string',
description: 'Billing address postal code',
},
p_country: {
type: 'string',
description: 'Billing address country',
},
},
required: ['email'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createClient(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
inputSchema: {
type: 'object',
properties: {
client_id: {
type: 'number',
description: 'Client ID to update',
},
fname: {
type: 'string',
description: 'First name',
},
lname: {
type: 'string',
description: 'Last name',
},
email: {
type: 'string',
description: 'Email address',
},
organization: {
type: 'string',
description: 'Company/organization name',
},
business_phone: {
type: 'string',
description: 'Business phone number',
},
mobile_phone: {
type: 'string',
description: 'Mobile phone number',
},
currency_code: {
type: 'string',
description: 'Currency code',
},
language: {
type: 'string',
description: 'Language code',
},
note: {
type: 'string',
description: 'Internal note',
},
s_street: {
type: 'string',
description: 'Shipping address street',
},
s_city: {
type: 'string',
description: 'Shipping address city',
},
s_province: {
type: 'string',
description: 'Shipping address province/state',
},
s_code: {
type: 'string',
description: 'Shipping address postal code',
},
s_country: {
type: 'string',
description: 'Shipping address country',
},
},
required: ['client_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { client_id, ...updates } = args;
const result = await client.updateClient(client_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete a client from FreshBooks',
inputSchema: {
type: 'object',
properties: {
client_id: {
type: 'number',
description: 'Client ID to delete',
},
},
required: ['client_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteClient(args.client_id);
return {
content: [
{
type: 'text',
text: `Client ${args.client_id} deleted successfully`,
},
],
};
},
},
{
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 || [];
name: 'freshbooks_search_clients',
description: 'Search for clients by name, email, or organization',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getClients({ search: args.query });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -0,0 +1,187 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const creditNotesTools = [
{
name: 'freshbooks_list_credit_notes',
description: 'List all credit notes with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getCreditNotes(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_credit_note',
description: 'Get detailed information about a specific credit note',
inputSchema: {
type: 'object',
properties: {
credit_note_id: {
type: 'number',
description: 'Credit note ID',
},
},
required: ['credit_note_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getCreditNote(args.credit_note_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_credit_note',
description: 'Create a new credit note in FreshBooks',
inputSchema: {
type: 'object',
properties: {
clientid: {
type: 'number',
description: 'Client ID',
},
create_date: {
type: 'string',
description: 'Creation date (YYYY-MM-DD)',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
credit_type: {
type: 'string',
description: 'Credit type (goodwill, prepayment, etc.)',
},
notes: {
type: 'string',
description: 'Credit note notes',
},
lines: {
type: 'array',
description: 'Credit note line items',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Line item name',
},
description: {
type: 'string',
description: 'Line item description',
},
qty: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Unit cost amount',
},
},
},
},
},
},
},
required: ['clientid'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createCreditNote(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_update_credit_note',
description: 'Update an existing credit note',
inputSchema: {
type: 'object',
properties: {
credit_note_id: {
type: 'number',
description: 'Credit note ID to update',
},
notes: {
type: 'string',
description: 'Credit note notes',
},
lines: {
type: 'array',
description: 'Credit note line items',
},
},
required: ['credit_note_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { credit_note_id, ...updates } = args;
const result = await client.updateCreditNote(credit_note_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_delete_credit_note',
description: 'Delete a credit note from FreshBooks',
inputSchema: {
type: 'object',
properties: {
credit_note_id: {
type: 'number',
description: 'Credit note ID to delete',
},
},
required: ['credit_note_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteCreditNote(args.credit_note_id);
return {
content: [
{
type: 'text',
text: `Credit note ${args.credit_note_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -1,194 +1,317 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Estimate } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
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
);
description: 'List all estimates with optional search and pagination',
inputSchema: {
type: 'object',
properties: {
search: {
type: 'string',
description: 'Search term to filter estimates',
},
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getEstimates(args);
return {
estimates: response.response.result.estimates || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific estimate',
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID',
},
},
required: ['estimate_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getEstimate(args.estimate_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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,
description: 'Create a new estimate in FreshBooks',
inputSchema: {
type: 'object',
properties: {
customerid: {
type: 'number',
description: 'Client ID for this estimate',
},
create_date: {
type: 'string',
description: 'Estimate creation date (YYYY-MM-DD)',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD, CAD)',
},
language: {
type: 'string',
description: 'Language code (e.g., en)',
},
notes: {
type: 'string',
description: 'Estimate notes',
},
terms: {
type: 'string',
description: 'Estimate terms',
},
discount_value: {
type: 'string',
description: 'Discount value',
},
lines: {
type: 'array',
description: 'Estimate line items',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Line item name',
},
description: {
type: 'string',
description: 'Line item description',
},
qty: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Unit cost amount',
},
code: {
type: 'string',
description: 'Currency code',
},
},
},
},
},
},
},
required: ['customerid'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createEstimate(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
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;
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID to update',
},
notes: {
type: 'string',
description: 'Estimate notes',
},
terms: {
type: 'string',
description: 'Estimate terms',
},
lines: {
type: 'array',
description: 'Estimate line items',
},
},
required: ['estimate_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { estimate_id, ...updates } = args;
const result = await client.updateEstimate(estimate_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete an estimate from FreshBooks',
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID to delete',
},
},
required: ['estimate_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteEstimate(args.estimate_id);
return {
content: [
{
type: 'text',
text: `Estimate ${args.estimate_id} deleted successfully`,
},
],
};
},
},
{
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` };
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID to send',
},
email: {
type: 'string',
description: 'Email address to send to (optional)',
},
},
required: ['estimate_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.sendEstimate(args.estimate_id, args.email);
return {
content: [
{
type: 'text',
text: `Estimate ${args.estimate_id} sent successfully`,
},
],
};
},
},
{
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,
name: 'freshbooks_accept_estimate',
description: 'Mark an estimate as accepted',
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID to accept',
},
},
required: ['estimate_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.acceptEstimate(args.estimate_id);
return {
content: [
{
type: 'text',
text: `Estimate ${args.estimate_id} accepted`,
},
],
};
},
},
{
name: 'freshbooks_add_estimate_line',
description: 'Add a line item to an existing estimate',
inputSchema: {
type: 'object',
properties: {
estimate_id: {
type: 'number',
description: 'Estimate ID',
},
name: {
type: 'string',
description: 'Line item name',
},
description: {
type: 'string',
description: 'Line item description',
},
qty: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'string',
description: 'Unit cost amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
},
required: ['estimate_id', 'name', 'qty', 'unit_cost'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const estimate = await client.getEstimate(args.estimate_id);
const newLine = {
name: args.name,
description: args.description || '',
qty: args.qty,
unit_cost: {
amount: args.unit_cost,
code: args.currency_code || estimate.currency_code,
},
};
const response = await client.post(
'/invoices/invoices',
invoiceData
);
return response;
const lines = [...estimate.lines, newLine];
const result = await client.updateEstimate(args.estimate_id, { lines });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,139 +1,249 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Expense, ExpenseCategory } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
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
);
description: 'List all expenses with optional search and pagination',
inputSchema: {
type: 'object',
properties: {
search: {
type: 'string',
description: 'Search term to filter expenses',
},
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getExpenses(args);
return {
expenses: response.response.result.expenses || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific expense',
inputSchema: {
type: 'object',
properties: {
expense_id: {
type: 'number',
description: 'Expense ID',
},
},
required: ['expense_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getExpense(args.expense_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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' },
description: 'Create a new expense in FreshBooks',
inputSchema: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Expense amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
vendor: {
type: 'string',
description: 'Vendor name',
},
date: {
type: 'string',
description: 'Expense date (YYYY-MM-DD)',
},
categoryid: {
type: 'number',
description: 'Expense category ID',
},
clientid: {
type: 'number',
description: 'Client ID (optional)',
},
projectid: {
type: 'number',
description: 'Project ID (optional)',
},
notes: {
type: 'string',
description: 'Expense notes',
},
markup_percent: {
type: 'string',
description: 'Markup percentage for billable expenses',
},
},
required: ['amount', 'vendor', 'date', 'categoryid'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const expense = {
amount: {
amount: args.amount,
code: args.currency_code || 'USD',
},
vendor: args.vendor,
date: args.date,
categoryid: args.categoryid,
clientid: args.clientid,
projectid: args.projectid,
notes: args.notes,
markup_percent: args.markup_percent,
};
const result = await client.createExpense(expense);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
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' };
inputSchema: {
type: 'object',
properties: {
expense_id: {
type: 'number',
description: 'Expense ID to update',
},
amount: {
type: 'string',
description: 'Expense amount',
},
vendor: {
type: 'string',
description: 'Vendor name',
},
date: {
type: 'string',
description: 'Expense date',
},
notes: {
type: 'string',
description: 'Expense notes',
},
},
required: ['expense_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { expense_id, amount, ...updates } = args;
if (amount) {
const expense = await client.getExpense(expense_id);
updates.amount = {
amount,
code: expense.amount.code,
};
}
const expenseData = { expense: updateFields };
const response = await client.put<{ response: { result: { expense: Expense } } }>(
`/expenses/expenses/${expense_id}`,
expenseData
);
return response.response.result.expense;
const result = await client.updateExpense(expense_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete an expense from FreshBooks',
inputSchema: {
type: 'object',
properties: {
expense_id: {
type: 'number',
description: 'Expense ID to delete',
},
},
required: ['expense_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteExpense(args.expense_id);
return {
content: [
{
type: 'text',
text: `Expense ${args.expense_id} deleted successfully`,
},
],
};
},
},
{
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 || [];
inputSchema: {
type: 'object',
properties: {},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getExpenseCategories();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_search_expenses',
description: 'Search for expenses by vendor, category, or notes',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getExpenses({ search: args.query });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,268 +1,399 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Invoice, Payment } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const invoicesTools = [
{
name: 'freshbooks_list_invoices',
description: 'List all invoices with optional filtering (client, status, date range)',
inputSchema: z.object({
clientid: z.number().optional().describe('Filter by client ID'),
status: z.enum(['draft', 'sent', 'viewed', 'paid', 'partial', 'overdue', 'disputed']).optional(),
date_min: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
date_max: z.string().optional().describe('Maximum date (YYYY-MM-DD)'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<string, any> = {};
if (args.clientid) params.clientid = args.clientid;
if (args.status) params.status = args.status;
if (args.date_min) params.date_min = args.date_min;
if (args.date_max) params.date_max = args.date_max;
const response = await client.getPaginated<{ invoices: Invoice[] }>(
'/invoices/invoices',
args.page,
args.per_page,
params
);
description: 'List all invoices with optional search and pagination',
inputSchema: {
type: 'object',
properties: {
search: {
type: 'string',
description: 'Search term to filter invoices',
},
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getInvoices(args);
return {
invoices: response.response.result.invoices || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific invoice',
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getInvoice(args.invoice_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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,
description: 'Create a new invoice in FreshBooks',
inputSchema: {
type: 'object',
properties: {
customerid: {
type: 'number',
description: 'Client ID for this invoice',
},
create_date: {
type: 'string',
description: 'Invoice creation date (YYYY-MM-DD)',
},
due_offset_days: {
type: 'number',
description: 'Number of days until invoice is due',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD, CAD)',
},
language: {
type: 'string',
description: 'Language code (e.g., en)',
},
notes: {
type: 'string',
description: 'Invoice notes',
},
terms: {
type: 'string',
description: 'Invoice terms',
},
po_number: {
type: 'string',
description: 'Purchase order number',
},
discount_value: {
type: 'string',
description: 'Discount value as percentage or amount',
},
discount_description: {
type: 'string',
description: 'Description of discount',
},
lines: {
type: 'array',
description: 'Invoice line items',
items: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Line item name',
},
description: {
type: 'string',
description: 'Line item description',
},
qty: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Unit cost amount',
},
code: {
type: 'string',
description: 'Currency code',
},
},
},
taxName1: {
type: 'string',
description: 'First tax name',
},
taxAmount1: {
type: 'string',
description: 'First tax amount',
},
},
},
},
},
required: ['customerid'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createInvoice(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
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;
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID to update',
},
customerid: {
type: 'number',
description: 'Client ID',
},
notes: {
type: 'string',
description: 'Invoice notes',
},
terms: {
type: 'string',
description: 'Invoice terms',
},
po_number: {
type: 'string',
description: 'Purchase order number',
},
discount_value: {
type: 'string',
description: 'Discount value',
},
lines: {
type: 'array',
description: 'Invoice line items',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { invoice_id, ...updates } = args;
const result = await client.updateInvoice(invoice_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete an invoice from FreshBooks',
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID to delete',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteInvoice(args.invoice_id);
return {
content: [
{
type: 'text',
text: `Invoice ${args.invoice_id} deleted successfully`,
},
],
};
},
},
{
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` };
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID to send',
},
email: {
type: 'string',
description: 'Email address to send to (optional, uses client email if not provided)',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.sendInvoice(args.invoice_id, args.email);
return {
content: [
{
type: 'text',
text: `Invoice ${args.invoice_id} sent successfully`,
},
],
};
},
},
{
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,
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID to mark as paid',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.markInvoicePaid(args.invoice_id);
return {
content: [
{
type: 'text',
text: `Invoice ${args.invoice_id} marked as paid`,
},
date: args.payment_date || new Date().toISOString().split('T')[0],
type: args.payment_type,
],
};
},
},
{
name: 'freshbooks_get_invoice_share_link',
description: 'Get the shareable link for an invoice',
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID',
},
},
required: ['invoice_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const link = await client.getInvoiceShareLink(args.invoice_id);
return {
content: [
{
type: 'text',
text: link,
},
],
};
},
},
{
name: 'freshbooks_add_invoice_line',
description: 'Add a line item to an existing invoice',
inputSchema: {
type: 'object',
properties: {
invoice_id: {
type: 'number',
description: 'Invoice ID',
},
name: {
type: 'string',
description: 'Line item name',
},
description: {
type: 'string',
description: 'Line item description',
},
qty: {
type: 'string',
description: 'Quantity',
},
unit_cost: {
type: 'string',
description: 'Unit cost amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
},
required: ['invoice_id', 'name', 'qty', 'unit_cost'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const invoice = await client.getInvoice(args.invoice_id);
const newLine = {
name: args.name,
description: args.description || '',
qty: args.qty,
unit_cost: {
amount: args.unit_cost,
code: args.currency_code || invoice.currency_code,
},
};
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 lines = [...invoice.lines, newLine];
const result = await client.updateInvoice(args.invoice_id, { lines });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_search_invoices',
description: 'Search for invoices by various criteria',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query',
},
},
required: ['query'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getInvoices({ search: args.query });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
const response = await client.post<{ response: { result: { payment: Payment } } }>(
'/payments/payments',
paymentData
);
return response.response.result.payment;
},
},
];

View File

@ -1,110 +1,191 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Item } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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
);
description: 'List all items/services with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getItems(args);
return {
items: response.response.result.items || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific item/service',
inputSchema: {
type: 'object',
properties: {
item_id: {
type: 'number',
description: 'Item ID',
},
},
required: ['item_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getItem(args.item_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Create a new item/service in FreshBooks',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Item name',
},
description: {
type: 'string',
description: 'Item description',
},
quantity: {
type: 'string',
description: 'Quantity',
},
inventory: {
type: 'string',
description: 'Inventory count',
},
unit_cost: {
type: 'string',
description: 'Unit cost amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
sku: {
type: 'string',
description: 'SKU',
},
},
required: ['name', 'unit_cost'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const item = {
name: args.name,
description: args.description || '',
quantity: args.quantity || '1',
inventory: args.inventory || '0',
unit_cost: {
amount: args.unit_cost,
code: args.currency_code || 'USD',
},
sku: args.sku || '',
};
const result = await client.createItem(item);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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' };
description: 'Update an existing item/service',
inputSchema: {
type: 'object',
properties: {
item_id: {
type: 'number',
description: 'Item ID to update',
},
name: {
type: 'string',
description: 'Item name',
},
description: {
type: 'string',
description: 'Item description',
},
unit_cost: {
type: 'string',
description: 'Unit cost amount',
},
inventory: {
type: 'string',
description: 'Inventory count',
},
},
required: ['item_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { item_id, unit_cost, ...updates } = args;
if (unit_cost) {
const item = await client.getItem(item_id);
updates.unit_cost = {
amount: unit_cost,
code: item.unit_cost.code,
};
}
const itemData = { item: updateFields };
const response = await client.put<{ response: { result: { item: Item } } }>(
`/items/items/${item_id}`,
itemData
);
return response.response.result.item;
const result = await client.updateItem(item_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete an item/service from FreshBooks',
inputSchema: {
type: 'object',
properties: {
item_id: {
type: 'number',
description: 'Item ID to delete',
},
},
required: ['item_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteItem(args.item_id);
return {
content: [
{
type: 'text',
text: `Item ${args.item_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -0,0 +1,129 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const journalEntriesTools = [
{
name: 'freshbooks_list_journal_entries',
description: 'List all journal entries with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getJournalEntries(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_journal_entry',
description: 'Get detailed information about a specific journal entry',
inputSchema: {
type: 'object',
properties: {
journal_entry_id: {
type: 'number',
description: 'Journal entry ID',
},
},
required: ['journal_entry_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getJournalEntry(args.journal_entry_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_journal_entry',
description: 'Create a new journal entry in FreshBooks',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Journal entry name',
},
description: {
type: 'string',
description: 'Journal entry description',
},
user_entered_date: {
type: 'string',
description: 'Entry date (YYYY-MM-DD)',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
details: {
type: 'array',
description: 'Journal entry details (debits and credits)',
items: {
type: 'object',
properties: {
sub_accountid: {
type: 'number',
description: 'Sub-account ID',
},
debit_amount: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Debit amount (or null)',
},
},
},
credit_amount: {
type: 'object',
properties: {
amount: {
type: 'string',
description: 'Credit amount (or null)',
},
},
},
description: {
type: 'string',
description: 'Line description',
},
},
},
},
},
required: ['name', 'user_entered_date', 'details'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createJournalEntry(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,121 +1,187 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Payment } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
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
);
description: 'List all payments with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getPayments(args);
return {
payments: response.response.result.payments || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific payment',
inputSchema: {
type: 'object',
properties: {
payment_id: {
type: 'number',
description: 'Payment ID',
},
},
required: ['payment_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getPayment(args.payment_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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,
description: 'Record a new payment in FreshBooks',
inputSchema: {
type: 'object',
properties: {
invoiceid: {
type: 'number',
description: 'Invoice ID this payment is for',
},
amount: {
type: 'string',
description: 'Payment amount',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
date: {
type: 'string',
description: 'Payment date (YYYY-MM-DD)',
},
type: {
type: 'string',
description: 'Payment type (e.g., Cash, Check, Credit Card)',
},
note: {
type: 'string',
description: 'Payment note',
},
gateway: {
type: 'string',
description: 'Payment gateway name',
},
},
required: ['invoiceid', 'amount', 'date', 'type'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const payment = {
invoiceid: args.invoiceid,
amount: {
amount: args.amount,
code: args.currency_code || 'USD',
},
date: args.date,
type: args.type,
note: args.note,
gateway: args.gateway,
};
const result = await client.createPayment(payment);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
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' };
inputSchema: {
type: 'object',
properties: {
payment_id: {
type: 'number',
description: 'Payment ID to update',
},
amount: {
type: 'string',
description: 'Payment amount',
},
date: {
type: 'string',
description: 'Payment date',
},
note: {
type: 'string',
description: 'Payment note',
},
},
required: ['payment_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { payment_id, amount, ...updates } = args;
if (amount) {
const payment = await client.getPayment(payment_id);
updates.amount = {
amount,
code: payment.amount.code,
};
}
const paymentData = { payment: updateFields };
const response = await client.put<{ response: { result: { payment: Payment } } }>(
`/payments/payments/${payment_id}`,
paymentData
);
return response.response.result.payment;
const result = await client.updatePayment(payment_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete a payment from FreshBooks',
inputSchema: {
type: 'object',
properties: {
payment_id: {
type: 'number',
description: 'Payment ID to delete',
},
},
required: ['payment_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deletePayment(args.payment_id);
return {
content: [
{
type: 'text',
text: `Payment ${args.payment_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -1,124 +1,214 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Project, ProjectService } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
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
);
description: 'List all projects with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getProjects(args);
return {
projects: response.response.result.projects || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID',
},
},
required: ['project_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getProject(args.project_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Create a new project in FreshBooks',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Project title',
},
description: {
type: 'string',
description: 'Project description',
},
client_id: {
type: 'number',
description: 'Client ID',
},
due_date: {
type: 'string',
description: 'Project due date (YYYY-MM-DD)',
},
billing_method: {
type: 'string',
description: 'Billing method (project_rate, service_rate, task_rate, fixed_price)',
},
project_type: {
type: 'string',
description: 'Project type (fixed_price, hourly_rate)',
},
budget: {
type: 'number',
description: 'Project budget',
},
fixed_price: {
type: 'number',
description: 'Fixed price for the project',
},
rate: {
type: 'number',
description: 'Hourly rate',
},
internal: {
type: 'boolean',
description: 'Is this an internal project?',
},
},
required: ['title', 'client_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createProject(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID to update',
},
title: {
type: 'string',
description: 'Project title',
},
description: {
type: 'string',
description: 'Project description',
},
due_date: {
type: 'string',
description: 'Project due date',
},
active: {
type: 'boolean',
description: 'Is the project active?',
},
complete: {
type: 'boolean',
description: 'Is the project complete?',
},
},
required: ['project_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { project_id, ...updates } = args;
const result = await client.updateProject(project_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete a project from FreshBooks',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID to delete',
},
},
required: ['project_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteProject(args.project_id);
return {
content: [
{
type: 'text',
text: `Project ${args.project_id} deleted successfully`,
},
],
};
},
},
{
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 || [];
name: 'freshbooks_mark_project_complete',
description: 'Mark a project as complete',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID',
},
},
required: ['project_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.updateProject(args.project_id, { complete: true });
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,5 +1,5 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
import type { RecurringProfile } from '../types/index.js';
export const recurringTools = [
@ -11,21 +11,21 @@ export const recurringTools = [
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
handler: async (args: any, client: FreshBooksAPIClient) => {
const params: Record<string, any> = {};
if (args.clientid) params.clientid = args.clientid;
const response = await client.getPaginated<{ recurring: RecurringProfile[] }>(
const response = await client.getPaginated<any>(
'/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,
recurring: response.result?.recurring || response.recurring || [],
page: response.page || args.page,
pages: response.pages || 1,
total: response.total || 0,
};
},
},
@ -36,11 +36,11 @@ export const recurringTools = [
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 } } }>(
handler: async (args: any, client: FreshBooksAPIClient) => {
const response = await client.get<any>(
`/invoices/recurring/${args.recurring_id}`
);
return response.response.result.recurring;
return response.result?.recurring || response.recurring;
},
},
@ -61,7 +61,7 @@ export const recurringTools = [
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
handler: async (args: any, client: FreshBooksAPIClient) => {
const lines = args.lines.map((line: any) => ({
...line,
unit_cost: { amount: line.unit_cost, code: args.currency_code },
@ -80,11 +80,11 @@ export const recurringTools = [
},
};
const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>(
const response = await client.post<any>(
'/invoices/recurring',
recurringData
);
return response.response.result.recurring;
return response.result?.recurring || response.recurring;
},
},
@ -104,7 +104,7 @@ export const recurringTools = [
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
handler: async (args: any, client: FreshBooksAPIClient) => {
const { recurring_id, ...updateFields } = args;
if (updateFields.lines) {
@ -115,11 +115,11 @@ export const recurringTools = [
}
const recurringData = { recurring: updateFields };
const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>(
const response = await client.put<any>(
`/invoices/recurring/${recurring_id}`,
recurringData
);
return response.response.result.recurring;
return response.result?.recurring || response.recurring;
},
},
@ -129,7 +129,7 @@ export const recurringTools = [
inputSchema: z.object({
recurring_id: z.number().describe('Recurring profile ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
handler: async (args: any, client: FreshBooksAPIClient) => {
await client.put(
`/invoices/recurring/${args.recurring_id}`,
{ recurring: { vis_state: 1 } }

View File

@ -1,112 +1,110 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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;
description: 'Generate a profit and loss report for a date range',
inputSchema: {
type: 'object',
properties: {
start_date: {
type: 'string',
description: 'Start date (YYYY-MM-DD)',
},
end_date: {
type: 'string',
description: 'End date (YYYY-MM-DD)',
},
},
required: ['start_date', 'end_date'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getProfitLossReport(args.start_date, args.end_date);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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 || [];
description: 'Generate a tax summary report for a date range',
inputSchema: {
type: 'object',
properties: {
start_date: {
type: 'string',
description: 'Start date (YYYY-MM-DD)',
},
end_date: {
type: 'string',
description: 'End date (YYYY-MM-DD)',
},
},
required: ['start_date', 'end_date'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getTaxSummaryReport(args.start_date, args.end_date);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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_aging_report',
description: 'Generate an accounts aging report showing outstanding balances',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getAgingReport();
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Generate an expense report for a date range',
inputSchema: {
type: 'object',
properties: {
start_date: {
type: 'string',
description: 'Start date (YYYY-MM-DD)',
},
end_date: {
type: 'string',
description: 'End date (YYYY-MM-DD)',
},
},
required: ['start_date', 'end_date'],
},
},
{
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;
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getExpenseReport(args.start_date, args.end_date);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -0,0 +1,161 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const retainersTools = [
{
name: 'freshbooks_list_retainers',
description: 'List all retainers with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getRetainers(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_retainer',
description: 'Get detailed information about a specific retainer',
inputSchema: {
type: 'object',
properties: {
retainer_id: {
type: 'number',
description: 'Retainer ID',
},
},
required: ['retainer_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getRetainer(args.retainer_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_retainer',
description: 'Create a new retainer in FreshBooks',
inputSchema: {
type: 'object',
properties: {
client_id: {
type: 'number',
description: 'Client ID',
},
fee: {
type: 'string',
description: 'Retainer fee amount',
},
period: {
type: 'string',
description: 'Retainer period (monthly, quarterly, yearly)',
},
start_date: {
type: 'string',
description: 'Start date (YYYY-MM-DD)',
},
end_date: {
type: 'string',
description: 'End date (YYYY-MM-DD, optional)',
},
},
required: ['client_id', 'fee', 'period', 'start_date'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createRetainer(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_update_retainer',
description: 'Update an existing retainer',
inputSchema: {
type: 'object',
properties: {
retainer_id: {
type: 'number',
description: 'Retainer ID to update',
},
fee: {
type: 'string',
description: 'Retainer fee amount',
},
end_date: {
type: 'string',
description: 'End date',
},
active: {
type: 'boolean',
description: 'Is the retainer active?',
},
},
required: ['retainer_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { retainer_id, ...updates } = args;
const result = await client.updateRetainer(retainer_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_delete_retainer',
description: 'Delete a retainer from FreshBooks',
inputSchema: {
type: 'object',
properties: {
retainer_id: {
type: 'number',
description: 'Retainer ID to delete',
},
},
required: ['retainer_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteRetainer(args.retainer_id);
return {
content: [
{
type: 'text',
text: `Retainer ${args.retainer_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -0,0 +1,57 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const staffTools = [
{
name: 'freshbooks_list_staff',
description: 'List all staff members with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getStaff(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_staff_member',
description: 'Get detailed information about a specific staff member',
inputSchema: {
type: 'object',
properties: {
staff_id: {
type: 'number',
description: 'Staff member ID',
},
},
required: ['staff_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getStaffMember(args.staff_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -1,96 +1,148 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Tax } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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
);
description: 'List all taxes in FreshBooks',
inputSchema: {
type: 'object',
properties: {},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getTaxes();
return {
taxes: response.response.result.taxes || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific tax',
inputSchema: {
type: 'object',
properties: {
tax_id: {
type: 'number',
description: 'Tax ID',
},
},
required: ['tax_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getTax(args.tax_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Create a new tax in FreshBooks',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Tax name (e.g., GST, VAT, Sales Tax)',
},
amount: {
type: 'string',
description: 'Tax rate as a percentage (e.g., 5, 13.5)',
},
number: {
type: 'string',
description: 'Tax number/ID (optional)',
},
compound: {
type: 'boolean',
description: 'Is this a compound tax?',
},
},
required: ['name', 'amount'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createTax(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
inputSchema: {
type: 'object',
properties: {
tax_id: {
type: 'number',
description: 'Tax ID to update',
},
name: {
type: 'string',
description: 'Tax name',
},
amount: {
type: 'string',
description: 'Tax rate as a percentage',
},
number: {
type: 'string',
description: 'Tax number/ID',
},
},
required: ['tax_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { tax_id, ...updates } = args;
const result = await client.updateTax(tax_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete a tax from FreshBooks',
inputSchema: {
type: 'object',
properties: {
tax_id: {
type: 'number',
description: 'Tax ID to delete',
},
},
required: ['tax_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteTax(args.tax_id);
return {
content: [
{
type: 'text',
text: `Tax ${args.tax_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -1,125 +1,223 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { TimeEntry } from '../types/index.js';
import type { FreshBooksAPIClient } from '../clients/freshbooks.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<string, any> = {};
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
);
description: 'List all time entries with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getTimeEntries(args);
return {
time_entries: response.response.result.time_entries || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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;
description: 'Get detailed information about a specific time entry',
inputSchema: {
type: 'object',
properties: {
time_entry_id: {
type: 'number',
description: 'Time entry ID',
},
},
required: ['time_entry_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getTimeEntry(args.time_entry_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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,
description: 'Create a new time entry in FreshBooks',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID',
},
client_id: {
type: 'number',
description: 'Client ID',
},
duration: {
type: 'number',
description: 'Duration in seconds',
},
started_at: {
type: 'string',
description: 'Start time (ISO 8601)',
},
note: {
type: 'string',
description: 'Note about the time entry',
},
billable: {
type: 'boolean',
description: 'Is this time billable?',
},
internal: {
type: 'boolean',
description: 'Is this an internal time entry?',
},
},
required: ['project_id', 'duration', 'started_at'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createTimeEntry(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
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;
inputSchema: {
type: 'object',
properties: {
time_entry_id: {
type: 'number',
description: 'Time entry ID to update',
},
duration: {
type: 'number',
description: 'Duration in seconds',
},
note: {
type: 'string',
description: 'Note about the time entry',
},
billable: {
type: 'boolean',
description: 'Is this time billable?',
},
},
required: ['time_entry_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { time_entry_id, ...updates } = args;
const result = await client.updateTimeEntry(time_entry_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
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` };
description: 'Delete a time entry from FreshBooks',
inputSchema: {
type: 'object',
properties: {
time_entry_id: {
type: 'number',
description: 'Time entry ID to delete',
},
},
required: ['time_entry_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteTimeEntry(args.time_entry_id);
return {
content: [
{
type: 'text',
text: `Time entry ${args.time_entry_id} deleted successfully`,
},
],
};
},
},
{
name: 'freshbooks_start_timer',
description: 'Start a new timer for a project',
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'number',
description: 'Project ID',
},
note: {
type: 'string',
description: 'Note about what you are working on',
},
},
required: ['project_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.startTimer(args.project_id, args.note);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_stop_timer',
description: 'Stop a running timer',
inputSchema: {
type: 'object',
properties: {
time_entry_id: {
type: 'number',
description: 'Time entry ID of the running timer',
},
},
required: ['time_entry_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.stopTimer(args.time_entry_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
];

View File

@ -0,0 +1,201 @@
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
export const vendorsTools = [
{
name: 'freshbooks_list_vendors',
description: 'List all bill vendors with pagination',
inputSchema: {
type: 'object',
properties: {
page: {
type: 'number',
description: 'Page number for pagination',
},
per_page: {
type: 'number',
description: 'Number of results per page',
},
},
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getVendors(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_get_vendor',
description: 'Get detailed information about a specific vendor',
inputSchema: {
type: 'object',
properties: {
vendor_id: {
type: 'number',
description: 'Vendor ID',
},
},
required: ['vendor_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.getVendor(args.vendor_id);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_create_vendor',
description: 'Create a new vendor in FreshBooks',
inputSchema: {
type: 'object',
properties: {
vendor_name: {
type: 'string',
description: 'Vendor name',
},
primary_contact_first_name: {
type: 'string',
description: 'Primary contact first name',
},
primary_contact_last_name: {
type: 'string',
description: 'Primary contact last name',
},
primary_contact_email: {
type: 'string',
description: 'Primary contact email',
},
phone: {
type: 'string',
description: 'Phone number',
},
website: {
type: 'string',
description: 'Website URL',
},
street: {
type: 'string',
description: 'Street address',
},
city: {
type: 'string',
description: 'City',
},
province: {
type: 'string',
description: 'Province/state',
},
postal_code: {
type: 'string',
description: 'Postal code',
},
country: {
type: 'string',
description: 'Country',
},
currency_code: {
type: 'string',
description: 'Currency code (e.g., USD)',
},
is_1099: {
type: 'boolean',
description: 'Is this vendor subject to 1099 reporting?',
},
},
required: ['vendor_name', 'primary_contact_email'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const result = await client.createVendor(args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_update_vendor',
description: 'Update an existing vendor',
inputSchema: {
type: 'object',
properties: {
vendor_id: {
type: 'number',
description: 'Vendor ID to update',
},
vendor_name: {
type: 'string',
description: 'Vendor name',
},
primary_contact_email: {
type: 'string',
description: 'Primary contact email',
},
phone: {
type: 'string',
description: 'Phone number',
},
street: {
type: 'string',
description: 'Street address',
},
city: {
type: 'string',
description: 'City',
},
},
required: ['vendor_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
const { vendor_id, ...updates } = args;
const result = await client.updateVendor(vendor_id, updates);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
},
},
{
name: 'freshbooks_delete_vendor',
description: 'Delete a vendor from FreshBooks',
inputSchema: {
type: 'object',
properties: {
vendor_id: {
type: 'number',
description: 'Vendor ID to delete',
},
},
required: ['vendor_id'],
},
handler: async (client: FreshBooksAPIClient, args: any) => {
await client.deleteVendor(args.vendor_id);
return {
content: [
{
type: 'text',
text: `Vendor ${args.vendor_id} deleted successfully`,
},
],
};
},
},
];

View File

@ -2,71 +2,82 @@
export interface FreshBooksConfig {
accountId: string;
bearerToken: string;
baseUrl?: string;
accessToken: string;
apiBaseUrl?: string;
}
export interface PaginatedResponse<T> {
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 {
export interface FreshBooksClient {
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;
username: string;
home_phone: string | null;
business_phone: string | null;
mobile_phone: string | null;
fax: string | null;
company_industry: string | null;
company_size: string | null;
vat_name: string | null;
vat_number: string | null;
s_province: string;
s_city: string;
s_street: string;
s_street2: string;
s_code: string;
s_country: string;
p_province: string;
p_city: string;
p_street: string;
p_street2: string;
p_code: string;
p_country: string;
currency_code: string;
language: string;
note: string | null;
pref_email: boolean;
pref_gmail: boolean;
allow_late_fees: boolean;
allow_late_notifications: boolean;
role: string;
vis_state: number;
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 {
export interface FreshBooksInvoice {
id: number;
accountid: string;
accounting_systemid: string;
clientid: number;
create_date: string;
invoiceid: number;
invoice_number: string;
customerid: number;
create_date: string;
generation_date: string | null;
discount_value: string;
discount_description: string | null;
po_number: string | null;
template: string;
currency_code: string;
language: string;
terms: string | null;
notes: string | null;
address: string;
return_uri: string | null;
deposit_amount: string | null;
deposit_percentage: string | null;
deposit_status: string;
payment_status: string;
auto_bill: boolean;
v3_status: string;
date_paid: string | null;
estimateid: number;
basecampid: number;
sentid: number;
status: number;
parent: number;
fname: string;
lname: string;
organization: string;
amount: {
amount: string;
code: string;
@ -79,31 +90,21 @@ export interface Invoice {
amount: string;
code: string;
};
due_date: string;
status: string;
payment_status: string;
v3_status: string;
due_offset_days: number;
lines: InvoiceLine[];
terms?: string;
notes?: string;
discount_total?: {
amount: string;
code: string;
};
updated: string;
created_at: string;
presentation: InvoicePresentation;
}
export interface InvoiceLine {
id?: number;
name: string;
description?: string;
qty: number;
unit_cost: {
lineid?: number;
amount?: {
amount: string;
code: string;
};
amount: {
name: string;
description?: string;
qty: string;
unit_cost: {
amount: string;
code: string;
};
@ -111,219 +112,496 @@ export interface InvoiceLine {
taxAmount1?: string;
taxName2?: string;
taxAmount2?: string;
type?: number;
expenseid?: number;
}
export interface Payment {
export interface InvoicePresentation {
theme_primary_color: string;
theme_layout: string;
theme_font_name: string;
image_logo_src: string | null;
image_banner_src: string | null;
}
export interface FreshBooksEstimate {
id: number;
invoiceid: number;
accountid: string;
estimateid: number;
estimate_number: string;
customerid: number;
accepted: boolean;
create_date: string;
discount_value: string;
discount_description: string | null;
po_number: string | null;
template: string;
currency_code: string;
language: string;
terms: string | null;
notes: string | null;
address: string;
status: number;
fname: string;
lname: string;
organization: string;
amount: {
amount: string;
code: string;
};
date: string;
type: string;
note?: string;
updated: string;
created_at: string;
lines: EstimateLine[];
ui_status: string;
}
// Expense Types
export interface Expense {
id: number;
category_id: number;
clientid?: number;
projectid?: number;
vendor: string;
amount: {
export interface EstimateLine {
lineid?: number;
amount?: {
amount: string;
code: string;
};
name: string;
description?: string;
qty: string;
unit_cost: {
amount: string;
code: string;
};
date: string;
notes?: string;
taxName1?: string;
taxAmount1?: number;
taxPercent1?: number;
taxAmount1?: string;
taxName2?: string;
taxAmount2?: string;
type?: number;
}
export interface FreshBooksExpense {
id: number;
accountid: string;
amount: {
amount: string;
code: string;
};
vendor: string;
date: string;
categoryid: number;
clientid: number;
projectid: number;
staffid: number;
notes: string | null;
taxName1: string;
taxAmount1: number;
taxName2: string;
taxAmount2: number;
status: number;
is_cogs: boolean;
from_bulk_import: boolean;
attachment: ExpenseAttachment | null;
markup_percent: string;
updated: string;
created_at: string;
staffid?: number;
status?: string;
}
export interface ExpenseAttachment {
id: number;
jwt: string;
media_type: string;
}
export interface ExpenseCategory {
id: number;
category: string;
is_cogs?: boolean;
is_editable?: boolean;
parentid?: number;
categoryid: number;
created_at: string;
is_cogs: boolean;
is_editable: boolean;
parentid: number | null;
updated_at: string;
vis_state: number;
}
// Estimate Types
export interface Estimate {
export interface FreshBooksPayment {
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;
};
bulk_paymentid: number;
clientid: number;
creditid: number | null;
date: string;
from_credit: boolean;
gateway: string | null;
invoiceid: number;
logid: number;
note: string | null;
orderid: string | null;
overpaymentid: number;
transactionid: string | null;
type: string;
updated: string;
created_at: string;
vis_state: number;
}
export interface EstimateLine {
id?: number;
export interface FreshBooksProject {
id: number;
title: string;
description: string;
due_date: string | null;
client_id: number;
internal: boolean;
budget: number | null;
fixed_price: number | null;
rate: number | null;
billing_method: string;
project_type: string;
active: boolean;
complete: boolean;
sample: boolean;
created_at: string;
updated_at: string;
logged_duration: number;
services: ProjectService[];
billed_amount: string;
billed_status: string;
retainer_id: number | null;
}
export interface ProjectService {
business_id: number;
id: number;
name: string;
description?: string;
qty: number;
billable: boolean;
vis_state: number;
}
export interface FreshBooksTimeEntry {
id: number;
identity_id: number;
is_logged: boolean;
started_at: string;
created_at: string;
client_id: number;
project_id: number;
pending_client: string | null;
pending_project: string | null;
pending_task: string | null;
task_id: number | null;
service_id: number | null;
note: string | null;
active: boolean;
billable: boolean;
billed: boolean;
internal: boolean;
retainer_id: number | null;
duration: number;
timer: Timer | null;
}
export interface Timer {
id: number;
is_running: boolean;
started_at: string;
duration: number;
}
export interface FreshBooksTax {
id: number;
accounting_systemid: string;
name: string;
number: string | null;
amount: string;
compound: boolean;
updated: string;
}
export interface FreshBooksItem {
id: number;
accountid: string;
itemid: number;
name: string;
description: string;
quantity: string;
inventory: string;
unit_cost: {
amount: string;
code: string;
};
tax1: number;
tax2: number;
updated: string;
vis_state: number;
sku: string;
}
export interface FreshBooksStaff {
id: number;
identity_id: number;
first_name: string;
last_name: string;
email: string;
company: string;
business_id: number;
active: boolean;
created_at: string;
updated_at: string;
rate: {
amount: string;
code: string;
} | null;
}
export interface FreshBooksBill {
id: number;
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;
attachment: ExpenseAttachment | null;
bill_number: string | null;
bill_payments: BillPayment[];
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;
due_date: string;
due_offset_days: number;
issue_date: string;
language: string;
lines: BillLine[];
outstanding: {
amount: string;
code: string;
};
overall_category: string;
overall_description: string;
paid: {
amount: string;
code: string;
};
status: string;
tax_amount: {
amount: string;
code: string;
};
total_amount: {
amount: string;
code: string;
};
updated_at: string;
vendor_id: number;
vis_state: number;
}
export interface StaffMember {
export interface BillLine {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
role?: string;
amount: {
amount: string;
code: string;
};
category_id: number;
description: string;
list_index: number;
quantity: string;
tax_amount1: string | null;
tax_amount2: string | null;
tax_authorityid1: number | null;
tax_authorityid2: number | null;
tax_name1: string | null;
tax_name2: string | null;
tax_percent1: string | null;
tax_percent2: string | null;
total_amount: {
amount: string;
code: string;
};
unit_cost: {
amount: string;
code: string;
};
}
export interface BillPayment {
id: number;
amount: {
amount: string;
code: string;
};
bill_id: number;
matched_with_expense: boolean;
note: string | null;
paid_date: string;
payment_type: string;
vis_state: number;
}
export interface BillVendor {
id: number;
account_number: string | null;
city: string;
country: string;
currency_code: string;
is_1099: boolean;
language: string;
outstanding_balance: {
amount: string;
code: string;
}[];
overdue_balance: {
amount: string;
code: string;
}[];
phone: string | null;
postal_code: string;
primary_contact_email: string;
primary_contact_first_name: string;
primary_contact_last_name: string;
province: string;
street: string;
street2: string | null;
tax_defaults: TaxDefault[];
vendor_name: string;
vis_state: number;
website: string | null;
}
export interface TaxDefault {
systemid: number;
taxid: number;
}
export interface AccountingAccount {
id: number;
account_name: string;
account_number: string;
account_type: string;
balance: {
amount: string;
code: string;
};
currency_code: string;
custom: boolean;
parentid: number | null;
sub_accounts: AccountingAccount[];
}
export interface JournalEntry {
id: number;
created_at: string;
currency_code: string;
description: string;
details: JournalEntryDetail[];
name: string;
user_entered_date: string;
}
export interface JournalEntryDetail {
id: number;
credit_amount: {
amount: string;
code: string;
} | null;
currency_code: string;
debit_amount: {
amount: string;
code: string;
} | null;
description: string | null;
name: string;
sub_accountid: number;
user_entered_date: string;
}
export interface Retainer {
id: number;
active: boolean;
business_id: number;
client_id: number;
created_at: string;
end_date: string | null;
fee: string;
period: string;
start_date: string;
updated_at: string;
}
export interface CreditNote {
id: number;
accounting_systemid: string;
clientid: number;
creditid: number;
credit_number: string;
credit_type: string;
currency_code: string;
amount: {
amount: string;
code: string;
};
balance: {
amount: string;
code: string;
};
create_date: string;
language: string;
notes: string | null;
terms: string | null;
status: string;
lines: CreditNoteLine[];
}
export interface CreditNoteLine {
lineid: number;
amount: {
amount: string;
code: string;
};
name: string;
description: string;
qty: string;
unit_cost: {
amount: string;
code: string;
};
taxName1: string;
taxAmount1: string;
taxName2: string;
taxAmount2: string;
}
// Report Types
export interface ProfitLossReport {
total_income: {
amount: string;
code: string;
};
total_expenses: {
amount: string;
code: string;
};
start_date: string;
end_date: string;
currency_code: string;
income: ReportCategory[];
expenses: ReportCategory[];
net_profit: {
amount: string;
code: string;
};
start_date: string;
end_date: string;
}
export interface TaxSummary {
export interface ReportCategory {
category_name: string;
total: {
amount: string;
code: string;
};
children: ReportCategory[];
}
export interface TaxSummaryReport {
start_date: string;
end_date: string;
currency_code: string;
taxes: TaxSummaryItem[];
total_tax: {
amount: string;
code: string;
};
}
export interface TaxSummaryItem {
tax_name: string;
taxable_amount: {
amount: string;
code: string;
};
tax_collected: {
amount: string;
code: string;
@ -332,18 +610,115 @@ export interface TaxSummary {
amount: string;
code: string;
};
}
export interface AccountsAgingReport {
client_userid: number;
organization: string;
outstanding_balance: {
net_tax: {
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 };
}
export interface AgingReport {
currency_code: string;
current: {
amount: string;
code: string;
};
days_1_30: {
amount: string;
code: string;
};
days_31_60: {
amount: string;
code: string;
};
days_61_90: {
amount: string;
code: string;
};
days_over_90: {
amount: string;
code: string;
};
total: {
amount: string;
code: string;
};
clients: AgingReportClient[];
}
export interface AgingReportClient {
client_id: number;
client_name: string;
organization: string;
current: {
amount: string;
code: string;
};
days_1_30: {
amount: string;
code: string;
};
days_31_60: {
amount: string;
code: string;
};
days_61_90: {
amount: string;
code: string;
};
days_over_90: {
amount: string;
code: string;
};
total: {
amount: string;
code: string;
};
}
export interface ExpenseReport {
start_date: string;
end_date: string;
currency_code: string;
categories: ExpenseReportCategory[];
total: {
amount: string;
code: string;
};
}
export interface ExpenseReportCategory {
category_name: string;
total: {
amount: string;
code: string;
};
expenses: FreshBooksExpense[];
}
export interface PaginatedResponse<T> {
page: number;
pages: number;
per_page: number;
total: number;
results: T[];
}
export interface FreshBooksError {
message: string;
error_type?: string;
field?: string;
}
export interface RecurringProfile {
id: number;
recurring_id: number;
clientid: number;
frequency: string;
numberRecurring: number;
create_date: string;
currency_code: string;
lines: InvoiceLine[];
notes?: string;
terms?: string;
vis_state?: number;
}

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function AgingReport() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_aging_report', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading outstanding balance...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Outstanding Balance</h1>
<p className="subtitle">Accounts receivable aging</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,34 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1rem; }
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return (
<div className="container">
<h1>TITLE</h1>
<div className="card">
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Outstanding Balance - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #ef4444;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #ef4444;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #ef4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function BillManager() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_bills', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading bill manager...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Bill Manager</h1>
<p className="subtitle">Manage vendor bills</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bill Manager - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #f59e0b;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #f59e0b;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #f59e0b;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,37 @@
import { build } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { readdirSync, statSync } from 'fs';
const appsDir = resolve(process.cwd(), 'src/apps');
const apps = readdirSync(appsDir).filter(file => {
const fullPath = resolve(appsDir, file);
return statSync(fullPath).isDirectory();
});
console.log(`Building ${apps.length} apps...`);
for (const app of apps) {
console.log(`Building ${app}...`);
await build({
plugins: [react()],
build: {
outDir: resolve(process.cwd(), 'dist'),
rollupOptions: {
input: resolve(appsDir, app, 'index.html'),
output: {
entryFileNames: `${app}.js`,
assetFileNames: `${app}.[ext]`,
},
},
},
resolve: {
alias: {
'@': resolve(process.cwd(), 'src'),
},
},
});
}
console.log('Build complete!');

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ClientDashboard() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_clients', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading client directory...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Client Directory</h1>
<p className="subtitle">Manage all your clients</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,99 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clients Dashboard - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
h1 { font-size: 2rem; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
.btn:hover { background: #2563eb; }
.search-bar { margin-bottom: 1.5rem; }
.search-bar input { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
.client-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
.client-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
.client-card h3 { margin-bottom: 0.5rem; }
.client-card .email { color: #64748b; font-size: 0.875rem; margin-bottom: 1rem; }
.client-meta { display: flex; justify-content: space-between; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
.client-meta div { text-align: center; }
.client-meta .label { color: #64748b; font-size: 0.75rem; margin-bottom: 0.25rem; }
.client-meta .value { font-weight: bold; font-size: 1.125rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ClientDashboard() {
const [clients, setClients] = useState([]);
const [search, setSearch] = useState('');
useEffect(() => {
const sampleClients = [
{ id: 1, name: 'Acme Corp', email: 'contact@acme.com', invoices: 12, outstanding: 5600, lastInvoice: '2024-01-15' },
{ id: 2, name: 'Tech Solutions', email: 'hello@tech.com', invoices: 8, outstanding: 3200, lastInvoice: '2024-01-20' },
{ id: 3, name: 'Design Co', email: 'info@design.co', invoices: 15, outstanding: 0, lastInvoice: '2024-01-10' },
{ id: 4, name: 'Marketing Inc', email: 'team@marketing.com', invoices: 6, outstanding: 1800, lastInvoice: '2024-01-25' },
];
setClients(sampleClients);
}, []);
const filteredClients = clients.filter(client =>
client.name.toLowerCase().includes(search.toLowerCase()) ||
client.email.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="container">
<div className="header">
<div>
<h1>Clients</h1>
<p style={{ color: '#94a3b8' }}>{clients.length} active clients</p>
</div>
<button className="btn">+ Add Client</button>
</div>
<div className="search-bar">
<input type="text" placeholder="Search clients..." value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className="client-grid">
{filteredClients.map(client => (
<div key={client.id} className="client-card">
<h3>{client.name}</h3>
<div className="email">{client.email}</div>
<div className="client-meta">
<div>
<div className="label">Invoices</div>
<div className="value">{client.invoices}</div>
</div>
<div>
<div className="label">Outstanding</div>
<div className="value" style={{ color: client.outstanding > 0 ? '#fbbf24' : '#6ee7b7' }}>
${client.outstanding.toLocaleString()}
</div>
</div>
<div>
<div className="label">Last Invoice</div>
<div className="value" style={{ fontSize: '0.875rem' }}>{client.lastInvoice}</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<ClientDashboard />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Client Directory - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #8b5cf6;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #8b5cf6;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #8b5cf6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ClientDetail() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_get_client', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading client detail...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Client Detail</h1>
<p className="subtitle">View detailed client information</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,34 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1rem; }
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return (
<div className="container">
<h1>TITLE</h1>
<div className="card">
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Client Detail - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #8b5cf6;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #8b5cf6;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #8b5cf6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function CreditNoteViewer() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_credit_notes', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading credit note viewer...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Credit Note Viewer</h1>
<p className="subtitle">View and manage credit notes</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Credit Note Viewer - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #06b6d4;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #06b6d4;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #06b6d4;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function EstimateBuilder() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_estimates', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading estimate builder...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Estimate Builder</h1>
<p className="subtitle">Create and send estimates</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,34 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1rem; }
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return (
<div className="container">
<h1>TITLE</h1>
<div className="card">
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Estimate Builder - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #6366f1;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #6366f1;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #6366f1;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ExpenseCategories() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_expense_categories', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading expense categories...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Expense Categories</h1>
<p className="subtitle">Manage expense categories</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Expense Categories - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #f59e0b;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #f59e0b;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #f59e0b;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ExpenseTracker() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_expenses', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading expense tracker...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Expense Tracker</h1>
<p className="subtitle">Track and categorize all expenses</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,134 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expense Tracker - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
.header { margin-bottom: 2rem; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
.form-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
.form-group.full { grid-column: 1 / -1; }
label { font-size: 0.875rem; color: #94a3b8; }
input, select, textarea { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
.btn:hover { background: #2563eb; }
.expense-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
.expense-item { padding: 1rem; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
.expense-item:last-child { border-bottom: none; }
.expense-info .category { color: #64748b; font-size: 0.875rem; }
.expense-amount { font-weight: bold; font-size: 1.125rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function ExpenseTracker() {
const [expenses, setExpenses] = useState([
{ id: 1, vendor: 'Office Supplies Co', category: 'Office', amount: 245, date: '2024-01-15' },
{ id: 2, vendor: 'Tech Store', category: 'Equipment', amount: 1200, date: '2024-01-18' },
{ id: 3, vendor: 'Coffee Shop', category: 'Meals', amount: 45, date: '2024-01-20' },
]);
const [form, setForm] = useState({
vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '',
});
const totalExpenses = expenses.reduce((sum, exp) => sum + exp.amount, 0);
const thisMonth = expenses.filter(e => e.date.startsWith('2024-01')).reduce((sum, exp) => sum + exp.amount, 0);
const addExpense = () => {
if (form.vendor && form.amount) {
setExpenses([...expenses, { ...form, id: Date.now(), amount: parseFloat(form.amount) }]);
setForm({ vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '' });
}
};
return (
<div className="container">
<div className="header">
<h1>Expense Tracker</h1>
<p style={{ color: '#94a3b8' }}>Track and categorize business expenses</p>
</div>
<div className="stats">
<div className="stat-card">
<div className="value">${totalExpenses.toLocaleString()}</div>
<div className="label">Total Expenses</div>
</div>
<div className="stat-card">
<div className="value">${thisMonth.toLocaleString()}</div>
<div className="label">This Month</div>
</div>
<div className="stat-card">
<div className="value">{expenses.length}</div>
<div className="label">Transactions</div>
</div>
</div>
<div className="form-card">
<h3 style={{ marginBottom: '1rem' }}>Add Expense</h3>
<div className="form-grid">
<div className="form-group">
<label>Vendor</label>
<input type="text" value={form.vendor} onChange={(e) => setForm({ ...form, vendor: e.target.value })} placeholder="Vendor name" />
</div>
<div className="form-group">
<label>Category</label>
<select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })}>
<option value="Office">Office Supplies</option>
<option value="Equipment">Equipment</option>
<option value="Meals">Meals & Entertainment</option>
<option value="Travel">Travel</option>
<option value="Software">Software</option>
</select>
</div>
<div className="form-group">
<label>Amount</label>
<input type="number" value={form.amount} onChange={(e) => setForm({ ...form, amount: e.target.value })} placeholder="0.00" />
</div>
<div className="form-group">
<label>Date</label>
<input type="date" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} />
</div>
<div className="form-group full">
<label>Notes</label>
<textarea rows="2" value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} placeholder="Optional notes" />
</div>
</div>
<button className="btn" style={{ marginTop: '1rem' }} onClick={addExpense}>Add Expense</button>
</div>
<div className="expense-list">
{expenses.map(expense => (
<div key={expense.id} className="expense-item">
<div className="expense-info">
<div>{expense.vendor}</div>
<div className="category">{expense.category} • {expense.date}</div>
</div>
<div className="expense-amount">${expense.amount.toLocaleString()}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<ExpenseTracker />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Expense Tracker - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #ef4444;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #ef4444;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #ef4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,372 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const apps = [
{
name: 'invoice-dashboard',
title: 'Invoice Dashboard',
desc: 'Manage and track all your invoices',
tool: 'freshbooks_list_invoices',
color: '#10b981'
},
{
name: 'invoice-detail',
title: 'Invoice Detail',
desc: 'View detailed invoice information',
tool: 'freshbooks_get_invoice',
color: '#3b82f6'
},
{
name: 'client-dashboard',
title: 'Client Directory',
desc: 'Manage all your clients',
tool: 'freshbooks_list_clients',
color: '#8b5cf6'
},
{
name: 'client-detail',
title: 'Client Detail',
desc: 'View detailed client information',
tool: 'freshbooks_get_client',
color: '#8b5cf6'
},
{
name: 'expense-tracker',
title: 'Expense Tracker',
desc: 'Track and categorize all expenses',
tool: 'freshbooks_list_expenses',
color: '#ef4444'
},
{
name: 'expense-categories',
title: 'Expense Categories',
desc: 'Manage expense categories',
tool: 'freshbooks_list_expense_categories',
color: '#f59e0b'
},
{
name: 'time-entries',
title: 'Time Entry Log',
desc: 'Log and track time entries',
tool: 'freshbooks_list_time_entries',
color: '#06b6d4'
},
{
name: 'time-tracker',
title: 'Project Timer',
desc: 'Start and stop project timers',
tool: 'freshbooks_list_time_entries',
color: '#06b6d4'
},
{
name: 'project-dashboard',
title: 'Project Overview',
desc: 'View all active projects',
tool: 'freshbooks_list_projects',
color: '#10b981'
},
{
name: 'project-detail',
title: 'Project Detail',
desc: 'Detailed project information',
tool: 'freshbooks_get_project',
color: '#10b981'
},
{
name: 'payment-history',
title: 'Payment History',
desc: 'Track all payments',
tool: 'freshbooks_list_payments',
color: '#10b981'
},
{
name: 'estimate-builder',
title: 'Estimate Builder',
desc: 'Create and send estimates',
tool: 'freshbooks_list_estimates',
color: '#6366f1'
},
{
name: 'tax-summary',
title: 'Tax Summary',
desc: 'View tax reporting and summaries',
tool: 'freshbooks_tax_summary_report',
color: '#f59e0b'
},
{
name: 'recurring-invoices',
title: 'Recurring Templates',
desc: 'Manage recurring invoice templates',
tool: 'freshbooks_list_invoices',
color: '#8b5cf6'
},
{
name: 'profit-loss',
title: 'Profit & Loss',
desc: 'Financial P&L statements',
tool: 'freshbooks_profit_loss_report',
color: '#10b981'
},
{
name: 'revenue-chart',
title: 'Revenue Chart',
desc: 'Visual revenue analytics',
tool: 'freshbooks_list_invoices',
color: '#10b981'
},
{
name: 'aging-report',
title: 'Outstanding Balance',
desc: 'Accounts receivable aging',
tool: 'freshbooks_aging_report',
color: '#ef4444'
},
{
name: 'bill-manager',
title: 'Bill Manager',
desc: 'Manage vendor bills',
tool: 'freshbooks_list_bills',
color: '#f59e0b'
},
{
name: 'credit-note-viewer',
title: 'Credit Note Viewer',
desc: 'View and manage credit notes',
tool: 'freshbooks_list_credit_notes',
color: '#06b6d4'
},
{
name: 'reports-dashboard',
title: 'Report Builder',
desc: 'Build custom financial reports',
tool: 'freshbooks_profit_loss_report',
color: '#8b5cf6'
},
{
name: 'invoice-builder',
title: 'Invoice Builder',
desc: 'Create new invoices',
tool: 'freshbooks_create_invoice',
color: '#10b981'
},
];
const createApp = (app) => {
const appDir = path.join(__dirname, app.name);
if (!fs.existsSync(appDir)) {
fs.mkdirSync(appDir, { recursive: true });
}
// App.tsx
const appTsx = `import { useState, useEffect } from 'react';
import './styles.css';
export default function ${app.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('${app.tool}', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading ${app.title.toLowerCase()}...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>${app.title}</h1>
<p className="subtitle">${app.desc}</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}
`;
// styles.css
const stylesCss = `* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: ${app.color};
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid ${app.color};
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: ${app.color};
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}
`;
// main.tsx
const mainTsx = `import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
`;
// index.html
const indexHtml = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${app.title} - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
`;
// vite.config.ts
const viteConfig = `import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});
`;
// Write files
fs.writeFileSync(path.join(appDir, 'App.tsx'), appTsx);
fs.writeFileSync(path.join(appDir, 'styles.css'), stylesCss);
fs.writeFileSync(path.join(appDir, 'main.tsx'), mainTsx);
fs.writeFileSync(path.join(appDir, 'index.html'), indexHtml);
fs.writeFileSync(path.join(appDir, 'vite.config.ts'), viteConfig);
console.log(`✅ Created ${app.name}`);
};
console.log('🚀 Generating FreshBooks MCP Apps...\n');
apps.forEach(createApp);
console.log(`\n✨ Generated ${apps.length} apps successfully!`);

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function InvoiceBuilder() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_create_invoice', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading invoice builder...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Invoice Builder</h1>
<p className="subtitle">Create new invoices</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,139 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Builder - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
.header { margin-bottom: 2rem; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.form-grid { display: grid; gap: 1.5rem; }
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
label { font-size: 0.875rem; color: #94a3b8; font-weight: 500; }
input, select, textarea { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
input:focus, select:focus, textarea:focus { outline: none; border-color: #3b82f6; }
.line-items { margin-top: 2rem; }
.line-item { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 40px; gap: 1rem; margin-bottom: 1rem; align-items: end; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 500; }
.btn:hover { background: #2563eb; }
.btn-secondary { background: #334155; }
.btn-secondary:hover { background: #475569; }
.btn-danger { background: #ef4444; padding: 0.5rem; }
.btn-danger:hover { background: #dc2626; }
.total-section { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-top: 2rem; }
.total-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
.total-row.final { font-size: 1.25rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function InvoiceBuilder() {
const [invoice, setInvoice] = useState({
client: '',
invoiceNumber: 'INV-' + Date.now(),
date: new Date().toISOString().split('T')[0],
dueDate: '',
lines: [{ description: '', qty: 1, rate: 0 }],
notes: '',
});
const addLine = () => {
setInvoice({ ...invoice, lines: [...invoice.lines, { description: '', qty: 1, rate: 0 }] });
};
const removeLine = (index) => {
setInvoice({ ...invoice, lines: invoice.lines.filter((_, i) => i !== index) });
};
const updateLine = (index, field, value) => {
const newLines = [...invoice.lines];
newLines[index] = { ...newLines[index], [field]: value };
setInvoice({ ...invoice, lines: newLines });
};
const subtotal = invoice.lines.reduce((sum, line) => sum + (line.qty * line.rate), 0);
const tax = subtotal * 0.13; // 13% tax
const total = subtotal + tax;
return (
<div className="container">
<div className="header">
<h1>Create Invoice</h1>
<p style={{ color: '#94a3b8' }}>Build and send professional invoices</p>
</div>
<div className="form-grid">
<div className="form-group">
<label>Client</label>
<select value={invoice.client} onChange={(e) => setInvoice({ ...invoice, client: e.target.value })}>
<option value="">Select client...</option>
<option value="acme">Acme Corp</option>
<option value="tech">Tech Solutions</option>
<option value="design">Design Co</option>
</select>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem' }}>
<div className="form-group">
<label>Invoice #</label>
<input type="text" value={invoice.invoiceNumber} onChange={(e) => setInvoice({ ...invoice, invoiceNumber: e.target.value })} />
</div>
<div className="form-group">
<label>Date</label>
<input type="date" value={invoice.date} onChange={(e) => setInvoice({ ...invoice, date: e.target.value })} />
</div>
<div className="form-group">
<label>Due Date</label>
<input type="date" value={invoice.dueDate} onChange={(e) => setInvoice({ ...invoice, dueDate: e.target.value })} />
</div>
</div>
<div className="line-items">
<label>Line Items</label>
{invoice.lines.map((line, index) => (
<div key={index} className="line-item">
<input type="text" placeholder="Description" value={line.description} onChange={(e) => updateLine(index, 'description', e.target.value)} />
<input type="number" placeholder="Qty" value={line.qty} onChange={(e) => updateLine(index, 'qty', parseFloat(e.target.value))} />
<input type="number" placeholder="Rate" value={line.rate} onChange={(e) => updateLine(index, 'rate', parseFloat(e.target.value))} />
<div style={{ color: '#94a3b8', paddingTop: '0.75rem' }}>${(line.qty * line.rate).toFixed(2)}</div>
{invoice.lines.length > 1 && (
<button className="btn btn-danger" onClick={() => removeLine(index)}>×</button>
)}
</div>
))}
<button className="btn btn-secondary" onClick={addLine}>+ Add Line</button>
</div>
<div className="form-group">
<label>Notes</label>
<textarea rows="3" value={invoice.notes} onChange={(e) => setInvoice({ ...invoice, notes: e.target.value })} placeholder="Payment terms, thank you note, etc." />
</div>
</div>
<div className="total-section">
<div className="total-row"><span>Subtotal:</span><span>${subtotal.toFixed(2)}</span></div>
<div className="total-row"><span>Tax (13%):</span><span>${tax.toFixed(2)}</span></div>
<div className="total-row final"><span>Total:</span><span>${total.toFixed(2)}</span></div>
</div>
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem' }}>
<button className="btn">Save Draft</button>
<button className="btn" style={{ background: '#10b981' }}>Send Invoice</button>
</div>
</div>
);
}
ReactDOM.render(<InvoiceBuilder />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invoice Builder - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #10b981;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #10b981;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,194 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Invoice {
id: number;
number: string;
client: string;
amount: number;
status: 'paid' | 'partial' | 'overdue' | 'draft' | 'sent';
date: string;
dueDate: string;
}
export default function InvoiceDashboard() {
const [invoices, setInvoices] = useState<Invoice[]>([]);
const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0, outstanding: 0 });
const [filter, setFilter] = useState({ status: 'all', client: '' });
const [loading, setLoading] = useState(true);
useEffect(() => {
loadInvoices();
}, []);
const loadInvoices = async () => {
try {
// Call MCP tool to fetch invoices
const response = await (window as any).mcp?.callTool('freshbooks_list_invoices', {
page: 1,
per_page: 50
});
if (response?.invoices) {
setInvoices(response.invoices);
calculateStats(response.invoices);
} else {
// Fallback to sample data
loadSampleData();
}
} catch (error) {
console.error('Error loading invoices:', error);
loadSampleData();
} finally {
setLoading(false);
}
};
const loadSampleData = () => {
const sampleInvoices: Invoice[] = [
{ id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' },
{ id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' },
{ id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' },
{ id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' },
{ id: 5, number: 'INV-005', client: 'Startup Labs', amount: 5200, status: 'sent', date: '2024-02-01', dueDate: '2024-03-01' },
{ id: 6, number: 'INV-006', client: 'Enterprise Co', amount: 8900, status: 'paid', date: '2024-02-05', dueDate: '2024-03-05' },
];
setInvoices(sampleInvoices);
calculateStats(sampleInvoices);
};
const calculateStats = (invoices: Invoice[]) => {
const totalRevenue = invoices.reduce((sum, inv) => sum + inv.amount, 0);
const paidAmount = invoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0);
const overdueAmount = invoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0);
const draftCount = invoices.filter(i => i.status === 'draft').length;
const outstandingAmount = invoices.filter(i => i.status === 'sent' || i.status === 'partial').reduce((sum, inv) => sum + inv.amount, 0);
setStats({
total: totalRevenue,
paid: paidAmount,
overdue: overdueAmount,
draft: draftCount,
outstanding: outstandingAmount
});
};
const filteredInvoices = invoices.filter(inv => {
if (filter.status !== 'all' && inv.status !== filter.status) return false;
if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false;
return true;
});
const handleViewInvoice = async (invoiceId: number) => {
try {
await (window as any).mcp?.callTool('freshbooks_get_invoice', { invoice_id: invoiceId });
} catch (error) {
console.error('Error viewing invoice:', error);
}
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading invoices...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Invoice Dashboard</h1>
<p className="subtitle">Manage and track all your invoices</p>
</div>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Revenue</h3>
<div className="value">${stats.total.toLocaleString()}</div>
<div className="label">All invoices</div>
</div>
<div className="stat-card green">
<h3>Paid</h3>
<div className="value">${stats.paid.toLocaleString()}</div>
<div className="label">Received</div>
</div>
<div className="stat-card blue">
<h3>Outstanding</h3>
<div className="value">${stats.outstanding.toLocaleString()}</div>
<div className="label">Awaiting payment</div>
</div>
<div className="stat-card red">
<h3>Overdue</h3>
<div className="value">${stats.overdue.toLocaleString()}</div>
<div className="label">Past due</div>
</div>
<div className="stat-card yellow">
<h3>Drafts</h3>
<div className="value">{stats.draft}</div>
<div className="label">Pending</div>
</div>
</div>
<div className="filters">
<div className="filter-group">
<label>Status</label>
<select value={filter.status} onChange={(e) => setFilter({ ...filter, status: e.target.value })}>
<option value="all">All Statuses</option>
<option value="paid">Paid</option>
<option value="sent">Sent</option>
<option value="partial">Partial</option>
<option value="overdue">Overdue</option>
<option value="draft">Draft</option>
</select>
</div>
<div className="filter-group">
<label>Client</label>
<input type="text" placeholder="Search client..." value={filter.client} onChange={(e) => setFilter({ ...filter, client: e.target.value })} />
</div>
<div className="filter-group">
<button className="btn" onClick={loadInvoices}>Refresh</button>
</div>
</div>
<div className="table-container">
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Client</th>
<th>Date</th>
<th>Due Date</th>
<th>Amount</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredInvoices.length === 0 ? (
<tr>
<td colSpan={7} style={{ textAlign: 'center', padding: '2rem', color: '#94a3b8' }}>
No invoices found
</td>
</tr>
) : (
filteredInvoices.map(invoice => (
<tr key={invoice.id}>
<td className="font-medium">{invoice.number}</td>
<td>{invoice.client}</td>
<td>{invoice.date}</td>
<td>{invoice.dueDate}</td>
<td className="amount">${invoice.amount.toLocaleString()}</td>
<td><span className={`status status-${invoice.status}`}>{invoice.status}</span></td>
<td>
<button className="btn-small" onClick={() => handleViewInvoice(invoice.id)}>View</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}

View File

@ -1,154 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Dashboard - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.header { margin-bottom: 2rem; }
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
.stat-card h3 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; margin-bottom: 0.5rem; }
.stat-card .value { font-size: 2rem; font-weight: bold; color: #e2e8f0; }
.stat-card .label { color: #64748b; font-size: 0.875rem; margin-top: 0.25rem; }
.filters { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.filter-group { display: flex; flex-direction: column; gap: 0.5rem; }
.filter-group label { font-size: 0.875rem; color: #94a3b8; }
.filter-group select, .filter-group input { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.5rem; border-radius: 4px; }
.table-container { background: #1e293b; border-radius: 8px; overflow: hidden; }
table { width: 100%; border-collapse: collapse; }
th { background: #0f172a; text-align: left; padding: 1rem; font-weight: 600; color: #94a3b8; text-transform: uppercase; font-size: 0.75rem; }
td { padding: 1rem; border-top: 1px solid #334155; }
.status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
.status-paid { background: #065f46; color: #6ee7b7; }
.status-partial { background: #78350f; color: #fbbf24; }
.status-overdue { background: #7f1d1d; color: #fca5a5; }
.status-draft { background: #1e3a8a; color: #93c5fd; }
.amount { font-weight: 600; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; }
.btn:hover { background: #2563eb; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function InvoiceDashboard() {
const [invoices, setInvoices] = useState([]);
const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0 });
const [filter, setFilter] = useState({ status: 'all', client: '' });
useEffect(() => {
// Simulate data fetch
const sampleInvoices = [
{ id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' },
{ id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' },
{ id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' },
{ id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' },
];
setInvoices(sampleInvoices);
setStats({
total: sampleInvoices.reduce((sum, inv) => sum + inv.amount, 0),
paid: sampleInvoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0),
overdue: sampleInvoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0),
draft: sampleInvoices.filter(i => i.status === 'draft').length,
});
}, []);
const filteredInvoices = invoices.filter(inv => {
if (filter.status !== 'all' && inv.status !== filter.status) return false;
if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false;
return true;
});
return (
<div className="container">
<div className="header">
<h1>Invoice Dashboard</h1>
<p style={{ color: '#94a3b8' }}>Manage and track all your invoices</p>
</div>
<div className="stats-grid">
<div className="stat-card">
<h3>Total Revenue</h3>
<div className="value">${stats.total.toLocaleString()}</div>
<div className="label">All invoices</div>
</div>
<div className="stat-card" style={{ borderLeftColor: '#10b981' }}>
<h3>Paid</h3>
<div className="value">${stats.paid.toLocaleString()}</div>
<div className="label">Received</div>
</div>
<div className="stat-card" style={{ borderLeftColor: '#ef4444' }}>
<h3>Overdue</h3>
<div className="value">${stats.overdue.toLocaleString()}</div>
<div className="label">Past due</div>
</div>
<div className="stat-card" style={{ borderLeftColor: '#f59e0b' }}>
<h3>Drafts</h3>
<div className="value">{stats.draft}</div>
<div className="label">Pending</div>
</div>
</div>
<div className="filters">
<div className="filter-group">
<label>Status</label>
<select value={filter.status} onChange={(e) => setFilter({ ...filter, status: e.target.value })}>
<option value="all">All Statuses</option>
<option value="paid">Paid</option>
<option value="partial">Partial</option>
<option value="overdue">Overdue</option>
<option value="draft">Draft</option>
</select>
</div>
<div className="filter-group">
<label>Client</label>
<input type="text" placeholder="Search client..." value={filter.client} onChange={(e) => setFilter({ ...filter, client: e.target.value })} />
</div>
</div>
<div className="table-container">
<table>
<thead>
<tr>
<th>Invoice #</th>
<th>Client</th>
<th>Date</th>
<th>Due Date</th>
<th>Amount</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredInvoices.map(invoice => (
<tr key={invoice.id}>
<td>{invoice.number}</td>
<td>{invoice.client}</td>
<td>{invoice.date}</td>
<td>{invoice.dueDate}</td>
<td className="amount">${invoice.amount.toLocaleString()}</td>
<td><span className={`status status-${invoice.status}`}>{invoice.status}</span></td>
<td><button className="btn">View</button></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
ReactDOM.render(<InvoiceDashboard />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invoice Dashboard - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #10b981;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #10b981;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function InvoiceDetail() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_get_invoice', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading invoice detail...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Invoice Detail</h1>
<p className="subtitle">View detailed invoice information</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,116 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice Detail - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
.invoice-header { background: #1e293b; padding: 2rem; border-radius: 8px; margin-bottom: 2rem; }
.invoice-header h1 { font-size: 2rem; margin-bottom: 1rem; }
.status-badge { padding: 0.5rem 1rem; border-radius: 12px; display: inline-block; background: #10b981; color: white; }
.invoice-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem; }
.invoice-body { background: #1e293b; padding: 2rem; border-radius: 8px; }
table { width: 100%; margin-bottom: 2rem; }
th { text-align: left; padding: 0.75rem; background: #0f172a; color: #94a3b8; }
td { padding: 0.75rem; border-top: 1px solid #334155; }
.total-section { text-align: right; }
.total-row { display: flex; justify-content: flex-end; gap: 2rem; margin-bottom: 0.5rem; }
.total-row.final { font-size: 1.5rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
.actions { margin-top: 2rem; display: flex; gap: 1rem; }
.btn { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }
.btn-primary { background: #3b82f6; color: white; }
.btn-secondary { background: #334155; color: white; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function InvoiceDetail() {
const invoice = {
number: 'INV-001',
client: 'Acme Corp',
date: '2024-01-15',
dueDate: '2024-02-15',
status: 'Paid',
lines: [
{ description: 'Website Design', qty: 1, rate: 2000, amount: 2000 },
{ description: 'Logo Design', qty: 1, rate: 500, amount: 500 },
],
subtotal: 2500,
tax: 325,
total: 2825,
};
return (
<div className="container">
<div className="invoice-header">
<h1>{invoice.number}</h1>
<span className="status-badge">{invoice.status}</span>
<div className="invoice-meta">
<div>
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Client</div>
<div style={{ fontSize: '1.125rem', fontWeight: '500' }}>{invoice.client}</div>
</div>
<div>
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Amount</div>
<div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>${invoice.total}</div>
</div>
<div>
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Invoice Date</div>
<div>{invoice.date}</div>
</div>
<div>
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Due Date</div>
<div>{invoice.dueDate}</div>
</div>
</div>
</div>
<div className="invoice-body">
<table>
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th>Rate</th>
<th style={{ textAlign: 'right' }}>Amount</th>
</tr>
</thead>
<tbody>
{invoice.lines.map((line, i) => (
<tr key={i}>
<td>{line.description}</td>
<td>{line.qty}</td>
<td>${line.rate}</td>
<td style={{ textAlign: 'right' }}>${line.amount}</td>
</tr>
))}
</tbody>
</table>
<div className="total-section">
<div className="total-row"><span>Subtotal:</span><span>${invoice.subtotal}</span></div>
<div className="total-row"><span>Tax (13%):</span><span>${invoice.tax}</span></div>
<div className="total-row final"><span>Total:</span><span>${invoice.total}</span></div>
</div>
<div className="actions">
<button className="btn btn-primary">Send Invoice</button>
<button className="btn btn-secondary">Download PDF</button>
<button className="btn btn-secondary">Edit</button>
</div>
</div>
</div>
);
}
ReactDOM.render(<InvoiceDetail />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Invoice Detail - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #3b82f6;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #3b82f6;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,22 @@
{
"name": "freshbooks-mcp-apps",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "node build-all.js",
"dev": "vite"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^0.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.7"
}
}

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function PaymentHistory() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_payments', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading payment history...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Payment History</h1>
<p className="subtitle">Track all payments</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,86 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Payment History - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0; }
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
.payment-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
.payment-item { padding: 1.5rem; border-bottom: 1px solid #334155; display: grid; grid-template-columns: 1fr 2fr 1fr 1fr 1fr; gap: 1rem; align-items: center; }
.payment-item:last-child { border-bottom: none; }
.payment-date { color: #94a3b8; }
.payment-invoice { font-weight: 500; }
.payment-client { color: #64748b; font-size: 0.875rem; }
.payment-method { padding: 0.25rem 0.75rem; background: #0f172a; border-radius: 4px; font-size: 0.875rem; text-align: center; }
.payment-amount { font-size: 1.25rem; font-weight: bold; text-align: right; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function PaymentHistory() {
const [payments] = useState([
{ id: 1, date: '2024-01-25', invoice: 'INV-001', client: 'Acme Corp', method: 'Credit Card', amount: 2500 },
{ id: 2, date: '2024-01-23', invoice: 'INV-005', client: 'Tech Solutions', method: 'Bank Transfer', amount: 4200 },
{ id: 3, date: '2024-01-20', invoice: 'INV-003', client: 'Design Co', method: 'PayPal', amount: 1800 },
{ id: 4, date: '2024-01-18', invoice: 'INV-007', client: 'Marketing Inc', method: 'Check', amount: 3600 },
{ id: 5, date: '2024-01-15', invoice: 'INV-002', client: 'Acme Corp', method: 'Credit Card', amount: 5000 },
]);
const totalReceived = payments.reduce((sum, p) => sum + p.amount, 0);
const thisMonth = payments.filter(p => p.date.startsWith('2024-01')).reduce((sum, p) => sum + p.amount, 0);
return (
<div className="container">
<h1>Payment History</h1>
<p style={{ color: '#94a3b8' }}>Track all received payments</p>
<div className="stats">
<div className="stat-card">
<div className="value">${totalReceived.toLocaleString()}</div>
<div className="label">Total Received</div>
</div>
<div className="stat-card">
<div className="value">${thisMonth.toLocaleString()}</div>
<div className="label">This Month</div>
</div>
<div className="stat-card">
<div className="value">{payments.length}</div>
<div className="label">Payments</div>
</div>
</div>
<div className="payment-list">
{payments.map(payment => (
<div key={payment.id} className="payment-item">
<div className="payment-date">{payment.date}</div>
<div>
<div className="payment-invoice">{payment.invoice}</div>
<div className="payment-client">{payment.client}</div>
</div>
<div className="payment-method">{payment.method}</div>
<div className="payment-amount">${payment.amount.toLocaleString()}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<PaymentHistory />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payment History - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #10b981;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #10b981;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ProfitLoss() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_profit_loss_report', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading profit & loss...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Profit & Loss</h1>
<p className="subtitle">Financial P&L statements</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,34 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1rem; }
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return (
<div className="container">
<h1>TITLE</h1>
<div className="card">
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Profit & Loss - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #10b981;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #10b981;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ProjectDashboard() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_list_projects', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading project overview...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Project Overview</h1>
<p className="subtitle">View all active projects</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,92 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projects - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
h1 { font-size: 2rem; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
.project-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-top: 4px solid #3b82f6; }
.project-card.hourly { border-top-color: #10b981; }
.project-card.fixed { border-top-color: #f59e0b; }
.project-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; }
.project-title { font-size: 1.25rem; font-weight: 600; }
.project-status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
.status-active { background: #065f46; color: #6ee7b7; }
.status-complete { background: #1e40af; color: #93c5fd; }
.client-name { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1rem; }
.progress-bar { background: #0f172a; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 1rem; }
.progress-fill { background: linear-gradient(90deg, #3b82f6, #10b981); height: 100%; }
.project-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.stat { text-align: center; }
.stat .value { font-size: 1.5rem; font-weight: bold; }
.stat .label { color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function ProjectDashboard() {
const [projects] = useState([
{ id: 1, title: 'Website Redesign', client: 'Acme Corp', type: 'hourly', status: 'active', progress: 65, hours: 45, budget: 8000 },
{ id: 2, title: 'Mobile App', client: 'Tech Solutions', type: 'fixed', status: 'active', progress: 30, hours: 22, budget: 15000 },
{ id: 3, title: 'Brand Identity', client: 'Design Co', type: 'hourly', status: 'complete', progress: 100, hours: 60, budget: 12000 },
{ id: 4, title: 'E-commerce Store', client: 'Marketing Inc', type: 'fixed', status: 'active', progress: 50, hours: 35, budget: 20000 },
]);
return (
<div className="container">
<div className="header">
<div>
<h1>Projects</h1>
<p style={{ color: '#94a3b8' }}>{projects.filter(p => p.status === 'active').length} active projects</p>
</div>
<button className="btn">+ New Project</button>
</div>
<div className="project-grid">
{projects.map(project => (
<div key={project.id} className={`project-card ${project.type}`}>
<div className="project-header">
<div className="project-title">{project.title}</div>
<span className={`project-status status-${project.status}`}>{project.status}</span>
</div>
<div className="client-name">{project.client}</div>
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${project.progress}%` }} />
</div>
<div style={{ textAlign: 'center', marginBottom: '1rem', color: '#94a3b8', fontSize: '0.875rem' }}>
{project.progress}% complete
</div>
<div className="project-stats">
<div className="stat">
<div className="value">{project.hours}h</div>
<div className="label">Hours Tracked</div>
</div>
<div className="stat">
<div className="value">${(project.budget / 1000).toFixed(0)}k</div>
<div className="label">{project.type === 'fixed' ? 'Fixed Price' : 'Budget'}</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<ProjectDashboard />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project Overview - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,87 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #111827;
color: #e2e8f0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
color: #10b981;
}
.subtitle {
color: #94a3b8;
}
.loading {
text-align: center;
padding: 4rem;
font-size: 1.25rem;
color: #94a3b8;
}
.error {
background: #7f1d1d;
color: #fca5a5;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.card {
background: #1f2937;
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid #10b981;
}
.card h2 {
color: #e2e8f0;
margin-bottom: 1rem;
font-size: 1.25rem;
}
.data-preview {
background: #111827;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.875rem;
color: #94a3b8;
max-height: 600px;
overflow-y: auto;
}
.btn {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.btn:hover {
opacity: 0.9;
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,62 @@
import { useState, useEffect } from 'react';
import './styles.css';
export default function ProjectDetail() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Call MCP tool
const response = await (window as any).mcp?.callTool('freshbooks_get_project', {});
if (response) {
setData(response);
} else {
setData(getSampleData());
}
} catch (err) {
console.error('Error loading data:', err);
setError(err instanceof Error ? err.message : 'Failed to load data');
setData(getSampleData());
} finally {
setLoading(false);
}
};
const getSampleData = () => {
return { message: 'Sample data - MCP tools not available', items: [] };
};
if (loading) {
return (
<div className="container">
<div className="loading">Loading project detail...</div>
</div>
);
}
return (
<div className="container">
<div className="header">
<h1>Project Detail</h1>
<p className="subtitle">Detailed project information</p>
</div>
<div className="content">
{error && <div className="error">{error}</div>}
<div className="card">
<h2>Data</h2>
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
</div>
</div>
</div>
);
}

View File

@ -1,34 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TITLE - FreshBooks</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 1rem; }
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
return (
<div className="container">
<h1>TITLE</h1>
<div className="card">
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project Detail - FreshBooks MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More