FreshBooks: Complete MCP server with 55+ tools and 22 React apps

- 55+ tools across 12 categories (invoices, clients, expenses, estimates, time, projects, payments, items, taxes, reports, recurring, accounts)
- FreshBooks API client with OAuth2, pagination, error handling
- 22 dark-themed React MCP apps (invoice dashboard, builder, client dashboard, expense tracker, time tracker, project dashboard, reports, etc.)
- Full TypeScript types for all FreshBooks entities
- Comprehensive README with examples and architecture docs
This commit is contained in:
Jake Shore 2026-02-12 17:09:42 -05:00
parent 8e9d1ffb87
commit ff349dc88f
42 changed files with 3985 additions and 457 deletions

View File

@ -0,0 +1,241 @@
# FreshBooks MCP Server
Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting.
## Features
### 🎯 55+ Tools
**Invoices** (10 tools)
- List, get, create, update, delete invoices
- Send invoices via email
- Mark paid/unpaid, create payments
- Get payment history
**Clients** (6 tools)
- List, get, create, update, delete clients
- List client contacts
**Expenses** (6 tools)
- List, get, create, update, delete expenses
- List expense categories
**Estimates** (7 tools)
- List, get, create, update, delete estimates
- Send estimates, convert to invoices
**Time Tracking** (5 tools)
- List, get, create, update, delete time entries
**Projects** (6 tools)
- List, get, create, update, delete projects
- List project services
**Payments** (5 tools)
- List, get, create, update, delete payments
**Items** (5 tools)
- List, get, create, update, delete items (products/services)
**Taxes** (5 tools)
- List, get, create, update, delete taxes
**Reports** (5 tools)
- Profit & Loss report
- Tax summary
- Accounts aging
- Expense report
- Revenue by client
**Recurring** (5 tools)
- List, get, create, update, delete recurring profiles
**Accounts** (3 tools)
- Get account details
- List staff members
- Get current user
### 🎨 22 React MCP Apps
Dark-themed, client-side state React apps (inline HTML):
1. **invoice-dashboard** - Overview of all invoices with stats
2. **invoice-detail** - Single invoice view
3. **invoice-builder** - Create/edit invoices
4. **invoice-grid** - Grid view of invoices
5. **client-dashboard** - Client overview with metrics
6. **client-detail** - Single client view
7. **client-grid** - Grid view of clients
8. **expense-dashboard** - Expense overview
9. **expense-tracker** - Add and track expenses
10. **estimate-builder** - Create/edit estimates
11. **estimate-grid** - Grid view of estimates
12. **time-tracker** - Real-time timer for tracking hours
13. **time-entries** - List of time entries
14. **project-dashboard** - Project overview with progress
15. **project-detail** - Single project view
16. **payment-history** - List of all payments
17. **reports-dashboard** - Reports menu
18. **profit-loss** - Profit & loss report
19. **tax-summary** - Tax summary report
20. **aging-report** - Accounts aging report
21. **recurring-invoices** - Recurring invoice profiles
22. **revenue-chart** - Revenue visualization
## Installation
```bash
npm install
npm run build
```
## Configuration
Set environment variables:
```bash
export FRESHBOOKS_ACCOUNT_ID="your_account_id"
export FRESHBOOKS_BEARER_TOKEN="your_bearer_token"
```
## Usage
### As MCP Server
Add to your MCP settings:
```json
{
"mcpServers": {
"freshbooks": {
"command": "node",
"args": ["/path/to/freshbooks/dist/main.js"],
"env": {
"FRESHBOOKS_ACCOUNT_ID": "your_account_id",
"FRESHBOOKS_BEARER_TOKEN": "your_bearer_token"
}
}
}
}
```
### Direct Usage
```bash
npm start
```
## Architecture
```
src/
├── clients/
│ └── freshbooks.ts # API client with OAuth2, pagination, error handling
├── tools/
│ ├── invoices-tools.ts # 10 invoice tools
│ ├── clients-tools.ts # 6 client tools
│ ├── expenses-tools.ts # 6 expense tools
│ ├── estimates-tools.ts # 7 estimate tools
│ ├── time-entries-tools.ts # 5 time tracking tools
│ ├── projects-tools.ts # 6 project tools
│ ├── payments-tools.ts # 5 payment tools
│ ├── items-tools.ts # 5 item tools
│ ├── taxes-tools.ts # 5 tax tools
│ ├── reports-tools.ts # 5 report tools
│ ├── recurring-tools.ts # 5 recurring tools
│ └── accounts-tools.ts # 3 account tools
├── types/
│ └── index.ts # TypeScript types for FreshBooks API
├── ui/
│ └── react-app/ # 22 standalone React apps
├── server.ts # MCP server implementation
└── main.ts # Entry point
```
## API Client Features
- **OAuth2 Bearer Authentication**
- **Automatic Pagination** - Fetch all pages or paginated results
- **Error Handling** - Structured error responses
- **Rate Limiting** - Respects FreshBooks API limits
- **Type Safety** - Full TypeScript support
## Example Tool Calls
### Create Invoice
```typescript
{
"name": "freshbooks_create_invoice",
"arguments": {
"clientid": 12345,
"lines": [
{ "name": "Website Design", "qty": 1, "unit_cost": "2500.00" },
{ "name": "Hosting Setup", "qty": 1, "unit_cost": "150.00" }
],
"currency_code": "USD",
"notes": "Thank you for your business!"
}
}
```
### List Overdue Invoices
```typescript
{
"name": "freshbooks_list_invoices",
"arguments": {
"status": "overdue",
"per_page": 50
}
}
```
### Track Time
```typescript
{
"name": "freshbooks_create_time_entry",
"arguments": {
"duration": 7200,
"note": "Website development",
"started_at": "2024-01-15T09:00:00Z",
"projectid": 456
}
}
```
### Generate Profit/Loss Report
```typescript
{
"name": "freshbooks_profit_loss_report",
"arguments": {
"start_date": "2024-01-01",
"end_date": "2024-01-31",
"currency_code": "USD"
}
}
```
## Development
### Build
```bash
npm run build
```
### Watch Mode
```bash
npm run watch
```
## License
MIT
## Author
MCPEngine - Complete MCP implementations for modern platforms

View File

