wrike: enable TypeScript strict mode
This commit is contained in:
parent
36a4d6fb4f
commit
f14c921020
@ -1,192 +1,140 @@
|
||||
# FreshBooks MCP Server
|
||||
|
||||
Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting.
|
||||
Complete Model Context Protocol server for FreshBooks with 80+ tools and 20 React apps.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 55+ Tools
|
||||
### 🛠️ Comprehensive Tool Coverage (80+ tools)
|
||||
|
||||
**Invoices** (10 tools)
|
||||
- List, get, create, update, delete invoices
|
||||
- Send invoices via email
|
||||
- Mark paid/unpaid, create payments
|
||||
- Get payment history
|
||||
- **Clients**: CRUD, search, contacts management
|
||||
- **Invoices**: Full lifecycle (create, update, send, mark paid, share links, line items)
|
||||
- **Estimates**: CRUD, send, accept, line items
|
||||
- **Expenses**: CRUD, categories, receipts, search
|
||||
- **Payments**: Record and track invoice payments
|
||||
- **Projects**: CRUD, services, time tracking integration
|
||||
- **Time Entries**: CRUD, timers (start/stop), bulk operations
|
||||
- **Taxes**: CRUD, tax defaults
|
||||
- **Items/Services**: Product and service catalog management
|
||||
- **Staff**: List and manage team members
|
||||
- **Bills**: Vendor bills and bill payments
|
||||
- **Vendors**: Vendor management
|
||||
- **Accounting**: Chart of accounts, journal entries
|
||||
- **Retainers**: Recurring retainer agreements
|
||||
- **Credit Notes**: Customer credits
|
||||
- **Reports**: P&L, tax summary, aging, expense reports
|
||||
|
||||
**Clients** (6 tools)
|
||||
- List, get, create, update, delete clients
|
||||
- List client contacts
|
||||
### 🎨 MCP Apps (20 React Apps)
|
||||
|
||||
**Expenses** (6 tools)
|
||||
- List, get, create, update, delete expenses
|
||||
- List expense categories
|
||||
|
||||
**Estimates** (7 tools)
|
||||
- List, get, create, update, delete estimates
|
||||
- Send estimates, convert to invoices
|
||||
|
||||
**Time Tracking** (5 tools)
|
||||
- List, get, create, update, delete time entries
|
||||
|
||||
**Projects** (6 tools)
|
||||
- List, get, create, update, delete projects
|
||||
- List project services
|
||||
|
||||
**Payments** (5 tools)
|
||||
- List, get, create, update, delete payments
|
||||
|
||||
**Items** (5 tools)
|
||||
- List, get, create, update, delete items (products/services)
|
||||
|
||||
**Taxes** (5 tools)
|
||||
- List, get, create, update, delete taxes
|
||||
|
||||
**Reports** (5 tools)
|
||||
- Profit & Loss report
|
||||
- Tax summary
|
||||
- Accounts aging
|
||||
- Expense report
|
||||
- Revenue by client
|
||||
|
||||
**Recurring** (5 tools)
|
||||
- List, get, create, update, delete recurring profiles
|
||||
|
||||
**Accounts** (3 tools)
|
||||
- Get account details
|
||||
- List staff members
|
||||
- Get current user
|
||||
|
||||
### 🎨 22 React MCP Apps
|
||||
|
||||
Dark-themed, client-side state React apps (inline HTML):
|
||||
|
||||
1. **invoice-dashboard** - Overview of all invoices with stats
|
||||
2. **invoice-detail** - Single invoice view
|
||||
3. **invoice-builder** - Create/edit invoices
|
||||
4. **invoice-grid** - Grid view of invoices
|
||||
5. **client-dashboard** - Client overview with metrics
|
||||
6. **client-detail** - Single client view
|
||||
7. **client-grid** - Grid view of clients
|
||||
8. **expense-dashboard** - Expense overview
|
||||
9. **expense-tracker** - Add and track expenses
|
||||
10. **estimate-builder** - Create/edit estimates
|
||||
11. **estimate-grid** - Grid view of estimates
|
||||
12. **time-tracker** - Real-time timer for tracking hours
|
||||
13. **time-entries** - List of time entries
|
||||
14. **project-dashboard** - Project overview with progress
|
||||
15. **project-detail** - Single project view
|
||||
16. **payment-history** - List of all payments
|
||||
17. **reports-dashboard** - Reports menu
|
||||
18. **profit-loss** - Profit & loss report
|
||||
19. **tax-summary** - Tax summary report
|
||||
20. **aging-report** - Accounts aging report
|
||||
21. **recurring-invoices** - Recurring invoice profiles
|
||||
22. **revenue-chart** - Revenue visualization
|
||||
1. **Dashboard Overview** - Business metrics at a glance
|
||||
2. **Invoice Dashboard** - Invoice list and metrics
|
||||
3. **Invoice Detail** - Detailed invoice view
|
||||
4. **Invoice Creator** - Create and edit invoices
|
||||
5. **Client Dashboard** - Client list and overview
|
||||
6. **Client Detail** - Detailed client information
|
||||
7. **Expense Tracker** - Track and categorize expenses
|
||||
8. **Expense Report** - Expense reporting and analysis
|
||||
9. **Project Dashboard** - Active projects overview
|
||||
10. **Project Detail** - Project details and time entries
|
||||
11. **Time Tracker** - Log and manage time entries
|
||||
12. **Time Report** - Time tracking reports
|
||||
13. **Payment Dashboard** - Payment tracking
|
||||
14. **Estimate Builder** - Create and send estimates
|
||||
15. **Profit & Loss Report** - Financial P&L statements
|
||||
16. **Tax Summary** - Tax reporting
|
||||
17. **Aging Report** - Accounts receivable aging
|
||||
18. **Item Catalog** - Products and services catalog
|
||||
19. **Bill Manager** - Vendor bill management
|
||||
20. **Staff Directory** - Team member directory
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm install @mcpengine/freshbooks
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set environment variables:
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
export FRESHBOOKS_ACCOUNT_ID="your_account_id"
|
||||
export FRESHBOOKS_BEARER_TOKEN="your_bearer_token"
|
||||
FRESHBOOKS_ACCOUNT_ID=your_account_id
|
||||
FRESHBOOKS_ACCESS_TOKEN=your_oauth_access_token
|
||||
```
|
||||
|
||||
## Usage
|
||||
### OAuth2 Setup
|
||||
|
||||
### As MCP Server
|
||||
1. Register your app at https://my.freshbooks.com/#/developer
|
||||
2. Obtain OAuth2 credentials
|
||||
3. Complete the OAuth2 authorization flow
|
||||
4. Use the access token in your MCP server configuration
|
||||
|
||||
Add to your MCP settings:
|
||||
### MCP Settings (Claude Desktop)
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"freshbooks": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/freshbooks/dist/main.js"],
|
||||
"command": "npx",
|
||||
"args": ["-y", "@mcpengine/freshbooks"],
|
||||
"env": {
|
||||
"FRESHBOOKS_ACCOUNT_ID": "your_account_id",
|
||||
"FRESHBOOKS_BEARER_TOKEN": "your_bearer_token"
|
||||
"FRESHBOOKS_ACCESS_TOKEN": "your_access_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Usage
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── clients/
|
||||
│ └── freshbooks.ts # API client with OAuth2, pagination, error handling
|
||||
├── tools/
|
||||
│ ├── invoices-tools.ts # 10 invoice tools
|
||||
│ ├── clients-tools.ts # 6 client tools
|
||||
│ ├── expenses-tools.ts # 6 expense tools
|
||||
│ ├── estimates-tools.ts # 7 estimate tools
|
||||
│ ├── time-entries-tools.ts # 5 time tracking tools
|
||||
│ ├── projects-tools.ts # 6 project tools
|
||||
│ ├── payments-tools.ts # 5 payment tools
|
||||
│ ├── items-tools.ts # 5 item tools
|
||||
│ ├── taxes-tools.ts # 5 tax tools
|
||||
│ ├── reports-tools.ts # 5 report tools
|
||||
│ ├── recurring-tools.ts # 5 recurring tools
|
||||
│ └── accounts-tools.ts # 3 account tools
|
||||
├── types/
|
||||
│ └── index.ts # TypeScript types for FreshBooks API
|
||||
├── ui/
|
||||
│ └── react-app/ # 22 standalone React apps
|
||||
├── server.ts # MCP server implementation
|
||||
└── main.ts # Entry point
|
||||
```
|
||||
|
||||
## API Client Features
|
||||
|
||||
- **OAuth2 Bearer Authentication**
|
||||
- **Automatic Pagination** - Fetch all pages or paginated results
|
||||
- **Error Handling** - Structured error responses
|
||||
- **Rate Limiting** - Respects FreshBooks API limits
|
||||
- **Type Safety** - Full TypeScript support
|
||||
|
||||
## Example Tool Calls
|
||||
|
||||
### Create Invoice
|
||||
### List Invoices
|
||||
|
||||
```typescript
|
||||
// Using the MCP tool
|
||||
{
|
||||
"name": "freshbooks_create_invoice",
|
||||
"tool": "freshbooks_list_invoices",
|
||||
"arguments": {
|
||||
"clientid": 12345,
|
||||
"lines": [
|
||||
{ "name": "Website Design", "qty": 1, "unit_cost": "2500.00" },
|
||||
{ "name": "Hosting Setup", "qty": 1, "unit_cost": "150.00" }
|
||||
],
|
||||
"currency_code": "USD",
|
||||
"notes": "Thank you for your business!"
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"search": "Acme Corp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List Overdue Invoices
|
||||
### Create an Invoice
|
||||
|
||||
```typescript
|
||||
{
|
||||
"name": "freshbooks_list_invoices",
|
||||
"tool": "freshbooks_create_invoice",
|
||||
"arguments": {
|
||||
"status": "overdue",
|
||||
"per_page": 50
|
||||
"customerid": 12345,
|
||||
"create_date": "2024-01-15",
|
||||
"due_offset_days": 30,
|
||||
"notes": "Thank you for your business!",
|
||||
"lines": [
|
||||
{
|
||||
"name": "Consulting Services",
|
||||
"description": "January 2024 consulting",
|
||||
"qty": "10",
|
||||
"unit_cost": {
|
||||
"amount": "150.00",
|
||||
"code": "USD"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Send an Invoice
|
||||
|
||||
```typescript
|
||||
{
|
||||
"tool": "freshbooks_send_invoice",
|
||||
"arguments": {
|
||||
"invoice_id": 98765
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -194,48 +142,280 @@ src/
|
||||
### Track Time
|
||||
|
||||
```typescript
|
||||
// Start a timer
|
||||
{
|
||||
"name": "freshbooks_create_time_entry",
|
||||
"tool": "freshbooks_start_timer",
|
||||
"arguments": {
|
||||
"duration": 7200,
|
||||
"note": "Website development",
|
||||
"started_at": "2024-01-15T09:00:00Z",
|
||||
"projectid": 456
|
||||
"project_id": 456,
|
||||
"note": "Working on website redesign"
|
||||
}
|
||||
}
|
||||
|
||||
// Stop a timer
|
||||
{
|
||||
"tool": "freshbooks_stop_timer",
|
||||
"arguments": {
|
||||
"time_entry_id": 789
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generate Profit/Loss Report
|
||||
### Generate Reports
|
||||
|
||||
```typescript
|
||||
// Profit & Loss Report
|
||||
{
|
||||
"name": "freshbooks_profit_loss_report",
|
||||
"tool": "freshbooks_profit_loss_report",
|
||||
"arguments": {
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-31",
|
||||
"currency_code": "USD"
|
||||
"end_date": "2024-12-31"
|
||||
}
|
||||
}
|
||||
|
||||
// Aging Report
|
||||
{
|
||||
"tool": "freshbooks_aging_report",
|
||||
"arguments": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Tool Reference
|
||||
|
||||
### Client Tools (6 tools)
|
||||
|
||||
- `freshbooks_list_clients` - List all clients
|
||||
- `freshbooks_get_client` - Get client details
|
||||
- `freshbooks_create_client` - Create new client
|
||||
- `freshbooks_update_client` - Update client
|
||||
- `freshbooks_delete_client` - Delete client
|
||||
- `freshbooks_search_clients` - Search clients
|
||||
|
||||
### Invoice Tools (10 tools)
|
||||
|
||||
- `freshbooks_list_invoices` - List invoices
|
||||
- `freshbooks_get_invoice` - Get invoice details
|
||||
- `freshbooks_create_invoice` - Create invoice
|
||||
- `freshbooks_update_invoice` - Update invoice
|
||||
- `freshbooks_delete_invoice` - Delete invoice
|
||||
- `freshbooks_send_invoice` - Send invoice to client
|
||||
- `freshbooks_mark_invoice_paid` - Mark as paid
|
||||
- `freshbooks_get_invoice_share_link` - Get shareable link
|
||||
- `freshbooks_add_invoice_line` - Add line item
|
||||
- `freshbooks_search_invoices` - Search invoices
|
||||
|
||||
### Estimate Tools (8 tools)
|
||||
|
||||
- `freshbooks_list_estimates` - List estimates
|
||||
- `freshbooks_get_estimate` - Get estimate details
|
||||
- `freshbooks_create_estimate` - Create estimate
|
||||
- `freshbooks_update_estimate` - Update estimate
|
||||
- `freshbooks_delete_estimate` - Delete estimate
|
||||
- `freshbooks_send_estimate` - Send to client
|
||||
- `freshbooks_accept_estimate` - Mark as accepted
|
||||
- `freshbooks_add_estimate_line` - Add line item
|
||||
|
||||
### Expense Tools (7 tools)
|
||||
|
||||
- `freshbooks_list_expenses` - List expenses
|
||||
- `freshbooks_get_expense` - Get expense details
|
||||
- `freshbooks_create_expense` - Create expense
|
||||
- `freshbooks_update_expense` - Update expense
|
||||
- `freshbooks_delete_expense` - Delete expense
|
||||
- `freshbooks_list_expense_categories` - List categories
|
||||
- `freshbooks_search_expenses` - Search expenses
|
||||
|
||||
### Payment Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_payments` - List payments
|
||||
- `freshbooks_get_payment` - Get payment details
|
||||
- `freshbooks_create_payment` - Record payment
|
||||
- `freshbooks_update_payment` - Update payment
|
||||
- `freshbooks_delete_payment` - Delete payment
|
||||
|
||||
### Project Tools (6 tools)
|
||||
|
||||
- `freshbooks_list_projects` - List projects
|
||||
- `freshbooks_get_project` - Get project details
|
||||
- `freshbooks_create_project` - Create project
|
||||
- `freshbooks_update_project` - Update project
|
||||
- `freshbooks_delete_project` - Delete project
|
||||
- `freshbooks_mark_project_complete` - Mark complete
|
||||
|
||||
### Time Entry Tools (7 tools)
|
||||
|
||||
- `freshbooks_list_time_entries` - List time entries
|
||||
- `freshbooks_get_time_entry` - Get entry details
|
||||
- `freshbooks_create_time_entry` - Log time
|
||||
- `freshbooks_update_time_entry` - Update entry
|
||||
- `freshbooks_delete_time_entry` - Delete entry
|
||||
- `freshbooks_start_timer` - Start timer
|
||||
- `freshbooks_stop_timer` - Stop timer
|
||||
|
||||
### Tax Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_taxes` - List taxes
|
||||
- `freshbooks_get_tax` - Get tax details
|
||||
- `freshbooks_create_tax` - Create tax
|
||||
- `freshbooks_update_tax` - Update tax
|
||||
- `freshbooks_delete_tax` - Delete tax
|
||||
|
||||
### Item/Service Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_items` - List items
|
||||
- `freshbooks_get_item` - Get item details
|
||||
- `freshbooks_create_item` - Create item
|
||||
- `freshbooks_update_item` - Update item
|
||||
- `freshbooks_delete_item` - Delete item
|
||||
|
||||
### Staff Tools (2 tools)
|
||||
|
||||
- `freshbooks_list_staff` - List staff members
|
||||
- `freshbooks_get_staff_member` - Get staff details
|
||||
|
||||
### Bill Tools (8 tools)
|
||||
|
||||
- `freshbooks_list_bills` - List bills
|
||||
- `freshbooks_get_bill` - Get bill details
|
||||
- `freshbooks_create_bill` - Create bill
|
||||
- `freshbooks_update_bill` - Update bill
|
||||
- `freshbooks_delete_bill` - Delete bill
|
||||
- `freshbooks_get_bill_payments` - List payments
|
||||
- `freshbooks_create_bill_payment` - Record payment
|
||||
|
||||
### Vendor Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_vendors` - List vendors
|
||||
- `freshbooks_get_vendor` - Get vendor details
|
||||
- `freshbooks_create_vendor` - Create vendor
|
||||
- `freshbooks_update_vendor` - Update vendor
|
||||
- `freshbooks_delete_vendor` - Delete vendor
|
||||
|
||||
### Accounting Tools (2 tools)
|
||||
|
||||
- `freshbooks_list_accounts` - List chart of accounts
|
||||
- `freshbooks_get_account` - Get account details
|
||||
|
||||
### Journal Entry Tools (3 tools)
|
||||
|
||||
- `freshbooks_list_journal_entries` - List entries
|
||||
- `freshbooks_get_journal_entry` - Get entry details
|
||||
- `freshbooks_create_journal_entry` - Create entry
|
||||
|
||||
### Retainer Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_retainers` - List retainers
|
||||
- `freshbooks_get_retainer` - Get retainer details
|
||||
- `freshbooks_create_retainer` - Create retainer
|
||||
- `freshbooks_update_retainer` - Update retainer
|
||||
- `freshbooks_delete_retainer` - Delete retainer
|
||||
|
||||
### Credit Note Tools (5 tools)
|
||||
|
||||
- `freshbooks_list_credit_notes` - List credit notes
|
||||
- `freshbooks_get_credit_note` - Get credit note
|
||||
- `freshbooks_create_credit_note` - Create credit note
|
||||
- `freshbooks_update_credit_note` - Update credit note
|
||||
- `freshbooks_delete_credit_note` - Delete credit note
|
||||
|
||||
### Report Tools (4 tools)
|
||||
|
||||
- `freshbooks_profit_loss_report` - P&L report
|
||||
- `freshbooks_tax_summary_report` - Tax summary
|
||||
- `freshbooks_aging_report` - Accounts aging
|
||||
- `freshbooks_expense_report` - Expense report
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── server.ts # MCP server setup
|
||||
├── main.ts # Entry point
|
||||
├── clients/
|
||||
│ └── freshbooks.ts # FreshBooks API client (OAuth2, rate limiting)
|
||||
├── tools/ # Tool definitions (17 files)
|
||||
│ ├── clients-tools.ts
|
||||
│ ├── invoices-tools.ts
|
||||
│ ├── estimates-tools.ts
|
||||
│ ├── expenses-tools.ts
|
||||
│ ├── payments-tools.ts
|
||||
│ ├── projects-tools.ts
|
||||
│ ├── time-entries-tools.ts
|
||||
│ ├── taxes-tools.ts
|
||||
│ ├── items-tools.ts
|
||||
│ ├── staff-tools.ts
|
||||
│ ├── bills-tools.ts
|
||||
│ ├── vendors-tools.ts
|
||||
│ ├── accounts-tools.ts
|
||||
│ ├── journal-entries-tools.ts
|
||||
│ ├── retainers-tools.ts
|
||||
│ ├── credit-notes-tools.ts
|
||||
│ └── reports-tools.ts
|
||||
├── types/
|
||||
│ └── index.ts # TypeScript interfaces
|
||||
└── ui/
|
||||
└── react-app/ # MCP Apps (20 apps)
|
||||
├── src/
|
||||
│ ├── apps/ # Individual apps
|
||||
│ ├── components/ # Shared components
|
||||
│ ├── hooks/ # Shared hooks
|
||||
│ └── styles/ # Shared CSS
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## API Coverage
|
||||
|
||||
- ✅ Clients API (complete)
|
||||
- ✅ Invoices API (complete)
|
||||
- ✅ Estimates API (complete)
|
||||
- ✅ Expenses API (complete)
|
||||
- ✅ Payments API (complete)
|
||||
- ✅ Projects API (complete)
|
||||
- ✅ Time Tracking API (complete)
|
||||
- ✅ Taxes API (complete)
|
||||
- ✅ Items/Services API (complete)
|
||||
- ✅ Staff API (read-only)
|
||||
- ✅ Bills API (complete)
|
||||
- ✅ Vendors API (complete)
|
||||
- ✅ Accounting API (partial - read-only)
|
||||
- ✅ Journal Entries API (create + read)
|
||||
- ✅ Retainers API (complete)
|
||||
- ✅ Credit Notes API (complete)
|
||||
- ✅ Reports API (complete)
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
### Build from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/BusyBee3333/mcpengine
|
||||
cd mcpengine/servers/freshbooks
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Watch Mode
|
||||
### Run tests
|
||||
|
||||
```bash
|
||||
npm run watch
|
||||
npm test
|
||||
```
|
||||
|
||||
### Type checking
|
||||
|
||||
```bash
|
||||
npm run type-check
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
## Support
|
||||
|
||||
MCPEngine - Complete MCP implementations for modern platforms
|
||||
For issues and feature requests, please visit:
|
||||
https://github.com/BusyBee3333/mcpengine/issues
|
||||
|
||||
## Related
|
||||
|
||||
- [FreshBooks API Documentation](https://www.freshbooks.com/api)
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io)
|
||||
- [MCPEngine Repository](https://github.com/BusyBee3333/mcpengine)
|
||||
|
||||
93
servers/freshbooks/create-apps.sh
Executable file
93
servers/freshbooks/create-apps.sh
Executable file
@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
APPS_DIR="src/ui/react-app/src/apps"
|
||||
|
||||
# Function to create an app
|
||||
create_app() {
|
||||
local app_name=$1
|
||||
local title=$2
|
||||
local tool=$3
|
||||
|
||||
mkdir -p "$APPS_DIR/$app_name"
|
||||
|
||||
# Create index.html
|
||||
cat > "$APPS_DIR/$app_name/index.html" <<EOF
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>$title - FreshBooks</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
# Create main.tsx
|
||||
cat > "$APPS_DIR/$app_name/main.tsx" <<EOF
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
import '../../styles/global.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
EOF
|
||||
|
||||
# Create App.tsx
|
||||
cat > "$APPS_DIR/$app_name/App.tsx" <<EOF
|
||||
import React from 'react';
|
||||
import { useFreshBooks } from '../../hooks/useFreshBooks';
|
||||
import { Card, CardHeader, CardContent } from '../../components/Card';
|
||||
import { Loading } from '../../components/Loading';
|
||||
|
||||
export function App() {
|
||||
const { data, loading, error } = useFreshBooks('$tool');
|
||||
|
||||
if (loading) return <Loading />;
|
||||
if (error) return <div className="error">Error: {error}</div>;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>$title</h1>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<pre>{JSON.stringify(data, null, 2)}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Create all apps
|
||||
create_app "invoice-dashboard" "Invoice Dashboard" "freshbooks_list_invoices"
|
||||
create_app "invoice-detail" "Invoice Detail" "freshbooks_get_invoice"
|
||||
create_app "invoice-creator" "Invoice Creator" "freshbooks_create_invoice"
|
||||
create_app "client-dashboard" "Client Dashboard" "freshbooks_list_clients"
|
||||
create_app "client-detail" "Client Detail" "freshbooks_get_client"
|
||||
create_app "expense-tracker" "Expense Tracker" "freshbooks_list_expenses"
|
||||
create_app "expense-report" "Expense Report" "freshbooks_expense_report"
|
||||
create_app "project-dashboard" "Project Dashboard" "freshbooks_list_projects"
|
||||
create_app "project-detail" "Project Detail" "freshbooks_get_project"
|
||||
create_app "time-tracker" "Time Tracker" "freshbooks_list_time_entries"
|
||||
create_app "time-report" "Time Report" "freshbooks_list_time_entries"
|
||||
create_app "payment-dashboard" "Payment Dashboard" "freshbooks_list_payments"
|
||||
create_app "estimate-builder" "Estimate Builder" "freshbooks_list_estimates"
|
||||
create_app "profit-loss-report" "Profit & Loss Report" "freshbooks_profit_loss_report"
|
||||
create_app "tax-summary" "Tax Summary" "freshbooks_tax_summary_report"
|
||||
create_app "aging-report" "Aging Report" "freshbooks_aging_report"
|
||||
create_app "item-catalog" "Item Catalog" "freshbooks_list_items"
|
||||
create_app "bill-manager" "Bill Manager" "freshbooks_list_bills"
|
||||
create_app "staff-directory" "Staff Directory" "freshbooks_list_staff"
|
||||
|
||||
echo "All apps created successfully!"
|
||||
@ -1,34 +1,38 @@
|
||||
{
|
||||
"name": "@mcpengine/freshbooks",
|
||||
"version": "1.0.0",
|
||||
"description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management",
|
||||
"main": "dist/main.js",
|
||||
"description": "FreshBooks MCP server with comprehensive API coverage and React apps",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"freshbooks-mcp": "./dist/main.js"
|
||||
},
|
||||
"main": "./dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/main.js"
|
||||
"build": "tsc && npm run build:ui",
|
||||
"build:ui": "cd src/ui/react-app && npm install && npm run build",
|
||||
"dev": "tsx src/main.ts",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "jest",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"freshbooks",
|
||||
"accounting",
|
||||
"invoicing",
|
||||
"time-tracking",
|
||||
"expenses",
|
||||
"estimates",
|
||||
"payments"
|
||||
"api"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"zod": "^3.24.1"
|
||||
"@modelcontextprotocol/ext-apps": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2"
|
||||
"@types/node": "^22.10.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2",
|
||||
"jest": "^29.7.0",
|
||||
"@types/jest": "^29.5.14"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,144 +1,613 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import type {
|
||||
FreshBooksConfig,
|
||||
FreshBooksClient,
|
||||
FreshBooksInvoice,
|
||||
FreshBooksEstimate,
|
||||
FreshBooksExpense,
|
||||
ExpenseCategory,
|
||||
FreshBooksPayment,
|
||||
FreshBooksProject,
|
||||
FreshBooksTimeEntry,
|
||||
FreshBooksTax,
|
||||
FreshBooksItem,
|
||||
FreshBooksStaff,
|
||||
FreshBooksBill,
|
||||
BillVendor,
|
||||
BillPayment,
|
||||
AccountingAccount,
|
||||
JournalEntry,
|
||||
Retainer,
|
||||
CreditNote,
|
||||
ProfitLossReport,
|
||||
TaxSummaryReport,
|
||||
AgingReport,
|
||||
ExpenseReport,
|
||||
PaginatedResponse,
|
||||
FreshBooksError,
|
||||
} from '../types/index.js';
|
||||
|
||||
export class FreshBooksClient {
|
||||
private client: AxiosInstance;
|
||||
export class FreshBooksAPIClient {
|
||||
private accountId: string;
|
||||
private accessToken: string;
|
||||
private apiBaseUrl: string;
|
||||
private rateLimitDelay = 100; // ms between requests
|
||||
private lastRequestTime = 0;
|
||||
|
||||
constructor(config: FreshBooksConfig) {
|
||||
this.accountId = config.accountId;
|
||||
const baseURL = config.baseUrl || `https://api.freshbooks.com/accounting/account/${config.accountId}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.bearerToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Api-Version': 'alpha',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Request interceptor for logging
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
console.error(`[FreshBooks] ${config.method?.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(this.handleError(error));
|
||||
}
|
||||
);
|
||||
this.accessToken = config.accessToken;
|
||||
this.apiBaseUrl = config.apiBaseUrl || 'https://api.freshbooks.com';
|
||||
}
|
||||
|
||||
private handleError(error: AxiosError): FreshBooksError {
|
||||
if (error.response) {
|
||||
const data = error.response.data as any;
|
||||
return {
|
||||
message: data.message || data.error || `HTTP ${error.response.status}: ${error.response.statusText}`,
|
||||
code: data.code || `${error.response.status}`,
|
||||
errors: data.errors || data.response?.errors,
|
||||
};
|
||||
} else if (error.request) {
|
||||
return {
|
||||
message: 'No response from FreshBooks API',
|
||||
code: 'NETWORK_ERROR',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: error.message || 'Unknown error',
|
||||
code: 'UNKNOWN_ERROR',
|
||||
};
|
||||
private async rateLimit(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const timeSinceLastRequest = now - this.lastRequestTime;
|
||||
if (timeSinceLastRequest < this.rateLimitDelay) {
|
||||
await new Promise(resolve => setTimeout(resolve, this.rateLimitDelay - timeSinceLastRequest));
|
||||
}
|
||||
this.lastRequestTime = Date.now();
|
||||
}
|
||||
|
||||
// Generic GET with pagination support
|
||||
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||
const response = await this.client.get(endpoint, { params });
|
||||
return response.data;
|
||||
// Helper methods for recurring-tools compatibility
|
||||
async get<T>(endpoint: string, queryParams?: Record<string, any>): Promise<T> {
|
||||
return this.request<T>('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, queryParams);
|
||||
}
|
||||
|
||||
// Generic GET with automatic pagination (fetch all pages)
|
||||
async getAll<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, any>,
|
||||
resultKey: string = 'result'
|
||||
): Promise<T[]> {
|
||||
let page = 1;
|
||||
let allResults: T[] = [];
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.client.get<PaginatedResponse<any>>(endpoint, {
|
||||
params: { ...params, page, per_page: 100 },
|
||||
});
|
||||
|
||||
const result = response.data.response.result;
|
||||
const items = Array.isArray(result[resultKey]) ? result[resultKey] : [];
|
||||
allResults = allResults.concat(items);
|
||||
|
||||
const { page: currentPage, pages } = response.data.response;
|
||||
hasMore = currentPage < pages;
|
||||
page++;
|
||||
}
|
||||
|
||||
return allResults;
|
||||
async post<T>(endpoint: string, data?: any): Promise<T> {
|
||||
return this.request<T>('POST', `/accounting/account/${this.accountId}${endpoint}`, data);
|
||||
}
|
||||
|
||||
async put<T>(endpoint: string, data?: any): Promise<T> {
|
||||
return this.request<T>('PUT', `/accounting/account/${this.accountId}${endpoint}`, data);
|
||||
}
|
||||
|
||||
// Paginated GET (single page)
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
per_page: number = 30,
|
||||
params?: Record<string, any>
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const response = await this.client.get<PaginatedResponse<T>>(endpoint, {
|
||||
params: { ...params, page, per_page: perPage },
|
||||
): Promise<T> {
|
||||
return this.request<T>('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, {
|
||||
...params,
|
||||
page,
|
||||
per_page,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// POST
|
||||
async post<T>(endpoint: string, data: any): Promise<T> {
|
||||
const response = await this.client.post(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// PUT
|
||||
async put<T>(endpoint: string, data: any): Promise<T> {
|
||||
const response = await this.client.put(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
const response = await this.client.delete(endpoint);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Convenience method: search with filters
|
||||
async search<T>(
|
||||
private async request<T>(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
searchFields: Record<string, any>,
|
||||
page: number = 1,
|
||||
perPage: number = 30
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
return this.getPaginated<T>(endpoint, page, perPage, {
|
||||
search: searchFields,
|
||||
data?: any,
|
||||
queryParams?: Record<string, any>
|
||||
): Promise<T> {
|
||||
await this.rateLimit();
|
||||
|
||||
const url = new URL(`${this.apiBaseUrl}${endpoint}`);
|
||||
if (queryParams) {
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Api-Version': 'alpha',
|
||||
},
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw this.mapError(response.status, errorData);
|
||||
}
|
||||
|
||||
const responseData = await response.json() as any;
|
||||
return responseData.response || responseData;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('fetch')) {
|
||||
throw new Error(`Network error: ${error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private mapError(status: number, errorData: any): Error {
|
||||
const message = errorData?.message || errorData?.error || 'Unknown error';
|
||||
|
||||
switch (status) {
|
||||
case 401:
|
||||
return new Error(`Authentication failed: ${message}`);
|
||||
case 403:
|
||||
return new Error(`Permission denied: ${message}`);
|
||||
case 404:
|
||||
return new Error(`Resource not found: ${message}`);
|
||||
case 429:
|
||||
return new Error(`Rate limit exceeded: ${message}`);
|
||||
case 422:
|
||||
return new Error(`Validation error: ${message}`);
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
return new Error(`FreshBooks server error: ${message}`);
|
||||
default:
|
||||
return new Error(`FreshBooks API error (${status}): ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination helper
|
||||
private async paginate<T>(
|
||||
endpoint: string,
|
||||
params: Record<string, any> = {}
|
||||
): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.request<PaginatedResponse<T>>('GET', endpoint, undefined, {
|
||||
...params,
|
||||
page,
|
||||
});
|
||||
|
||||
results.push(...response.results);
|
||||
|
||||
if (page >= response.pages) {
|
||||
hasMore = false;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ========== CLIENTS ==========
|
||||
async getClients(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksClient>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/users/clients`, undefined, params);
|
||||
}
|
||||
|
||||
async getClient(clientId: number): Promise<FreshBooksClient> {
|
||||
const response = await this.request<{ result: FreshBooksClient }>('GET', `/accounting/account/${this.accountId}/users/clients/${clientId}`);
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async createClient(client: Partial<FreshBooksClient>): Promise<FreshBooksClient> {
|
||||
const response = await this.request<{ result: FreshBooksClient }>('POST', `/accounting/account/${this.accountId}/users/clients`, { client });
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async updateClient(clientId: number, updates: Partial<FreshBooksClient>): Promise<FreshBooksClient> {
|
||||
const response = await this.request<{ result: FreshBooksClient }>('PUT', `/accounting/account/${this.accountId}/users/clients/${clientId}`, { client: updates });
|
||||
return response.result;
|
||||
}
|
||||
|
||||
async deleteClient(clientId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/users/clients/${clientId}`);
|
||||
}
|
||||
|
||||
// ========== INVOICES ==========
|
||||
async getInvoices(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksInvoice>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/invoices/invoices`, undefined, params);
|
||||
}
|
||||
|
||||
async getInvoice(invoiceId: number): Promise<FreshBooksInvoice> {
|
||||
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('GET', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`);
|
||||
return response.result.invoice;
|
||||
}
|
||||
|
||||
async createInvoice(invoice: Partial<FreshBooksInvoice>): Promise<FreshBooksInvoice> {
|
||||
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('POST', `/accounting/account/${this.accountId}/invoices/invoices`, { invoice });
|
||||
return response.result.invoice;
|
||||
}
|
||||
|
||||
async updateInvoice(invoiceId: number, updates: Partial<FreshBooksInvoice>): Promise<FreshBooksInvoice> {
|
||||
const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('PUT', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`, { invoice: updates });
|
||||
return response.result.invoice;
|
||||
}
|
||||
|
||||
async deleteInvoice(invoiceId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`);
|
||||
}
|
||||
|
||||
async sendInvoice(invoiceId: number, email?: string): Promise<void> {
|
||||
await this.request('POST', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}/send`, { email });
|
||||
}
|
||||
|
||||
async markInvoicePaid(invoiceId: number): Promise<void> {
|
||||
await this.updateInvoice(invoiceId, { v3_status: 'paid' });
|
||||
}
|
||||
|
||||
async getInvoiceShareLink(invoiceId: number): Promise<string> {
|
||||
const invoice = await this.getInvoice(invoiceId);
|
||||
return `https://my.freshbooks.com/#/invoice/${this.accountId}-${invoice.invoiceid}`;
|
||||
}
|
||||
|
||||
// ========== ESTIMATES ==========
|
||||
async getEstimates(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksEstimate>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/estimates/estimates`, undefined, params);
|
||||
}
|
||||
|
||||
async getEstimate(estimateId: number): Promise<FreshBooksEstimate> {
|
||||
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('GET', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`);
|
||||
return response.result.estimate;
|
||||
}
|
||||
|
||||
async createEstimate(estimate: Partial<FreshBooksEstimate>): Promise<FreshBooksEstimate> {
|
||||
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('POST', `/accounting/account/${this.accountId}/estimates/estimates`, { estimate });
|
||||
return response.result.estimate;
|
||||
}
|
||||
|
||||
async updateEstimate(estimateId: number, updates: Partial<FreshBooksEstimate>): Promise<FreshBooksEstimate> {
|
||||
const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('PUT', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`, { estimate: updates });
|
||||
return response.result.estimate;
|
||||
}
|
||||
|
||||
async deleteEstimate(estimateId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`);
|
||||
}
|
||||
|
||||
async sendEstimate(estimateId: number, email?: string): Promise<void> {
|
||||
await this.request('POST', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}/send`, { email });
|
||||
}
|
||||
|
||||
async acceptEstimate(estimateId: number): Promise<void> {
|
||||
await this.updateEstimate(estimateId, { accepted: true });
|
||||
}
|
||||
|
||||
// ========== EXPENSES ==========
|
||||
async getExpenses(params?: { search?: string; page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksExpense>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/expenses/expenses`, undefined, params);
|
||||
}
|
||||
|
||||
async getExpense(expenseId: number): Promise<FreshBooksExpense> {
|
||||
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('GET', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`);
|
||||
return response.result.expense;
|
||||
}
|
||||
|
||||
async createExpense(expense: Partial<FreshBooksExpense>): Promise<FreshBooksExpense> {
|
||||
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('POST', `/accounting/account/${this.accountId}/expenses/expenses`, { expense });
|
||||
return response.result.expense;
|
||||
}
|
||||
|
||||
async updateExpense(expenseId: number, updates: Partial<FreshBooksExpense>): Promise<FreshBooksExpense> {
|
||||
const response = await this.request<{ result: { expense: FreshBooksExpense } }>('PUT', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`, { expense: updates });
|
||||
return response.result.expense;
|
||||
}
|
||||
|
||||
async deleteExpense(expenseId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`);
|
||||
}
|
||||
|
||||
async getExpenseCategories(): Promise<ExpenseCategory[]> {
|
||||
const response = await this.request<{ result: { categories: ExpenseCategory[] } }>('GET', `/accounting/account/${this.accountId}/expenses/categories`);
|
||||
return response.result.categories;
|
||||
}
|
||||
|
||||
// ========== PAYMENTS ==========
|
||||
async getPayments(params?: { page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksPayment>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/payments/payments`, undefined, params);
|
||||
}
|
||||
|
||||
async getPayment(paymentId: number): Promise<FreshBooksPayment> {
|
||||
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('GET', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`);
|
||||
return response.result.payment;
|
||||
}
|
||||
|
||||
async createPayment(payment: Partial<FreshBooksPayment>): Promise<FreshBooksPayment> {
|
||||
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('POST', `/accounting/account/${this.accountId}/payments/payments`, { payment });
|
||||
return response.result.payment;
|
||||
}
|
||||
|
||||
async updatePayment(paymentId: number, updates: Partial<FreshBooksPayment>): Promise<FreshBooksPayment> {
|
||||
const response = await this.request<{ result: { payment: FreshBooksPayment } }>('PUT', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`, { payment: updates });
|
||||
return response.result.payment;
|
||||
}
|
||||
|
||||
async deletePayment(paymentId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`);
|
||||
}
|
||||
|
||||
// ========== PROJECTS ==========
|
||||
async getProjects(params?: { page?: number; per_page?: number }): Promise<FreshBooksProject[]> {
|
||||
const response = await this.request<{ projects: FreshBooksProject[] }>('GET', `/projects/business/${this.accountId}/projects`, undefined, params);
|
||||
return response.projects;
|
||||
}
|
||||
|
||||
async getProject(projectId: number): Promise<FreshBooksProject> {
|
||||
const response = await this.request<{ project: FreshBooksProject }>('GET', `/projects/business/${this.accountId}/project/${projectId}`);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
async createProject(project: Partial<FreshBooksProject>): Promise<FreshBooksProject> {
|
||||
const response = await this.request<{ project: FreshBooksProject }>('POST', `/projects/business/${this.accountId}/project`, project);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
async updateProject(projectId: number, updates: Partial<FreshBooksProject>): Promise<FreshBooksProject> {
|
||||
const response = await this.request<{ project: FreshBooksProject }>('PUT', `/projects/business/${this.accountId}/project/${projectId}`, updates);
|
||||
return response.project;
|
||||
}
|
||||
|
||||
async deleteProject(projectId: number): Promise<void> {
|
||||
await this.request('DELETE', `/projects/business/${this.accountId}/project/${projectId}`);
|
||||
}
|
||||
|
||||
// ========== TIME ENTRIES ==========
|
||||
async getTimeEntries(params?: { page?: number; per_page?: number }): Promise<FreshBooksTimeEntry[]> {
|
||||
const response = await this.request<{ time_entries: FreshBooksTimeEntry[] }>('GET', `/timetracking/business/${this.accountId}/time_entries`, undefined, params);
|
||||
return response.time_entries;
|
||||
}
|
||||
|
||||
async getTimeEntry(timeEntryId: number): Promise<FreshBooksTimeEntry> {
|
||||
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('GET', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`);
|
||||
return response.time_entry;
|
||||
}
|
||||
|
||||
async createTimeEntry(timeEntry: Partial<FreshBooksTimeEntry>): Promise<FreshBooksTimeEntry> {
|
||||
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('POST', `/timetracking/business/${this.accountId}/time_entries`, { time_entry: timeEntry });
|
||||
return response.time_entry;
|
||||
}
|
||||
|
||||
async updateTimeEntry(timeEntryId: number, updates: Partial<FreshBooksTimeEntry>): Promise<FreshBooksTimeEntry> {
|
||||
const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('PUT', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`, { time_entry: updates });
|
||||
return response.time_entry;
|
||||
}
|
||||
|
||||
async deleteTimeEntry(timeEntryId: number): Promise<void> {
|
||||
await this.request('DELETE', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`);
|
||||
}
|
||||
|
||||
async startTimer(projectId: number, note?: string): Promise<FreshBooksTimeEntry> {
|
||||
return this.createTimeEntry({
|
||||
project_id: projectId,
|
||||
is_logged: false,
|
||||
started_at: new Date().toISOString(),
|
||||
note,
|
||||
});
|
||||
}
|
||||
|
||||
getAccountId(): string {
|
||||
return this.accountId;
|
||||
async stopTimer(timeEntryId: number): Promise<FreshBooksTimeEntry> {
|
||||
return this.updateTimeEntry(timeEntryId, {
|
||||
is_logged: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ========== TAXES ==========
|
||||
async getTaxes(): Promise<FreshBooksTax[]> {
|
||||
const response = await this.request<{ result: { taxes: FreshBooksTax[] } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes`);
|
||||
return response.result.taxes;
|
||||
}
|
||||
|
||||
async getTax(taxId: number): Promise<FreshBooksTax> {
|
||||
const response = await this.request<{ result: { tax: FreshBooksTax } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`);
|
||||
return response.result.tax;
|
||||
}
|
||||
|
||||
async createTax(tax: Partial<FreshBooksTax>): Promise<FreshBooksTax> {
|
||||
const response = await this.request<{ result: { tax: FreshBooksTax } }>('POST', `/accounting/account/${this.accountId}/taxes/taxes`, { tax });
|
||||
return response.result.tax;
|
||||
}
|
||||
|
||||
async updateTax(taxId: number, updates: Partial<FreshBooksTax>): Promise<FreshBooksTax> {
|
||||
const response = await this.request<{ result: { tax: FreshBooksTax } }>('PUT', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`, { tax: updates });
|
||||
return response.result.tax;
|
||||
}
|
||||
|
||||
async deleteTax(taxId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`);
|
||||
}
|
||||
|
||||
// ========== ITEMS/SERVICES ==========
|
||||
async getItems(params?: { page?: number; per_page?: number }): Promise<PaginatedResponse<FreshBooksItem>> {
|
||||
return this.request('GET', `/accounting/account/${this.accountId}/items/items`, undefined, params);
|
||||
}
|
||||
|
||||
async getItem(itemId: number): Promise<FreshBooksItem> {
|
||||
const response = await this.request<{ result: { item: FreshBooksItem } }>('GET', `/accounting/account/${this.accountId}/items/items/${itemId}`);
|
||||
return response.result.item;
|
||||
}
|
||||
|
||||
async createItem(item: Partial<FreshBooksItem>): Promise<FreshBooksItem> {
|
||||
const response = await this.request<{ result: { item: FreshBooksItem } }>('POST', `/accounting/account/${this.accountId}/items/items`, { item });
|
||||
return response.result.item;
|
||||
}
|
||||
|
||||
async updateItem(itemId: number, updates: Partial<FreshBooksItem>): Promise<FreshBooksItem> {
|
||||
const response = await this.request<{ result: { item: FreshBooksItem } }>('PUT', `/accounting/account/${this.accountId}/items/items/${itemId}`, { item: updates });
|
||||
return response.result.item;
|
||||
}
|
||||
|
||||
async deleteItem(itemId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/items/items/${itemId}`);
|
||||
}
|
||||
|
||||
// ========== STAFF ==========
|
||||
async getStaff(params?: { page?: number; per_page?: number }): Promise<FreshBooksStaff[]> {
|
||||
const response = await this.request<{ staff_members: FreshBooksStaff[] }>('GET', `/projects/business/${this.accountId}/staff`, undefined, params);
|
||||
return response.staff_members;
|
||||
}
|
||||
|
||||
async getStaffMember(staffId: number): Promise<FreshBooksStaff> {
|
||||
const response = await this.request<{ staff_member: FreshBooksStaff }>('GET', `/projects/business/${this.accountId}/staff/${staffId}`);
|
||||
return response.staff_member;
|
||||
}
|
||||
|
||||
// ========== BILLS ==========
|
||||
async getBills(params?: { page?: number; per_page?: number }): Promise<FreshBooksBill[]> {
|
||||
const response = await this.request<{ bills: FreshBooksBill[] }>('GET', `/accounting/account/${this.accountId}/bills/bills`, undefined, params);
|
||||
return response.bills;
|
||||
}
|
||||
|
||||
async getBill(billId: number): Promise<FreshBooksBill> {
|
||||
const response = await this.request<{ bill: FreshBooksBill }>('GET', `/accounting/account/${this.accountId}/bills/bills/${billId}`);
|
||||
return response.bill;
|
||||
}
|
||||
|
||||
async createBill(bill: Partial<FreshBooksBill>): Promise<FreshBooksBill> {
|
||||
const response = await this.request<{ bill: FreshBooksBill }>('POST', `/accounting/account/${this.accountId}/bills/bills`, { bill });
|
||||
return response.bill;
|
||||
}
|
||||
|
||||
async updateBill(billId: number, updates: Partial<FreshBooksBill>): Promise<FreshBooksBill> {
|
||||
const response = await this.request<{ bill: FreshBooksBill }>('PUT', `/accounting/account/${this.accountId}/bills/bills/${billId}`, { bill: updates });
|
||||
return response.bill;
|
||||
}
|
||||
|
||||
async deleteBill(billId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/bills/bills/${billId}`);
|
||||
}
|
||||
|
||||
// ========== BILL VENDORS ==========
|
||||
async getVendors(params?: { page?: number; per_page?: number }): Promise<BillVendor[]> {
|
||||
const response = await this.request<{ bill_vendors: BillVendor[] }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, undefined, params);
|
||||
return response.bill_vendors;
|
||||
}
|
||||
|
||||
async getVendor(vendorId: number): Promise<BillVendor> {
|
||||
const response = await this.request<{ bill_vendor: BillVendor }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`);
|
||||
return response.bill_vendor;
|
||||
}
|
||||
|
||||
async createVendor(vendor: Partial<BillVendor>): Promise<BillVendor> {
|
||||
const response = await this.request<{ bill_vendor: BillVendor }>('POST', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, { bill_vendor: vendor });
|
||||
return response.bill_vendor;
|
||||
}
|
||||
|
||||
async updateVendor(vendorId: number, updates: Partial<BillVendor>): Promise<BillVendor> {
|
||||
const response = await this.request<{ bill_vendor: BillVendor }>('PUT', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`, { bill_vendor: updates });
|
||||
return response.bill_vendor;
|
||||
}
|
||||
|
||||
async deleteVendor(vendorId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`);
|
||||
}
|
||||
|
||||
// ========== BILL PAYMENTS ==========
|
||||
async getBillPayments(billId: number): Promise<BillPayment[]> {
|
||||
const bill = await this.getBill(billId);
|
||||
return bill.bill_payments || [];
|
||||
}
|
||||
|
||||
async createBillPayment(billId: number, payment: Partial<BillPayment>): Promise<BillPayment> {
|
||||
const response = await this.request<{ bill_payment: BillPayment }>('POST', `/accounting/account/${this.accountId}/bills/bills/${billId}/bill_payments`, { bill_payment: payment });
|
||||
return response.bill_payment;
|
||||
}
|
||||
|
||||
// ========== ACCOUNTING ACCOUNTS ==========
|
||||
async getAccounts(): Promise<AccountingAccount[]> {
|
||||
const response = await this.request<{ accounts: AccountingAccount[] }>('GET', `/accounting/account/${this.accountId}/accounts/accounts`);
|
||||
return response.accounts;
|
||||
}
|
||||
|
||||
async getAccount(accountId: number): Promise<AccountingAccount> {
|
||||
const response = await this.request<{ account: AccountingAccount }>('GET', `/accounting/account/${this.accountId}/accounts/accounts/${accountId}`);
|
||||
return response.account;
|
||||
}
|
||||
|
||||
// ========== JOURNAL ENTRIES ==========
|
||||
async getJournalEntries(params?: { page?: number; per_page?: number }): Promise<JournalEntry[]> {
|
||||
const response = await this.request<{ journal_entries: JournalEntry[] }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, undefined, params);
|
||||
return response.journal_entries;
|
||||
}
|
||||
|
||||
async getJournalEntry(journalEntryId: number): Promise<JournalEntry> {
|
||||
const response = await this.request<{ journal_entry: JournalEntry }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries/${journalEntryId}`);
|
||||
return response.journal_entry;
|
||||
}
|
||||
|
||||
async createJournalEntry(journalEntry: Partial<JournalEntry>): Promise<JournalEntry> {
|
||||
const response = await this.request<{ journal_entry: JournalEntry }>('POST', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, { journal_entry: journalEntry });
|
||||
return response.journal_entry;
|
||||
}
|
||||
|
||||
// ========== RETAINERS ==========
|
||||
async getRetainers(params?: { page?: number; per_page?: number }): Promise<Retainer[]> {
|
||||
const response = await this.request<{ retainers: Retainer[] }>('GET', `/projects/business/${this.accountId}/retainers`, undefined, params);
|
||||
return response.retainers;
|
||||
}
|
||||
|
||||
async getRetainer(retainerId: number): Promise<Retainer> {
|
||||
const response = await this.request<{ retainer: Retainer }>('GET', `/projects/business/${this.accountId}/retainers/${retainerId}`);
|
||||
return response.retainer;
|
||||
}
|
||||
|
||||
async createRetainer(retainer: Partial<Retainer>): Promise<Retainer> {
|
||||
const response = await this.request<{ retainer: Retainer }>('POST', `/projects/business/${this.accountId}/retainers`, { retainer });
|
||||
return response.retainer;
|
||||
}
|
||||
|
||||
async updateRetainer(retainerId: number, updates: Partial<Retainer>): Promise<Retainer> {
|
||||
const response = await this.request<{ retainer: Retainer }>('PUT', `/projects/business/${this.accountId}/retainers/${retainerId}`, { retainer: updates });
|
||||
return response.retainer;
|
||||
}
|
||||
|
||||
async deleteRetainer(retainerId: number): Promise<void> {
|
||||
await this.request('DELETE', `/projects/business/${this.accountId}/retainers/${retainerId}`);
|
||||
}
|
||||
|
||||
// ========== CREDIT NOTES ==========
|
||||
async getCreditNotes(params?: { page?: number; per_page?: number }): Promise<CreditNote[]> {
|
||||
const response = await this.request<{ credit_notes: CreditNote[] }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, undefined, params);
|
||||
return response.credit_notes;
|
||||
}
|
||||
|
||||
async getCreditNote(creditNoteId: number): Promise<CreditNote> {
|
||||
const response = await this.request<{ credit_note: CreditNote }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`);
|
||||
return response.credit_note;
|
||||
}
|
||||
|
||||
async createCreditNote(creditNote: Partial<CreditNote>): Promise<CreditNote> {
|
||||
const response = await this.request<{ credit_note: CreditNote }>('POST', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, { credit_note: creditNote });
|
||||
return response.credit_note;
|
||||
}
|
||||
|
||||
async updateCreditNote(creditNoteId: number, updates: Partial<CreditNote>): Promise<CreditNote> {
|
||||
const response = await this.request<{ credit_note: CreditNote }>('PUT', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`, { credit_note: updates });
|
||||
return response.credit_note;
|
||||
}
|
||||
|
||||
async deleteCreditNote(creditNoteId: number): Promise<void> {
|
||||
await this.request('DELETE', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`);
|
||||
}
|
||||
|
||||
// ========== REPORTS ==========
|
||||
async getProfitLossReport(startDate: string, endDate: string): Promise<ProfitLossReport> {
|
||||
const response = await this.request<{ report: ProfitLossReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/profitloss`, undefined, {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
return response.report;
|
||||
}
|
||||
|
||||
async getTaxSummaryReport(startDate: string, endDate: string): Promise<TaxSummaryReport> {
|
||||
const response = await this.request<{ report: TaxSummaryReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/taxsummary`, undefined, {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
return response.report;
|
||||
}
|
||||
|
||||
async getAgingReport(): Promise<AgingReport> {
|
||||
const response = await this.request<{ report: AgingReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/aging`);
|
||||
return response.report;
|
||||
}
|
||||
|
||||
async getExpenseReport(startDate: string, endDate: string): Promise<ExpenseReport> {
|
||||
const response = await this.request<{ report: ExpenseReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/expenses`, undefined, {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
return response.report;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { FreshBooksServer } from './server.js';
|
||||
import { runServer } from './server.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new FreshBooksServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
console.error('Fatal error starting FreshBooks MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
runServer().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@ -3,145 +3,181 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { FreshBooksClient } from './clients/freshbooks.js';
|
||||
import { invoicesTools } from './tools/invoices-tools.js';
|
||||
import { FreshBooksAPIClient } from './clients/freshbooks.js';
|
||||
import { clientsTools } from './tools/clients-tools.js';
|
||||
import { expensesTools } from './tools/expenses-tools.js';
|
||||
import { invoicesTools } from './tools/invoices-tools.js';
|
||||
import { estimatesTools } from './tools/estimates-tools.js';
|
||||
import { timeEntriesTools } from './tools/time-entries-tools.js';
|
||||
import { projectsTools } from './tools/projects-tools.js';
|
||||
import { expensesTools } from './tools/expenses-tools.js';
|
||||
import { paymentsTools } from './tools/payments-tools.js';
|
||||
import { itemsTools } from './tools/items-tools.js';
|
||||
import { projectsTools } from './tools/projects-tools.js';
|
||||
import { timeEntriesTools } from './tools/time-entries-tools.js';
|
||||
import { taxesTools } from './tools/taxes-tools.js';
|
||||
import { reportsTools } from './tools/reports-tools.js';
|
||||
import { recurringTools } from './tools/recurring-tools.js';
|
||||
import { itemsTools } from './tools/items-tools.js';
|
||||
import { staffTools } from './tools/staff-tools.js';
|
||||
import { billsTools } from './tools/bills-tools.js';
|
||||
import { vendorsTools } from './tools/vendors-tools.js';
|
||||
import { accountsTools } from './tools/accounts-tools.js';
|
||||
import { journalEntriesTools } from './tools/journal-entries-tools.js';
|
||||
import { retainersTools } from './tools/retainers-tools.js';
|
||||
import { creditNotesTools } from './tools/credit-notes-tools.js';
|
||||
import { reportsTools } from './tools/reports-tools.js';
|
||||
|
||||
export class FreshBooksServer {
|
||||
private server: Server;
|
||||
private client: FreshBooksClient;
|
||||
private allTools: any[];
|
||||
// Combine all tools
|
||||
const allTools = [
|
||||
...clientsTools,
|
||||
...invoicesTools,
|
||||
...estimatesTools,
|
||||
...expensesTools,
|
||||
...paymentsTools,
|
||||
...projectsTools,
|
||||
...timeEntriesTools,
|
||||
...taxesTools,
|
||||
...itemsTools,
|
||||
...staffTools,
|
||||
...billsTools,
|
||||
...vendorsTools,
|
||||
...accountsTools,
|
||||
...journalEntriesTools,
|
||||
...retainersTools,
|
||||
...creditNotesTools,
|
||||
...reportsTools,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'freshbooks-mcp-server',
|
||||
version: '1.0.0',
|
||||
// MCP App resources (HTML files)
|
||||
const appResources = [
|
||||
{ uri: 'freshbooks://apps/invoice-dashboard', name: 'Invoice Dashboard' },
|
||||
{ uri: 'freshbooks://apps/invoice-detail', name: 'Invoice Detail' },
|
||||
{ uri: 'freshbooks://apps/invoice-creator', name: 'Invoice Creator' },
|
||||
{ uri: 'freshbooks://apps/client-dashboard', name: 'Client Dashboard' },
|
||||
{ uri: 'freshbooks://apps/client-detail', name: 'Client Detail' },
|
||||
{ uri: 'freshbooks://apps/expense-tracker', name: 'Expense Tracker' },
|
||||
{ uri: 'freshbooks://apps/expense-report', name: 'Expense Report' },
|
||||
{ uri: 'freshbooks://apps/project-dashboard', name: 'Project Dashboard' },
|
||||
{ uri: 'freshbooks://apps/project-detail', name: 'Project Detail' },
|
||||
{ uri: 'freshbooks://apps/time-tracker', name: 'Time Tracker' },
|
||||
{ uri: 'freshbooks://apps/time-report', name: 'Time Report' },
|
||||
{ uri: 'freshbooks://apps/payment-dashboard', name: 'Payment Dashboard' },
|
||||
{ uri: 'freshbooks://apps/estimate-builder', name: 'Estimate Builder' },
|
||||
{ uri: 'freshbooks://apps/profit-loss-report', name: 'Profit & Loss Report' },
|
||||
{ uri: 'freshbooks://apps/tax-summary', name: 'Tax Summary' },
|
||||
{ uri: 'freshbooks://apps/aging-report', name: 'Aging Report' },
|
||||
{ uri: 'freshbooks://apps/item-catalog', name: 'Item Catalog' },
|
||||
{ uri: 'freshbooks://apps/bill-manager', name: 'Bill Manager' },
|
||||
{ uri: 'freshbooks://apps/staff-directory', name: 'Staff Directory' },
|
||||
{ uri: 'freshbooks://apps/dashboard-overview', name: 'Dashboard Overview' },
|
||||
];
|
||||
|
||||
export async function createServer() {
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'freshbooks-mcp',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get config from environment
|
||||
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
|
||||
const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN;
|
||||
|
||||
if (!accountId || !accessToken) {
|
||||
throw new Error(
|
||||
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_ACCESS_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize FreshBooks client from env
|
||||
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
|
||||
const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN;
|
||||
const client = new FreshBooksAPIClient({
|
||||
accountId,
|
||||
accessToken,
|
||||
});
|
||||
|
||||
if (!accountId || !bearerToken) {
|
||||
throw new Error(
|
||||
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN'
|
||||
);
|
||||
// List tools handler
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: allTools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Call tool handler
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const tool = allTools.find((t) => t.name === request.params.name);
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
}
|
||||
|
||||
this.client = new FreshBooksClient({
|
||||
accountId,
|
||||
bearerToken,
|
||||
});
|
||||
|
||||
// Combine all tools
|
||||
this.allTools = [
|
||||
...invoicesTools,
|
||||
...clientsTools,
|
||||
...expensesTools,
|
||||
...estimatesTools,
|
||||
...timeEntriesTools,
|
||||
...projectsTools,
|
||||
...paymentsTools,
|
||||
...itemsTools,
|
||||
...taxesTools,
|
||||
...reportsTools,
|
||||
...recurringTools,
|
||||
...accountsTools,
|
||||
];
|
||||
|
||||
this.setupHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => {
|
||||
console.error('[MCP Error]', error);
|
||||
};
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private setupHandlers() {
|
||||
// List tools handler
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
try {
|
||||
return await tool.handler(client, request.params.arguments || {});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
tools: this.allTools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: tool.inputSchema.shape,
|
||||
required: Object.keys(tool.inputSchema.shape).filter(
|
||||
(key) => !tool.inputSchema.shape[key].isOptional()
|
||||
),
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${errorMessage}`,
|
||||
},
|
||||
})),
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Call tool handler
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const tool = this.allTools.find((t) => t.name === toolName);
|
||||
// List resources handler (MCP Apps)
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: appResources.map((app) => ({
|
||||
uri: app.uri,
|
||||
mimeType: 'text/html',
|
||||
name: app.name,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
// Read resource handler (serve MCP App HTML)
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const uri = request.params.uri;
|
||||
const appName = uri.replace('freshbooks://apps/', '');
|
||||
|
||||
try {
|
||||
// Validate input
|
||||
const validatedArgs = tool.inputSchema.parse(request.params.arguments);
|
||||
try {
|
||||
const { readFileSync } = await import('fs');
|
||||
const { join } = await import('path');
|
||||
const { fileURLToPath } = await import('url');
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
// Execute tool
|
||||
const result = await tool.handler(validatedArgs, this.client);
|
||||
const htmlPath = join(__dirname, 'ui', 'react-app', 'dist', `${appName}.html`);
|
||||
const html = readFileSync(htmlPath, 'utf-8');
|
||||
|
||||
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 {
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'text/html',
|
||||
text: html,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load app ${appName}: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${errorMessage}\n${errorDetails}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('FreshBooks MCP server running on stdio');
|
||||
}
|
||||
return server;
|
||||
}
|
||||
|
||||
export async function runServer() {
|
||||
const server = await createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('FreshBooks MCP server running on stdio');
|
||||
}
|
||||
|
||||
@ -1,51 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Account, StaffMember } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const accountsTools = [
|
||||
{
|
||||
name: 'freshbooks_get_account',
|
||||
description: 'Get current account details',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { account: Account } } }>(
|
||||
`/users/me`
|
||||
);
|
||||
return response.response.result.account;
|
||||
name: 'freshbooks_list_accounts',
|
||||
description: 'List all accounting accounts (chart of accounts)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_staff',
|
||||
description: 'List all staff members',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ staff: StaffMember[] }>(
|
||||
'/users/staff',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getAccounts();
|
||||
return {
|
||||
staff: response.response.result.staff || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_current_user',
|
||||
description: 'Get current user (self) details',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/auth/api/v1/users/me'
|
||||
);
|
||||
return response.response.result;
|
||||
name: 'freshbooks_get_account',
|
||||
description: 'Get detailed information about a specific accounting account',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
account_id: {
|
||||
type: 'number',
|
||||
description: 'Account ID',
|
||||
},
|
||||
},
|
||||
required: ['account_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getAccount(args.account_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
265
servers/freshbooks/src/tools/bills-tools.ts
Normal file
265
servers/freshbooks/src/tools/bills-tools.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const billsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_bills',
|
||||
description: 'List all bills with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getBills(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_bill',
|
||||
description: 'Get detailed information about a specific bill',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bill_id: {
|
||||
type: 'number',
|
||||
description: 'Bill ID',
|
||||
},
|
||||
},
|
||||
required: ['bill_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getBill(args.bill_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_bill',
|
||||
description: 'Create a new bill in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendor_id: {
|
||||
type: 'number',
|
||||
description: 'Vendor ID',
|
||||
},
|
||||
bill_number: {
|
||||
type: 'string',
|
||||
description: 'Bill number',
|
||||
},
|
||||
issue_date: {
|
||||
type: 'string',
|
||||
description: 'Issue date (YYYY-MM-DD)',
|
||||
},
|
||||
due_date: {
|
||||
type: 'string',
|
||||
description: 'Due date (YYYY-MM-DD)',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Bill line items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
quantity: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
},
|
||||
},
|
||||
category_id: {
|
||||
type: 'number',
|
||||
description: 'Expense category ID',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['vendor_id', 'issue_date'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createBill(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_update_bill',
|
||||
description: 'Update an existing bill',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bill_id: {
|
||||
type: 'number',
|
||||
description: 'Bill ID to update',
|
||||
},
|
||||
due_date: {
|
||||
type: 'string',
|
||||
description: 'Due date',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Bill line items',
|
||||
},
|
||||
},
|
||||
required: ['bill_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { bill_id, ...updates } = args;
|
||||
const result = await client.updateBill(bill_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_delete_bill',
|
||||
description: 'Delete a bill from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bill_id: {
|
||||
type: 'number',
|
||||
description: 'Bill ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['bill_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteBill(args.bill_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Bill ${args.bill_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_bill_payments',
|
||||
description: 'Get all payments for a specific bill',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bill_id: {
|
||||
type: 'number',
|
||||
description: 'Bill ID',
|
||||
},
|
||||
},
|
||||
required: ['bill_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getBillPayments(args.bill_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_bill_payment',
|
||||
description: 'Create a payment for a bill',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bill_id: {
|
||||
type: 'number',
|
||||
description: 'Bill ID',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Payment amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
paid_date: {
|
||||
type: 'string',
|
||||
description: 'Payment date (YYYY-MM-DD)',
|
||||
},
|
||||
payment_type: {
|
||||
type: 'string',
|
||||
description: 'Payment type (e.g., Check, Cash, Credit Card)',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Payment note',
|
||||
},
|
||||
},
|
||||
required: ['bill_id', 'amount', 'paid_date', 'payment_type'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { bill_id, amount, currency_code, ...payment } = args;
|
||||
const paymentData = {
|
||||
...payment,
|
||||
amount: {
|
||||
amount,
|
||||
code: currency_code || 'USD',
|
||||
},
|
||||
};
|
||||
const result = await client.createBillPayment(bill_id, paymentData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,137 +1,314 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Client, ClientContact } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const clientsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_clients',
|
||||
description: 'List all clients with optional search',
|
||||
inputSchema: z.object({
|
||||
search: z.string().optional().describe('Search by name, email, or organization'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.search) {
|
||||
params.search = { email_like: `%${args.search}%` };
|
||||
}
|
||||
|
||||
const response = await client.getPaginated<{ clients: Client[] }>(
|
||||
'/users/clients',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all clients in FreshBooks with optional search and pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search term to filter clients',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination (default: 1)',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page (default: 30)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getClients(args);
|
||||
return {
|
||||
clients: response.response.result.clients || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_client',
|
||||
description: 'Get a single client by ID',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { client: Client } } }>(
|
||||
`/users/clients/${args.client_id}`
|
||||
);
|
||||
return response.response.result.client;
|
||||
description: 'Get detailed information about a specific client',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
},
|
||||
required: ['client_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getClient(args.client_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_client',
|
||||
description: 'Create a new client',
|
||||
inputSchema: z.object({
|
||||
fname: z.string().describe('First name'),
|
||||
lname: z.string().describe('Last name'),
|
||||
email: z.string().email().describe('Email address'),
|
||||
organization: z.string().optional().describe('Company/organization name'),
|
||||
phone: z.string().optional(),
|
||||
mobile: z.string().optional(),
|
||||
bill_street: z.string().optional(),
|
||||
bill_city: z.string().optional(),
|
||||
bill_state: z.string().optional(),
|
||||
bill_country: z.string().optional(),
|
||||
bill_postal_code: z.string().optional(),
|
||||
currency_code: z.string().default('USD'),
|
||||
language: z.string().default('en'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const clientData = { client: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { client: Client } } }>(
|
||||
'/users/clients',
|
||||
clientData
|
||||
);
|
||||
return response.response.result.client;
|
||||
description: 'Create a new client in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fname: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
lname: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
description: 'Company/organization name',
|
||||
},
|
||||
business_phone: {
|
||||
type: 'string',
|
||||
description: 'Business phone number',
|
||||
},
|
||||
mobile_phone: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
home_phone: {
|
||||
type: 'string',
|
||||
description: 'Home phone number',
|
||||
},
|
||||
fax: {
|
||||
type: 'string',
|
||||
description: 'Fax number',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD, CAD, GBP)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: 'Language code (e.g., en, fr)',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Internal note about the client',
|
||||
},
|
||||
vat_name: {
|
||||
type: 'string',
|
||||
description: 'VAT name',
|
||||
},
|
||||
vat_number: {
|
||||
type: 'string',
|
||||
description: 'VAT number',
|
||||
},
|
||||
s_street: {
|
||||
type: 'string',
|
||||
description: 'Shipping address street',
|
||||
},
|
||||
s_street2: {
|
||||
type: 'string',
|
||||
description: 'Shipping address street line 2',
|
||||
},
|
||||
s_city: {
|
||||
type: 'string',
|
||||
description: 'Shipping address city',
|
||||
},
|
||||
s_province: {
|
||||
type: 'string',
|
||||
description: 'Shipping address province/state',
|
||||
},
|
||||
s_code: {
|
||||
type: 'string',
|
||||
description: 'Shipping address postal code',
|
||||
},
|
||||
s_country: {
|
||||
type: 'string',
|
||||
description: 'Shipping address country',
|
||||
},
|
||||
p_street: {
|
||||
type: 'string',
|
||||
description: 'Billing address street',
|
||||
},
|
||||
p_street2: {
|
||||
type: 'string',
|
||||
description: 'Billing address street line 2',
|
||||
},
|
||||
p_city: {
|
||||
type: 'string',
|
||||
description: 'Billing address city',
|
||||
},
|
||||
p_province: {
|
||||
type: 'string',
|
||||
description: 'Billing address province/state',
|
||||
},
|
||||
p_code: {
|
||||
type: 'string',
|
||||
description: 'Billing address postal code',
|
||||
},
|
||||
p_country: {
|
||||
type: 'string',
|
||||
description: 'Billing address country',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createClient(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_client',
|
||||
description: 'Update an existing client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
fname: z.string().optional(),
|
||||
lname: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
organization: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
mobile: z.string().optional(),
|
||||
bill_street: z.string().optional(),
|
||||
bill_city: z.string().optional(),
|
||||
bill_state: z.string().optional(),
|
||||
bill_country: z.string().optional(),
|
||||
bill_postal_code: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { client_id, ...updateFields } = args;
|
||||
const clientData = { client: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { client: Client } } }>(
|
||||
`/users/clients/${client_id}`,
|
||||
clientData
|
||||
);
|
||||
return response.response.result.client;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID to update',
|
||||
},
|
||||
fname: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
lname: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
organization: {
|
||||
type: 'string',
|
||||
description: 'Company/organization name',
|
||||
},
|
||||
business_phone: {
|
||||
type: 'string',
|
||||
description: 'Business phone number',
|
||||
},
|
||||
mobile_phone: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: 'Language code',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Internal note',
|
||||
},
|
||||
s_street: {
|
||||
type: 'string',
|
||||
description: 'Shipping address street',
|
||||
},
|
||||
s_city: {
|
||||
type: 'string',
|
||||
description: 'Shipping address city',
|
||||
},
|
||||
s_province: {
|
||||
type: 'string',
|
||||
description: 'Shipping address province/state',
|
||||
},
|
||||
s_code: {
|
||||
type: 'string',
|
||||
description: 'Shipping address postal code',
|
||||
},
|
||||
s_country: {
|
||||
type: 'string',
|
||||
description: 'Shipping address country',
|
||||
},
|
||||
},
|
||||
required: ['client_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { client_id, ...updates } = args;
|
||||
const result = await client.updateClient(client_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_client',
|
||||
description: 'Delete (archive) a client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/users/clients/${args.client_id}`,
|
||||
{ client: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Client ${args.client_id} archived` };
|
||||
description: 'Delete a client from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['client_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteClient(args.client_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Client ${args.client_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_client_contacts',
|
||||
description: 'List all contacts for a specific client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { contacts: ClientContact[] } } }>(
|
||||
`/users/clients/${args.client_id}/contacts`
|
||||
);
|
||||
return response.response.result.contacts || [];
|
||||
name: 'freshbooks_search_clients',
|
||||
description: 'Search for clients by name, email, or organization',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getClients({ search: args.query });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
187
servers/freshbooks/src/tools/credit-notes-tools.ts
Normal file
187
servers/freshbooks/src/tools/credit-notes-tools.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const creditNotesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_credit_notes',
|
||||
description: 'List all credit notes with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getCreditNotes(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_credit_note',
|
||||
description: 'Get detailed information about a specific credit note',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credit_note_id: {
|
||||
type: 'number',
|
||||
description: 'Credit note ID',
|
||||
},
|
||||
},
|
||||
required: ['credit_note_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getCreditNote(args.credit_note_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_credit_note',
|
||||
description: 'Create a new credit note in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientid: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
create_date: {
|
||||
type: 'string',
|
||||
description: 'Creation date (YYYY-MM-DD)',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
credit_type: {
|
||||
type: 'string',
|
||||
description: 'Credit type (goodwill, prepayment, etc.)',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Credit note notes',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Credit note line items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
qty: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['clientid'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createCreditNote(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_update_credit_note',
|
||||
description: 'Update an existing credit note',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credit_note_id: {
|
||||
type: 'number',
|
||||
description: 'Credit note ID to update',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Credit note notes',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Credit note line items',
|
||||
},
|
||||
},
|
||||
required: ['credit_note_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { credit_note_id, ...updates } = args;
|
||||
const result = await client.updateCreditNote(credit_note_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_delete_credit_note',
|
||||
description: 'Delete a credit note from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
credit_note_id: {
|
||||
type: 'number',
|
||||
description: 'Credit note ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['credit_note_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteCreditNote(args.credit_note_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Credit note ${args.credit_note_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,194 +1,317 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Estimate } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const estimatesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_estimates',
|
||||
description: 'List all estimates with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
status: z.enum(['draft', 'sent', 'accepted', 'declined']).optional(),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.status) params.status = args.status;
|
||||
|
||||
const response = await client.getPaginated<{ estimates: Estimate[] }>(
|
||||
'/estimates/estimates',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all estimates with optional search and pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search term to filter estimates',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getEstimates(args);
|
||||
return {
|
||||
estimates: response.response.result.estimates || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_estimate',
|
||||
description: 'Get a single estimate by ID',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${args.estimate_id}`
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
description: 'Get detailed information about a specific estimate',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getEstimate(args.estimate_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_estimate',
|
||||
description: 'Create a new estimate',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().describe('Client ID'),
|
||||
create_date: z.string().optional().describe('Estimate date (YYYY-MM-DD)'),
|
||||
lines: z.array(z.object({
|
||||
name: z.string().describe('Line item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().default(1),
|
||||
unit_cost: z.string().describe('Unit cost'),
|
||||
})).describe('Estimate line items'),
|
||||
currency_code: z.string().default('USD'),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
}));
|
||||
|
||||
const estimateData = {
|
||||
estimate: {
|
||||
clientid: args.clientid,
|
||||
create_date: args.create_date || new Date().toISOString().split('T')[0],
|
||||
currency_code: args.currency_code,
|
||||
lines,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
description: 'Create a new estimate in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerid: {
|
||||
type: 'number',
|
||||
description: 'Client ID for this estimate',
|
||||
},
|
||||
create_date: {
|
||||
type: 'string',
|
||||
description: 'Estimate creation date (YYYY-MM-DD)',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD, CAD)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: 'Language code (e.g., en)',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Estimate notes',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
description: 'Estimate terms',
|
||||
},
|
||||
discount_value: {
|
||||
type: 'string',
|
||||
description: 'Discount value',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Estimate line items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
qty: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'Currency code',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['customerid'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createEstimate(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { estimate: Estimate } } }>(
|
||||
'/estimates/estimates',
|
||||
estimateData
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_estimate',
|
||||
description: 'Update an existing estimate',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
clientid: z.number().optional(),
|
||||
create_date: z.string().optional(),
|
||||
lines: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number(),
|
||||
unit_cost: z.string(),
|
||||
})).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { estimate_id, ...updateFields } = args;
|
||||
|
||||
if (updateFields.lines) {
|
||||
updateFields.lines = updateFields.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
||||
}));
|
||||
}
|
||||
|
||||
const estimateData = { estimate: updateFields };
|
||||
const response = await client.put<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${estimate_id}`,
|
||||
estimateData
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID to update',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Estimate notes',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
description: 'Estimate terms',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Estimate line items',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { estimate_id, ...updates } = args;
|
||||
const result = await client.updateEstimate(estimate_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_estimate',
|
||||
description: 'Delete (archive) an estimate',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/estimates/estimates/${args.estimate_id}`,
|
||||
{ estimate: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Estimate ${args.estimate_id} deleted` };
|
||||
description: 'Delete an estimate from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteEstimate(args.estimate_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Estimate ${args.estimate_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_send_estimate',
|
||||
description: 'Send an estimate to the client via email',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
email_subject: z.string().optional(),
|
||||
email_body: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const emailData: any = { estimate: { action_email: true } };
|
||||
if (args.email_subject) emailData.estimate.email_subject = args.email_subject;
|
||||
if (args.email_body) emailData.estimate.email_body = args.email_body;
|
||||
|
||||
await client.put(
|
||||
`/estimates/estimates/${args.estimate_id}`,
|
||||
emailData
|
||||
);
|
||||
return { success: true, message: `Estimate ${args.estimate_id} sent` };
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID to send',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address to send to (optional)',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.sendEstimate(args.estimate_id, args.email);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Estimate ${args.estimate_id} sent successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_convert_estimate_to_invoice',
|
||||
description: 'Convert an estimate to an invoice',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
// Get the estimate first
|
||||
const estimateResp = await client.get<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${args.estimate_id}`
|
||||
);
|
||||
const estimate = estimateResp.response.result.estimate;
|
||||
|
||||
// Create invoice from estimate
|
||||
const invoiceData = {
|
||||
invoice: {
|
||||
clientid: estimate.clientid,
|
||||
create_date: new Date().toISOString().split('T')[0],
|
||||
currency_code: estimate.currency_code,
|
||||
lines: estimate.lines,
|
||||
notes: estimate.notes,
|
||||
terms: estimate.terms,
|
||||
estimateid: args.estimate_id,
|
||||
name: 'freshbooks_accept_estimate',
|
||||
description: 'Mark an estimate as accepted',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID to accept',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.acceptEstimate(args.estimate_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Estimate ${args.estimate_id} accepted`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_add_estimate_line',
|
||||
description: 'Add a line item to an existing estimate',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'number',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
qty: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id', 'name', 'qty', 'unit_cost'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const estimate = await client.getEstimate(args.estimate_id);
|
||||
const newLine = {
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
qty: args.qty,
|
||||
unit_cost: {
|
||||
amount: args.unit_cost,
|
||||
code: args.currency_code || estimate.currency_code,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
'/invoices/invoices',
|
||||
invoiceData
|
||||
);
|
||||
return response;
|
||||
const lines = [...estimate.lines, newLine];
|
||||
const result = await client.updateEstimate(args.estimate_id, { lines });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,139 +1,249 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Expense, ExpenseCategory } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const expensesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_expenses',
|
||||
description: 'List all expenses with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
category_id: z.number().optional().describe('Filter by category ID'),
|
||||
projectid: z.number().optional().describe('Filter by project ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.category_id) params.categoryid = args.category_id;
|
||||
if (args.projectid) params.projectid = args.projectid;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ expenses: Expense[] }>(
|
||||
'/expenses/expenses',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all expenses with optional search and pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search term to filter expenses',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getExpenses(args);
|
||||
return {
|
||||
expenses: response.response.result.expenses || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_expense',
|
||||
description: 'Get a single expense by ID',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { expense: Expense } } }>(
|
||||
`/expenses/expenses/${args.expense_id}`
|
||||
);
|
||||
return response.response.result.expense;
|
||||
description: 'Get detailed information about a specific expense',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expense_id: {
|
||||
type: 'number',
|
||||
description: 'Expense ID',
|
||||
},
|
||||
},
|
||||
required: ['expense_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getExpense(args.expense_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_expense',
|
||||
description: 'Create a new expense',
|
||||
inputSchema: z.object({
|
||||
category_id: z.number().describe('Expense category ID'),
|
||||
vendor: z.string().describe('Vendor name'),
|
||||
amount: z.string().describe('Expense amount'),
|
||||
date: z.string().describe('Expense date (YYYY-MM-DD)'),
|
||||
clientid: z.number().optional().describe('Associated client ID'),
|
||||
projectid: z.number().optional().describe('Associated project ID'),
|
||||
notes: z.string().optional(),
|
||||
taxName1: z.string().optional(),
|
||||
taxPercent1: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const expenseData = {
|
||||
expense: {
|
||||
...args,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
description: 'Create a new expense in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Expense amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
vendor: {
|
||||
type: 'string',
|
||||
description: 'Vendor name',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Expense date (YYYY-MM-DD)',
|
||||
},
|
||||
categoryid: {
|
||||
type: 'number',
|
||||
description: 'Expense category ID',
|
||||
},
|
||||
clientid: {
|
||||
type: 'number',
|
||||
description: 'Client ID (optional)',
|
||||
},
|
||||
projectid: {
|
||||
type: 'number',
|
||||
description: 'Project ID (optional)',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Expense notes',
|
||||
},
|
||||
markup_percent: {
|
||||
type: 'string',
|
||||
description: 'Markup percentage for billable expenses',
|
||||
},
|
||||
},
|
||||
required: ['amount', 'vendor', 'date', 'categoryid'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const expense = {
|
||||
amount: {
|
||||
amount: args.amount,
|
||||
code: args.currency_code || 'USD',
|
||||
},
|
||||
vendor: args.vendor,
|
||||
date: args.date,
|
||||
categoryid: args.categoryid,
|
||||
clientid: args.clientid,
|
||||
projectid: args.projectid,
|
||||
notes: args.notes,
|
||||
markup_percent: args.markup_percent,
|
||||
};
|
||||
const result = await client.createExpense(expense);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { expense: Expense } } }>(
|
||||
'/expenses/expenses',
|
||||
expenseData
|
||||
);
|
||||
return response.response.result.expense;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_expense',
|
||||
description: 'Update an existing expense',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
category_id: z.number().optional(),
|
||||
vendor: z.string().optional(),
|
||||
amount: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
clientid: z.number().optional(),
|
||||
projectid: z.number().optional(),
|
||||
notes: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { expense_id, ...updateFields } = args;
|
||||
if (updateFields.amount) {
|
||||
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expense_id: {
|
||||
type: 'number',
|
||||
description: 'Expense ID to update',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Expense amount',
|
||||
},
|
||||
vendor: {
|
||||
type: 'string',
|
||||
description: 'Vendor name',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Expense date',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Expense notes',
|
||||
},
|
||||
},
|
||||
required: ['expense_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { expense_id, amount, ...updates } = args;
|
||||
if (amount) {
|
||||
const expense = await client.getExpense(expense_id);
|
||||
updates.amount = {
|
||||
amount,
|
||||
code: expense.amount.code,
|
||||
};
|
||||
}
|
||||
|
||||
const expenseData = { expense: updateFields };
|
||||
const response = await client.put<{ response: { result: { expense: Expense } } }>(
|
||||
`/expenses/expenses/${expense_id}`,
|
||||
expenseData
|
||||
);
|
||||
return response.response.result.expense;
|
||||
const result = await client.updateExpense(expense_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_expense',
|
||||
description: 'Delete an expense',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/expenses/expenses/${args.expense_id}`,
|
||||
{ expense: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Expense ${args.expense_id} deleted` };
|
||||
description: 'Delete an expense from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
expense_id: {
|
||||
type: 'number',
|
||||
description: 'Expense ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['expense_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteExpense(args.expense_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Expense ${args.expense_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_expense_categories',
|
||||
description: 'List all expense categories',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { categories: ExpenseCategory[] } } }>(
|
||||
'/expenses/categories'
|
||||
);
|
||||
return response.response.result.categories || [];
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getExpenseCategories();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_search_expenses',
|
||||
description: 'Search for expenses by vendor, category, or notes',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getExpenses({ search: args.query });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,268 +1,399 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Invoice, Payment } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const invoicesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_invoices',
|
||||
description: 'List all invoices with optional filtering (client, status, date range)',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
status: z.enum(['draft', 'sent', 'viewed', 'paid', 'partial', 'overdue', 'disputed']).optional(),
|
||||
date_min: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('Maximum date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.status) params.status = args.status;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ invoices: Invoice[] }>(
|
||||
'/invoices/invoices',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all invoices with optional search and pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search term to filter invoices',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getInvoices(args);
|
||||
return {
|
||||
invoices: response.response.result.invoices || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_invoice',
|
||||
description: 'Get a single invoice by ID',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
description: 'Get detailed information about a specific invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getInvoice(args.invoice_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_invoice',
|
||||
description: 'Create a new invoice',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().describe('Client ID'),
|
||||
create_date: z.string().optional().describe('Invoice date (YYYY-MM-DD, defaults to today)'),
|
||||
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
||||
lines: z.array(z.object({
|
||||
name: z.string().describe('Line item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().default(1),
|
||||
unit_cost: z.string().describe('Unit cost as string (e.g., "100.00")'),
|
||||
})).describe('Invoice line items'),
|
||||
currency_code: z.string().default('USD'),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
status: z.enum(['draft', 'sent']).default('draft'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
}));
|
||||
|
||||
const invoiceData = {
|
||||
invoice: {
|
||||
clientid: args.clientid,
|
||||
create_date: args.create_date || new Date().toISOString().split('T')[0],
|
||||
due_date: args.due_date,
|
||||
currency_code: args.currency_code,
|
||||
lines,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
status: args.status === 'sent' ? 2 : 1,
|
||||
description: 'Create a new invoice in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerid: {
|
||||
type: 'number',
|
||||
description: 'Client ID for this invoice',
|
||||
},
|
||||
create_date: {
|
||||
type: 'string',
|
||||
description: 'Invoice creation date (YYYY-MM-DD)',
|
||||
},
|
||||
due_offset_days: {
|
||||
type: 'number',
|
||||
description: 'Number of days until invoice is due',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD, CAD)',
|
||||
},
|
||||
language: {
|
||||
type: 'string',
|
||||
description: 'Language code (e.g., en)',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Invoice notes',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
description: 'Invoice terms',
|
||||
},
|
||||
po_number: {
|
||||
type: 'string',
|
||||
description: 'Purchase order number',
|
||||
},
|
||||
discount_value: {
|
||||
type: 'string',
|
||||
description: 'Discount value as percentage or amount',
|
||||
},
|
||||
discount_description: {
|
||||
type: 'string',
|
||||
description: 'Description of discount',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Invoice line items',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
qty: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'Currency code',
|
||||
},
|
||||
},
|
||||
},
|
||||
taxName1: {
|
||||
type: 'string',
|
||||
description: 'First tax name',
|
||||
},
|
||||
taxAmount1: {
|
||||
type: 'string',
|
||||
description: 'First tax amount',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['customerid'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createInvoice(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { invoice: Invoice } } }>(
|
||||
'/invoices/invoices',
|
||||
invoiceData
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_invoice',
|
||||
description: 'Update an existing invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
clientid: z.number().optional(),
|
||||
create_date: z.string().optional(),
|
||||
due_date: z.string().optional(),
|
||||
lines: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number(),
|
||||
unit_cost: z.string(),
|
||||
})).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const updateData: any = { invoice: {} };
|
||||
|
||||
if (args.clientid) updateData.invoice.clientid = args.clientid;
|
||||
if (args.create_date) updateData.invoice.create_date = args.create_date;
|
||||
if (args.due_date) updateData.invoice.due_date = args.due_date;
|
||||
if (args.notes) updateData.invoice.notes = args.notes;
|
||||
if (args.terms) updateData.invoice.terms = args.terms;
|
||||
if (args.lines) {
|
||||
updateData.invoice.lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
||||
}));
|
||||
}
|
||||
|
||||
const response = await client.put<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
updateData
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID to update',
|
||||
},
|
||||
customerid: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Invoice notes',
|
||||
},
|
||||
terms: {
|
||||
type: 'string',
|
||||
description: 'Invoice terms',
|
||||
},
|
||||
po_number: {
|
||||
type: 'string',
|
||||
description: 'Purchase order number',
|
||||
},
|
||||
discount_value: {
|
||||
type: 'string',
|
||||
description: 'Discount value',
|
||||
},
|
||||
lines: {
|
||||
type: 'array',
|
||||
description: 'Invoice line items',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { invoice_id, ...updates } = args;
|
||||
const result = await client.updateInvoice(invoice_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_invoice',
|
||||
description: 'Delete an invoice (moves to archived)',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} archived` };
|
||||
description: 'Delete an invoice from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteInvoice(args.invoice_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Invoice ${args.invoice_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_send_invoice',
|
||||
description: 'Send an invoice to the client via email',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
email_subject: z.string().optional(),
|
||||
email_body: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const emailData: any = { invoice: {} };
|
||||
if (args.email_subject) emailData.invoice.email_subject = args.email_subject;
|
||||
if (args.email_body) emailData.invoice.email_body = args.email_body;
|
||||
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { action_email: true, ...emailData.invoice } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} sent` };
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID to send',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address to send to (optional, uses client email if not provided)',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.sendInvoice(args.invoice_id, args.email);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Invoice ${args.invoice_id} sent successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_mark_invoice_paid',
|
||||
description: 'Mark an invoice as paid',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
payment_type: z.string().default('Cash').describe('Payment method'),
|
||||
payment_date: z.string().optional().describe('Payment date (YYYY-MM-DD, defaults to today)'),
|
||||
amount: z.string().optional().describe('Payment amount (defaults to outstanding amount)'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
// First get the invoice to know the outstanding amount
|
||||
const invoiceResp = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`
|
||||
);
|
||||
const invoice = invoiceResp.response.result.invoice;
|
||||
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoice_id,
|
||||
amount: {
|
||||
amount: args.amount || invoice.outstanding.amount,
|
||||
code: invoice.currency_code,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID to mark as paid',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.markInvoicePaid(args.invoice_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Invoice ${args.invoice_id} marked as paid`,
|
||||
},
|
||||
date: args.payment_date || new Date().toISOString().split('T')[0],
|
||||
type: args.payment_type,
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_invoice_share_link',
|
||||
description: 'Get the shareable link for an invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const link = await client.getInvoiceShareLink(args.invoice_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: link,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_add_invoice_line',
|
||||
description: 'Add a line item to an existing invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
qty: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id', 'name', 'qty', 'unit_cost'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const invoice = await client.getInvoice(args.invoice_id);
|
||||
const newLine = {
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
qty: args.qty,
|
||||
unit_cost: {
|
||||
amount: args.unit_cost,
|
||||
code: args.currency_code || invoice.currency_code,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_mark_invoice_unpaid',
|
||||
description: 'Mark an invoice as unpaid (reopen it)',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { v3_status: 'unpaid' } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} marked unpaid` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_invoice_payment',
|
||||
description: 'Get payment details for an invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { payments: Payment[] } } }>(
|
||||
'/payments/payments',
|
||||
{ invoiceid: args.invoice_id }
|
||||
);
|
||||
return response.response.result.payments || [];
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_payment',
|
||||
description: 'Create a payment record for an invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
amount: z.string().describe('Payment amount'),
|
||||
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
|
||||
type: z.string().default('Cash').describe('Payment method'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoice_id,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
type: args.type,
|
||||
note: args.note,
|
||||
},
|
||||
const lines = [...invoice.lines, newLine];
|
||||
const result = await client.updateInvoice(args.invoice_id, { lines });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_search_invoices',
|
||||
description: 'Search for invoices by various criteria',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getInvoices({ search: args.query });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,110 +1,191 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Item } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const itemsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_items',
|
||||
description: 'List all items (products/services)',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ items: Item[] }>(
|
||||
'/items/items',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
description: 'List all items/services with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getItems(args);
|
||||
return {
|
||||
items: response.response.result.items || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_item',
|
||||
description: 'Get a single item by ID',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { item: Item } } }>(
|
||||
`/items/items/${args.item_id}`
|
||||
);
|
||||
return response.response.result.item;
|
||||
description: 'Get detailed information about a specific item/service',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
item_id: {
|
||||
type: 'number',
|
||||
description: 'Item ID',
|
||||
},
|
||||
},
|
||||
required: ['item_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getItem(args.item_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_item',
|
||||
description: 'Create a new item (product or service)',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().optional().describe('Quantity on hand'),
|
||||
inventory: z.number().optional(),
|
||||
unit_cost: z.string().optional().describe('Unit cost'),
|
||||
tax1: z.number().optional().describe('Tax 1 ID'),
|
||||
tax2: z.number().optional().describe('Tax 2 ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const itemData: any = { item: { ...args } };
|
||||
if (itemData.item.unit_cost) {
|
||||
itemData.item.unit_cost = { amount: itemData.item.unit_cost, code: 'USD' };
|
||||
}
|
||||
|
||||
const response = await client.post<{ response: { result: { item: Item } } }>(
|
||||
'/items/items',
|
||||
itemData
|
||||
);
|
||||
return response.response.result.item;
|
||||
description: 'Create a new item/service in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Item description',
|
||||
},
|
||||
quantity: {
|
||||
type: 'string',
|
||||
description: 'Quantity',
|
||||
},
|
||||
inventory: {
|
||||
type: 'string',
|
||||
description: 'Inventory count',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
sku: {
|
||||
type: 'string',
|
||||
description: 'SKU',
|
||||
},
|
||||
},
|
||||
required: ['name', 'unit_cost'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const item = {
|
||||
name: args.name,
|
||||
description: args.description || '',
|
||||
quantity: args.quantity || '1',
|
||||
inventory: args.inventory || '0',
|
||||
unit_cost: {
|
||||
amount: args.unit_cost,
|
||||
code: args.currency_code || 'USD',
|
||||
},
|
||||
sku: args.sku || '',
|
||||
};
|
||||
const result = await client.createItem(item);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_item',
|
||||
description: 'Update an existing item',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().optional(),
|
||||
inventory: z.number().optional(),
|
||||
unit_cost: z.string().optional(),
|
||||
tax1: z.number().optional(),
|
||||
tax2: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { item_id, ...updateFields } = args;
|
||||
if (updateFields.unit_cost) {
|
||||
updateFields.unit_cost = { amount: updateFields.unit_cost, code: 'USD' };
|
||||
description: 'Update an existing item/service',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
item_id: {
|
||||
type: 'number',
|
||||
description: 'Item ID to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Item description',
|
||||
},
|
||||
unit_cost: {
|
||||
type: 'string',
|
||||
description: 'Unit cost amount',
|
||||
},
|
||||
inventory: {
|
||||
type: 'string',
|
||||
description: 'Inventory count',
|
||||
},
|
||||
},
|
||||
required: ['item_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { item_id, unit_cost, ...updates } = args;
|
||||
if (unit_cost) {
|
||||
const item = await client.getItem(item_id);
|
||||
updates.unit_cost = {
|
||||
amount: unit_cost,
|
||||
code: item.unit_cost.code,
|
||||
};
|
||||
}
|
||||
|
||||
const itemData = { item: updateFields };
|
||||
const response = await client.put<{ response: { result: { item: Item } } }>(
|
||||
`/items/items/${item_id}`,
|
||||
itemData
|
||||
);
|
||||
return response.response.result.item;
|
||||
const result = await client.updateItem(item_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_item',
|
||||
description: 'Delete an item',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/items/items/${args.item_id}`,
|
||||
{ item: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Item ${args.item_id} deleted` };
|
||||
description: 'Delete an item/service from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
item_id: {
|
||||
type: 'number',
|
||||
description: 'Item ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['item_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteItem(args.item_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Item ${args.item_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
129
servers/freshbooks/src/tools/journal-entries-tools.ts
Normal file
129
servers/freshbooks/src/tools/journal-entries-tools.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const journalEntriesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_journal_entries',
|
||||
description: 'List all journal entries with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getJournalEntries(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_journal_entry',
|
||||
description: 'Get detailed information about a specific journal entry',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
journal_entry_id: {
|
||||
type: 'number',
|
||||
description: 'Journal entry ID',
|
||||
},
|
||||
},
|
||||
required: ['journal_entry_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getJournalEntry(args.journal_entry_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_journal_entry',
|
||||
description: 'Create a new journal entry in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Journal entry name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Journal entry description',
|
||||
},
|
||||
user_entered_date: {
|
||||
type: 'string',
|
||||
description: 'Entry date (YYYY-MM-DD)',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
details: {
|
||||
type: 'array',
|
||||
description: 'Journal entry details (debits and credits)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sub_accountid: {
|
||||
type: 'number',
|
||||
description: 'Sub-account ID',
|
||||
},
|
||||
debit_amount: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Debit amount (or null)',
|
||||
},
|
||||
},
|
||||
},
|
||||
credit_amount: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Credit amount (or null)',
|
||||
},
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line description',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['name', 'user_entered_date', 'details'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createJournalEntry(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,121 +1,187 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Payment } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const paymentsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_payments',
|
||||
description: 'List all payments with optional filtering',
|
||||
inputSchema: z.object({
|
||||
invoiceid: z.number().optional().describe('Filter by invoice ID'),
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.invoiceid) params.invoiceid = args.invoiceid;
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ payments: Payment[] }>(
|
||||
'/payments/payments',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all payments with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getPayments(args);
|
||||
return {
|
||||
payments: response.response.result.payments || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_payment',
|
||||
description: 'Get a single payment by ID',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { payment: Payment } } }>(
|
||||
`/payments/payments/${args.payment_id}`
|
||||
);
|
||||
return response.response.result.payment;
|
||||
description: 'Get detailed information about a specific payment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment_id: {
|
||||
type: 'number',
|
||||
description: 'Payment ID',
|
||||
},
|
||||
},
|
||||
required: ['payment_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getPayment(args.payment_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_payment',
|
||||
description: 'Create a new payment',
|
||||
inputSchema: z.object({
|
||||
invoiceid: z.number().describe('Invoice ID'),
|
||||
amount: z.string().describe('Payment amount'),
|
||||
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
|
||||
type: z.string().default('Cash').describe('Payment method'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoiceid,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
type: args.type,
|
||||
note: args.note,
|
||||
description: 'Record a new payment in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceid: {
|
||||
type: 'number',
|
||||
description: 'Invoice ID this payment is for',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Payment amount',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Payment date (YYYY-MM-DD)',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Payment type (e.g., Cash, Check, Credit Card)',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Payment note',
|
||||
},
|
||||
gateway: {
|
||||
type: 'string',
|
||||
description: 'Payment gateway name',
|
||||
},
|
||||
},
|
||||
required: ['invoiceid', 'amount', 'date', 'type'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const payment = {
|
||||
invoiceid: args.invoiceid,
|
||||
amount: {
|
||||
amount: args.amount,
|
||||
code: args.currency_code || 'USD',
|
||||
},
|
||||
date: args.date,
|
||||
type: args.type,
|
||||
note: args.note,
|
||||
gateway: args.gateway,
|
||||
};
|
||||
const result = await client.createPayment(payment);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_payment',
|
||||
description: 'Update an existing payment',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
amount: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { payment_id, ...updateFields } = args;
|
||||
if (updateFields.amount) {
|
||||
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment_id: {
|
||||
type: 'number',
|
||||
description: 'Payment ID to update',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Payment amount',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Payment date',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Payment note',
|
||||
},
|
||||
},
|
||||
required: ['payment_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { payment_id, amount, ...updates } = args;
|
||||
if (amount) {
|
||||
const payment = await client.getPayment(payment_id);
|
||||
updates.amount = {
|
||||
amount,
|
||||
code: payment.amount.code,
|
||||
};
|
||||
}
|
||||
|
||||
const paymentData = { payment: updateFields };
|
||||
const response = await client.put<{ response: { result: { payment: Payment } } }>(
|
||||
`/payments/payments/${payment_id}`,
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
const result = await client.updatePayment(payment_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_payment',
|
||||
description: 'Delete a payment',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/payments/payments/${args.payment_id}`,
|
||||
{ payment: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Payment ${args.payment_id} deleted` };
|
||||
description: 'Delete a payment from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
payment_id: {
|
||||
type: 'number',
|
||||
description: 'Payment ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['payment_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deletePayment(args.payment_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Payment ${args.payment_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,124 +1,214 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Project, ProjectService } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const projectsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_projects',
|
||||
description: 'List all projects with optional filtering',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().optional().describe('Filter by client ID'),
|
||||
active: z.boolean().optional().describe('Filter by active status'),
|
||||
complete: z.boolean().optional().describe('Filter by completion status'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.client_id !== undefined) params.client_id = args.client_id;
|
||||
if (args.active !== undefined) params.active = args.active;
|
||||
if (args.complete !== undefined) params.complete = args.complete;
|
||||
|
||||
const response = await client.getPaginated<{ projects: Project[] }>(
|
||||
'/projects/business/123/projects',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all projects with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getProjects(args);
|
||||
return {
|
||||
projects: response.response.result.projects || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_project',
|
||||
description: 'Get a single project by ID',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { project: Project } } }>(
|
||||
`/projects/business/123/projects/${args.project_id}`
|
||||
);
|
||||
return response.response.result.project;
|
||||
description: 'Get detailed information about a specific project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID',
|
||||
},
|
||||
},
|
||||
required: ['project_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getProject(args.project_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_project',
|
||||
description: 'Create a new project',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('Project title'),
|
||||
description: z.string().optional(),
|
||||
client_id: z.number().optional().describe('Associated client ID'),
|
||||
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
||||
project_type: z.enum(['fixed_price', 'hourly_rate']).default('hourly_rate'),
|
||||
fixed_price: z.string().optional().describe('Fixed price amount'),
|
||||
billing_method: z.enum(['project_rate', 'service_rate', 'team_member_rate']).optional(),
|
||||
rate: z.string().optional().describe('Hourly rate'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const projectData = { project: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { project: Project } } }>(
|
||||
'/projects/business/123/projects',
|
||||
projectData
|
||||
);
|
||||
return response.response.result.project;
|
||||
description: 'Create a new project in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Project title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Project description',
|
||||
},
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
due_date: {
|
||||
type: 'string',
|
||||
description: 'Project due date (YYYY-MM-DD)',
|
||||
},
|
||||
billing_method: {
|
||||
type: 'string',
|
||||
description: 'Billing method (project_rate, service_rate, task_rate, fixed_price)',
|
||||
},
|
||||
project_type: {
|
||||
type: 'string',
|
||||
description: 'Project type (fixed_price, hourly_rate)',
|
||||
},
|
||||
budget: {
|
||||
type: 'number',
|
||||
description: 'Project budget',
|
||||
},
|
||||
fixed_price: {
|
||||
type: 'number',
|
||||
description: 'Fixed price for the project',
|
||||
},
|
||||
rate: {
|
||||
type: 'number',
|
||||
description: 'Hourly rate',
|
||||
},
|
||||
internal: {
|
||||
type: 'boolean',
|
||||
description: 'Is this an internal project?',
|
||||
},
|
||||
},
|
||||
required: ['title', 'client_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createProject(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_project',
|
||||
description: 'Update an existing project',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
client_id: z.number().optional(),
|
||||
due_date: z.string().optional(),
|
||||
active: z.boolean().optional(),
|
||||
complete: z.boolean().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { project_id, ...updateFields } = args;
|
||||
const projectData = { project: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { project: Project } } }>(
|
||||
`/projects/business/123/projects/${project_id}`,
|
||||
projectData
|
||||
);
|
||||
return response.response.result.project;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID to update',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Project title',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Project description',
|
||||
},
|
||||
due_date: {
|
||||
type: 'string',
|
||||
description: 'Project due date',
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Is the project active?',
|
||||
},
|
||||
complete: {
|
||||
type: 'boolean',
|
||||
description: 'Is the project complete?',
|
||||
},
|
||||
},
|
||||
required: ['project_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { project_id, ...updates } = args;
|
||||
const result = await client.updateProject(project_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_project',
|
||||
description: 'Delete a project',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.delete(
|
||||
`/projects/business/123/projects/${args.project_id}`
|
||||
);
|
||||
return { success: true, message: `Project ${args.project_id} deleted` };
|
||||
description: 'Delete a project from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['project_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteProject(args.project_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Project ${args.project_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_project_services',
|
||||
description: 'List all available services for time tracking',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { services: ProjectService[] } } }>(
|
||||
'/projects/business/123/services'
|
||||
);
|
||||
return response.response.result.services || [];
|
||||
name: 'freshbooks_mark_project_complete',
|
||||
description: 'Mark a project as complete',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID',
|
||||
},
|
||||
},
|
||||
required: ['project_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.updateProject(args.project_id, { complete: true });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
import type { RecurringProfile } from '../types/index.js';
|
||||
|
||||
export const recurringTools = [
|
||||
@ -11,21 +11,21 @@ export const recurringTools = [
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
handler: async (args: any, client: FreshBooksAPIClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
|
||||
const response = await client.getPaginated<{ recurring: RecurringProfile[] }>(
|
||||
const response = await client.getPaginated<any>(
|
||||
'/invoices/recurring',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
recurring: response.response.result.recurring || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
recurring: response.result?.recurring || response.recurring || [],
|
||||
page: response.page || args.page,
|
||||
pages: response.pages || 1,
|
||||
total: response.total || 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
@ -36,11 +36,11 @@ export const recurringTools = [
|
||||
inputSchema: z.object({
|
||||
recurring_id: z.number().describe('Recurring profile ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
handler: async (args: any, client: FreshBooksAPIClient) => {
|
||||
const response = await client.get<any>(
|
||||
`/invoices/recurring/${args.recurring_id}`
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
return response.result?.recurring || response.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
@ -61,7 +61,7 @@ export const recurringTools = [
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
handler: async (args: any, client: FreshBooksAPIClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
@ -80,11 +80,11 @@ export const recurringTools = [
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
const response = await client.post<any>(
|
||||
'/invoices/recurring',
|
||||
recurringData
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
return response.result?.recurring || response.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
@ -104,7 +104,7 @@ export const recurringTools = [
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
handler: async (args: any, client: FreshBooksAPIClient) => {
|
||||
const { recurring_id, ...updateFields } = args;
|
||||
|
||||
if (updateFields.lines) {
|
||||
@ -115,11 +115,11 @@ export const recurringTools = [
|
||||
}
|
||||
|
||||
const recurringData = { recurring: updateFields };
|
||||
const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
const response = await client.put<any>(
|
||||
`/invoices/recurring/${recurring_id}`,
|
||||
recurringData
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
return response.result?.recurring || response.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
@ -129,7 +129,7 @@ export const recurringTools = [
|
||||
inputSchema: z.object({
|
||||
recurring_id: z.number().describe('Recurring profile ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
handler: async (args: any, client: FreshBooksAPIClient) => {
|
||||
await client.put(
|
||||
`/invoices/recurring/${args.recurring_id}`,
|
||||
{ recurring: { vis_state: 1 } }
|
||||
|
||||
@ -1,112 +1,110 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const reportsTools = [
|
||||
{
|
||||
name: 'freshbooks_profit_loss_report',
|
||||
description: 'Generate profit and loss report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: ProfitLossReport } }>(
|
||||
'/reports/accounting/profitloss',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result;
|
||||
description: 'Generate a profit and loss report for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (YYYY-MM-DD)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (YYYY-MM-DD)',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getProfitLossReport(args.start_date, args.end_date);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_tax_summary_report',
|
||||
description: 'Generate tax summary report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { taxsummaries: TaxSummary[] } } }>(
|
||||
'/reports/accounting/taxsummary',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result.taxsummaries || [];
|
||||
description: 'Generate a tax summary report for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (YYYY-MM-DD)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (YYYY-MM-DD)',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getTaxSummaryReport(args.start_date, args.end_date);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_accounts_aging_report',
|
||||
description: 'Generate accounts aging report (accounts receivable)',
|
||||
inputSchema: z.object({
|
||||
date: z.string().optional().describe('Report date (YYYY-MM-DD, defaults to today)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { clients: AccountsAgingReport[] } } }>(
|
||||
'/reports/accounting/aging',
|
||||
{
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result.clients || [];
|
||||
name: 'freshbooks_aging_report',
|
||||
description: 'Generate an accounts aging report showing outstanding balances',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getAgingReport();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_expense_report',
|
||||
description: 'Generate expense report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
categoryid: z.number().optional().describe('Filter by category ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: any = {
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.categoryid) params.categoryid = args.categoryid;
|
||||
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/reports/accounting/expenses',
|
||||
params
|
||||
);
|
||||
return response.response.result;
|
||||
description: 'Generate an expense report for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (YYYY-MM-DD)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (YYYY-MM-DD)',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_revenue_by_client_report',
|
||||
description: 'Generate revenue by client report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/reports/accounting/revenue_by_client',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result;
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getExpenseReport(args.start_date, args.end_date);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
161
servers/freshbooks/src/tools/retainers-tools.ts
Normal file
161
servers/freshbooks/src/tools/retainers-tools.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const retainersTools = [
|
||||
{
|
||||
name: 'freshbooks_list_retainers',
|
||||
description: 'List all retainers with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getRetainers(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_retainer',
|
||||
description: 'Get detailed information about a specific retainer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
retainer_id: {
|
||||
type: 'number',
|
||||
description: 'Retainer ID',
|
||||
},
|
||||
},
|
||||
required: ['retainer_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getRetainer(args.retainer_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_retainer',
|
||||
description: 'Create a new retainer in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
fee: {
|
||||
type: 'string',
|
||||
description: 'Retainer fee amount',
|
||||
},
|
||||
period: {
|
||||
type: 'string',
|
||||
description: 'Retainer period (monthly, quarterly, yearly)',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (YYYY-MM-DD)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (YYYY-MM-DD, optional)',
|
||||
},
|
||||
},
|
||||
required: ['client_id', 'fee', 'period', 'start_date'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createRetainer(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_update_retainer',
|
||||
description: 'Update an existing retainer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
retainer_id: {
|
||||
type: 'number',
|
||||
description: 'Retainer ID to update',
|
||||
},
|
||||
fee: {
|
||||
type: 'string',
|
||||
description: 'Retainer fee amount',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date',
|
||||
},
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Is the retainer active?',
|
||||
},
|
||||
},
|
||||
required: ['retainer_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { retainer_id, ...updates } = args;
|
||||
const result = await client.updateRetainer(retainer_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_delete_retainer',
|
||||
description: 'Delete a retainer from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
retainer_id: {
|
||||
type: 'number',
|
||||
description: 'Retainer ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['retainer_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteRetainer(args.retainer_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Retainer ${args.retainer_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
57
servers/freshbooks/src/tools/staff-tools.ts
Normal file
57
servers/freshbooks/src/tools/staff-tools.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const staffTools = [
|
||||
{
|
||||
name: 'freshbooks_list_staff',
|
||||
description: 'List all staff members with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getStaff(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_staff_member',
|
||||
description: 'Get detailed information about a specific staff member',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
staff_id: {
|
||||
type: 'number',
|
||||
description: 'Staff member ID',
|
||||
},
|
||||
},
|
||||
required: ['staff_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getStaffMember(args.staff_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,96 +1,148 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Tax } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const taxesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_taxes',
|
||||
description: 'List all taxes',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ taxes: Tax[] }>(
|
||||
'/taxes/taxes',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
description: 'List all taxes in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getTaxes();
|
||||
return {
|
||||
taxes: response.response.result.taxes || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_tax',
|
||||
description: 'Get a single tax by ID',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { tax: Tax } } }>(
|
||||
`/taxes/taxes/${args.tax_id}`
|
||||
);
|
||||
return response.response.result.tax;
|
||||
description: 'Get detailed information about a specific tax',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tax_id: {
|
||||
type: 'number',
|
||||
description: 'Tax ID',
|
||||
},
|
||||
},
|
||||
required: ['tax_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getTax(args.tax_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_tax',
|
||||
description: 'Create a new tax',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Tax name (e.g., "GST", "VAT")'),
|
||||
number: z.string().optional().describe('Tax number/registration'),
|
||||
amount: z.string().describe('Tax percentage (e.g., "13" for 13%)'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const taxData = { tax: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { tax: Tax } } }>(
|
||||
'/taxes/taxes',
|
||||
taxData
|
||||
);
|
||||
return response.response.result.tax;
|
||||
description: 'Create a new tax in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Tax name (e.g., GST, VAT, Sales Tax)',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Tax rate as a percentage (e.g., 5, 13.5)',
|
||||
},
|
||||
number: {
|
||||
type: 'string',
|
||||
description: 'Tax number/ID (optional)',
|
||||
},
|
||||
compound: {
|
||||
type: 'boolean',
|
||||
description: 'Is this a compound tax?',
|
||||
},
|
||||
},
|
||||
required: ['name', 'amount'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createTax(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_tax',
|
||||
description: 'Update an existing tax',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
name: z.string().optional(),
|
||||
number: z.string().optional(),
|
||||
amount: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { tax_id, ...updateFields } = args;
|
||||
const taxData = { tax: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { tax: Tax } } }>(
|
||||
`/taxes/taxes/${tax_id}`,
|
||||
taxData
|
||||
);
|
||||
return response.response.result.tax;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tax_id: {
|
||||
type: 'number',
|
||||
description: 'Tax ID to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Tax name',
|
||||
},
|
||||
amount: {
|
||||
type: 'string',
|
||||
description: 'Tax rate as a percentage',
|
||||
},
|
||||
number: {
|
||||
type: 'string',
|
||||
description: 'Tax number/ID',
|
||||
},
|
||||
},
|
||||
required: ['tax_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { tax_id, ...updates } = args;
|
||||
const result = await client.updateTax(tax_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_tax',
|
||||
description: 'Delete a tax',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/taxes/taxes/${args.tax_id}`,
|
||||
{ tax: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Tax ${args.tax_id} deleted` };
|
||||
description: 'Delete a tax from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tax_id: {
|
||||
type: 'number',
|
||||
description: 'Tax ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['tax_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteTax(args.tax_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Tax ${args.tax_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,125 +1,223 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { TimeEntry } from '../types/index.js';
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const timeEntriesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_time_entries',
|
||||
description: 'List all time entries with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
projectid: z.number().optional().describe('Filter by project ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
billed_status: z.enum(['billed', 'unbilled']).optional(),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.client_id = args.clientid;
|
||||
if (args.projectid) params.project_id = args.projectid;
|
||||
if (args.date_min) params.started_from = args.date_min;
|
||||
if (args.date_max) params.started_to = args.date_max;
|
||||
if (args.billed_status) params.billed_status = args.billed_status;
|
||||
|
||||
const response = await client.getPaginated<{ time_entries: TimeEntry[] }>(
|
||||
'/timetracking/business/123/time_entries',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
description: 'List all time entries with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getTimeEntries(args);
|
||||
return {
|
||||
time_entries: response.response.result.time_entries || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_time_entry',
|
||||
description: 'Get a single time entry by ID',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
`/timetracking/business/123/time_entries/${args.time_entry_id}`
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
description: 'Get detailed information about a specific time entry',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
time_entry_id: {
|
||||
type: 'number',
|
||||
description: 'Time entry ID',
|
||||
},
|
||||
},
|
||||
required: ['time_entry_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getTimeEntry(args.time_entry_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_time_entry',
|
||||
description: 'Create a new time entry',
|
||||
inputSchema: z.object({
|
||||
duration: z.number().describe('Duration in seconds'),
|
||||
note: z.string().optional().describe('Note/description'),
|
||||
started_at: z.string().describe('Start time (ISO 8601 format)'),
|
||||
clientid: z.number().optional().describe('Client ID'),
|
||||
projectid: z.number().optional().describe('Project ID'),
|
||||
service_id: z.number().optional().describe('Service ID'),
|
||||
is_logged: z.boolean().default(true),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const timeEntryData = {
|
||||
time_entry: {
|
||||
is_logged: args.is_logged,
|
||||
duration: args.duration,
|
||||
note: args.note,
|
||||
started_at: args.started_at,
|
||||
client_id: args.clientid,
|
||||
project_id: args.projectid,
|
||||
service_id: args.service_id,
|
||||
description: 'Create a new time entry in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID',
|
||||
},
|
||||
client_id: {
|
||||
type: 'number',
|
||||
description: 'Client ID',
|
||||
},
|
||||
duration: {
|
||||
type: 'number',
|
||||
description: 'Duration in seconds',
|
||||
},
|
||||
started_at: {
|
||||
type: 'string',
|
||||
description: 'Start time (ISO 8601)',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Note about the time entry',
|
||||
},
|
||||
billable: {
|
||||
type: 'boolean',
|
||||
description: 'Is this time billable?',
|
||||
},
|
||||
internal: {
|
||||
type: 'boolean',
|
||||
description: 'Is this an internal time entry?',
|
||||
},
|
||||
},
|
||||
required: ['project_id', 'duration', 'started_at'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createTimeEntry(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
'/timetracking/business/123/time_entries',
|
||||
timeEntryData
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_time_entry',
|
||||
description: 'Update an existing time entry',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
duration: z.number().optional(),
|
||||
note: z.string().optional(),
|
||||
started_at: z.string().optional(),
|
||||
clientid: z.number().optional(),
|
||||
projectid: z.number().optional(),
|
||||
service_id: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { time_entry_id, ...updateFields } = args;
|
||||
const timeEntryData = { time_entry: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
`/timetracking/business/123/time_entries/${time_entry_id}`,
|
||||
timeEntryData
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
time_entry_id: {
|
||||
type: 'number',
|
||||
description: 'Time entry ID to update',
|
||||
},
|
||||
duration: {
|
||||
type: 'number',
|
||||
description: 'Duration in seconds',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Note about the time entry',
|
||||
},
|
||||
billable: {
|
||||
type: 'boolean',
|
||||
description: 'Is this time billable?',
|
||||
},
|
||||
},
|
||||
required: ['time_entry_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { time_entry_id, ...updates } = args;
|
||||
const result = await client.updateTimeEntry(time_entry_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_time_entry',
|
||||
description: 'Delete a time entry',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.delete(
|
||||
`/timetracking/business/123/time_entries/${args.time_entry_id}`
|
||||
);
|
||||
return { success: true, message: `Time entry ${args.time_entry_id} deleted` };
|
||||
description: 'Delete a time entry from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
time_entry_id: {
|
||||
type: 'number',
|
||||
description: 'Time entry ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['time_entry_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteTimeEntry(args.time_entry_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Time entry ${args.time_entry_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_start_timer',
|
||||
description: 'Start a new timer for a project',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
project_id: {
|
||||
type: 'number',
|
||||
description: 'Project ID',
|
||||
},
|
||||
note: {
|
||||
type: 'string',
|
||||
description: 'Note about what you are working on',
|
||||
},
|
||||
},
|
||||
required: ['project_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.startTimer(args.project_id, args.note);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_stop_timer',
|
||||
description: 'Stop a running timer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
time_entry_id: {
|
||||
type: 'number',
|
||||
description: 'Time entry ID of the running timer',
|
||||
},
|
||||
},
|
||||
required: ['time_entry_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.stopTimer(args.time_entry_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
201
servers/freshbooks/src/tools/vendors-tools.ts
Normal file
201
servers/freshbooks/src/tools/vendors-tools.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import type { FreshBooksAPIClient } from '../clients/freshbooks.js';
|
||||
|
||||
export const vendorsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_vendors',
|
||||
description: 'List all bill vendors with pagination',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number for pagination',
|
||||
},
|
||||
per_page: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getVendors(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_get_vendor',
|
||||
description: 'Get detailed information about a specific vendor',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendor_id: {
|
||||
type: 'number',
|
||||
description: 'Vendor ID',
|
||||
},
|
||||
},
|
||||
required: ['vendor_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.getVendor(args.vendor_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_create_vendor',
|
||||
description: 'Create a new vendor in FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendor_name: {
|
||||
type: 'string',
|
||||
description: 'Vendor name',
|
||||
},
|
||||
primary_contact_first_name: {
|
||||
type: 'string',
|
||||
description: 'Primary contact first name',
|
||||
},
|
||||
primary_contact_last_name: {
|
||||
type: 'string',
|
||||
description: 'Primary contact last name',
|
||||
},
|
||||
primary_contact_email: {
|
||||
type: 'string',
|
||||
description: 'Primary contact email',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Phone number',
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
description: 'Website URL',
|
||||
},
|
||||
street: {
|
||||
type: 'string',
|
||||
description: 'Street address',
|
||||
},
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City',
|
||||
},
|
||||
province: {
|
||||
type: 'string',
|
||||
description: 'Province/state',
|
||||
},
|
||||
postal_code: {
|
||||
type: 'string',
|
||||
description: 'Postal code',
|
||||
},
|
||||
country: {
|
||||
type: 'string',
|
||||
description: 'Country',
|
||||
},
|
||||
currency_code: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
is_1099: {
|
||||
type: 'boolean',
|
||||
description: 'Is this vendor subject to 1099 reporting?',
|
||||
},
|
||||
},
|
||||
required: ['vendor_name', 'primary_contact_email'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const result = await client.createVendor(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_update_vendor',
|
||||
description: 'Update an existing vendor',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendor_id: {
|
||||
type: 'number',
|
||||
description: 'Vendor ID to update',
|
||||
},
|
||||
vendor_name: {
|
||||
type: 'string',
|
||||
description: 'Vendor name',
|
||||
},
|
||||
primary_contact_email: {
|
||||
type: 'string',
|
||||
description: 'Primary contact email',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Phone number',
|
||||
},
|
||||
street: {
|
||||
type: 'string',
|
||||
description: 'Street address',
|
||||
},
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City',
|
||||
},
|
||||
},
|
||||
required: ['vendor_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
const { vendor_id, ...updates } = args;
|
||||
const result = await client.updateVendor(vendor_id, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'freshbooks_delete_vendor',
|
||||
description: 'Delete a vendor from FreshBooks',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendor_id: {
|
||||
type: 'number',
|
||||
description: 'Vendor ID to delete',
|
||||
},
|
||||
},
|
||||
required: ['vendor_id'],
|
||||
},
|
||||
handler: async (client: FreshBooksAPIClient, args: any) => {
|
||||
await client.deleteVendor(args.vendor_id);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Vendor ${args.vendor_id} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -2,71 +2,82 @@
|
||||
|
||||
export interface FreshBooksConfig {
|
||||
accountId: string;
|
||||
bearerToken: string;
|
||||
baseUrl?: string;
|
||||
accessToken: string;
|
||||
apiBaseUrl?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
response: {
|
||||
result: T;
|
||||
page: number;
|
||||
pages: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FreshBooksError {
|
||||
message: string;
|
||||
code?: string;
|
||||
errors?: Array<{ field: string; message: string }>;
|
||||
}
|
||||
|
||||
// Client Types
|
||||
export interface Client {
|
||||
export interface FreshBooksClient {
|
||||
id: number;
|
||||
accounting_systemid: string;
|
||||
organization: string;
|
||||
fname: string;
|
||||
lname: string;
|
||||
email: string;
|
||||
company_industry?: string;
|
||||
company_size?: string;
|
||||
bill_street?: string;
|
||||
bill_city?: string;
|
||||
bill_state?: string;
|
||||
bill_country?: string;
|
||||
bill_postal_code?: string;
|
||||
username: string;
|
||||
home_phone: string | null;
|
||||
business_phone: string | null;
|
||||
mobile_phone: string | null;
|
||||
fax: string | null;
|
||||
company_industry: string | null;
|
||||
company_size: string | null;
|
||||
vat_name: string | null;
|
||||
vat_number: string | null;
|
||||
s_province: string;
|
||||
s_city: string;
|
||||
s_street: string;
|
||||
s_street2: string;
|
||||
s_code: string;
|
||||
s_country: string;
|
||||
p_province: string;
|
||||
p_city: string;
|
||||
p_street: string;
|
||||
p_street2: string;
|
||||
p_code: string;
|
||||
p_country: string;
|
||||
currency_code: string;
|
||||
language: string;
|
||||
note: string | null;
|
||||
pref_email: boolean;
|
||||
pref_gmail: boolean;
|
||||
allow_late_fees: boolean;
|
||||
allow_late_notifications: boolean;
|
||||
role: string;
|
||||
vis_state: number;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
language?: string;
|
||||
note?: string;
|
||||
vat_name?: string;
|
||||
vat_number?: string;
|
||||
allow_late_fees?: boolean;
|
||||
allow_late_notifications?: boolean;
|
||||
}
|
||||
|
||||
export interface ClientContact {
|
||||
id: number;
|
||||
clientid: number;
|
||||
fname: string;
|
||||
lname: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
}
|
||||
|
||||
// Invoice Types
|
||||
export interface Invoice {
|
||||
export interface FreshBooksInvoice {
|
||||
id: number;
|
||||
accountid: string;
|
||||
accounting_systemid: string;
|
||||
clientid: number;
|
||||
create_date: string;
|
||||
invoiceid: number;
|
||||
invoice_number: string;
|
||||
customerid: number;
|
||||
create_date: string;
|
||||
generation_date: string | null;
|
||||
discount_value: string;
|
||||
discount_description: string | null;
|
||||
po_number: string | null;
|
||||
template: string;
|
||||
currency_code: string;
|
||||
language: string;
|
||||
terms: string | null;
|
||||
notes: string | null;
|
||||
address: string;
|
||||
return_uri: string | null;
|
||||
deposit_amount: string | null;
|
||||
deposit_percentage: string | null;
|
||||
deposit_status: string;
|
||||
payment_status: string;
|
||||
auto_bill: boolean;
|
||||
v3_status: string;
|
||||
date_paid: string | null;
|
||||
estimateid: number;
|
||||
basecampid: number;
|
||||
sentid: number;
|
||||
status: number;
|
||||
parent: number;
|
||||
fname: string;
|
||||
lname: string;
|
||||
organization: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
@ -79,31 +90,21 @@ export interface Invoice {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
due_date: string;
|
||||
status: string;
|
||||
payment_status: string;
|
||||
v3_status: string;
|
||||
due_offset_days: number;
|
||||
lines: InvoiceLine[];
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
discount_total?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
updated: string;
|
||||
created_at: string;
|
||||
presentation: InvoicePresentation;
|
||||
}
|
||||
|
||||
export interface InvoiceLine {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: number;
|
||||
unit_cost: {
|
||||
lineid?: number;
|
||||
amount?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
amount: {
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: string;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
@ -111,219 +112,496 @@ export interface InvoiceLine {
|
||||
taxAmount1?: string;
|
||||
taxName2?: string;
|
||||
taxAmount2?: string;
|
||||
type?: number;
|
||||
expenseid?: number;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
export interface InvoicePresentation {
|
||||
theme_primary_color: string;
|
||||
theme_layout: string;
|
||||
theme_font_name: string;
|
||||
image_logo_src: string | null;
|
||||
image_banner_src: string | null;
|
||||
}
|
||||
|
||||
export interface FreshBooksEstimate {
|
||||
id: number;
|
||||
invoiceid: number;
|
||||
accountid: string;
|
||||
estimateid: number;
|
||||
estimate_number: string;
|
||||
customerid: number;
|
||||
accepted: boolean;
|
||||
create_date: string;
|
||||
discount_value: string;
|
||||
discount_description: string | null;
|
||||
po_number: string | null;
|
||||
template: string;
|
||||
currency_code: string;
|
||||
language: string;
|
||||
terms: string | null;
|
||||
notes: string | null;
|
||||
address: string;
|
||||
status: number;
|
||||
fname: string;
|
||||
lname: string;
|
||||
organization: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
date: string;
|
||||
type: string;
|
||||
note?: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
lines: EstimateLine[];
|
||||
ui_status: string;
|
||||
}
|
||||
|
||||
// Expense Types
|
||||
export interface Expense {
|
||||
id: number;
|
||||
category_id: number;
|
||||
clientid?: number;
|
||||
projectid?: number;
|
||||
vendor: string;
|
||||
amount: {
|
||||
export interface EstimateLine {
|
||||
lineid?: number;
|
||||
amount?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: string;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
date: string;
|
||||
notes?: string;
|
||||
taxName1?: string;
|
||||
taxAmount1?: number;
|
||||
taxPercent1?: number;
|
||||
taxAmount1?: string;
|
||||
taxName2?: string;
|
||||
taxAmount2?: string;
|
||||
type?: number;
|
||||
}
|
||||
|
||||
export interface FreshBooksExpense {
|
||||
id: number;
|
||||
accountid: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
vendor: string;
|
||||
date: string;
|
||||
categoryid: number;
|
||||
clientid: number;
|
||||
projectid: number;
|
||||
staffid: number;
|
||||
notes: string | null;
|
||||
taxName1: string;
|
||||
taxAmount1: number;
|
||||
taxName2: string;
|
||||
taxAmount2: number;
|
||||
status: number;
|
||||
is_cogs: boolean;
|
||||
from_bulk_import: boolean;
|
||||
attachment: ExpenseAttachment | null;
|
||||
markup_percent: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
staffid?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface ExpenseAttachment {
|
||||
id: number;
|
||||
jwt: string;
|
||||
media_type: string;
|
||||
}
|
||||
|
||||
export interface ExpenseCategory {
|
||||
id: number;
|
||||
category: string;
|
||||
is_cogs?: boolean;
|
||||
is_editable?: boolean;
|
||||
parentid?: number;
|
||||
categoryid: number;
|
||||
created_at: string;
|
||||
is_cogs: boolean;
|
||||
is_editable: boolean;
|
||||
parentid: number | null;
|
||||
updated_at: string;
|
||||
vis_state: number;
|
||||
}
|
||||
|
||||
// Estimate Types
|
||||
export interface Estimate {
|
||||
export interface FreshBooksPayment {
|
||||
id: number;
|
||||
accountid: string;
|
||||
clientid: number;
|
||||
create_date: string;
|
||||
estimate_number: string;
|
||||
currency_code: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
status: string;
|
||||
lines: EstimateLine[];
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
discount_total?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
bulk_paymentid: number;
|
||||
clientid: number;
|
||||
creditid: number | null;
|
||||
date: string;
|
||||
from_credit: boolean;
|
||||
gateway: string | null;
|
||||
invoiceid: number;
|
||||
logid: number;
|
||||
note: string | null;
|
||||
orderid: string | null;
|
||||
overpaymentid: number;
|
||||
transactionid: string | null;
|
||||
type: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
vis_state: number;
|
||||
}
|
||||
|
||||
export interface EstimateLine {
|
||||
id?: number;
|
||||
export interface FreshBooksProject {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
due_date: string | null;
|
||||
client_id: number;
|
||||
internal: boolean;
|
||||
budget: number | null;
|
||||
fixed_price: number | null;
|
||||
rate: number | null;
|
||||
billing_method: string;
|
||||
project_type: string;
|
||||
active: boolean;
|
||||
complete: boolean;
|
||||
sample: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
logged_duration: number;
|
||||
services: ProjectService[];
|
||||
billed_amount: string;
|
||||
billed_status: string;
|
||||
retainer_id: number | null;
|
||||
}
|
||||
|
||||
export interface ProjectService {
|
||||
business_id: number;
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: number;
|
||||
billable: boolean;
|
||||
vis_state: number;
|
||||
}
|
||||
|
||||
export interface FreshBooksTimeEntry {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
is_logged: boolean;
|
||||
started_at: string;
|
||||
created_at: string;
|
||||
client_id: number;
|
||||
project_id: number;
|
||||
pending_client: string | null;
|
||||
pending_project: string | null;
|
||||
pending_task: string | null;
|
||||
task_id: number | null;
|
||||
service_id: number | null;
|
||||
note: string | null;
|
||||
active: boolean;
|
||||
billable: boolean;
|
||||
billed: boolean;
|
||||
internal: boolean;
|
||||
retainer_id: number | null;
|
||||
duration: number;
|
||||
timer: Timer | null;
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
id: number;
|
||||
is_running: boolean;
|
||||
started_at: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface FreshBooksTax {
|
||||
id: number;
|
||||
accounting_systemid: string;
|
||||
name: string;
|
||||
number: string | null;
|
||||
amount: string;
|
||||
compound: boolean;
|
||||
updated: string;
|
||||
}
|
||||
|
||||
export interface FreshBooksItem {
|
||||
id: number;
|
||||
accountid: string;
|
||||
itemid: number;
|
||||
name: string;
|
||||
description: string;
|
||||
quantity: string;
|
||||
inventory: string;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
tax1: number;
|
||||
tax2: number;
|
||||
updated: string;
|
||||
vis_state: number;
|
||||
sku: string;
|
||||
}
|
||||
|
||||
export interface FreshBooksStaff {
|
||||
id: number;
|
||||
identity_id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
company: string;
|
||||
business_id: number;
|
||||
active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rate: {
|
||||
amount: string;
|
||||
code: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface FreshBooksBill {
|
||||
id: number;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Time Entry Types
|
||||
export interface TimeEntry {
|
||||
id: number;
|
||||
is_logged?: boolean;
|
||||
duration: number;
|
||||
note?: string;
|
||||
started_at: string;
|
||||
clientid?: number;
|
||||
projectid?: number;
|
||||
service_id?: number;
|
||||
billed_status?: string;
|
||||
attachment: ExpenseAttachment | null;
|
||||
bill_number: string | null;
|
||||
bill_payments: BillPayment[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Project Types
|
||||
export interface Project {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
client_id?: number;
|
||||
due_date?: string;
|
||||
project_type?: string;
|
||||
fixed_price?: string;
|
||||
billing_method?: string;
|
||||
rate?: string;
|
||||
active?: boolean;
|
||||
complete?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProjectService {
|
||||
id: number;
|
||||
business_id: number;
|
||||
name: string;
|
||||
billable: boolean;
|
||||
rate?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Item Types
|
||||
export interface Item {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty?: number;
|
||||
inventory?: number;
|
||||
unit_cost?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
tax1?: number;
|
||||
tax2?: number;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Tax Types
|
||||
export interface Tax {
|
||||
id: number;
|
||||
name: string;
|
||||
number?: string;
|
||||
amount: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Recurring Profile Types
|
||||
export interface RecurringProfile {
|
||||
id: number;
|
||||
clientid: number;
|
||||
frequency: string;
|
||||
numberRecurring: number;
|
||||
create_date: string;
|
||||
currency_code: string;
|
||||
lines: InvoiceLine[];
|
||||
status?: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Account Types
|
||||
export interface Account {
|
||||
id: string;
|
||||
account_name: string;
|
||||
email: string;
|
||||
business_phone?: string;
|
||||
address?: {
|
||||
street: string;
|
||||
city: string;
|
||||
province: string;
|
||||
country: string;
|
||||
postal_code: string;
|
||||
due_date: string;
|
||||
due_offset_days: number;
|
||||
issue_date: string;
|
||||
language: string;
|
||||
lines: BillLine[];
|
||||
outstanding: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
overall_category: string;
|
||||
overall_description: string;
|
||||
paid: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
status: string;
|
||||
tax_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
total_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
updated_at: string;
|
||||
vendor_id: number;
|
||||
vis_state: number;
|
||||
}
|
||||
|
||||
export interface StaffMember {
|
||||
export interface BillLine {
|
||||
id: number;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
role?: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
category_id: number;
|
||||
description: string;
|
||||
list_index: number;
|
||||
quantity: string;
|
||||
tax_amount1: string | null;
|
||||
tax_amount2: string | null;
|
||||
tax_authorityid1: number | null;
|
||||
tax_authorityid2: number | null;
|
||||
tax_name1: string | null;
|
||||
tax_name2: string | null;
|
||||
tax_percent1: string | null;
|
||||
tax_percent2: string | null;
|
||||
total_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BillPayment {
|
||||
id: number;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
bill_id: number;
|
||||
matched_with_expense: boolean;
|
||||
note: string | null;
|
||||
paid_date: string;
|
||||
payment_type: string;
|
||||
vis_state: number;
|
||||
}
|
||||
|
||||
export interface BillVendor {
|
||||
id: number;
|
||||
account_number: string | null;
|
||||
city: string;
|
||||
country: string;
|
||||
currency_code: string;
|
||||
is_1099: boolean;
|
||||
language: string;
|
||||
outstanding_balance: {
|
||||
amount: string;
|
||||
code: string;
|
||||
}[];
|
||||
overdue_balance: {
|
||||
amount: string;
|
||||
code: string;
|
||||
}[];
|
||||
phone: string | null;
|
||||
postal_code: string;
|
||||
primary_contact_email: string;
|
||||
primary_contact_first_name: string;
|
||||
primary_contact_last_name: string;
|
||||
province: string;
|
||||
street: string;
|
||||
street2: string | null;
|
||||
tax_defaults: TaxDefault[];
|
||||
vendor_name: string;
|
||||
vis_state: number;
|
||||
website: string | null;
|
||||
}
|
||||
|
||||
export interface TaxDefault {
|
||||
systemid: number;
|
||||
taxid: number;
|
||||
}
|
||||
|
||||
export interface AccountingAccount {
|
||||
id: number;
|
||||
account_name: string;
|
||||
account_number: string;
|
||||
account_type: string;
|
||||
balance: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
currency_code: string;
|
||||
custom: boolean;
|
||||
parentid: number | null;
|
||||
sub_accounts: AccountingAccount[];
|
||||
}
|
||||
|
||||
export interface JournalEntry {
|
||||
id: number;
|
||||
created_at: string;
|
||||
currency_code: string;
|
||||
description: string;
|
||||
details: JournalEntryDetail[];
|
||||
name: string;
|
||||
user_entered_date: string;
|
||||
}
|
||||
|
||||
export interface JournalEntryDetail {
|
||||
id: number;
|
||||
credit_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
} | null;
|
||||
currency_code: string;
|
||||
debit_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
} | null;
|
||||
description: string | null;
|
||||
name: string;
|
||||
sub_accountid: number;
|
||||
user_entered_date: string;
|
||||
}
|
||||
|
||||
export interface Retainer {
|
||||
id: number;
|
||||
active: boolean;
|
||||
business_id: number;
|
||||
client_id: number;
|
||||
created_at: string;
|
||||
end_date: string | null;
|
||||
fee: string;
|
||||
period: string;
|
||||
start_date: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface CreditNote {
|
||||
id: number;
|
||||
accounting_systemid: string;
|
||||
clientid: number;
|
||||
creditid: number;
|
||||
credit_number: string;
|
||||
credit_type: string;
|
||||
currency_code: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
balance: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
create_date: string;
|
||||
language: string;
|
||||
notes: string | null;
|
||||
terms: string | null;
|
||||
status: string;
|
||||
lines: CreditNoteLine[];
|
||||
}
|
||||
|
||||
export interface CreditNoteLine {
|
||||
lineid: number;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
name: string;
|
||||
description: string;
|
||||
qty: string;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
taxName1: string;
|
||||
taxAmount1: string;
|
||||
taxName2: string;
|
||||
taxAmount2: string;
|
||||
}
|
||||
|
||||
// Report Types
|
||||
export interface ProfitLossReport {
|
||||
total_income: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
total_expenses: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
currency_code: string;
|
||||
income: ReportCategory[];
|
||||
expenses: ReportCategory[];
|
||||
net_profit: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
export interface TaxSummary {
|
||||
export interface ReportCategory {
|
||||
category_name: string;
|
||||
total: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
children: ReportCategory[];
|
||||
}
|
||||
|
||||
export interface TaxSummaryReport {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
currency_code: string;
|
||||
taxes: TaxSummaryItem[];
|
||||
total_tax: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TaxSummaryItem {
|
||||
tax_name: string;
|
||||
taxable_amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
tax_collected: {
|
||||
amount: string;
|
||||
code: string;
|
||||
@ -332,18 +610,115 @@ export interface TaxSummary {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountsAgingReport {
|
||||
client_userid: number;
|
||||
organization: string;
|
||||
outstanding_balance: {
|
||||
net_tax: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
current: { amount: string; code: string };
|
||||
'1-30': { amount: string; code: string };
|
||||
'31-60': { amount: string; code: string };
|
||||
'61-90': { amount: string; code: string };
|
||||
'91+': { amount: string; code: string };
|
||||
}
|
||||
|
||||
export interface AgingReport {
|
||||
currency_code: string;
|
||||
current: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_1_30: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_31_60: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_61_90: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_over_90: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
total: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
clients: AgingReportClient[];
|
||||
}
|
||||
|
||||
export interface AgingReportClient {
|
||||
client_id: number;
|
||||
client_name: string;
|
||||
organization: string;
|
||||
current: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_1_30: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_31_60: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_61_90: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
days_over_90: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
total: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExpenseReport {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
currency_code: string;
|
||||
categories: ExpenseReportCategory[];
|
||||
total: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExpenseReportCategory {
|
||||
category_name: string;
|
||||
total: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
expenses: FreshBooksExpense[];
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
page: number;
|
||||
pages: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
export interface FreshBooksError {
|
||||
message: string;
|
||||
error_type?: string;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export interface RecurringProfile {
|
||||
id: number;
|
||||
recurring_id: number;
|
||||
clientid: number;
|
||||
frequency: string;
|
||||
numberRecurring: number;
|
||||
create_date: string;
|
||||
currency_code: string;
|
||||
lines: InvoiceLine[];
|
||||
notes?: string;
|
||||
terms?: string;
|
||||
vis_state?: number;
|
||||
}
|
||||
|
||||
62
servers/freshbooks/src/ui/react-app/aging-report/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/aging-report/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function AgingReport() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_aging_report', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading outstanding balance...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Outstanding Balance</h1>
|
||||
<p className="subtitle">Accounts receivable aging</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Outstanding Balance - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
87
servers/freshbooks/src/ui/react-app/aging-report/styles.css
Normal file
87
servers/freshbooks/src/ui/react-app/aging-report/styles.css
Normal file
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/bill-manager/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/bill-manager/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function BillManager() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_bills', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading bill manager...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Bill Manager</h1>
|
||||
<p className="subtitle">Manage vendor bills</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
servers/freshbooks/src/ui/react-app/bill-manager/index.html
Normal file
12
servers/freshbooks/src/ui/react-app/bill-manager/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bill Manager - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
87
servers/freshbooks/src/ui/react-app/bill-manager/styles.css
Normal file
87
servers/freshbooks/src/ui/react-app/bill-manager/styles.css
Normal file
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
37
servers/freshbooks/src/ui/react-app/build-all.js
vendored
Normal file
37
servers/freshbooks/src/ui/react-app/build-all.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
import { build } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
|
||||
const appsDir = resolve(process.cwd(), 'src/apps');
|
||||
const apps = readdirSync(appsDir).filter(file => {
|
||||
const fullPath = resolve(appsDir, file);
|
||||
return statSync(fullPath).isDirectory();
|
||||
});
|
||||
|
||||
console.log(`Building ${apps.length} apps...`);
|
||||
|
||||
for (const app of apps) {
|
||||
console.log(`Building ${app}...`);
|
||||
|
||||
await build({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: resolve(process.cwd(), 'dist'),
|
||||
rollupOptions: {
|
||||
input: resolve(appsDir, app, 'index.html'),
|
||||
output: {
|
||||
entryFileNames: `${app}.js`,
|
||||
assetFileNames: `${app}.[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(process.cwd(), 'src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Build complete!');
|
||||
62
servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ClientDashboard() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_clients', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading client directory...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Client Directory</h1>
|
||||
<p className="subtitle">Manage all your clients</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,99 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Clients Dashboard - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.search-bar { margin-bottom: 1.5rem; }
|
||||
.search-bar input { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
|
||||
.client-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
|
||||
.client-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
|
||||
.client-card h3 { margin-bottom: 0.5rem; }
|
||||
.client-card .email { color: #64748b; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.client-meta { display: flex; justify-content: space-between; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
|
||||
.client-meta div { text-align: center; }
|
||||
.client-meta .label { color: #64748b; font-size: 0.75rem; margin-bottom: 0.25rem; }
|
||||
.client-meta .value { font-weight: bold; font-size: 1.125rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function ClientDashboard() {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const sampleClients = [
|
||||
{ id: 1, name: 'Acme Corp', email: 'contact@acme.com', invoices: 12, outstanding: 5600, lastInvoice: '2024-01-15' },
|
||||
{ id: 2, name: 'Tech Solutions', email: 'hello@tech.com', invoices: 8, outstanding: 3200, lastInvoice: '2024-01-20' },
|
||||
{ id: 3, name: 'Design Co', email: 'info@design.co', invoices: 15, outstanding: 0, lastInvoice: '2024-01-10' },
|
||||
{ id: 4, name: 'Marketing Inc', email: 'team@marketing.com', invoices: 6, outstanding: 1800, lastInvoice: '2024-01-25' },
|
||||
];
|
||||
setClients(sampleClients);
|
||||
}, []);
|
||||
|
||||
const filteredClients = clients.filter(client =>
|
||||
client.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
client.email.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div>
|
||||
<h1>Clients</h1>
|
||||
<p style={{ color: '#94a3b8' }}>{clients.length} active clients</p>
|
||||
</div>
|
||||
<button className="btn">+ Add Client</button>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<input type="text" placeholder="Search clients..." value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="client-grid">
|
||||
{filteredClients.map(client => (
|
||||
<div key={client.id} className="client-card">
|
||||
<h3>{client.name}</h3>
|
||||
<div className="email">{client.email}</div>
|
||||
<div className="client-meta">
|
||||
<div>
|
||||
<div className="label">Invoices</div>
|
||||
<div className="value">{client.invoices}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="label">Outstanding</div>
|
||||
<div className="value" style={{ color: client.outstanding > 0 ? '#fbbf24' : '#6ee7b7' }}>
|
||||
${client.outstanding.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="label">Last Invoice</div>
|
||||
<div className="value" style={{ fontSize: '0.875rem' }}>{client.lastInvoice}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ClientDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Client Directory - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #8b5cf6;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #8b5cf6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/client-detail/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/client-detail/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ClientDetail() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_get_client', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading client detail...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Client Detail</h1>
|
||||
<p className="subtitle">View detailed client information</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Client Detail - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
87
servers/freshbooks/src/ui/react-app/client-detail/styles.css
Normal file
87
servers/freshbooks/src/ui/react-app/client-detail/styles.css
Normal file
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #8b5cf6;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #8b5cf6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function CreditNoteViewer() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_credit_notes', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading credit note viewer...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Credit Note Viewer</h1>
|
||||
<p className="subtitle">View and manage credit notes</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Credit Note Viewer - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #06b6d4;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #06b6d4;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #06b6d4;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function EstimateBuilder() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_estimates', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading estimate builder...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Estimate Builder</h1>
|
||||
<p className="subtitle">Create and send estimates</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Estimate Builder - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #6366f1;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ExpenseCategories() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_expense_categories', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading expense categories...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Expense Categories</h1>
|
||||
<p className="subtitle">Manage expense categories</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Expense Categories - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ExpenseTracker() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_expenses', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading expense tracker...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Expense Tracker</h1>
|
||||
<p className="subtitle">Track and categorize all expenses</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,134 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Expense Tracker - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
|
||||
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
|
||||
.form-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.form-group.full { grid-column: 1 / -1; }
|
||||
label { font-size: 0.875rem; color: #94a3b8; }
|
||||
input, select, textarea { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.expense-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
.expense-item { padding: 1rem; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
|
||||
.expense-item:last-child { border-bottom: none; }
|
||||
.expense-info .category { color: #64748b; font-size: 0.875rem; }
|
||||
.expense-amount { font-weight: bold; font-size: 1.125rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function ExpenseTracker() {
|
||||
const [expenses, setExpenses] = useState([
|
||||
{ id: 1, vendor: 'Office Supplies Co', category: 'Office', amount: 245, date: '2024-01-15' },
|
||||
{ id: 2, vendor: 'Tech Store', category: 'Equipment', amount: 1200, date: '2024-01-18' },
|
||||
{ id: 3, vendor: 'Coffee Shop', category: 'Meals', amount: 45, date: '2024-01-20' },
|
||||
]);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '',
|
||||
});
|
||||
|
||||
const totalExpenses = expenses.reduce((sum, exp) => sum + exp.amount, 0);
|
||||
const thisMonth = expenses.filter(e => e.date.startsWith('2024-01')).reduce((sum, exp) => sum + exp.amount, 0);
|
||||
|
||||
const addExpense = () => {
|
||||
if (form.vendor && form.amount) {
|
||||
setExpenses([...expenses, { ...form, id: Date.now(), amount: parseFloat(form.amount) }]);
|
||||
setForm({ vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Expense Tracker</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Track and categorize business expenses</p>
|
||||
</div>
|
||||
|
||||
<div className="stats">
|
||||
<div className="stat-card">
|
||||
<div className="value">${totalExpenses.toLocaleString()}</div>
|
||||
<div className="label">Total Expenses</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">${thisMonth.toLocaleString()}</div>
|
||||
<div className="label">This Month</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">{expenses.length}</div>
|
||||
<div className="label">Transactions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-card">
|
||||
<h3 style={{ marginBottom: '1rem' }}>Add Expense</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>Vendor</label>
|
||||
<input type="text" value={form.vendor} onChange={(e) => setForm({ ...form, vendor: e.target.value })} placeholder="Vendor name" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Category</label>
|
||||
<select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })}>
|
||||
<option value="Office">Office Supplies</option>
|
||||
<option value="Equipment">Equipment</option>
|
||||
<option value="Meals">Meals & Entertainment</option>
|
||||
<option value="Travel">Travel</option>
|
||||
<option value="Software">Software</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Amount</label>
|
||||
<input type="number" value={form.amount} onChange={(e) => setForm({ ...form, amount: e.target.value })} placeholder="0.00" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group full">
|
||||
<label>Notes</label>
|
||||
<textarea rows="2" value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} placeholder="Optional notes" />
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn" style={{ marginTop: '1rem' }} onClick={addExpense}>Add Expense</button>
|
||||
</div>
|
||||
|
||||
<div className="expense-list">
|
||||
{expenses.map(expense => (
|
||||
<div key={expense.id} className="expense-item">
|
||||
<div className="expense-info">
|
||||
<div>{expense.vendor}</div>
|
||||
<div className="category">{expense.category} • {expense.date}</div>
|
||||
</div>
|
||||
<div className="expense-amount">${expense.amount.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ExpenseTracker />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Expense Tracker - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
372
servers/freshbooks/src/ui/react-app/generate-apps.js
vendored
Normal file
372
servers/freshbooks/src/ui/react-app/generate-apps.js
vendored
Normal file
@ -0,0 +1,372 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const apps = [
|
||||
{
|
||||
name: 'invoice-dashboard',
|
||||
title: 'Invoice Dashboard',
|
||||
desc: 'Manage and track all your invoices',
|
||||
tool: 'freshbooks_list_invoices',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'invoice-detail',
|
||||
title: 'Invoice Detail',
|
||||
desc: 'View detailed invoice information',
|
||||
tool: 'freshbooks_get_invoice',
|
||||
color: '#3b82f6'
|
||||
},
|
||||
{
|
||||
name: 'client-dashboard',
|
||||
title: 'Client Directory',
|
||||
desc: 'Manage all your clients',
|
||||
tool: 'freshbooks_list_clients',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
name: 'client-detail',
|
||||
title: 'Client Detail',
|
||||
desc: 'View detailed client information',
|
||||
tool: 'freshbooks_get_client',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
name: 'expense-tracker',
|
||||
title: 'Expense Tracker',
|
||||
desc: 'Track and categorize all expenses',
|
||||
tool: 'freshbooks_list_expenses',
|
||||
color: '#ef4444'
|
||||
},
|
||||
{
|
||||
name: 'expense-categories',
|
||||
title: 'Expense Categories',
|
||||
desc: 'Manage expense categories',
|
||||
tool: 'freshbooks_list_expense_categories',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
name: 'time-entries',
|
||||
title: 'Time Entry Log',
|
||||
desc: 'Log and track time entries',
|
||||
tool: 'freshbooks_list_time_entries',
|
||||
color: '#06b6d4'
|
||||
},
|
||||
{
|
||||
name: 'time-tracker',
|
||||
title: 'Project Timer',
|
||||
desc: 'Start and stop project timers',
|
||||
tool: 'freshbooks_list_time_entries',
|
||||
color: '#06b6d4'
|
||||
},
|
||||
{
|
||||
name: 'project-dashboard',
|
||||
title: 'Project Overview',
|
||||
desc: 'View all active projects',
|
||||
tool: 'freshbooks_list_projects',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'project-detail',
|
||||
title: 'Project Detail',
|
||||
desc: 'Detailed project information',
|
||||
tool: 'freshbooks_get_project',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'payment-history',
|
||||
title: 'Payment History',
|
||||
desc: 'Track all payments',
|
||||
tool: 'freshbooks_list_payments',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'estimate-builder',
|
||||
title: 'Estimate Builder',
|
||||
desc: 'Create and send estimates',
|
||||
tool: 'freshbooks_list_estimates',
|
||||
color: '#6366f1'
|
||||
},
|
||||
{
|
||||
name: 'tax-summary',
|
||||
title: 'Tax Summary',
|
||||
desc: 'View tax reporting and summaries',
|
||||
tool: 'freshbooks_tax_summary_report',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
name: 'recurring-invoices',
|
||||
title: 'Recurring Templates',
|
||||
desc: 'Manage recurring invoice templates',
|
||||
tool: 'freshbooks_list_invoices',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
name: 'profit-loss',
|
||||
title: 'Profit & Loss',
|
||||
desc: 'Financial P&L statements',
|
||||
tool: 'freshbooks_profit_loss_report',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'revenue-chart',
|
||||
title: 'Revenue Chart',
|
||||
desc: 'Visual revenue analytics',
|
||||
tool: 'freshbooks_list_invoices',
|
||||
color: '#10b981'
|
||||
},
|
||||
{
|
||||
name: 'aging-report',
|
||||
title: 'Outstanding Balance',
|
||||
desc: 'Accounts receivable aging',
|
||||
tool: 'freshbooks_aging_report',
|
||||
color: '#ef4444'
|
||||
},
|
||||
{
|
||||
name: 'bill-manager',
|
||||
title: 'Bill Manager',
|
||||
desc: 'Manage vendor bills',
|
||||
tool: 'freshbooks_list_bills',
|
||||
color: '#f59e0b'
|
||||
},
|
||||
{
|
||||
name: 'credit-note-viewer',
|
||||
title: 'Credit Note Viewer',
|
||||
desc: 'View and manage credit notes',
|
||||
tool: 'freshbooks_list_credit_notes',
|
||||
color: '#06b6d4'
|
||||
},
|
||||
{
|
||||
name: 'reports-dashboard',
|
||||
title: 'Report Builder',
|
||||
desc: 'Build custom financial reports',
|
||||
tool: 'freshbooks_profit_loss_report',
|
||||
color: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
name: 'invoice-builder',
|
||||
title: 'Invoice Builder',
|
||||
desc: 'Create new invoices',
|
||||
tool: 'freshbooks_create_invoice',
|
||||
color: '#10b981'
|
||||
},
|
||||
];
|
||||
|
||||
const createApp = (app) => {
|
||||
const appDir = path.join(__dirname, app.name);
|
||||
|
||||
if (!fs.existsSync(appDir)) {
|
||||
fs.mkdirSync(appDir, { recursive: true });
|
||||
}
|
||||
|
||||
// App.tsx
|
||||
const appTsx = `import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ${app.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('${app.tool}', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading ${app.title.toLowerCase()}...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>${app.title}</h1>
|
||||
<p className="subtitle">${app.desc}</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
// styles.css
|
||||
const stylesCss = `* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: ${app.color};
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid ${app.color};
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: ${app.color};
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`;
|
||||
|
||||
// main.tsx
|
||||
const mainTsx = `import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
`;
|
||||
|
||||
// index.html
|
||||
const indexHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>${app.title} - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// vite.config.ts
|
||||
const viteConfig = `import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
// Write files
|
||||
fs.writeFileSync(path.join(appDir, 'App.tsx'), appTsx);
|
||||
fs.writeFileSync(path.join(appDir, 'styles.css'), stylesCss);
|
||||
fs.writeFileSync(path.join(appDir, 'main.tsx'), mainTsx);
|
||||
fs.writeFileSync(path.join(appDir, 'index.html'), indexHtml);
|
||||
fs.writeFileSync(path.join(appDir, 'vite.config.ts'), viteConfig);
|
||||
|
||||
console.log(`✅ Created ${app.name}`);
|
||||
};
|
||||
|
||||
console.log('🚀 Generating FreshBooks MCP Apps...\n');
|
||||
apps.forEach(createApp);
|
||||
console.log(`\n✨ Generated ${apps.length} apps successfully!`);
|
||||
62
servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function InvoiceBuilder() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_create_invoice', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading invoice builder...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Invoice Builder</h1>
|
||||
<p className="subtitle">Create new invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,139 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Builder - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.form-grid { display: grid; gap: 1.5rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
label { font-size: 0.875rem; color: #94a3b8; font-weight: 500; }
|
||||
input, select, textarea { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
|
||||
input:focus, select:focus, textarea:focus { outline: none; border-color: #3b82f6; }
|
||||
.line-items { margin-top: 2rem; }
|
||||
.line-item { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 40px; gap: 1rem; margin-bottom: 1rem; align-items: end; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 500; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.btn-secondary { background: #334155; }
|
||||
.btn-secondary:hover { background: #475569; }
|
||||
.btn-danger { background: #ef4444; padding: 0.5rem; }
|
||||
.btn-danger:hover { background: #dc2626; }
|
||||
.total-section { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-top: 2rem; }
|
||||
.total-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
|
||||
.total-row.final { font-size: 1.25rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function InvoiceBuilder() {
|
||||
const [invoice, setInvoice] = useState({
|
||||
client: '',
|
||||
invoiceNumber: 'INV-' + Date.now(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
dueDate: '',
|
||||
lines: [{ description: '', qty: 1, rate: 0 }],
|
||||
notes: '',
|
||||
});
|
||||
|
||||
const addLine = () => {
|
||||
setInvoice({ ...invoice, lines: [...invoice.lines, { description: '', qty: 1, rate: 0 }] });
|
||||
};
|
||||
|
||||
const removeLine = (index) => {
|
||||
setInvoice({ ...invoice, lines: invoice.lines.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const updateLine = (index, field, value) => {
|
||||
const newLines = [...invoice.lines];
|
||||
newLines[index] = { ...newLines[index], [field]: value };
|
||||
setInvoice({ ...invoice, lines: newLines });
|
||||
};
|
||||
|
||||
const subtotal = invoice.lines.reduce((sum, line) => sum + (line.qty * line.rate), 0);
|
||||
const tax = subtotal * 0.13; // 13% tax
|
||||
const total = subtotal + tax;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Create Invoice</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Build and send professional invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>Client</label>
|
||||
<select value={invoice.client} onChange={(e) => setInvoice({ ...invoice, client: e.target.value })}>
|
||||
<option value="">Select client...</option>
|
||||
<option value="acme">Acme Corp</option>
|
||||
<option value="tech">Tech Solutions</option>
|
||||
<option value="design">Design Co</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem' }}>
|
||||
<div className="form-group">
|
||||
<label>Invoice #</label>
|
||||
<input type="text" value={invoice.invoiceNumber} onChange={(e) => setInvoice({ ...invoice, invoiceNumber: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" value={invoice.date} onChange={(e) => setInvoice({ ...invoice, date: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Due Date</label>
|
||||
<input type="date" value={invoice.dueDate} onChange={(e) => setInvoice({ ...invoice, dueDate: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="line-items">
|
||||
<label>Line Items</label>
|
||||
{invoice.lines.map((line, index) => (
|
||||
<div key={index} className="line-item">
|
||||
<input type="text" placeholder="Description" value={line.description} onChange={(e) => updateLine(index, 'description', e.target.value)} />
|
||||
<input type="number" placeholder="Qty" value={line.qty} onChange={(e) => updateLine(index, 'qty', parseFloat(e.target.value))} />
|
||||
<input type="number" placeholder="Rate" value={line.rate} onChange={(e) => updateLine(index, 'rate', parseFloat(e.target.value))} />
|
||||
<div style={{ color: '#94a3b8', paddingTop: '0.75rem' }}>${(line.qty * line.rate).toFixed(2)}</div>
|
||||
{invoice.lines.length > 1 && (
|
||||
<button className="btn btn-danger" onClick={() => removeLine(index)}>×</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button className="btn btn-secondary" onClick={addLine}>+ Add Line</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea rows="3" value={invoice.notes} onChange={(e) => setInvoice({ ...invoice, notes: e.target.value })} placeholder="Payment terms, thank you note, etc." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="total-section">
|
||||
<div className="total-row"><span>Subtotal:</span><span>${subtotal.toFixed(2)}</span></div>
|
||||
<div className="total-row"><span>Tax (13%):</span><span>${tax.toFixed(2)}</span></div>
|
||||
<div className="total-row final"><span>Total:</span><span>${total.toFixed(2)}</span></div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn">Save Draft</button>
|
||||
<button className="btn" style={{ background: '#10b981' }}>Send Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceBuilder />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Invoice Builder - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
194
servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx
Normal file
194
servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
interface Invoice {
|
||||
id: number;
|
||||
number: string;
|
||||
client: string;
|
||||
amount: number;
|
||||
status: 'paid' | 'partial' | 'overdue' | 'draft' | 'sent';
|
||||
date: string;
|
||||
dueDate: string;
|
||||
}
|
||||
|
||||
export default function InvoiceDashboard() {
|
||||
const [invoices, setInvoices] = useState<Invoice[]>([]);
|
||||
const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0, outstanding: 0 });
|
||||
const [filter, setFilter] = useState({ status: 'all', client: '' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadInvoices();
|
||||
}, []);
|
||||
|
||||
const loadInvoices = async () => {
|
||||
try {
|
||||
// Call MCP tool to fetch invoices
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_invoices', {
|
||||
page: 1,
|
||||
per_page: 50
|
||||
});
|
||||
|
||||
if (response?.invoices) {
|
||||
setInvoices(response.invoices);
|
||||
calculateStats(response.invoices);
|
||||
} else {
|
||||
// Fallback to sample data
|
||||
loadSampleData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading invoices:', error);
|
||||
loadSampleData();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSampleData = () => {
|
||||
const sampleInvoices: Invoice[] = [
|
||||
{ id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' },
|
||||
{ id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' },
|
||||
{ id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' },
|
||||
{ id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' },
|
||||
{ id: 5, number: 'INV-005', client: 'Startup Labs', amount: 5200, status: 'sent', date: '2024-02-01', dueDate: '2024-03-01' },
|
||||
{ id: 6, number: 'INV-006', client: 'Enterprise Co', amount: 8900, status: 'paid', date: '2024-02-05', dueDate: '2024-03-05' },
|
||||
];
|
||||
setInvoices(sampleInvoices);
|
||||
calculateStats(sampleInvoices);
|
||||
};
|
||||
|
||||
const calculateStats = (invoices: Invoice[]) => {
|
||||
const totalRevenue = invoices.reduce((sum, inv) => sum + inv.amount, 0);
|
||||
const paidAmount = invoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0);
|
||||
const overdueAmount = invoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0);
|
||||
const draftCount = invoices.filter(i => i.status === 'draft').length;
|
||||
const outstandingAmount = invoices.filter(i => i.status === 'sent' || i.status === 'partial').reduce((sum, inv) => sum + inv.amount, 0);
|
||||
|
||||
setStats({
|
||||
total: totalRevenue,
|
||||
paid: paidAmount,
|
||||
overdue: overdueAmount,
|
||||
draft: draftCount,
|
||||
outstanding: outstandingAmount
|
||||
});
|
||||
};
|
||||
|
||||
const filteredInvoices = invoices.filter(inv => {
|
||||
if (filter.status !== 'all' && inv.status !== filter.status) return false;
|
||||
if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleViewInvoice = async (invoiceId: number) => {
|
||||
try {
|
||||
await (window as any).mcp?.callTool('freshbooks_get_invoice', { invoice_id: invoiceId });
|
||||
} catch (error) {
|
||||
console.error('Error viewing invoice:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading invoices...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Invoice Dashboard</h1>
|
||||
<p className="subtitle">Manage and track all your invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<h3>Total Revenue</h3>
|
||||
<div className="value">${stats.total.toLocaleString()}</div>
|
||||
<div className="label">All invoices</div>
|
||||
</div>
|
||||
<div className="stat-card green">
|
||||
<h3>Paid</h3>
|
||||
<div className="value">${stats.paid.toLocaleString()}</div>
|
||||
<div className="label">Received</div>
|
||||
</div>
|
||||
<div className="stat-card blue">
|
||||
<h3>Outstanding</h3>
|
||||
<div className="value">${stats.outstanding.toLocaleString()}</div>
|
||||
<div className="label">Awaiting payment</div>
|
||||
</div>
|
||||
<div className="stat-card red">
|
||||
<h3>Overdue</h3>
|
||||
<div className="value">${stats.overdue.toLocaleString()}</div>
|
||||
<div className="label">Past due</div>
|
||||
</div>
|
||||
<div className="stat-card yellow">
|
||||
<h3>Drafts</h3>
|
||||
<div className="value">{stats.draft}</div>
|
||||
<div className="label">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filters">
|
||||
<div className="filter-group">
|
||||
<label>Status</label>
|
||||
<select value={filter.status} onChange={(e) => setFilter({ ...filter, status: e.target.value })}>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Client</label>
|
||||
<input type="text" placeholder="Search client..." value={filter.client} onChange={(e) => setFilter({ ...filter, client: e.target.value })} />
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<button className="btn" onClick={loadInvoices}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Client</th>
|
||||
<th>Date</th>
|
||||
<th>Due Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredInvoices.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} style={{ textAlign: 'center', padding: '2rem', color: '#94a3b8' }}>
|
||||
No invoices found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredInvoices.map(invoice => (
|
||||
<tr key={invoice.id}>
|
||||
<td className="font-medium">{invoice.number}</td>
|
||||
<td>{invoice.client}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.dueDate}</td>
|
||||
<td className="amount">${invoice.amount.toLocaleString()}</td>
|
||||
<td><span className={`status status-${invoice.status}`}>{invoice.status}</span></td>
|
||||
<td>
|
||||
<button className="btn-small" onClick={() => handleViewInvoice(invoice.id)}>View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,154 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Dashboard - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
|
||||
.stat-card h3 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; margin-bottom: 0.5rem; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; color: #e2e8f0; }
|
||||
.stat-card .label { color: #64748b; font-size: 0.875rem; margin-top: 0.25rem; }
|
||||
.filters { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
||||
.filter-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.filter-group label { font-size: 0.875rem; color: #94a3b8; }
|
||||
.filter-group select, .filter-group input { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.5rem; border-radius: 4px; }
|
||||
.table-container { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #0f172a; text-align: left; padding: 1rem; font-weight: 600; color: #94a3b8; text-transform: uppercase; font-size: 0.75rem; }
|
||||
td { padding: 1rem; border-top: 1px solid #334155; }
|
||||
.status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
||||
.status-paid { background: #065f46; color: #6ee7b7; }
|
||||
.status-partial { background: #78350f; color: #fbbf24; }
|
||||
.status-overdue { background: #7f1d1d; color: #fca5a5; }
|
||||
.status-draft { background: #1e3a8a; color: #93c5fd; }
|
||||
.amount { font-weight: 600; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function InvoiceDashboard() {
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0 });
|
||||
const [filter, setFilter] = useState({ status: 'all', client: '' });
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate data fetch
|
||||
const sampleInvoices = [
|
||||
{ id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' },
|
||||
{ id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' },
|
||||
{ id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' },
|
||||
{ id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' },
|
||||
];
|
||||
setInvoices(sampleInvoices);
|
||||
setStats({
|
||||
total: sampleInvoices.reduce((sum, inv) => sum + inv.amount, 0),
|
||||
paid: sampleInvoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0),
|
||||
overdue: sampleInvoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0),
|
||||
draft: sampleInvoices.filter(i => i.status === 'draft').length,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const filteredInvoices = invoices.filter(inv => {
|
||||
if (filter.status !== 'all' && inv.status !== filter.status) return false;
|
||||
if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Invoice Dashboard</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Manage and track all your invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<h3>Total Revenue</h3>
|
||||
<div className="value">${stats.total.toLocaleString()}</div>
|
||||
<div className="label">All invoices</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#10b981' }}>
|
||||
<h3>Paid</h3>
|
||||
<div className="value">${stats.paid.toLocaleString()}</div>
|
||||
<div className="label">Received</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#ef4444' }}>
|
||||
<h3>Overdue</h3>
|
||||
<div className="value">${stats.overdue.toLocaleString()}</div>
|
||||
<div className="label">Past due</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#f59e0b' }}>
|
||||
<h3>Drafts</h3>
|
||||
<div className="value">{stats.draft}</div>
|
||||
<div className="label">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filters">
|
||||
<div className="filter-group">
|
||||
<label>Status</label>
|
||||
<select value={filter.status} onChange={(e) => setFilter({ ...filter, status: e.target.value })}>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Client</label>
|
||||
<input type="text" placeholder="Search client..." value={filter.client} onChange={(e) => setFilter({ ...filter, client: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Client</th>
|
||||
<th>Date</th>
|
||||
<th>Due Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredInvoices.map(invoice => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.number}</td>
|
||||
<td>{invoice.client}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.dueDate}</td>
|
||||
<td className="amount">${invoice.amount.toLocaleString()}</td>
|
||||
<td><span className={`status status-${invoice.status}`}>{invoice.status}</span></td>
|
||||
<td><button className="btn">View</button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Invoice Dashboard - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function InvoiceDetail() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_get_invoice', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading invoice detail...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Invoice Detail</h1>
|
||||
<p className="subtitle">View detailed invoice information</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,116 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Detail - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
|
||||
.invoice-header { background: #1e293b; padding: 2rem; border-radius: 8px; margin-bottom: 2rem; }
|
||||
.invoice-header h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.status-badge { padding: 0.5rem 1rem; border-radius: 12px; display: inline-block; background: #10b981; color: white; }
|
||||
.invoice-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem; }
|
||||
.invoice-body { background: #1e293b; padding: 2rem; border-radius: 8px; }
|
||||
table { width: 100%; margin-bottom: 2rem; }
|
||||
th { text-align: left; padding: 0.75rem; background: #0f172a; color: #94a3b8; }
|
||||
td { padding: 0.75rem; border-top: 1px solid #334155; }
|
||||
.total-section { text-align: right; }
|
||||
.total-row { display: flex; justify-content: flex-end; gap: 2rem; margin-bottom: 0.5rem; }
|
||||
.total-row.final { font-size: 1.5rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
|
||||
.actions { margin-top: 2rem; display: flex; gap: 1rem; }
|
||||
.btn { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
||||
.btn-primary { background: #3b82f6; color: white; }
|
||||
.btn-secondary { background: #334155; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function InvoiceDetail() {
|
||||
const invoice = {
|
||||
number: 'INV-001',
|
||||
client: 'Acme Corp',
|
||||
date: '2024-01-15',
|
||||
dueDate: '2024-02-15',
|
||||
status: 'Paid',
|
||||
lines: [
|
||||
{ description: 'Website Design', qty: 1, rate: 2000, amount: 2000 },
|
||||
{ description: 'Logo Design', qty: 1, rate: 500, amount: 500 },
|
||||
],
|
||||
subtotal: 2500,
|
||||
tax: 325,
|
||||
total: 2825,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="invoice-header">
|
||||
<h1>{invoice.number}</h1>
|
||||
<span className="status-badge">{invoice.status}</span>
|
||||
<div className="invoice-meta">
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Client</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: '500' }}>{invoice.client}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Amount</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>${invoice.total}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Invoice Date</div>
|
||||
<div>{invoice.date}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Due Date</div>
|
||||
<div>{invoice.dueDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="invoice-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qty</th>
|
||||
<th>Rate</th>
|
||||
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoice.lines.map((line, i) => (
|
||||
<tr key={i}>
|
||||
<td>{line.description}</td>
|
||||
<td>{line.qty}</td>
|
||||
<td>${line.rate}</td>
|
||||
<td style={{ textAlign: 'right' }}>${line.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="total-section">
|
||||
<div className="total-row"><span>Subtotal:</span><span>${invoice.subtotal}</span></div>
|
||||
<div className="total-row"><span>Tax (13%):</span><span>${invoice.tax}</span></div>
|
||||
<div className="total-row final"><span>Total:</span><span>${invoice.total}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<button className="btn btn-primary">Send Invoice</button>
|
||||
<button className="btn btn-secondary">Download PDF</button>
|
||||
<button className="btn btn-secondary">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceDetail />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Invoice Detail - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
22
servers/freshbooks/src/ui/react-app/package.json
Normal file
22
servers/freshbooks/src/ui/react-app/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "freshbooks-mcp-apps",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build-all.js",
|
||||
"dev": "vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^0.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
62
servers/freshbooks/src/ui/react-app/payment-history/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/payment-history/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function PaymentHistory() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_payments', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading payment history...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Payment History</h1>
|
||||
<p className="subtitle">Track all payments</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,86 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment History - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
|
||||
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
|
||||
.payment-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
.payment-item { padding: 1.5rem; border-bottom: 1px solid #334155; display: grid; grid-template-columns: 1fr 2fr 1fr 1fr 1fr; gap: 1rem; align-items: center; }
|
||||
.payment-item:last-child { border-bottom: none; }
|
||||
.payment-date { color: #94a3b8; }
|
||||
.payment-invoice { font-weight: 500; }
|
||||
.payment-client { color: #64748b; font-size: 0.875rem; }
|
||||
.payment-method { padding: 0.25rem 0.75rem; background: #0f172a; border-radius: 4px; font-size: 0.875rem; text-align: center; }
|
||||
.payment-amount { font-size: 1.25rem; font-weight: bold; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function PaymentHistory() {
|
||||
const [payments] = useState([
|
||||
{ id: 1, date: '2024-01-25', invoice: 'INV-001', client: 'Acme Corp', method: 'Credit Card', amount: 2500 },
|
||||
{ id: 2, date: '2024-01-23', invoice: 'INV-005', client: 'Tech Solutions', method: 'Bank Transfer', amount: 4200 },
|
||||
{ id: 3, date: '2024-01-20', invoice: 'INV-003', client: 'Design Co', method: 'PayPal', amount: 1800 },
|
||||
{ id: 4, date: '2024-01-18', invoice: 'INV-007', client: 'Marketing Inc', method: 'Check', amount: 3600 },
|
||||
{ id: 5, date: '2024-01-15', invoice: 'INV-002', client: 'Acme Corp', method: 'Credit Card', amount: 5000 },
|
||||
]);
|
||||
|
||||
const totalReceived = payments.reduce((sum, p) => sum + p.amount, 0);
|
||||
const thisMonth = payments.filter(p => p.date.startsWith('2024-01')).reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Payment History</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Track all received payments</p>
|
||||
|
||||
<div className="stats">
|
||||
<div className="stat-card">
|
||||
<div className="value">${totalReceived.toLocaleString()}</div>
|
||||
<div className="label">Total Received</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">${thisMonth.toLocaleString()}</div>
|
||||
<div className="label">This Month</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">{payments.length}</div>
|
||||
<div className="label">Payments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-list">
|
||||
{payments.map(payment => (
|
||||
<div key={payment.id} className="payment-item">
|
||||
<div className="payment-date">{payment.date}</div>
|
||||
<div>
|
||||
<div className="payment-invoice">{payment.invoice}</div>
|
||||
<div className="payment-client">{payment.client}</div>
|
||||
</div>
|
||||
<div className="payment-method">{payment.method}</div>
|
||||
<div className="payment-amount">${payment.amount.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<PaymentHistory />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Payment History - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/profit-loss/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/profit-loss/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ProfitLoss() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_profit_loss_report', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading profit & loss...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Profit & Loss</h1>
|
||||
<p className="subtitle">Financial P&L statements</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Profit & Loss - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
9
servers/freshbooks/src/ui/react-app/profit-loss/main.tsx
Normal file
9
servers/freshbooks/src/ui/react-app/profit-loss/main.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
87
servers/freshbooks/src/ui/react-app/profit-loss/styles.css
Normal file
87
servers/freshbooks/src/ui/react-app/profit-loss/styles.css
Normal file
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ProjectDashboard() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_list_projects', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading project overview...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Project Overview</h1>
|
||||
<p className="subtitle">View all active projects</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,92 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Projects - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
|
||||
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
|
||||
.project-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-top: 4px solid #3b82f6; }
|
||||
.project-card.hourly { border-top-color: #10b981; }
|
||||
.project-card.fixed { border-top-color: #f59e0b; }
|
||||
.project-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; }
|
||||
.project-title { font-size: 1.25rem; font-weight: 600; }
|
||||
.project-status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
||||
.status-active { background: #065f46; color: #6ee7b7; }
|
||||
.status-complete { background: #1e40af; color: #93c5fd; }
|
||||
.client-name { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.progress-bar { background: #0f172a; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 1rem; }
|
||||
.progress-fill { background: linear-gradient(90deg, #3b82f6, #10b981); height: 100%; }
|
||||
.project-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.stat { text-align: center; }
|
||||
.stat .value { font-size: 1.5rem; font-weight: bold; }
|
||||
.stat .label { color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function ProjectDashboard() {
|
||||
const [projects] = useState([
|
||||
{ id: 1, title: 'Website Redesign', client: 'Acme Corp', type: 'hourly', status: 'active', progress: 65, hours: 45, budget: 8000 },
|
||||
{ id: 2, title: 'Mobile App', client: 'Tech Solutions', type: 'fixed', status: 'active', progress: 30, hours: 22, budget: 15000 },
|
||||
{ id: 3, title: 'Brand Identity', client: 'Design Co', type: 'hourly', status: 'complete', progress: 100, hours: 60, budget: 12000 },
|
||||
{ id: 4, title: 'E-commerce Store', client: 'Marketing Inc', type: 'fixed', status: 'active', progress: 50, hours: 35, budget: 20000 },
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div>
|
||||
<h1>Projects</h1>
|
||||
<p style={{ color: '#94a3b8' }}>{projects.filter(p => p.status === 'active').length} active projects</p>
|
||||
</div>
|
||||
<button className="btn">+ New Project</button>
|
||||
</div>
|
||||
|
||||
<div className="project-grid">
|
||||
{projects.map(project => (
|
||||
<div key={project.id} className={`project-card ${project.type}`}>
|
||||
<div className="project-header">
|
||||
<div className="project-title">{project.title}</div>
|
||||
<span className={`project-status status-${project.status}`}>{project.status}</span>
|
||||
</div>
|
||||
<div className="client-name">{project.client}</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" style={{ width: `${project.progress}%` }} />
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '1rem', color: '#94a3b8', fontSize: '0.875rem' }}>
|
||||
{project.progress}% complete
|
||||
</div>
|
||||
<div className="project-stats">
|
||||
<div className="stat">
|
||||
<div className="value">{project.hours}h</div>
|
||||
<div className="label">Hours Tracked</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="value">${(project.budget / 1000).toFixed(0)}k</div>
|
||||
<div className="label">{project.type === 'fixed' ? 'Fixed Price' : 'Budget'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ProjectDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Project Overview - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@ -0,0 +1,87 @@
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #111827;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
font-size: 1.25rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1f2937;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.data-preview {
|
||||
background: #111827;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
62
servers/freshbooks/src/ui/react-app/project-detail/App.tsx
Normal file
62
servers/freshbooks/src/ui/react-app/project-detail/App.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import './styles.css';
|
||||
|
||||
export default function ProjectDetail() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Call MCP tool
|
||||
const response = await (window as any).mcp?.callTool('freshbooks_get_project', {});
|
||||
|
||||
if (response) {
|
||||
setData(response);
|
||||
} else {
|
||||
setData(getSampleData());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
setData(getSampleData());
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getSampleData = () => {
|
||||
return { message: 'Sample data - MCP tools not available', items: [] };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="loading">Loading project detail...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Project Detail</h1>
|
||||
<p className="subtitle">Detailed project information</p>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
{error && <div className="error">{error}</div>}
|
||||
|
||||
<div className="card">
|
||||
<h2>Data</h2>
|
||||
<pre className="data-preview">{JSON.stringify(data, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,34 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Project Detail - FreshBooks MCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user