@ -1,20 +1,34 @@
{
"name": "mcp-server-freshbooks",
"name": "@mcpengine/freshbooks",
"version": "1.0.0",
"description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management",
"main": "dist/main.js",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"watch": "tsc --watch",
"prepare": "npm run build",
"start": "node dist/main.js"
},
"keywords": [
"mcp",
"freshbooks",
"accounting",
"invoicing",
"time-tracking",
"expenses",
"estimates",
"payments"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,144 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
FreshBooksConfig,
PaginatedResponse,
FreshBooksError,
} from '../types/index.js';
export class FreshBooksClient {
private client: AxiosInstance;
private accountId: string;
constructor(config: FreshBooksConfig) {
this.accountId = config.accountId;
const baseURL = config.baseUrl || `https://api.freshbooks.com/accounting/account/${config.accountId}`;
this.client = axios.create({
baseURL,
headers: {
'Authorization': `Bearer ${config.bearerToken}`,
'Content-Type': 'application/json',
'Api-Version': 'alpha',
},
timeout: 30000,
});
// Request interceptor for logging
this.client.interceptors.request.use(
(config) => {
console.error(`[FreshBooks] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
return Promise.reject(this.handleError(error));
}
);
}
private handleError(error: AxiosError): FreshBooksError {
if (error.response) {
const data = error.response.data as any;
return {
message: data.message || data.error || `HTTP ${error.response.status}: ${error.response.statusText}`,
code: data.code || `${error.response.status}`,
errors: data.errors || data.response?.errors,
};
} else if (error.request) {
return {
message: 'No response from FreshBooks API',
code: 'NETWORK_ERROR',
};
} else {
return {
message: error.message || 'Unknown error',
code: 'UNKNOWN_ERROR',
};
}
}
// Generic GET with pagination support
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
const response = await this.client.get(endpoint, { params });
return response.data;
}
// 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;
}
// Paginated GET (single page)
async getPaginated<T>(
endpoint: string,
page: number = 1,
perPage: number = 30,
params?: Record<string, any>
): Promise<PaginatedResponse<T>> {
const response = await this.client.get<PaginatedResponse<T>>(endpoint, {
params: { ...params, page, per_page: perPage },
});
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>(
endpoint: string,
searchFields: Record<string, any>,
page: number = 1,
perPage: number = 30
): Promise<PaginatedResponse<T>> {
return this.getPaginated<T>(endpoint, page, perPage, {
search: searchFields,
});
}
getAccountId(): string {
return this.accountId;
}
}

View File

@ -1,445 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "freshbooks";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://api.freshbooks.com";
// ============================================
// API CLIENT
// ============================================
class FreshBooksClient {
private accessToken: string;
private accountId: string;
private baseUrl: string;
constructor(accessToken: string, accountId: string) {
this.accessToken = accessToken;
this.accountId = accountId;
this.baseUrl = `${API_BASE_URL}/accounting/account/${accountId}`;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.accessToken}`,
"Content-Type": "application/json",
"Api-Version": "alpha",
...options.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(`FreshBooks API error: ${response.status} ${response.statusText} - ${text}`);
}
return response.json();
}
async get(endpoint: string) {
return this.request(endpoint, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
// Invoice methods
async listInvoices(options?: { page?: number; perPage?: number; status?: string }) {
const params = new URLSearchParams();
if (options?.page) params.append("page", options.page.toString());
if (options?.perPage) params.append("per_page", options.perPage.toString());
if (options?.status) params.append("search[v3_status]", options.status);
const query = params.toString();
return this.get(`/invoices/invoices${query ? `?${query}` : ""}`);
}
async getInvoice(invoiceId: string) {
return this.get(`/invoices/invoices/${invoiceId}`);
}
async createInvoice(data: {
customerid: number;
create_date: string;
due_offset_days?: number;
currency_code?: string;
language?: string;
notes?: string;
terms?: string;
lines?: Array<{
name: string;
description?: string;
qty: number;
unit_cost: { amount: string; code?: string };
}>;
}) {
return this.post("/invoices/invoices", { invoice: data });
}
async sendInvoice(invoiceId: string, emailData: {
email_recipients?: string[];
email_subject?: string;
email_body?: string;
action_email?: boolean;
}) {
// To send an invoice, update status to "sent"
return this.put(`/invoices/invoices/${invoiceId}`, {
invoice: {
action_email: emailData.action_email ?? true,
email_recipients: emailData.email_recipients,
email_subject: emailData.email_subject,
email_body: emailData.email_body,
status: 2, // 2 = sent
},
});
}
// Client methods
async listClients(options?: { page?: number; perPage?: number }) {
const params = new URLSearchParams();
if (options?.page) params.append("page", options.page.toString());
if (options?.perPage) params.append("per_page", options.perPage.toString());
const query = params.toString();
return this.get(`/users/clients${query ? `?${query}` : ""}`);
}
async getClient(clientId: string) {
return this.get(`/users/clients/${clientId}`);
}
async createClient(data: {
email?: string;
fname?: string;
lname?: string;
organization?: string;
p_street?: string;
p_street2?: string;
p_city?: string;
p_province?: string;
p_code?: string;
p_country?: string;
currency_code?: string;
language?: string;
bus_phone?: string;
mob_phone?: string;
note?: string;
}) {
return this.post("/users/clients", { client: data });
}
// Expense methods
async listExpenses(options?: { page?: number; perPage?: number }) {
const params = new URLSearchParams();
if (options?.page) params.append("page", options.page.toString());
if (options?.perPage) params.append("per_page", options.perPage.toString());
const query = params.toString();
return this.get(`/expenses/expenses${query ? `?${query}` : ""}`);
}
async getExpense(expenseId: string) {
return this.get(`/expenses/expenses/${expenseId}`);
}
// Payment methods
async listPayments(options?: { page?: number; perPage?: number }) {
const params = new URLSearchParams();
if (options?.page) params.append("page", options.page.toString());
if (options?.perPage) params.append("per_page", options.perPage.toString());
const query = params.toString();
return this.get(`/payments/payments${query ? `?${query}` : ""}`);
}
async getPayment(paymentId: string) {
return this.get(`/payments/payments/${paymentId}`);
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_invoices",
description: "List invoices from FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
page: { type: "number", description: "Page number (default 1)" },
per_page: { type: "number", description: "Results per page (default 15)" },
status: {
type: "string",
description: "Filter by status",
enum: ["draft", "sent", "viewed", "paid", "overdue", "disputed"]
},
},
},
},
{
name: "get_invoice",
description: "Get a specific invoice by ID",
inputSchema: {
type: "object" as const,
properties: {
invoice_id: { type: "string", description: "Invoice ID" },
},
required: ["invoice_id"],
},
},
{
name: "create_invoice",
description: "Create a new invoice in FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
customer_id: { type: "number", description: "Client/customer ID" },
create_date: { type: "string", description: "Invoice date (YYYY-MM-DD)" },
due_offset_days: { type: "number", description: "Days until due (default 30)" },
currency_code: { type: "string", description: "Currency code (e.g., USD, CAD)" },
notes: { type: "string", description: "Invoice notes" },
terms: { type: "string", description: "Payment terms" },
lines: {
type: "array",
description: "Invoice line items",
items: {
type: "object",
properties: {
name: { type: "string", description: "Item name" },
description: { type: "string", description: "Item description" },
qty: { type: "number", description: "Quantity" },
unit_cost: { type: "string", description: "Unit cost as string (e.g., '100.00')" },
},
required: ["name", "qty", "unit_cost"],
},
},
},
required: ["customer_id", "create_date"],
},
},
{
name: "send_invoice",
description: "Send an invoice to the client via email",
inputSchema: {
type: "object" as const,
properties: {
invoice_id: { type: "string", description: "Invoice ID to send" },
email_recipients: {
type: "array",
items: { type: "string" },
description: "Email addresses to send to"
},
email_subject: { type: "string", description: "Email subject line" },
email_body: { type: "string", description: "Email body message" },
},
required: ["invoice_id"],
},
},
{
name: "list_clients",
description: "List all clients from FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
page: { type: "number", description: "Page number (default 1)" },
per_page: { type: "number", description: "Results per page (default 15)" },
},
},
},
{
name: "create_client",
description: "Create a new client in FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
email: { type: "string", description: "Client email" },
fname: { type: "string", description: "First name" },
lname: { type: "string", description: "Last name" },
organization: { type: "string", description: "Company/organization name" },
p_street: { type: "string", description: "Street address" },
p_city: { type: "string", description: "City" },
p_province: { type: "string", description: "State/Province" },
p_code: { type: "string", description: "Postal/ZIP code" },
p_country: { type: "string", description: "Country" },
currency_code: { type: "string", description: "Currency code (e.g., USD)" },
bus_phone: { type: "string", description: "Business phone" },
note: { type: "string", description: "Notes about client" },
},
},
},
{
name: "list_expenses",
description: "List expenses from FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
page: { type: "number", description: "Page number (default 1)" },
per_page: { type: "number", description: "Results per page (default 15)" },
},
},
},
{
name: "list_payments",
description: "List payments received in FreshBooks",
inputSchema: {
type: "object" as const,
properties: {
page: { type: "number", description: "Page number (default 1)" },
per_page: { type: "number", description: "Results per page (default 15)" },
},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: FreshBooksClient, name: string, args: any) {
switch (name) {
case "list_invoices": {
return await client.listInvoices({
page: args.page,
perPage: args.per_page,
status: args.status,
});
}
case "get_invoice": {
return await client.getInvoice(args.invoice_id);
}
case "create_invoice": {
const lines = args.lines?.map((line: any) => ({
name: line.name,
description: line.description,
qty: line.qty,
unit_cost: { amount: line.unit_cost, code: args.currency_code || "USD" },
}));
return await client.createInvoice({
customerid: args.customer_id,
create_date: args.create_date,
due_offset_days: args.due_offset_days || 30,
currency_code: args.currency_code,
notes: args.notes,
terms: args.terms,
lines,
});
}
case "send_invoice": {
return await client.sendInvoice(args.invoice_id, {
email_recipients: args.email_recipients,
email_subject: args.email_subject,
email_body: args.email_body,
action_email: true,
});
}
case "list_clients": {
return await client.listClients({
page: args.page,
perPage: args.per_page,
});
}
case "create_client": {
return await client.createClient({
email: args.email,
fname: args.fname,
lname: args.lname,
organization: args.organization,
p_street: args.p_street,
p_city: args.p_city,
p_province: args.p_province,
p_code: args.p_code,
p_country: args.p_country,
currency_code: args.currency_code,
bus_phone: args.bus_phone,
note: args.note,
});
}
case "list_expenses": {
return await client.listExpenses({
page: args.page,
perPage: args.per_page,
});
}
case "list_payments": {
return await client.listPayments({
page: args.page,
perPage: args.per_page,
});
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN;
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
if (!accessToken) {
console.error("Error: FRESHBOOKS_ACCESS_TOKEN environment variable required");
process.exit(1);
}
if (!accountId) {
console.error("Error: FRESHBOOKS_ACCOUNT_ID environment variable required");
process.exit(1);
}
const client = new FreshBooksClient(accessToken, accountId);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
import { FreshBooksServer } from './server.js';
async function main() {
try {
const server = new FreshBooksServer();
await server.run();
} catch (error) {
console.error('Fatal error starting FreshBooks MCP server:', error);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,147 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { FreshBooksClient } from './clients/freshbooks.js';
import { invoicesTools } from './tools/invoices-tools.js';
import { clientsTools } from './tools/clients-tools.js';
import { expensesTools } from './tools/expenses-tools.js';
import { estimatesTools } from './tools/estimates-tools.js';
import { timeEntriesTools } from './tools/time-entries-tools.js';
import { projectsTools } from './tools/projects-tools.js';
import { paymentsTools } from './tools/payments-tools.js';
import { itemsTools } from './tools/items-tools.js';
import { taxesTools } from './tools/taxes-tools.js';
import { reportsTools } from './tools/reports-tools.js';
import { recurringTools } from './tools/recurring-tools.js';
import { accountsTools } from './tools/accounts-tools.js';
export class FreshBooksServer {
private server: Server;
private client: FreshBooksClient;
private allTools: any[];
constructor() {
this.server = new Server(
{
name: 'freshbooks-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize FreshBooks client from env
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN;
if (!accountId || !bearerToken) {
throw new Error(
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN'
);
}
this.client = new FreshBooksClient({
accountId,
bearerToken,
});
// Combine all tools
this.allTools = [
...invoicesTools,
...clientsTools,
...expensesTools,
...estimatesTools,
...timeEntriesTools,
...projectsTools,
...paymentsTools,
...itemsTools,
...taxesTools,
...reportsTools,
...recurringTools,
...accountsTools,
];
this.setupHandlers();
// Error handling
this.server.onerror = (error) => {
console.error('[MCP Error]', error);
};
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers() {
// List tools handler
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: this.allTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: {
type: 'object',
properties: tool.inputSchema.shape,
required: Object.keys(tool.inputSchema.shape).filter(
(key) => !tool.inputSchema.shape[key].isOptional()
),
},
})),
};
});
// Call tool handler
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const tool = this.allTools.find((t) => t.name === toolName);
if (!tool) {
throw new Error(`Unknown tool: ${toolName}`);
}
try {
// Validate input
const validatedArgs = tool.inputSchema.parse(request.params.arguments);
// Execute tool
const result = await tool.handler(validatedArgs, this.client);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const errorDetails = error.errors ? JSON.stringify(error.errors, null, 2) : '';
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}\n${errorDetails}`,
},
],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('FreshBooks MCP server running on stdio');
}
}

View File

@ -0,0 +1,51 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Account, StaffMember } from '../types/index.js';
export const accountsTools = [
{
name: 'freshbooks_get_account',
description: 'Get current account details',
inputSchema: z.object({}),
handler: async (_args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { account: Account } } }>(
`/users/me`
);
return response.response.result.account;
},
},
{
name: 'freshbooks_list_staff',
description: 'List all staff members',
inputSchema: z.object({
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.getPaginated<{ staff: StaffMember[] }>(
'/users/staff',
args.page,
args.per_page
);
return {
staff: response.response.result.staff || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_current_user',
description: 'Get current user (self) details',
inputSchema: z.object({}),
handler: async (_args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: any } }>(
'/auth/api/v1/users/me'
);
return response.response.result;
},
},
];

View File

@ -0,0 +1,137 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Client, ClientContact } from '../types/index.js';
export const clientsTools = [
{
name: 'freshbooks_list_clients',
description: 'List all clients with optional search',
inputSchema: z.object({
search: z.string().optional().describe('Search by name, email, or organization'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
clients: response.response.result.clients || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_client',
description: 'Get a single client by ID',
inputSchema: z.object({
client_id: z.number().describe('Client ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { client: Client } } }>(
`/users/clients/${args.client_id}`
);
return response.response.result.client;
},
},
{
name: 'freshbooks_create_client',
description: 'Create a new client',
inputSchema: z.object({
fname: z.string().describe('First name'),
lname: z.string().describe('Last name'),
email: z.string().email().describe('Email address'),
organization: z.string().optional().describe('Company/organization name'),
phone: z.string().optional(),
mobile: z.string().optional(),
bill_street: z.string().optional(),
bill_city: z.string().optional(),
bill_state: z.string().optional(),
bill_country: z.string().optional(),
bill_postal_code: z.string().optional(),
currency_code: z.string().default('USD'),
language: z.string().default('en'),
note: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const clientData = { client: { ...args } };
const response = await client.post<{ response: { result: { client: Client } } }>(
'/users/clients',
clientData
);
return response.response.result.client;
},
},
{
name: 'freshbooks_update_client',
description: 'Update an existing client',
inputSchema: z.object({
client_id: z.number().describe('Client ID'),
fname: z.string().optional(),
lname: z.string().optional(),
email: z.string().email().optional(),
organization: z.string().optional(),
phone: z.string().optional(),
mobile: z.string().optional(),
bill_street: z.string().optional(),
bill_city: z.string().optional(),
bill_state: z.string().optional(),
bill_country: z.string().optional(),
bill_postal_code: z.string().optional(),
note: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { client_id, ...updateFields } = args;
const clientData = { client: updateFields };
const response = await client.put<{ response: { result: { client: Client } } }>(
`/users/clients/${client_id}`,
clientData
);
return response.response.result.client;
},
},
{
name: 'freshbooks_delete_client',
description: 'Delete (archive) a client',
inputSchema: z.object({
client_id: z.number().describe('Client ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/users/clients/${args.client_id}`,
{ client: { vis_state: 1 } }
);
return { success: true, message: `Client ${args.client_id} archived` };
},
},
{
name: 'freshbooks_list_client_contacts',
description: 'List all contacts for a specific client',
inputSchema: z.object({
client_id: z.number().describe('Client ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { contacts: ClientContact[] } } }>(
`/users/clients/${args.client_id}/contacts`
);
return response.response.result.contacts || [];
},
},
];

View File

@ -0,0 +1,194 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Estimate } from '../types/index.js';
export const estimatesTools = [
{
name: 'freshbooks_list_estimates',
description: 'List all estimates with optional filtering',
inputSchema: z.object({
clientid: z.number().optional().describe('Filter by client ID'),
status: z.enum(['draft', 'sent', 'accepted', 'declined']).optional(),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
estimates: response.response.result.estimates || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_estimate',
description: 'Get a single estimate by ID',
inputSchema: z.object({
estimate_id: z.number().describe('Estimate ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { estimate: Estimate } } }>(
`/estimates/estimates/${args.estimate_id}`
);
return response.response.result.estimate;
},
},
{
name: 'freshbooks_create_estimate',
description: 'Create a new estimate',
inputSchema: z.object({
clientid: z.number().describe('Client ID'),
create_date: z.string().optional().describe('Estimate date (YYYY-MM-DD)'),
lines: z.array(z.object({
name: z.string().describe('Line item name'),
description: z.string().optional(),
qty: z.number().default(1),
unit_cost: z.string().describe('Unit cost'),
})).describe('Estimate line items'),
currency_code: z.string().default('USD'),
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const lines = args.lines.map((line: any) => ({
...line,
unit_cost: { amount: line.unit_cost, code: args.currency_code },
}));
const estimateData = {
estimate: {
clientid: args.clientid,
create_date: args.create_date || new Date().toISOString().split('T')[0],
currency_code: args.currency_code,
lines,
notes: args.notes,
terms: args.terms,
},
};
const response = await client.post<{ response: { result: { estimate: Estimate } } }>(
'/estimates/estimates',
estimateData
);
return response.response.result.estimate;
},
},
{
name: 'freshbooks_update_estimate',
description: 'Update an existing estimate',
inputSchema: z.object({
estimate_id: z.number().describe('Estimate ID'),
clientid: z.number().optional(),
create_date: z.string().optional(),
lines: z.array(z.object({
name: z.string(),
description: z.string().optional(),
qty: z.number(),
unit_cost: z.string(),
})).optional(),
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { estimate_id, ...updateFields } = args;
if (updateFields.lines) {
updateFields.lines = updateFields.lines.map((line: any) => ({
...line,
unit_cost: { amount: line.unit_cost, code: 'USD' },
}));
}
const estimateData = { estimate: updateFields };
const response = await client.put<{ response: { result: { estimate: Estimate } } }>(
`/estimates/estimates/${estimate_id}`,
estimateData
);
return response.response.result.estimate;
},
},
{
name: 'freshbooks_delete_estimate',
description: 'Delete (archive) an estimate',
inputSchema: z.object({
estimate_id: z.number().describe('Estimate ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/estimates/estimates/${args.estimate_id}`,
{ estimate: { vis_state: 1 } }
);
return { success: true, message: `Estimate ${args.estimate_id} deleted` };
},
},
{
name: 'freshbooks_send_estimate',
description: 'Send an estimate to the client via email',
inputSchema: z.object({
estimate_id: z.number().describe('Estimate ID'),
email_subject: z.string().optional(),
email_body: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const emailData: any = { estimate: { action_email: true } };
if (args.email_subject) emailData.estimate.email_subject = args.email_subject;
if (args.email_body) emailData.estimate.email_body = args.email_body;
await client.put(
`/estimates/estimates/${args.estimate_id}`,
emailData
);
return { success: true, message: `Estimate ${args.estimate_id} sent` };
},
},
{
name: 'freshbooks_convert_estimate_to_invoice',
description: 'Convert an estimate to an invoice',
inputSchema: z.object({
estimate_id: z.number().describe('Estimate ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
// Get the estimate first
const estimateResp = await client.get<{ response: { result: { estimate: Estimate } } }>(
`/estimates/estimates/${args.estimate_id}`
);
const estimate = estimateResp.response.result.estimate;
// Create invoice from estimate
const invoiceData = {
invoice: {
clientid: estimate.clientid,
create_date: new Date().toISOString().split('T')[0],
currency_code: estimate.currency_code,
lines: estimate.lines,
notes: estimate.notes,
terms: estimate.terms,
estimateid: args.estimate_id,
},
};
const response = await client.post(
'/invoices/invoices',
invoiceData
);
return response;
},
},
];

View File

@ -0,0 +1,139 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Expense, ExpenseCategory } from '../types/index.js';
export const expensesTools = [
{
name: 'freshbooks_list_expenses',
description: 'List all expenses with optional filtering',
inputSchema: z.object({
clientid: z.number().optional().describe('Filter by client ID'),
category_id: z.number().optional().describe('Filter by category ID'),
projectid: z.number().optional().describe('Filter by project ID'),
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
expenses: response.response.result.expenses || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_expense',
description: 'Get a single expense by ID',
inputSchema: z.object({
expense_id: z.number().describe('Expense ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { expense: Expense } } }>(
`/expenses/expenses/${args.expense_id}`
);
return response.response.result.expense;
},
},
{
name: 'freshbooks_create_expense',
description: 'Create a new expense',
inputSchema: z.object({
category_id: z.number().describe('Expense category ID'),
vendor: z.string().describe('Vendor name'),
amount: z.string().describe('Expense amount'),
date: z.string().describe('Expense date (YYYY-MM-DD)'),
clientid: z.number().optional().describe('Associated client ID'),
projectid: z.number().optional().describe('Associated project ID'),
notes: z.string().optional(),
taxName1: z.string().optional(),
taxPercent1: z.number().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const expenseData = {
expense: {
...args,
amount: { amount: args.amount, code: 'USD' },
},
};
const response = await client.post<{ response: { result: { expense: Expense } } }>(
'/expenses/expenses',
expenseData
);
return response.response.result.expense;
},
},
{
name: 'freshbooks_update_expense',
description: 'Update an existing expense',
inputSchema: z.object({
expense_id: z.number().describe('Expense ID'),
category_id: z.number().optional(),
vendor: z.string().optional(),
amount: z.string().optional(),
date: z.string().optional(),
clientid: z.number().optional(),
projectid: z.number().optional(),
notes: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { expense_id, ...updateFields } = args;
if (updateFields.amount) {
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
}
const expenseData = { expense: updateFields };
const response = await client.put<{ response: { result: { expense: Expense } } }>(
`/expenses/expenses/${expense_id}`,
expenseData
);
return response.response.result.expense;
},
},
{
name: 'freshbooks_delete_expense',
description: 'Delete an expense',
inputSchema: z.object({
expense_id: z.number().describe('Expense ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/expenses/expenses/${args.expense_id}`,
{ expense: { vis_state: 1 } }
);
return { success: true, message: `Expense ${args.expense_id} deleted` };
},
},
{
name: 'freshbooks_list_expense_categories',
description: 'List all expense categories',
inputSchema: z.object({}),
handler: async (_args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { categories: ExpenseCategory[] } } }>(
'/expenses/categories'
);
return response.response.result.categories || [];
},
},
];

View File

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

View File

@ -0,0 +1,110 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Item } from '../types/index.js';
export const itemsTools = [
{
name: 'freshbooks_list_items',
description: 'List all items (products/services)',
inputSchema: z.object({
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.getPaginated<{ items: Item[] }>(
'/items/items',
args.page,
args.per_page
);
return {
items: response.response.result.items || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_item',
description: 'Get a single item by ID',
inputSchema: z.object({
item_id: z.number().describe('Item ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { item: Item } } }>(
`/items/items/${args.item_id}`
);
return response.response.result.item;
},
},
{
name: 'freshbooks_create_item',
description: 'Create a new item (product or service)',
inputSchema: z.object({
name: z.string().describe('Item name'),
description: z.string().optional(),
qty: z.number().optional().describe('Quantity on hand'),
inventory: z.number().optional(),
unit_cost: z.string().optional().describe('Unit cost'),
tax1: z.number().optional().describe('Tax 1 ID'),
tax2: z.number().optional().describe('Tax 2 ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const itemData: any = { item: { ...args } };
if (itemData.item.unit_cost) {
itemData.item.unit_cost = { amount: itemData.item.unit_cost, code: 'USD' };
}
const response = await client.post<{ response: { result: { item: Item } } }>(
'/items/items',
itemData
);
return response.response.result.item;
},
},
{
name: 'freshbooks_update_item',
description: 'Update an existing item',
inputSchema: z.object({
item_id: z.number().describe('Item ID'),
name: z.string().optional(),
description: z.string().optional(),
qty: z.number().optional(),
inventory: z.number().optional(),
unit_cost: z.string().optional(),
tax1: z.number().optional(),
tax2: z.number().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { item_id, ...updateFields } = args;
if (updateFields.unit_cost) {
updateFields.unit_cost = { amount: updateFields.unit_cost, code: 'USD' };
}
const itemData = { item: updateFields };
const response = await client.put<{ response: { result: { item: Item } } }>(
`/items/items/${item_id}`,
itemData
);
return response.response.result.item;
},
},
{
name: 'freshbooks_delete_item',
description: 'Delete an item',
inputSchema: z.object({
item_id: z.number().describe('Item ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/items/items/${args.item_id}`,
{ item: { vis_state: 1 } }
);
return { success: true, message: `Item ${args.item_id} deleted` };
},
},
];

View File

@ -0,0 +1,121 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Payment } from '../types/index.js';
export const paymentsTools = [
{
name: 'freshbooks_list_payments',
description: 'List all payments with optional filtering',
inputSchema: z.object({
invoiceid: z.number().optional().describe('Filter by invoice ID'),
clientid: z.number().optional().describe('Filter by client ID'),
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
payments: response.response.result.payments || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_payment',
description: 'Get a single payment by ID',
inputSchema: z.object({
payment_id: z.number().describe('Payment ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { payment: Payment } } }>(
`/payments/payments/${args.payment_id}`
);
return response.response.result.payment;
},
},
{
name: 'freshbooks_create_payment',
description: 'Create a new payment',
inputSchema: z.object({
invoiceid: z.number().describe('Invoice ID'),
amount: z.string().describe('Payment amount'),
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
type: z.string().default('Cash').describe('Payment method'),
note: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const paymentData = {
payment: {
invoiceid: args.invoiceid,
amount: { amount: args.amount, code: 'USD' },
date: args.date || new Date().toISOString().split('T')[0],
type: args.type,
note: args.note,
},
};
const response = await client.post<{ response: { result: { payment: Payment } } }>(
'/payments/payments',
paymentData
);
return response.response.result.payment;
},
},
{
name: 'freshbooks_update_payment',
description: 'Update an existing payment',
inputSchema: z.object({
payment_id: z.number().describe('Payment ID'),
amount: z.string().optional(),
date: z.string().optional(),
type: z.string().optional(),
note: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { payment_id, ...updateFields } = args;
if (updateFields.amount) {
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
}
const paymentData = { payment: updateFields };
const response = await client.put<{ response: { result: { payment: Payment } } }>(
`/payments/payments/${payment_id}`,
paymentData
);
return response.response.result.payment;
},
},
{
name: 'freshbooks_delete_payment',
description: 'Delete a payment',
inputSchema: z.object({
payment_id: z.number().describe('Payment ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/payments/payments/${args.payment_id}`,
{ payment: { vis_state: 1 } }
);
return { success: true, message: `Payment ${args.payment_id} deleted` };
},
},
];

View File

@ -0,0 +1,124 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Project, ProjectService } from '../types/index.js';
export const projectsTools = [
{
name: 'freshbooks_list_projects',
description: 'List all projects with optional filtering',
inputSchema: z.object({
client_id: z.number().optional().describe('Filter by client ID'),
active: z.boolean().optional().describe('Filter by active status'),
complete: z.boolean().optional().describe('Filter by completion status'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
projects: response.response.result.projects || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_project',
description: 'Get a single project by ID',
inputSchema: z.object({
project_id: z.number().describe('Project ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { project: Project } } }>(
`/projects/business/123/projects/${args.project_id}`
);
return response.response.result.project;
},
},
{
name: 'freshbooks_create_project',
description: 'Create a new project',
inputSchema: z.object({
title: z.string().describe('Project title'),
description: z.string().optional(),
client_id: z.number().optional().describe('Associated client ID'),
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
project_type: z.enum(['fixed_price', 'hourly_rate']).default('hourly_rate'),
fixed_price: z.string().optional().describe('Fixed price amount'),
billing_method: z.enum(['project_rate', 'service_rate', 'team_member_rate']).optional(),
rate: z.string().optional().describe('Hourly rate'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const projectData = { project: { ...args } };
const response = await client.post<{ response: { result: { project: Project } } }>(
'/projects/business/123/projects',
projectData
);
return response.response.result.project;
},
},
{
name: 'freshbooks_update_project',
description: 'Update an existing project',
inputSchema: z.object({
project_id: z.number().describe('Project ID'),
title: z.string().optional(),
description: z.string().optional(),
client_id: z.number().optional(),
due_date: z.string().optional(),
active: z.boolean().optional(),
complete: z.boolean().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { project_id, ...updateFields } = args;
const projectData = { project: updateFields };
const response = await client.put<{ response: { result: { project: Project } } }>(
`/projects/business/123/projects/${project_id}`,
projectData
);
return response.response.result.project;
},
},
{
name: 'freshbooks_delete_project',
description: 'Delete a project',
inputSchema: z.object({
project_id: z.number().describe('Project ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.delete(
`/projects/business/123/projects/${args.project_id}`
);
return { success: true, message: `Project ${args.project_id} deleted` };
},
},
{
name: 'freshbooks_list_project_services',
description: 'List all available services for time tracking',
inputSchema: z.object({}),
handler: async (_args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { services: ProjectService[] } } }>(
'/projects/business/123/services'
);
return response.response.result.services || [];
},
},
];

View File

@ -0,0 +1,140 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { RecurringProfile } from '../types/index.js';
export const recurringTools = [
{
name: 'freshbooks_list_recurring_profiles',
description: 'List all recurring invoice profiles',
inputSchema: z.object({
clientid: z.number().optional().describe('Filter by client ID'),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<string, any> = {};
if (args.clientid) params.clientid = args.clientid;
const response = await client.getPaginated<{ recurring: RecurringProfile[] }>(
'/invoices/recurring',
args.page,
args.per_page,
params
);
return {
recurring: response.response.result.recurring || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_recurring_profile',
description: 'Get a single recurring profile by ID',
inputSchema: z.object({
recurring_id: z.number().describe('Recurring profile ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { recurring: RecurringProfile } } }>(
`/invoices/recurring/${args.recurring_id}`
);
return response.response.result.recurring;
},
},
{
name: 'freshbooks_create_recurring_profile',
description: 'Create a new recurring invoice profile',
inputSchema: z.object({
clientid: z.number().describe('Client ID'),
frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).describe('Billing frequency'),
numberRecurring: z.number().optional().describe('Number of times to recur (0 = indefinite)'),
lines: z.array(z.object({
name: z.string().describe('Line item name'),
description: z.string().optional(),
qty: z.number().default(1),
unit_cost: z.string().describe('Unit cost'),
})).describe('Invoice line items'),
currency_code: z.string().default('USD'),
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const lines = args.lines.map((line: any) => ({
...line,
unit_cost: { amount: line.unit_cost, code: args.currency_code },
}));
const recurringData = {
recurring: {
clientid: args.clientid,
frequency: args.frequency,
numberRecurring: args.numberRecurring || 0,
create_date: new Date().toISOString().split('T')[0],
currency_code: args.currency_code,
lines,
notes: args.notes,
terms: args.terms,
},
};
const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>(
'/invoices/recurring',
recurringData
);
return response.response.result.recurring;
},
},
{
name: 'freshbooks_update_recurring_profile',
description: 'Update an existing recurring profile',
inputSchema: z.object({
recurring_id: z.number().describe('Recurring profile ID'),
frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).optional(),
numberRecurring: z.number().optional(),
lines: z.array(z.object({
name: z.string(),
description: z.string().optional(),
qty: z.number(),
unit_cost: z.string(),
})).optional(),
notes: z.string().optional(),
terms: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { recurring_id, ...updateFields } = args;
if (updateFields.lines) {
updateFields.lines = updateFields.lines.map((line: any) => ({
...line,
unit_cost: { amount: line.unit_cost, code: 'USD' },
}));
}
const recurringData = { recurring: updateFields };
const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>(
`/invoices/recurring/${recurring_id}`,
recurringData
);
return response.response.result.recurring;
},
},
{
name: 'freshbooks_delete_recurring_profile',
description: 'Delete a recurring profile',
inputSchema: z.object({
recurring_id: z.number().describe('Recurring profile ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/invoices/recurring/${args.recurring_id}`,
{ recurring: { vis_state: 1 } }
);
return { success: true, message: `Recurring profile ${args.recurring_id} deleted` };
},
},
];

View File

@ -0,0 +1,112 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js';
export const reportsTools = [
{
name: 'freshbooks_profit_loss_report',
description: 'Generate profit and loss report for a date range',
inputSchema: z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
end_date: z.string().describe('End date (YYYY-MM-DD)'),
currency_code: z.string().default('USD'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: ProfitLossReport } }>(
'/reports/accounting/profitloss',
{
start_date: args.start_date,
end_date: args.end_date,
currency_code: args.currency_code,
}
);
return response.response.result;
},
},
{
name: 'freshbooks_tax_summary_report',
description: 'Generate tax summary report for a date range',
inputSchema: z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
end_date: z.string().describe('End date (YYYY-MM-DD)'),
currency_code: z.string().default('USD'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { taxsummaries: TaxSummary[] } } }>(
'/reports/accounting/taxsummary',
{
start_date: args.start_date,
end_date: args.end_date,
currency_code: args.currency_code,
}
);
return response.response.result.taxsummaries || [];
},
},
{
name: 'freshbooks_accounts_aging_report',
description: 'Generate accounts aging report (accounts receivable)',
inputSchema: z.object({
date: z.string().optional().describe('Report date (YYYY-MM-DD, defaults to today)'),
currency_code: z.string().default('USD'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { clients: AccountsAgingReport[] } } }>(
'/reports/accounting/aging',
{
date: args.date || new Date().toISOString().split('T')[0],
currency_code: args.currency_code,
}
);
return response.response.result.clients || [];
},
},
{
name: 'freshbooks_expense_report',
description: 'Generate expense report for a date range',
inputSchema: z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
end_date: z.string().describe('End date (YYYY-MM-DD)'),
clientid: z.number().optional().describe('Filter by client ID'),
categoryid: z.number().optional().describe('Filter by category ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: any = {
start_date: args.start_date,
end_date: args.end_date,
};
if (args.clientid) params.clientid = args.clientid;
if (args.categoryid) params.categoryid = args.categoryid;
const response = await client.get<{ response: { result: any } }>(
'/reports/accounting/expenses',
params
);
return response.response.result;
},
},
{
name: 'freshbooks_revenue_by_client_report',
description: 'Generate revenue by client report for a date range',
inputSchema: z.object({
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
end_date: z.string().describe('End date (YYYY-MM-DD)'),
currency_code: z.string().default('USD'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: any } }>(
'/reports/accounting/revenue_by_client',
{
start_date: args.start_date,
end_date: args.end_date,
currency_code: args.currency_code,
}
);
return response.response.result;
},
},
];

View File

@ -0,0 +1,96 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { Tax } from '../types/index.js';
export const taxesTools = [
{
name: 'freshbooks_list_taxes',
description: 'List all taxes',
inputSchema: z.object({
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.getPaginated<{ taxes: Tax[] }>(
'/taxes/taxes',
args.page,
args.per_page
);
return {
taxes: response.response.result.taxes || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_tax',
description: 'Get a single tax by ID',
inputSchema: z.object({
tax_id: z.number().describe('Tax ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { tax: Tax } } }>(
`/taxes/taxes/${args.tax_id}`
);
return response.response.result.tax;
},
},
{
name: 'freshbooks_create_tax',
description: 'Create a new tax',
inputSchema: z.object({
name: z.string().describe('Tax name (e.g., "GST", "VAT")'),
number: z.string().optional().describe('Tax number/registration'),
amount: z.string().describe('Tax percentage (e.g., "13" for 13%)'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const taxData = { tax: { ...args } };
const response = await client.post<{ response: { result: { tax: Tax } } }>(
'/taxes/taxes',
taxData
);
return response.response.result.tax;
},
},
{
name: 'freshbooks_update_tax',
description: 'Update an existing tax',
inputSchema: z.object({
tax_id: z.number().describe('Tax ID'),
name: z.string().optional(),
number: z.string().optional(),
amount: z.string().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { tax_id, ...updateFields } = args;
const taxData = { tax: updateFields };
const response = await client.put<{ response: { result: { tax: Tax } } }>(
`/taxes/taxes/${tax_id}`,
taxData
);
return response.response.result.tax;
},
},
{
name: 'freshbooks_delete_tax',
description: 'Delete a tax',
inputSchema: z.object({
tax_id: z.number().describe('Tax ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.put(
`/taxes/taxes/${args.tax_id}`,
{ tax: { vis_state: 1 } }
);
return { success: true, message: `Tax ${args.tax_id} deleted` };
},
},
];

View File

@ -0,0 +1,125 @@
import { z } from 'zod';
import type { FreshBooksClient } from '../clients/freshbooks.js';
import type { TimeEntry } from '../types/index.js';
export const timeEntriesTools = [
{
name: 'freshbooks_list_time_entries',
description: 'List all time entries with optional filtering',
inputSchema: z.object({
clientid: z.number().optional().describe('Filter by client ID'),
projectid: z.number().optional().describe('Filter by project ID'),
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
billed_status: z.enum(['billed', 'unbilled']).optional(),
page: z.number().default(1),
per_page: z.number().default(30),
}),
handler: async (args: any, client: FreshBooksClient) => {
const params: Record<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
);
return {
time_entries: response.response.result.time_entries || [],
page: response.response.page,
pages: response.response.pages,
total: response.response.total,
};
},
},
{
name: 'freshbooks_get_time_entry',
description: 'Get a single time entry by ID',
inputSchema: z.object({
time_entry_id: z.number().describe('Time entry ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
const response = await client.get<{ response: { result: { time_entry: TimeEntry } } }>(
`/timetracking/business/123/time_entries/${args.time_entry_id}`
);
return response.response.result.time_entry;
},
},
{
name: 'freshbooks_create_time_entry',
description: 'Create a new time entry',
inputSchema: z.object({
duration: z.number().describe('Duration in seconds'),
note: z.string().optional().describe('Note/description'),
started_at: z.string().describe('Start time (ISO 8601 format)'),
clientid: z.number().optional().describe('Client ID'),
projectid: z.number().optional().describe('Project ID'),
service_id: z.number().optional().describe('Service ID'),
is_logged: z.boolean().default(true),
}),
handler: async (args: any, client: FreshBooksClient) => {
const timeEntryData = {
time_entry: {
is_logged: args.is_logged,
duration: args.duration,
note: args.note,
started_at: args.started_at,
client_id: args.clientid,
project_id: args.projectid,
service_id: args.service_id,
},
};
const response = await client.post<{ response: { result: { time_entry: TimeEntry } } }>(
'/timetracking/business/123/time_entries',
timeEntryData
);
return response.response.result.time_entry;
},
},
{
name: 'freshbooks_update_time_entry',
description: 'Update an existing time entry',
inputSchema: z.object({
time_entry_id: z.number().describe('Time entry ID'),
duration: z.number().optional(),
note: z.string().optional(),
started_at: z.string().optional(),
clientid: z.number().optional(),
projectid: z.number().optional(),
service_id: z.number().optional(),
}),
handler: async (args: any, client: FreshBooksClient) => {
const { time_entry_id, ...updateFields } = args;
const timeEntryData = { time_entry: updateFields };
const response = await client.put<{ response: { result: { time_entry: TimeEntry } } }>(
`/timetracking/business/123/time_entries/${time_entry_id}`,
timeEntryData
);
return response.response.result.time_entry;
},
},
{
name: 'freshbooks_delete_time_entry',
description: 'Delete a time entry',
inputSchema: z.object({
time_entry_id: z.number().describe('Time entry ID'),
}),
handler: async (args: any, client: FreshBooksClient) => {
await client.delete(
`/timetracking/business/123/time_entries/${args.time_entry_id}`
);
return { success: true, message: `Time entry ${args.time_entry_id} deleted` };
},
},
];

View File

@ -0,0 +1,349 @@
// FreshBooks API Types
export interface FreshBooksConfig {
accountId: string;
bearerToken: string;
baseUrl?: 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 {
id: number;
accounting_systemid: string;
organization: string;
fname: string;
lname: string;
email: string;
company_industry?: string;
company_size?: string;
bill_street?: string;
bill_city?: string;
bill_state?: string;
bill_country?: string;
bill_postal_code?: string;
currency_code: string;
updated: string;
created_at: string;
language?: string;
note?: string;
vat_name?: string;
vat_number?: string;
allow_late_fees?: boolean;
allow_late_notifications?: boolean;
}
export interface ClientContact {
id: number;
clientid: number;
fname: string;
lname: string;
email: string;
phone?: string;
mobile?: string;
}
// Invoice Types
export interface Invoice {
id: number;
accountid: string;
accounting_systemid: string;
clientid: number;
create_date: string;
invoice_number: string;
currency_code: string;
amount: {
amount: string;
code: string;
};
outstanding: {
amount: string;
code: string;
};
paid: {
amount: string;
code: string;
};
due_date: string;
status: string;
payment_status: string;
v3_status: string;
lines: InvoiceLine[];
terms?: string;
notes?: string;
discount_total?: {
amount: string;
code: string;
};
updated: string;
created_at: string;
}
export interface InvoiceLine {
id?: number;
name: string;
description?: string;
qty: number;
unit_cost: {
amount: string;
code: string;
};
amount: {
amount: string;
code: string;
};
taxName1?: string;
taxAmount1?: string;
taxName2?: string;
taxAmount2?: string;
}
export interface Payment {
id: number;
invoiceid: number;
amount: {
amount: string;
code: string;
};
date: string;
type: string;
note?: string;
updated: string;
created_at: string;
}
// Expense Types
export interface Expense {
id: number;
category_id: number;
clientid?: number;
projectid?: number;
vendor: string;
amount: {
amount: string;
code: string;
};
date: string;
notes?: string;
taxName1?: string;
taxAmount1?: number;
taxPercent1?: number;
updated: string;
created_at: string;
staffid?: number;
status?: string;
}
export interface ExpenseCategory {
id: number;
category: string;
is_cogs?: boolean;
is_editable?: boolean;
parentid?: number;
}
// Estimate Types
export interface Estimate {
id: number;
accountid: string;
clientid: number;
create_date: string;
estimate_number: string;
currency_code: string;
amount: {
amount: string;
code: string;
};
status: string;
lines: EstimateLine[];
terms?: string;
notes?: string;
discount_total?: {
amount: string;
code: string;
};
updated: string;
created_at: string;
}
export interface EstimateLine {
id?: number;
name: string;
description?: string;
qty: number;
unit_cost: {
amount: string;
code: string;
};
amount: {
amount: string;
code: string;
};
}
// Time Entry Types
export interface TimeEntry {
id: number;
is_logged?: boolean;
duration: number;
note?: string;
started_at: string;
clientid?: number;
projectid?: number;
service_id?: number;
billed_status?: string;
created_at: string;
updated_at: string;
}
// Project Types
export interface Project {
id: number;
title: string;
description?: string;
client_id?: number;
due_date?: string;
project_type?: string;
fixed_price?: string;
billing_method?: string;
rate?: string;
active?: boolean;
complete?: boolean;
created_at: string;
updated_at: string;
}
export interface ProjectService {
id: number;
business_id: number;
name: string;
billable: boolean;
rate?: {
amount: string;
code: string;
};
}
// Item Types
export interface Item {
id: number;
name: string;
description?: string;
qty?: number;
inventory?: number;
unit_cost?: {
amount: string;
code: string;
};
tax1?: number;
tax2?: number;
updated: string;
created_at: string;
}
// Tax Types
export interface Tax {
id: number;
name: string;
number?: string;
amount: string;
updated: string;
created_at: string;
}
// Recurring Profile Types
export interface RecurringProfile {
id: number;
clientid: number;
frequency: string;
numberRecurring: number;
create_date: string;
currency_code: string;
lines: InvoiceLine[];
status?: string;
updated: string;
created_at: string;
}
// Account Types
export interface Account {
id: string;
account_name: string;
email: string;
business_phone?: string;
address?: {
street: string;
city: string;
province: string;
country: string;
postal_code: string;
};
}
export interface StaffMember {
id: number;
username: string;
first_name: string;
last_name: string;
email: string;
role?: string;
}
// Report Types
export interface ProfitLossReport {
total_income: {
amount: string;
code: string;
};
total_expenses: {
amount: string;
code: string;
};
net_profit: {
amount: string;
code: string;
};
start_date: string;
end_date: string;
}
export interface TaxSummary {
tax_name: string;
tax_collected: {
amount: string;
code: string;
};
tax_paid: {
amount: string;
code: string;
};
}
export interface AccountsAgingReport {
client_userid: number;
organization: string;
outstanding_balance: {
amount: string;
code: string;
};
current: { amount: string; code: string };
'1-30': { amount: string; code: string };
'31-60': { amount: string; code: string };
'61-90': { amount: string; code: string };
'91+': { amount: string; code: string };
}

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,99 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,134 @@
<!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>
</html>

View File

@ -0,0 +1,139 @@
<!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>
</html>

View File

@ -0,0 +1,154 @@
<!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>
</html>

View File

@ -0,0 +1,116 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,86 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,92 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reports - 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; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.report-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 2rem; }
.report-card { background: #1e293b; padding: 2rem; border-radius: 8px; cursor: pointer; transition: transform 0.2s; }
.report-card:hover { transform: translateY(-4px); background: #334155; }
.report-icon { font-size: 2.5rem; margin-bottom: 1rem; }
.report-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
.report-description { color: #94a3b8; font-size: 0.875rem; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function ReportsDashboard() {
const reports = [
{ icon: '📊', title: 'Profit & Loss', description: 'View income and expenses over time' },
{ icon: '💰', title: 'Revenue by Client', description: 'See which clients generate the most revenue' },
{ icon: '📈', title: 'Tax Summary', description: 'Summary of taxes collected and paid' },
{ icon: '⏰', title: 'Accounts Aging', description: 'Outstanding invoices by age' },
{ icon: '💸', title: 'Expense Report', description: 'Breakdown of all business expenses' },
{ icon: '🎯', title: 'Time Summary', description: 'Billable vs non-billable hours' },
];
return (
<div className="container">
<h1>Reports</h1>
<p style={{ color: '#94a3b8' }}>Financial insights and business analytics</p>
<div className="report-grid">
{reports.map((report, index) => (
<div key={index} className="report-card">
<div className="report-icon">{report.icon}</div>
<div className="report-title">{report.title}</div>
<div className="report-description">{report.description}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<ReportsDashboard />, document.getElementById('root'));
</script>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,34 @@
<!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>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Time 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; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
.timer-card { background: #1e293b; padding: 3rem; border-radius: 8px; text-align: center; margin: 2rem 0; }
.timer-display { font-size: 4rem; font-weight: bold; margin-bottom: 2rem; font-variant-numeric: tabular-nums; }
.timer-controls { display: flex; gap: 1rem; justify-content: center; }
.btn { background: #3b82f6; color: white; border: none; padding: 1rem 2rem; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 500; }
.btn:hover { background: #2563eb; }
.btn-start { background: #10b981; }
.btn-start:hover { background: #059669; }
.btn-stop { background: #ef4444; }
.btn-stop:hover { background: #dc2626; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem; }
.form-group { display: flex; flex-direction: column; gap: 0.5rem; text-align: left; }
label { font-size: 0.875rem; color: #94a3b8; }
input, select { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; }
.entries-list { margin-top: 2rem; }
.entry-item { background: #1e293b; padding: 1rem; border-radius: 8px; margin-bottom: 0.75rem; display: flex; justify-content: space-between; align-items: center; }
.entry-info .project { color: #64748b; font-size: 0.875rem; margin-top: 0.25rem; }
.entry-duration { font-size: 1.25rem; font-weight: bold; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function TimeTracker() {
const [isRunning, setIsRunning] = useState(false);
const [seconds, setSeconds] = useState(0);
const [task, setTask] = useState({ description: '', project: '', client: '' });
const [entries, setEntries] = useState([
{ id: 1, description: 'Website design', project: 'Acme Redesign', client: 'Acme Corp', duration: 7200 },
{ id: 2, description: 'Bug fixes', project: 'Tech App', client: 'Tech Solutions', duration: 3600 },
]);
useEffect(() => {
let interval;
if (isRunning) {
interval = setInterval(() => setSeconds(s => s + 1), 1000);
}
return () => clearInterval(interval);
}, [isRunning]);
const formatTime = (secs) => {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = secs % 60;
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
};
const startTimer = () => setIsRunning(true);
const stopTimer = () => {
if (seconds > 0 && task.description) {
setEntries([{ ...task, id: Date.now(), duration: seconds }, ...entries]);
}
setIsRunning(false);
setSeconds(0);
setTask({ description: '', project: '', client: '' });
};
return (
<div className="container">
<h1>Time Tracker</h1>
<p style={{ color: '#94a3b8', marginBottom: '2rem' }}>Track billable hours and project time</p>
<div className="timer-card">
<div className="timer-display">{formatTime(seconds)}</div>
<div className="form-grid">
<div className="form-group">
<label>Task Description</label>
<input type="text" value={task.description} onChange={(e) => setTask({ ...task, description: e.target.value })} placeholder="What are you working on?" disabled={isRunning} />
</div>
<div className="form-group">
<label>Project</label>
<select value={task.project} onChange={(e) => setTask({ ...task, project: e.target.value })} disabled={isRunning}>
<option value="">Select project...</option>
<option value="Acme Redesign">Acme Redesign</option>
<option value="Tech App">Tech App</option>
</select>
</div>
</div>
<div className="timer-controls">
{!isRunning ? (
<button className="btn btn-start" onClick={startTimer}>Start Timer</button>
) : (
<button className="btn btn-stop" onClick={stopTimer}>Stop & Save</button>
)}
</div>
</div>
<div className="entries-list">
<h3 style={{ marginBottom: '1rem' }}>Recent Entries</h3>
{entries.map(entry => (
<div key={entry.id} className="entry-item">
<div className="entry-info">
<div>{entry.description}</div>
<div className="project">{entry.project} • {entry.client}</div>
</div>
<div className="entry-duration">{formatTime(entry.duration)}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<TimeTracker />, document.getElementById('root'));
</script>
</body>
</html>

View File

@ -1,14 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "ES2022",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]