Update 6 MCP servers — fieldedge, lightspeed, squarespace, toast, touchbistro, servicetitan — 2026-02-12
=== UPDATES === - fieldedge: Added apps, tools, main server entry, full rebuild - lightspeed: Added complete src/ directory with tools + apps - squarespace: Full rebuild — new apps, clients, tools, types modules - toast: Full rebuild — api-client, apps, tools, types - touchbistro: Full rebuild — api-client, tools, types, gitignore - servicetitan: Added 4 React UI apps (call-tracking, lead-source-analytics, performance-metrics, schedule-calendar) All servers restructured from single-file to modular architecture.
This commit is contained in:
parent
c435e6c3a4
commit
d3382ec35a
@ -1,50 +1,132 @@
|
||||
# FieldEdge MCP Server
|
||||
|
||||
MCP server for integrating with [FieldEdge](https://fieldedge.com/) field service management software for HVAC, plumbing, and electrical contractors.
|
||||
Complete Model Context Protocol (MCP) server for FieldEdge field service management platform. Provides comprehensive access to jobs, customers, invoices, estimates, technicians, dispatch, equipment, inventory, service agreements, and reporting.
|
||||
|
||||
## Features
|
||||
|
||||
- **Work Orders**: List, get, and create work orders
|
||||
- **Customers**: Search and list customer records
|
||||
- **Technicians**: List technicians by department and status
|
||||
- **Invoices**: List invoices with filtering
|
||||
- **Equipment**: Track equipment at customer locations
|
||||
### 45+ Tools Across 10 Categories
|
||||
|
||||
## Setup
|
||||
#### Jobs Management (9 tools)
|
||||
- `fieldedge_jobs_list` - List and filter jobs
|
||||
- `fieldedge_jobs_get` - Get job details
|
||||
- `fieldedge_jobs_create` - Create new job
|
||||
- `fieldedge_jobs_update` - Update job
|
||||
- `fieldedge_jobs_complete` - Mark job complete
|
||||
- `fieldedge_jobs_cancel` - Cancel job
|
||||
- `fieldedge_jobs_line_items_list` - List job line items
|
||||
- `fieldedge_jobs_line_items_add` - Add line item to job
|
||||
- `fieldedge_jobs_equipment_list` - List equipment on job
|
||||
|
||||
### Prerequisites
|
||||
#### Customer Management (8 tools)
|
||||
- `fieldedge_customers_list` - List and filter customers
|
||||
- `fieldedge_customers_get` - Get customer details
|
||||
- `fieldedge_customers_create` - Create new customer
|
||||
- `fieldedge_customers_update` - Update customer
|
||||
- `fieldedge_customers_delete` - Delete/deactivate customer
|
||||
- `fieldedge_customers_search` - Search customers
|
||||
- `fieldedge_customers_locations_list` - List customer locations
|
||||
- `fieldedge_customers_equipment_list` - List customer equipment
|
||||
|
||||
- Node.js 18+
|
||||
- FieldEdge account with API access
|
||||
- API credentials from FieldEdge partner program
|
||||
#### Invoice Management (6 tools)
|
||||
- `fieldedge_invoices_list` - List and filter invoices
|
||||
- `fieldedge_invoices_get` - Get invoice details
|
||||
- `fieldedge_invoices_create` - Create invoice
|
||||
- `fieldedge_invoices_update` - Update invoice
|
||||
- `fieldedge_invoices_payments_list` - List payments
|
||||
- `fieldedge_invoices_payments_add` - Add payment
|
||||
|
||||
### Getting API Access
|
||||
#### Estimate Management (6 tools)
|
||||
- `fieldedge_estimates_list` - List estimates
|
||||
- `fieldedge_estimates_get` - Get estimate details
|
||||
- `fieldedge_estimates_create` - Create estimate
|
||||
- `fieldedge_estimates_update` - Update estimate
|
||||
- `fieldedge_estimates_send` - Send estimate to customer
|
||||
- `fieldedge_estimates_approve` - Approve and convert to job
|
||||
|
||||
FieldEdge API access is available through their partner program. Visit [docs.api.fieldedge.com](https://docs.api.fieldedge.com/) to learn more.
|
||||
#### Technician Management (6 tools)
|
||||
- `fieldedge_technicians_list` - List technicians
|
||||
- `fieldedge_technicians_get` - Get technician details
|
||||
- `fieldedge_technicians_create` - Create technician
|
||||
- `fieldedge_technicians_update` - Update technician
|
||||
- `fieldedge_technicians_performance_get` - Get performance metrics
|
||||
- `fieldedge_technicians_time_entries_list` - List time entries
|
||||
|
||||
### Installation
|
||||
#### Dispatch Management (5 tools)
|
||||
- `fieldedge_dispatch_board_get` - Get dispatch board
|
||||
- `fieldedge_dispatch_assign_tech` - Assign technician to job
|
||||
- `fieldedge_dispatch_technician_availability_get` - Get technician availability
|
||||
- `fieldedge_dispatch_zones_list` - List dispatch zones
|
||||
- `fieldedge_dispatch_optimize` - Auto-optimize dispatch schedule
|
||||
|
||||
#### Equipment Management (5 tools)
|
||||
- `fieldedge_equipment_list` - List equipment
|
||||
- `fieldedge_equipment_get` - Get equipment details
|
||||
- `fieldedge_equipment_create` - Create equipment record
|
||||
- `fieldedge_equipment_update` - Update equipment
|
||||
- `fieldedge_equipment_service_history_list` - List service history
|
||||
|
||||
#### Inventory Management (6 tools)
|
||||
- `fieldedge_inventory_parts_list` - List inventory parts
|
||||
- `fieldedge_inventory_parts_get` - Get part details
|
||||
- `fieldedge_inventory_stock_update` - Update stock levels
|
||||
- `fieldedge_inventory_purchase_orders_list` - List purchase orders
|
||||
- `fieldedge_inventory_purchase_orders_get` - Get PO details
|
||||
- `fieldedge_inventory_reorder_report` - Get reorder report
|
||||
|
||||
#### Service Agreements (6 tools)
|
||||
- `fieldedge_agreements_list` - List service agreements
|
||||
- `fieldedge_agreements_get` - Get agreement details
|
||||
- `fieldedge_agreements_create` - Create agreement
|
||||
- `fieldedge_agreements_update` - Update agreement
|
||||
- `fieldedge_agreements_cancel` - Cancel agreement
|
||||
- `fieldedge_agreements_renew` - Renew agreement
|
||||
|
||||
#### Reporting & Analytics (6 tools)
|
||||
- `fieldedge_reports_revenue` - Revenue report
|
||||
- `fieldedge_reports_job_profitability` - Job profitability analysis
|
||||
- `fieldedge_reports_technician_performance` - Tech performance metrics
|
||||
- `fieldedge_reports_aging` - A/R aging report
|
||||
- `fieldedge_reports_service_agreement_revenue` - Agreement revenue
|
||||
- `fieldedge_reports_equipment_service_due` - Equipment service due
|
||||
|
||||
### 16 Interactive MCP Apps
|
||||
|
||||
- **job-dashboard** - Interactive jobs overview with filtering
|
||||
- **job-detail** - Detailed job view with all information
|
||||
- **job-grid** - Spreadsheet-style bulk job management
|
||||
- **customer-detail** - Complete customer profile
|
||||
- **customer-grid** - Customer data grid view
|
||||
- **invoice-dashboard** - Invoice and payment tracking
|
||||
- **estimate-builder** - Interactive estimate creation
|
||||
- **dispatch-board** - Visual dispatch board
|
||||
- **schedule-calendar** - Job scheduling calendar
|
||||
- **technician-dashboard** - Tech performance dashboard
|
||||
- **equipment-tracker** - Equipment and maintenance tracking
|
||||
- **inventory-manager** - Inventory and stock management
|
||||
- **agreement-manager** - Service agreement management
|
||||
- **revenue-dashboard** - Revenue analytics
|
||||
- **performance-metrics** - Performance analytics
|
||||
- **aging-report** - A/R aging visualization
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
## Configuration
|
||||
|
||||
Set the following environment variables:
|
||||
|
||||
```bash
|
||||
export FIELDEDGE_API_KEY="your-api-key-here"
|
||||
export FIELDEDGE_SUBSCRIPTION_KEY="your-subscription-key" # Optional, for Azure API Management
|
||||
export FIELDEDGE_API_KEY="your_api_key_here"
|
||||
export FIELDEDGE_BASE_URL="https://api.fieldedge.com/v2" # Optional, defaults to production
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Run the server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Configure in Claude Desktop
|
||||
### With Claude Desktop
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
@ -53,49 +135,104 @@ Add to your `claude_desktop_config.json`:
|
||||
"mcpServers": {
|
||||
"fieldedge": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/fieldedge/dist/index.js"],
|
||||
"args": ["/path/to/fieldedge/dist/main.js"],
|
||||
"env": {
|
||||
"FIELDEDGE_API_KEY": "your-api-key",
|
||||
"FIELDEDGE_SUBSCRIPTION_KEY": "your-subscription-key"
|
||||
"FIELDEDGE_API_KEY": "your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
### Standalone
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_work_orders` | List work orders with filters for status, customer, technician, date range |
|
||||
| `get_work_order` | Get detailed work order information by ID |
|
||||
| `create_work_order` | Create a new work order |
|
||||
| `list_customers` | Search and list customers |
|
||||
| `list_technicians` | List technicians by department/active status |
|
||||
| `list_invoices` | List invoices with status and date filtering |
|
||||
| `list_equipment` | List equipment by customer, location, or type |
|
||||
```bash
|
||||
FIELDEDGE_API_KEY=your_key npm start
|
||||
```
|
||||
|
||||
## Work Order Statuses
|
||||
## Example Queries
|
||||
|
||||
- `open` - New work order
|
||||
- `scheduled` - Scheduled for service
|
||||
- `in_progress` - Technician working on it
|
||||
- `completed` - Work finished
|
||||
- `canceled` - Canceled
|
||||
- `on_hold` - On hold
|
||||
**Jobs:**
|
||||
- "Show me all emergency jobs scheduled for today"
|
||||
- "Create a new HVAC maintenance job for customer C123"
|
||||
- "What jobs are assigned to technician T456?"
|
||||
- "Mark job J789 as complete"
|
||||
|
||||
## Equipment Types
|
||||
**Customers:**
|
||||
- "Find all commercial customers in Chicago"
|
||||
- "Show me customer details for account C123"
|
||||
- "List all equipment for customer C456"
|
||||
|
||||
- `hvac` - HVAC systems
|
||||
- `plumbing` - Plumbing equipment
|
||||
- `electrical` - Electrical systems
|
||||
- `appliance` - Appliances
|
||||
- `other` - Other equipment
|
||||
**Invoices:**
|
||||
- "Show me all overdue invoices"
|
||||
- "Create an invoice for job J789"
|
||||
- "Add a $500 payment to invoice INV-123"
|
||||
|
||||
## API Reference
|
||||
**Dispatch:**
|
||||
- "Show me today's dispatch board"
|
||||
- "Assign technician T123 to job J456"
|
||||
- "What's the technician availability for tomorrow?"
|
||||
- "Optimize the dispatch schedule for tomorrow"
|
||||
|
||||
Base URL: `https://api.fieldedge.com/v1`
|
||||
**Reports:**
|
||||
- "Show me revenue for the last 30 days"
|
||||
- "What's the profitability of job J123?"
|
||||
- "Generate an aging report"
|
||||
- "Show technician performance metrics for this month"
|
||||
|
||||
Authentication: Bearer token + Azure subscription key
|
||||
## Architecture
|
||||
|
||||
See [FieldEdge API Documentation](https://docs.api.fieldedge.com/) for partner access.
|
||||
```
|
||||
src/
|
||||
├── client.ts # API client with auth, pagination, error handling
|
||||
├── types.ts # TypeScript type definitions
|
||||
├── tools/ # Tool implementations
|
||||
│ ├── jobs-tools.ts
|
||||
│ ├── customers-tools.ts
|
||||
│ ├── invoices-tools.ts
|
||||
│ ├── estimates-tools.ts
|
||||
│ ├── technicians-tools.ts
|
||||
│ ├── dispatch-tools.ts
|
||||
│ ├── equipment-tools.ts
|
||||
│ ├── inventory-tools.ts
|
||||
│ ├── agreements-tools.ts
|
||||
│ └── reporting-tools.ts
|
||||
├── apps/ # MCP app implementations
|
||||
│ └── index.ts
|
||||
├── server.ts # MCP server setup
|
||||
└── main.ts # Entry point
|
||||
```
|
||||
|
||||
## API Client Features
|
||||
|
||||
- **Bearer Token Authentication** - Automatic authorization header injection
|
||||
- **Pagination Support** - Built-in pagination handling with `getPaginated()` and `getAllPages()`
|
||||
- **Error Handling** - Comprehensive error handling with detailed error messages
|
||||
- **Request Methods** - Full REST support (GET, POST, PUT, PATCH, DELETE)
|
||||
- **Type Safety** - Full TypeScript typing for all API responses
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Development with watch mode
|
||||
npm run build -- --watch
|
||||
|
||||
# Run tests (if implemented)
|
||||
npm test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- FieldEdge API documentation: https://developer.fieldedge.com
|
||||
- MCP Protocol: https://modelcontextprotocol.io
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
363
servers/fieldedge/src/apps/index.ts
Normal file
363
servers/fieldedge/src/apps/index.ts
Normal file
@ -0,0 +1,363 @@
|
||||
/**
|
||||
* FieldEdge MCP Apps
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
|
||||
export function createApps(client: FieldEdgeClient) {
|
||||
return [
|
||||
// Job Management Apps
|
||||
{
|
||||
name: 'job-dashboard',
|
||||
description: 'Interactive dashboard showing all jobs with filtering and sorting',
|
||||
type: 'dashboard',
|
||||
handler: async () => {
|
||||
const jobs = await client.get('/jobs', { pageSize: 100 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Jobs Dashboard',
|
||||
content: `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.dashboard { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { color: #1a1a1a; margin-bottom: 24px; font-size: 28px; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||
.stat-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.stat-value { font-size: 32px; font-weight: bold; color: #2563eb; }
|
||||
.stat-label { color: #666; margin-top: 8px; font-size: 14px; }
|
||||
.filters { background: white; padding: 16px; border-radius: 12px; margin-bottom: 16px; display: flex; gap: 12px; flex-wrap: wrap; }
|
||||
.filter-group { display: flex; flex-direction: column; gap: 4px; }
|
||||
.filter-group label { font-size: 12px; color: #666; font-weight: 500; }
|
||||
select, input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
.jobs-grid { display: grid; gap: 16px; }
|
||||
.job-card { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 4px solid #2563eb; }
|
||||
.job-card.emergency { border-left-color: #dc2626; }
|
||||
.job-card.high { border-left-color: #f59e0b; }
|
||||
.job-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px; }
|
||||
.job-number { font-size: 18px; font-weight: bold; color: #1a1a1a; }
|
||||
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.status-scheduled { background: #dbeafe; color: #1e40af; }
|
||||
.status-in_progress { background: #fef3c7; color: #92400e; }
|
||||
.status-completed { background: #d1fae5; color: #065f46; }
|
||||
.job-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; color: #666; font-size: 14px; }
|
||||
.detail-item { display: flex; flex-direction: column; gap: 4px; }
|
||||
.detail-label { font-size: 12px; color: #999; }
|
||||
.detail-value { color: #1a1a1a; font-weight: 500; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<h1>📋 Jobs Dashboard</h1>
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-jobs">0</div>
|
||||
<div class="stat-label">Total Jobs</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="scheduled-jobs">0</div>
|
||||
<div class="stat-label">Scheduled</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="in-progress-jobs">0</div>
|
||||
<div class="stat-label">In Progress</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="completed-jobs">0</div>
|
||||
<div class="stat-label">Completed Today</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<label>Status</label>
|
||||
<select id="status-filter">
|
||||
<option value="">All</option>
|
||||
<option value="scheduled">Scheduled</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="on_hold">On Hold</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Priority</label>
|
||||
<select id="priority-filter">
|
||||
<option value="">All</option>
|
||||
<option value="emergency">Emergency</option>
|
||||
<option value="high">High</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>Search</label>
|
||||
<input type="text" id="search" placeholder="Job #, customer...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="jobs-grid" id="jobs-container"></div>
|
||||
</div>
|
||||
<script>
|
||||
const data = ${JSON.stringify(jobs)};
|
||||
const jobsContainer = document.getElementById('jobs-container');
|
||||
|
||||
function renderJobs(jobs) {
|
||||
jobsContainer.innerHTML = jobs.map(job => \`
|
||||
<div class="job-card \${job.priority}">
|
||||
<div class="job-header">
|
||||
<div class="job-number">#\${job.jobNumber}</div>
|
||||
<span class="status-badge status-\${job.status}">\${job.status.replace('_', ' ').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="job-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Customer</span>
|
||||
<span class="detail-value">\${job.customerName || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Technician</span>
|
||||
<span class="detail-value">\${job.assignedTechName || 'Unassigned'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Scheduled</span>
|
||||
<span class="detail-value">\${job.scheduledStart ? new Date(job.scheduledStart).toLocaleString() : 'TBD'}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Priority</span>
|
||||
<span class="detail-value">\${job.priority.toUpperCase()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
\`).join('');
|
||||
|
||||
// Update stats
|
||||
document.getElementById('total-jobs').textContent = jobs.length;
|
||||
document.getElementById('scheduled-jobs').textContent = jobs.filter(j => j.status === 'scheduled').length;
|
||||
document.getElementById('in-progress-jobs').textContent = jobs.filter(j => j.status === 'in_progress').length;
|
||||
document.getElementById('completed-jobs').textContent = jobs.filter(j => j.status === 'completed').length;
|
||||
}
|
||||
|
||||
renderJobs(data.data || []);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`.trim(),
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'job-detail',
|
||||
description: 'Detailed view of a specific job with all information',
|
||||
type: 'detail',
|
||||
handler: async (jobId: string) => {
|
||||
const job = await client.get(`/jobs/${jobId}`);
|
||||
const lineItems = await client.get(`/jobs/${jobId}/line-items`);
|
||||
return {
|
||||
type: 'ui',
|
||||
title: `Job #${(job as any).jobNumber}`,
|
||||
content: `Job details for ${jobId} with line items...`,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'job-grid',
|
||||
description: 'Spreadsheet-style grid view of jobs for bulk operations',
|
||||
type: 'grid',
|
||||
handler: async () => {
|
||||
const jobs = await client.get('/jobs', { pageSize: 200 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Jobs Grid',
|
||||
content: 'Table/grid view of all jobs...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Customer Apps
|
||||
{
|
||||
name: 'customer-detail',
|
||||
description: 'Complete customer profile with history, equipment, and agreements',
|
||||
type: 'detail',
|
||||
handler: async (customerId: string) => {
|
||||
const customer = await client.get(`/customers/${customerId}`);
|
||||
return {
|
||||
type: 'ui',
|
||||
title: `Customer: ${(customer as any).firstName} ${(customer as any).lastName}`,
|
||||
content: 'Customer profile...',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'customer-grid',
|
||||
description: 'Grid view of all customers',
|
||||
type: 'grid',
|
||||
handler: async () => {
|
||||
const customers = await client.get('/customers', { pageSize: 200 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Customers Grid',
|
||||
content: 'Customer table...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Financial Apps
|
||||
{
|
||||
name: 'invoice-dashboard',
|
||||
description: 'Invoice management dashboard with payment tracking',
|
||||
type: 'dashboard',
|
||||
handler: async () => {
|
||||
const invoices = await client.get('/invoices', { pageSize: 100 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Invoices Dashboard',
|
||||
content: 'Invoice dashboard...',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'estimate-builder',
|
||||
description: 'Interactive estimate creation and editing tool',
|
||||
type: 'builder',
|
||||
handler: async () => {
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Estimate Builder',
|
||||
content: 'Estimate builder UI...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Dispatch Apps
|
||||
{
|
||||
name: 'dispatch-board',
|
||||
description: 'Visual dispatch board showing technicians and job assignments',
|
||||
type: 'board',
|
||||
handler: async (date?: string) => {
|
||||
const board = await client.get('/dispatch/board', { date: date || new Date().toISOString().split('T')[0] });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Dispatch Board',
|
||||
content: 'Dispatch board visualization...',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'schedule-calendar',
|
||||
description: 'Calendar view of scheduled jobs and technician availability',
|
||||
type: 'calendar',
|
||||
handler: async () => {
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Schedule Calendar',
|
||||
content: 'Calendar view...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Technician Apps
|
||||
{
|
||||
name: 'technician-dashboard',
|
||||
description: 'Technician performance and schedule dashboard',
|
||||
type: 'dashboard',
|
||||
handler: async () => {
|
||||
const technicians = await client.get('/technicians');
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Technician Dashboard',
|
||||
content: 'Technician metrics...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Equipment Apps
|
||||
{
|
||||
name: 'equipment-tracker',
|
||||
description: 'Equipment tracking with service history and maintenance schedules',
|
||||
type: 'tracker',
|
||||
handler: async () => {
|
||||
const equipment = await client.get('/equipment', { pageSize: 200 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Equipment Tracker',
|
||||
content: 'Equipment list with service tracking...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Inventory Apps
|
||||
{
|
||||
name: 'inventory-manager',
|
||||
description: 'Inventory management with stock levels and reorder alerts',
|
||||
type: 'manager',
|
||||
handler: async () => {
|
||||
const parts = await client.get('/inventory/parts', { pageSize: 200 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Inventory Manager',
|
||||
content: 'Inventory management...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Agreement Apps
|
||||
{
|
||||
name: 'agreement-manager',
|
||||
description: 'Service agreement management and renewal tracking',
|
||||
type: 'manager',
|
||||
handler: async () => {
|
||||
const agreements = await client.get('/agreements', { pageSize: 100 });
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Agreement Manager',
|
||||
content: 'Service agreements...',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Reporting Apps
|
||||
{
|
||||
name: 'revenue-dashboard',
|
||||
description: 'Revenue analytics and trends',
|
||||
type: 'dashboard',
|
||||
handler: async () => {
|
||||
const report = await client.get('/reports/revenue', {
|
||||
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||
endDate: new Date().toISOString(),
|
||||
});
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Revenue Dashboard',
|
||||
content: 'Revenue charts and metrics...',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'performance-metrics',
|
||||
description: 'Technician and job performance metrics',
|
||||
type: 'metrics',
|
||||
handler: async () => {
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'Performance Metrics',
|
||||
content: 'Performance analytics...',
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'aging-report',
|
||||
description: 'Accounts receivable aging report',
|
||||
type: 'report',
|
||||
handler: async () => {
|
||||
const report = await client.get('/reports/aging');
|
||||
return {
|
||||
type: 'ui',
|
||||
title: 'A/R Aging Report',
|
||||
content: 'Aging report...',
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -1,391 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// ============================================
|
||||
// CONFIGURATION
|
||||
// ============================================
|
||||
const MCP_NAME = "fieldedge";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
const API_BASE_URL = "https://api.fieldedge.com/v1";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT
|
||||
// ============================================
|
||||
class FieldEdgeClient {
|
||||
private apiKey: string;
|
||||
private subscriptionKey: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(apiKey: string, subscriptionKey?: string) {
|
||||
this.apiKey = apiKey;
|
||||
this.subscriptionKey = subscriptionKey || apiKey;
|
||||
this.baseUrl = API_BASE_URL;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
"Ocp-Apim-Subscription-Key": this.subscriptionKey,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`FieldEdge API error: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string) {
|
||||
return this.request(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(endpoint: string) {
|
||||
return this.request(endpoint, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// Work Orders
|
||||
async listWorkOrders(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
customerId?: string;
|
||||
technicianId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.status) query.append("status", params.status);
|
||||
if (params.customerId) query.append("customerId", params.customerId);
|
||||
if (params.technicianId) query.append("technicianId", params.technicianId);
|
||||
if (params.startDate) query.append("startDate", params.startDate);
|
||||
if (params.endDate) query.append("endDate", params.endDate);
|
||||
return this.get(`/work-orders?${query.toString()}`);
|
||||
}
|
||||
|
||||
async getWorkOrder(id: string) {
|
||||
return this.get(`/work-orders/${id}`);
|
||||
}
|
||||
|
||||
async createWorkOrder(data: {
|
||||
customerId: string;
|
||||
locationId?: string;
|
||||
description: string;
|
||||
workType?: string;
|
||||
priority?: string;
|
||||
scheduledDate?: string;
|
||||
scheduledTime?: string;
|
||||
technicianId?: string;
|
||||
equipmentIds?: string[];
|
||||
notes?: string;
|
||||
}) {
|
||||
return this.post("/work-orders", data);
|
||||
}
|
||||
|
||||
// Customers
|
||||
async listCustomers(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.search) query.append("search", params.search);
|
||||
if (params.sortBy) query.append("sortBy", params.sortBy);
|
||||
if (params.sortOrder) query.append("sortOrder", params.sortOrder);
|
||||
return this.get(`/customers?${query.toString()}`);
|
||||
}
|
||||
|
||||
// Technicians
|
||||
async listTechnicians(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
active?: boolean;
|
||||
departmentId?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.active !== undefined) query.append("active", params.active.toString());
|
||||
if (params.departmentId) query.append("departmentId", params.departmentId);
|
||||
return this.get(`/technicians?${query.toString()}`);
|
||||
}
|
||||
|
||||
// Invoices
|
||||
async listInvoices(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
customerId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.status) query.append("status", params.status);
|
||||
if (params.customerId) query.append("customerId", params.customerId);
|
||||
if (params.startDate) query.append("startDate", params.startDate);
|
||||
if (params.endDate) query.append("endDate", params.endDate);
|
||||
return this.get(`/invoices?${query.toString()}`);
|
||||
}
|
||||
|
||||
// Equipment
|
||||
async listEquipment(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
customerId?: string;
|
||||
locationId?: string;
|
||||
equipmentType?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.customerId) query.append("customerId", params.customerId);
|
||||
if (params.locationId) query.append("locationId", params.locationId);
|
||||
if (params.equipmentType) query.append("equipmentType", params.equipmentType);
|
||||
return this.get(`/equipment?${query.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_work_orders",
|
||||
description: "List work orders from FieldEdge. Filter by status, customer, technician, and date range.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination (default: 1)" },
|
||||
pageSize: { type: "number", description: "Number of results per page (default: 25, max: 100)" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by work order status",
|
||||
enum: ["open", "scheduled", "in_progress", "completed", "canceled", "on_hold"]
|
||||
},
|
||||
customerId: { type: "string", description: "Filter work orders by customer ID" },
|
||||
technicianId: { type: "string", description: "Filter work orders by assigned technician ID" },
|
||||
startDate: { type: "string", description: "Filter by scheduled date (start) in YYYY-MM-DD format" },
|
||||
endDate: { type: "string", description: "Filter by scheduled date (end) in YYYY-MM-DD format" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_work_order",
|
||||
description: "Get detailed information about a specific work order by ID",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "string", description: "The work order ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_work_order",
|
||||
description: "Create a new work order in FieldEdge",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
customerId: { type: "string", description: "The customer ID (required)" },
|
||||
locationId: { type: "string", description: "The service location ID" },
|
||||
description: { type: "string", description: "Work order description (required)" },
|
||||
workType: {
|
||||
type: "string",
|
||||
description: "Type of work",
|
||||
enum: ["service", "repair", "installation", "maintenance", "inspection"]
|
||||
},
|
||||
priority: {
|
||||
type: "string",
|
||||
description: "Priority level",
|
||||
enum: ["low", "normal", "high", "emergency"]
|
||||
},
|
||||
scheduledDate: { type: "string", description: "Scheduled date in YYYY-MM-DD format" },
|
||||
scheduledTime: { type: "string", description: "Scheduled time in HH:MM format" },
|
||||
technicianId: { type: "string", description: "Assigned technician ID" },
|
||||
equipmentIds: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Array of equipment IDs related to this work order"
|
||||
},
|
||||
notes: { type: "string", description: "Additional notes or instructions" },
|
||||
},
|
||||
required: ["customerId", "description"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_customers",
|
||||
description: "List customers from FieldEdge with search and pagination",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
search: { type: "string", description: "Search query to filter customers by name, email, phone, or address" },
|
||||
sortBy: { type: "string", description: "Sort field (e.g., 'name', 'createdAt')" },
|
||||
sortOrder: { type: "string", enum: ["asc", "desc"], description: "Sort order" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_technicians",
|
||||
description: "List technicians/employees from FieldEdge",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
active: { type: "boolean", description: "Filter by active status" },
|
||||
departmentId: { type: "string", description: "Filter by department ID" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_invoices",
|
||||
description: "List invoices from FieldEdge",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by invoice status",
|
||||
enum: ["draft", "pending", "sent", "paid", "partial", "overdue", "void"]
|
||||
},
|
||||
customerId: { type: "string", description: "Filter invoices by customer ID" },
|
||||
startDate: { type: "string", description: "Filter by invoice date (start) in YYYY-MM-DD format" },
|
||||
endDate: { type: "string", description: "Filter by invoice date (end) in YYYY-MM-DD format" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_equipment",
|
||||
description: "List equipment records from FieldEdge. Track HVAC units, appliances, and other equipment at customer locations.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
customerId: { type: "string", description: "Filter equipment by customer ID" },
|
||||
locationId: { type: "string", description: "Filter equipment by location ID" },
|
||||
equipmentType: {
|
||||
type: "string",
|
||||
description: "Filter by equipment type",
|
||||
enum: ["hvac", "plumbing", "electrical", "appliance", "other"]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: FieldEdgeClient, name: string, args: any) {
|
||||
switch (name) {
|
||||
case "list_work_orders":
|
||||
return await client.listWorkOrders(args);
|
||||
|
||||
case "get_work_order":
|
||||
return await client.getWorkOrder(args.id);
|
||||
|
||||
case "create_work_order":
|
||||
return await client.createWorkOrder(args);
|
||||
|
||||
case "list_customers":
|
||||
return await client.listCustomers(args);
|
||||
|
||||
case "list_technicians":
|
||||
return await client.listTechnicians(args);
|
||||
|
||||
case "list_invoices":
|
||||
return await client.listInvoices(args);
|
||||
|
||||
case "list_equipment":
|
||||
return await client.listEquipment(args);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const apiKey = process.env.FIELDEDGE_API_KEY;
|
||||
const subscriptionKey = process.env.FIELDEDGE_SUBSCRIPTION_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: FIELDEDGE_API_KEY environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new FieldEdgeClient(apiKey, subscriptionKey);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
27
servers/fieldedge/src/main.ts
Normal file
27
servers/fieldedge/src/main.ts
Normal file
@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* FieldEdge MCP Server - Main Entry Point
|
||||
*/
|
||||
|
||||
import { FieldEdgeServer } from './server.js';
|
||||
|
||||
async function main() {
|
||||
const apiKey = process.env.FIELDEDGE_API_KEY;
|
||||
const baseUrl = process.env.FIELDEDGE_BASE_URL;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error('Error: FIELDEDGE_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const server = new FieldEdgeServer(apiKey, baseUrl);
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
112
servers/fieldedge/src/server.ts
Normal file
112
servers/fieldedge/src/server.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/**
|
||||
* FieldEdge MCP Server Implementation
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
import { FieldEdgeClient } from './clients/fieldedge.js';
|
||||
import { customerTools } from './tools/customers.js';
|
||||
import { jobTools } from './tools/jobs.js';
|
||||
import { invoiceTools } from './tools/invoices.js';
|
||||
import { estimateTools } from './tools/estimates.js';
|
||||
import { schedulingTools } from './tools/scheduling.js';
|
||||
import { inventoryTools } from './tools/inventory.js';
|
||||
import { technicianTools } from './tools/technicians.js';
|
||||
import { paymentTools } from './tools/payments.js';
|
||||
import { equipmentTools } from './tools/equipment.js';
|
||||
import { reportingTools } from './tools/reporting.js';
|
||||
|
||||
export class FieldEdgeServer {
|
||||
private server: Server;
|
||||
private client: FieldEdgeClient;
|
||||
private tools: any[];
|
||||
|
||||
constructor(apiKey: string, companyId?: string, apiUrl?: string) {
|
||||
this.client = new FieldEdgeClient({
|
||||
apiKey,
|
||||
companyId,
|
||||
apiUrl,
|
||||
});
|
||||
|
||||
this.tools = [
|
||||
...customerTools,
|
||||
...jobTools,
|
||||
...invoiceTools,
|
||||
...estimateTools,
|
||||
...schedulingTools,
|
||||
...inventoryTools,
|
||||
...technicianTools,
|
||||
...paymentTools,
|
||||
...equipmentTools,
|
||||
...reportingTools,
|
||||
];
|
||||
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'fieldedge-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: this.tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})) as Tool[],
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool execution
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const args = request.params.arguments || {};
|
||||
|
||||
const tool = this.tools.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.handler(args, this.client);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Error executing ${toolName}: ${errorMessage}`,
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('FieldEdge MCP Server running on stdio');
|
||||
}
|
||||
|
||||
getToolCount(): number {
|
||||
return this.tools.length;
|
||||
}
|
||||
}
|
||||
259
servers/fieldedge/src/tools/agreements-tools.ts
Normal file
259
servers/fieldedge/src/tools/agreements-tools.ts
Normal file
@ -0,0 +1,259 @@
|
||||
/**
|
||||
* FieldEdge Service Agreements Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { ServiceAgreement, PaginationParams } from '../types.js';
|
||||
|
||||
export function createAgreementsTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_agreements_list',
|
||||
description: 'List all service agreements/memberships',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by agreement status',
|
||||
enum: ['active', 'cancelled', 'expired', 'suspended'],
|
||||
},
|
||||
customerId: { type: 'string', description: 'Filter by customer ID' },
|
||||
type: { type: 'string', description: 'Filter by agreement type' },
|
||||
expiringWithinDays: {
|
||||
type: 'number',
|
||||
description: 'Show agreements expiring within X days',
|
||||
},
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
status?: string;
|
||||
customerId?: string;
|
||||
type?: string;
|
||||
expiringWithinDays?: number;
|
||||
}) => {
|
||||
const result = await client.getPaginated<ServiceAgreement>(
|
||||
'/agreements',
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_agreements_get',
|
||||
description: 'Get detailed information about a specific service agreement',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agreementId: { type: 'string', description: 'Agreement ID' },
|
||||
},
|
||||
required: ['agreementId'],
|
||||
},
|
||||
handler: async (params: { agreementId: string }) => {
|
||||
const agreement = await client.get<ServiceAgreement>(
|
||||
`/agreements/${params.agreementId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(agreement, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_agreements_create',
|
||||
description: 'Create a new service agreement',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
type: { type: 'string', description: 'Agreement type' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
billingFrequency: {
|
||||
type: 'string',
|
||||
description: 'Billing frequency',
|
||||
enum: ['monthly', 'quarterly', 'annually'],
|
||||
},
|
||||
amount: { type: 'number', description: 'Billing amount' },
|
||||
equipmentCovered: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of equipment IDs covered',
|
||||
},
|
||||
servicesCovered: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of services included',
|
||||
},
|
||||
visitsPerYear: {
|
||||
type: 'number',
|
||||
description: 'Number of included visits per year',
|
||||
},
|
||||
autoRenew: { type: 'boolean', description: 'Auto-renew on expiration' },
|
||||
notes: { type: 'string', description: 'Agreement notes' },
|
||||
},
|
||||
required: ['customerId', 'type', 'startDate', 'billingFrequency', 'amount'],
|
||||
},
|
||||
handler: async (params: {
|
||||
customerId: string;
|
||||
locationId?: string;
|
||||
type: string;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
billingFrequency: string;
|
||||
amount: number;
|
||||
equipmentCovered?: string[];
|
||||
servicesCovered?: string[];
|
||||
visitsPerYear?: number;
|
||||
autoRenew?: boolean;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const agreement = await client.post<ServiceAgreement>('/agreements', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(agreement, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_agreements_update',
|
||||
description: 'Update an existing service agreement',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agreementId: { type: 'string', description: 'Agreement ID' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'cancelled', 'expired', 'suspended'],
|
||||
},
|
||||
endDate: { type: 'string' },
|
||||
amount: { type: 'number' },
|
||||
equipmentCovered: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
servicesCovered: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
visitsPerYear: { type: 'number' },
|
||||
autoRenew: { type: 'boolean' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['agreementId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
agreementId: string;
|
||||
status?: string;
|
||||
endDate?: string;
|
||||
amount?: number;
|
||||
equipmentCovered?: string[];
|
||||
servicesCovered?: string[];
|
||||
visitsPerYear?: number;
|
||||
autoRenew?: boolean;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { agreementId, ...updateData } = params;
|
||||
const agreement = await client.patch<ServiceAgreement>(
|
||||
`/agreements/${agreementId}`,
|
||||
updateData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(agreement, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_agreements_cancel',
|
||||
description: 'Cancel a service agreement',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agreementId: { type: 'string', description: 'Agreement ID' },
|
||||
reason: { type: 'string', description: 'Cancellation reason' },
|
||||
effectiveDate: {
|
||||
type: 'string',
|
||||
description: 'Effective cancellation date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['agreementId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
agreementId: string;
|
||||
reason?: string;
|
||||
effectiveDate?: string;
|
||||
}) => {
|
||||
const { agreementId, ...cancelData } = params;
|
||||
const agreement = await client.post<ServiceAgreement>(
|
||||
`/agreements/${agreementId}/cancel`,
|
||||
cancelData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(agreement, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_agreements_renew',
|
||||
description: 'Renew a service agreement',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
agreementId: { type: 'string', description: 'Agreement ID' },
|
||||
newStartDate: { type: 'string', description: 'New start date (ISO 8601)' },
|
||||
newEndDate: { type: 'string', description: 'New end date (ISO 8601)' },
|
||||
newAmount: { type: 'number', description: 'New billing amount' },
|
||||
},
|
||||
required: ['agreementId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
agreementId: string;
|
||||
newStartDate?: string;
|
||||
newEndDate?: string;
|
||||
newAmount?: number;
|
||||
}) => {
|
||||
const { agreementId, ...renewData } = params;
|
||||
const agreement = await client.post<ServiceAgreement>(
|
||||
`/agreements/${agreementId}/renew`,
|
||||
renewData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(agreement, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
164
servers/fieldedge/src/tools/dispatch-tools.ts
Normal file
164
servers/fieldedge/src/tools/dispatch-tools.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* FieldEdge Dispatch Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { DispatchBoard, TechnicianAvailability, DispatchZone } from '../types.js';
|
||||
|
||||
export function createDispatchTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_dispatch_board_get',
|
||||
description: 'Get the dispatch board for a specific date',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Date for dispatch board (ISO 8601 date)',
|
||||
},
|
||||
zoneId: {
|
||||
type: 'string',
|
||||
description: 'Filter by specific dispatch zone',
|
||||
},
|
||||
},
|
||||
required: ['date'],
|
||||
},
|
||||
handler: async (params: { date: string; zoneId?: string }) => {
|
||||
const board = await client.get<DispatchBoard>('/dispatch/board', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(board, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_dispatch_assign_tech',
|
||||
description: 'Assign a technician to a job',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
jobId: { type: 'string', description: 'Job ID' },
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
scheduledStart: {
|
||||
type: 'string',
|
||||
description: 'Scheduled start time (ISO 8601)',
|
||||
},
|
||||
scheduledEnd: {
|
||||
type: 'string',
|
||||
description: 'Scheduled end time (ISO 8601)',
|
||||
},
|
||||
notify: {
|
||||
type: 'boolean',
|
||||
description: 'Notify technician of assignment',
|
||||
},
|
||||
},
|
||||
required: ['jobId', 'technicianId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
jobId: string;
|
||||
technicianId: string;
|
||||
scheduledStart?: string;
|
||||
scheduledEnd?: string;
|
||||
notify?: boolean;
|
||||
}) => {
|
||||
const result = await client.post<{ success: boolean; job: unknown }>(
|
||||
'/dispatch/assign',
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_dispatch_technician_availability_get',
|
||||
description: 'Get availability for a technician on a specific date',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
date: { type: 'string', description: 'Date (ISO 8601)' },
|
||||
},
|
||||
required: ['technicianId', 'date'],
|
||||
},
|
||||
handler: async (params: { technicianId: string; date: string }) => {
|
||||
const availability = await client.get<TechnicianAvailability>(
|
||||
`/dispatch/technicians/${params.technicianId}/availability`,
|
||||
{ date: params.date }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(availability, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_dispatch_zones_list',
|
||||
description: 'List all dispatch zones',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async () => {
|
||||
const zones = await client.get<{ data: DispatchZone[] }>('/dispatch/zones');
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(zones, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_dispatch_optimize',
|
||||
description: 'Optimize dispatch schedule for a date (auto-assign based on location, skills, availability)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Date to optimize (ISO 8601)' },
|
||||
zoneId: { type: 'string', description: 'Limit to specific zone' },
|
||||
preview: {
|
||||
type: 'boolean',
|
||||
description: 'Preview changes without applying',
|
||||
},
|
||||
},
|
||||
required: ['date'],
|
||||
},
|
||||
handler: async (params: {
|
||||
date: string;
|
||||
zoneId?: string;
|
||||
preview?: boolean;
|
||||
}) => {
|
||||
const result = await client.post<{
|
||||
success: boolean;
|
||||
changes: unknown[];
|
||||
preview: boolean;
|
||||
}>('/dispatch/optimize', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
198
servers/fieldedge/src/tools/equipment-tools.ts
Normal file
198
servers/fieldedge/src/tools/equipment-tools.ts
Normal file
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* FieldEdge Equipment Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { Equipment, ServiceHistory, PaginationParams } from '../types.js';
|
||||
|
||||
export function createEquipmentTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_equipment_list',
|
||||
description: 'List all equipment with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Filter by customer ID' },
|
||||
locationId: { type: 'string', description: 'Filter by location ID' },
|
||||
type: { type: 'string', description: 'Filter by equipment type' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by status',
|
||||
enum: ['active', 'inactive', 'retired'],
|
||||
},
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
customerId?: string;
|
||||
locationId?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<Equipment>('/equipment', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_equipment_get',
|
||||
description: 'Get detailed information about specific equipment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (params: { equipmentId: string }) => {
|
||||
const equipment = await client.get<Equipment>(
|
||||
`/equipment/${params.equipmentId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(equipment, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_equipment_create',
|
||||
description: 'Create a new equipment record',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
locationId: { type: 'string', description: 'Location ID' },
|
||||
type: { type: 'string', description: 'Equipment type' },
|
||||
manufacturer: { type: 'string', description: 'Manufacturer' },
|
||||
model: { type: 'string', description: 'Model number' },
|
||||
serialNumber: { type: 'string', description: 'Serial number' },
|
||||
installDate: { type: 'string', description: 'Install date (ISO 8601)' },
|
||||
warrantyExpiration: {
|
||||
type: 'string',
|
||||
description: 'Warranty expiration date (ISO 8601)',
|
||||
},
|
||||
notes: { type: 'string', description: 'Equipment notes' },
|
||||
},
|
||||
required: ['customerId', 'type'],
|
||||
},
|
||||
handler: async (params: {
|
||||
customerId: string;
|
||||
locationId?: string;
|
||||
type: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
serialNumber?: string;
|
||||
installDate?: string;
|
||||
warrantyExpiration?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const equipment = await client.post<Equipment>('/equipment', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(equipment, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_equipment_update',
|
||||
description: 'Update an existing equipment record',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
type: { type: 'string' },
|
||||
manufacturer: { type: 'string' },
|
||||
model: { type: 'string' },
|
||||
serialNumber: { type: 'string' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'retired'],
|
||||
},
|
||||
installDate: { type: 'string' },
|
||||
warrantyExpiration: { type: 'string' },
|
||||
lastServiceDate: { type: 'string' },
|
||||
nextServiceDate: { type: 'string' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
equipmentId: string;
|
||||
type?: string;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
serialNumber?: string;
|
||||
status?: string;
|
||||
installDate?: string;
|
||||
warrantyExpiration?: string;
|
||||
lastServiceDate?: string;
|
||||
nextServiceDate?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { equipmentId, ...updateData } = params;
|
||||
const equipment = await client.patch<Equipment>(
|
||||
`/equipment/${equipmentId}`,
|
||||
updateData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(equipment, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_equipment_service_history_list',
|
||||
description: 'List service history for equipment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
equipmentId: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const { equipmentId, ...queryParams } = params;
|
||||
const result = await client.getPaginated<ServiceHistory>(
|
||||
`/equipment/${equipmentId}/service-history`,
|
||||
queryParams
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
213
servers/fieldedge/src/tools/equipment.ts
Normal file
213
servers/fieldedge/src/tools/equipment.ts
Normal file
@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Equipment Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { FieldEdgeClient } from '../clients/fieldedge.js';
|
||||
|
||||
export const equipmentTools = [
|
||||
{
|
||||
name: 'fieldedge_list_equipment',
|
||||
description: 'List all equipment with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
customerId: { type: 'string', description: 'Filter by customer' },
|
||||
type: { type: 'string', description: 'Filter by equipment type' },
|
||||
manufacturer: { type: 'string', description: 'Filter by manufacturer' },
|
||||
status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] },
|
||||
},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const result = await client.getEquipment(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_equipment',
|
||||
description: 'Get detailed information about a specific equipment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const equipment = await client.getEquipmentById(args.equipmentId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(equipment, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_create_equipment',
|
||||
description: 'Create a new equipment record',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
type: { type: 'string', description: 'Equipment type (e.g., "HVAC Unit", "Water Heater", "Furnace")' },
|
||||
manufacturer: { type: 'string', description: 'Manufacturer name' },
|
||||
model: { type: 'string', description: 'Model number' },
|
||||
serialNumber: { type: 'string', description: 'Serial number' },
|
||||
installDate: { type: 'string', description: 'Installation date (ISO 8601)' },
|
||||
warrantyExpiry: { type: 'string', description: 'Warranty expiry date (ISO 8601)' },
|
||||
notes: { type: 'string', description: 'Additional notes' },
|
||||
},
|
||||
required: ['customerId', 'type', 'manufacturer', 'model'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const equipment = await client.createEquipment(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Equipment created successfully:\n${JSON.stringify(equipment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_update_equipment',
|
||||
description: 'Update an existing equipment record',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
serialNumber: { type: 'string' },
|
||||
installDate: { type: 'string' },
|
||||
warrantyExpiry: { type: 'string' },
|
||||
lastServiceDate: { type: 'string' },
|
||||
nextServiceDue: { type: 'string' },
|
||||
status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const { equipmentId, ...updateData } = args;
|
||||
const equipment = await client.updateEquipment(equipmentId, updateData);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Equipment updated successfully:\n${JSON.stringify(equipment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_delete_equipment',
|
||||
description: 'Delete an equipment record',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
await client.deleteEquipment(args.equipmentId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Equipment ${args.equipmentId} deleted successfully`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_equipment_by_customer',
|
||||
description: 'Get all equipment for a specific customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const equipment = await client.getEquipmentByCustomer(args.customerId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Found ${equipment.length} equipment:\n${JSON.stringify(equipment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_service_history',
|
||||
description: 'Get service history for a specific equipment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
},
|
||||
required: ['equipmentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const history = await client.getServiceHistory(args.equipmentId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Service history (${history.length} records):\n${JSON.stringify(history, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_add_service_history',
|
||||
description: 'Add a service record to equipment history',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
equipmentId: { type: 'string', description: 'Equipment ID' },
|
||||
jobId: { type: 'string', description: 'Associated job ID' },
|
||||
serviceDate: { type: 'string', description: 'Service date (ISO 8601)' },
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
serviceType: { type: 'string', description: 'Type of service (e.g., "Maintenance", "Repair", "Installation")' },
|
||||
description: { type: 'string', description: 'Service description' },
|
||||
cost: { type: 'number', description: 'Service cost' },
|
||||
notes: { type: 'string', description: 'Additional notes' },
|
||||
},
|
||||
required: ['equipmentId', 'jobId', 'serviceDate', 'technicianId', 'serviceType', 'description', 'cost'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const { equipmentId, ...serviceData } = args;
|
||||
const history = await client.addServiceHistory(equipmentId, serviceData);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Service history added:\n${JSON.stringify(history, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_equipment_due_for_service',
|
||||
description: 'Get all equipment that is due for service',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const equipment = await client.getEquipmentDueForService();
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Equipment due for service (${equipment.length}):\n${JSON.stringify(equipment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
215
servers/fieldedge/src/tools/estimates-tools.ts
Normal file
215
servers/fieldedge/src/tools/estimates-tools.ts
Normal file
@ -0,0 +1,215 @@
|
||||
/**
|
||||
* FieldEdge Estimates Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { Estimate, PaginationParams } from '../types.js';
|
||||
|
||||
export function createEstimatesTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_estimates_list',
|
||||
description: 'List all estimates with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by estimate status',
|
||||
enum: ['draft', 'sent', 'approved', 'declined', 'expired'],
|
||||
},
|
||||
customerId: { type: 'string', description: 'Filter by customer ID' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
status?: string;
|
||||
customerId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<Estimate>('/estimates', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_estimates_get',
|
||||
description: 'Get detailed information about a specific estimate',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||
},
|
||||
required: ['estimateId'],
|
||||
},
|
||||
handler: async (params: { estimateId: string }) => {
|
||||
const estimate = await client.get<Estimate>(
|
||||
`/estimates/${params.estimateId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(estimate, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_estimates_create',
|
||||
description: 'Create a new estimate',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
locationId: { type: 'string', description: 'Customer location ID' },
|
||||
estimateDate: { type: 'string', description: 'Estimate date (ISO 8601)' },
|
||||
expirationDate: { type: 'string', description: 'Expiration date (ISO 8601)' },
|
||||
terms: { type: 'string', description: 'Terms and conditions' },
|
||||
notes: { type: 'string', description: 'Estimate notes' },
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
customerId: string;
|
||||
locationId?: string;
|
||||
estimateDate?: string;
|
||||
expirationDate?: string;
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const estimate = await client.post<Estimate>('/estimates', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(estimate, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_estimates_update',
|
||||
description: 'Update an existing estimate',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['draft', 'sent', 'approved', 'declined', 'expired'],
|
||||
},
|
||||
expirationDate: { type: 'string' },
|
||||
terms: { type: 'string' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['estimateId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
estimateId: string;
|
||||
status?: string;
|
||||
expirationDate?: string;
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { estimateId, ...updateData } = params;
|
||||
const estimate = await client.patch<Estimate>(
|
||||
`/estimates/${estimateId}`,
|
||||
updateData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(estimate, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_estimates_send',
|
||||
description: 'Send an estimate to the customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||
email: { type: 'string', description: 'Email address to send to' },
|
||||
subject: { type: 'string', description: 'Email subject' },
|
||||
message: { type: 'string', description: 'Email message' },
|
||||
},
|
||||
required: ['estimateId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
estimateId: string;
|
||||
email?: string;
|
||||
subject?: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
const { estimateId, ...sendData } = params;
|
||||
const result = await client.post<{ success: boolean }>(
|
||||
`/estimates/${estimateId}/send`,
|
||||
sendData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_estimates_approve',
|
||||
description: 'Approve an estimate and optionally convert to a job',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimateId: { type: 'string', description: 'Estimate ID' },
|
||||
convertToJob: {
|
||||
type: 'boolean',
|
||||
description: 'Convert to job automatically',
|
||||
},
|
||||
scheduledStart: {
|
||||
type: 'string',
|
||||
description: 'Scheduled start time if converting (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['estimateId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
estimateId: string;
|
||||
convertToJob?: boolean;
|
||||
scheduledStart?: string;
|
||||
}) => {
|
||||
const { estimateId, ...approveData } = params;
|
||||
const result = await client.post<{ estimate: Estimate; jobId?: string }>(
|
||||
`/estimates/${estimateId}/approve`,
|
||||
approveData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
207
servers/fieldedge/src/tools/inventory-tools.ts
Normal file
207
servers/fieldedge/src/tools/inventory-tools.ts
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* FieldEdge Inventory Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { InventoryPart, PurchaseOrder, PaginationParams } from '../types.js';
|
||||
|
||||
export function createInventoryTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_inventory_parts_list',
|
||||
description: 'List all inventory parts with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: { type: 'string', description: 'Filter by category' },
|
||||
manufacturer: { type: 'string', description: 'Filter by manufacturer' },
|
||||
lowStock: {
|
||||
type: 'boolean',
|
||||
description: 'Show only low stock items',
|
||||
},
|
||||
search: { type: 'string', description: 'Search by part number or description' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
category?: string;
|
||||
manufacturer?: string;
|
||||
lowStock?: boolean;
|
||||
search?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<InventoryPart>(
|
||||
'/inventory/parts',
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_inventory_parts_get',
|
||||
description: 'Get detailed information about a specific part',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
partId: { type: 'string', description: 'Part ID' },
|
||||
},
|
||||
required: ['partId'],
|
||||
},
|
||||
handler: async (params: { partId: string }) => {
|
||||
const part = await client.get<InventoryPart>(
|
||||
`/inventory/parts/${params.partId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(part, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_inventory_stock_update',
|
||||
description: 'Update stock quantity for a part',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
partId: { type: 'string', description: 'Part ID' },
|
||||
quantityChange: {
|
||||
type: 'number',
|
||||
description: 'Quantity change (positive for add, negative for subtract)',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Reason for stock adjustment',
|
||||
enum: [
|
||||
'purchase',
|
||||
'return',
|
||||
'adjustment',
|
||||
'damage',
|
||||
'theft',
|
||||
'transfer',
|
||||
'cycle_count',
|
||||
],
|
||||
},
|
||||
notes: { type: 'string', description: 'Adjustment notes' },
|
||||
},
|
||||
required: ['partId', 'quantityChange', 'reason'],
|
||||
},
|
||||
handler: async (params: {
|
||||
partId: string;
|
||||
quantityChange: number;
|
||||
reason: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { partId, ...adjustmentData } = params;
|
||||
const result = await client.post<InventoryPart>(
|
||||
`/inventory/parts/${partId}/adjust`,
|
||||
adjustmentData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_inventory_purchase_orders_list',
|
||||
description: 'List all purchase orders',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by PO status',
|
||||
enum: ['draft', 'submitted', 'approved', 'received', 'cancelled'],
|
||||
},
|
||||
vendorId: { type: 'string', description: 'Filter by vendor ID' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
status?: string;
|
||||
vendorId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<PurchaseOrder>(
|
||||
'/inventory/purchase-orders',
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_inventory_purchase_orders_get',
|
||||
description: 'Get detailed information about a purchase order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
poId: { type: 'string', description: 'Purchase Order ID' },
|
||||
},
|
||||
required: ['poId'],
|
||||
},
|
||||
handler: async (params: { poId: string }) => {
|
||||
const po = await client.get<PurchaseOrder>(
|
||||
`/inventory/purchase-orders/${params.poId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(po, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_inventory_reorder_report',
|
||||
description: 'Get a report of parts that need reordering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
category: { type: 'string', description: 'Filter by category' },
|
||||
},
|
||||
},
|
||||
handler: async (params: { category?: string }) => {
|
||||
const report = await client.get<{ data: InventoryPart[] }>(
|
||||
'/inventory/reorder-report',
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
207
servers/fieldedge/src/tools/invoices-tools.ts
Normal file
207
servers/fieldedge/src/tools/invoices-tools.ts
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* FieldEdge Invoices Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { Invoice, Payment, PaginationParams } from '../types.js';
|
||||
|
||||
export function createInvoicesTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_invoices_list',
|
||||
description: 'List all invoices with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by invoice status',
|
||||
enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'],
|
||||
},
|
||||
customerId: { type: 'string', description: 'Filter by customer ID' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
status?: string;
|
||||
customerId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<Invoice>('/invoices', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_invoices_get',
|
||||
description: 'Get detailed information about a specific invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
},
|
||||
required: ['invoiceId'],
|
||||
},
|
||||
handler: async (params: { invoiceId: string }) => {
|
||||
const invoice = await client.get<Invoice>(`/invoices/${params.invoiceId}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(invoice, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_invoices_create',
|
||||
description: 'Create a new invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
jobId: { type: 'string', description: 'Related job ID' },
|
||||
invoiceDate: { type: 'string', description: 'Invoice date (ISO 8601)' },
|
||||
dueDate: { type: 'string', description: 'Due date (ISO 8601)' },
|
||||
terms: { type: 'string', description: 'Payment terms' },
|
||||
notes: { type: 'string', description: 'Invoice notes' },
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
customerId: string;
|
||||
jobId?: string;
|
||||
invoiceDate?: string;
|
||||
dueDate?: string;
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const invoice = await client.post<Invoice>('/invoices', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(invoice, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_invoices_update',
|
||||
description: 'Update an existing invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'],
|
||||
},
|
||||
dueDate: { type: 'string' },
|
||||
terms: { type: 'string' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['invoiceId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
invoiceId: string;
|
||||
status?: string;
|
||||
dueDate?: string;
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { invoiceId, ...updateData } = params;
|
||||
const invoice = await client.patch<Invoice>(
|
||||
`/invoices/${invoiceId}`,
|
||||
updateData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(invoice, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_invoices_payments_list',
|
||||
description: 'List all payments for an invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
},
|
||||
required: ['invoiceId'],
|
||||
},
|
||||
handler: async (params: { invoiceId: string }) => {
|
||||
const payments = await client.get<{ data: Payment[] }>(
|
||||
`/invoices/${params.invoiceId}/payments`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(payments, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_invoices_payments_add',
|
||||
description: 'Add a payment to an invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
amount: { type: 'number', description: 'Payment amount' },
|
||||
paymentMethod: {
|
||||
type: 'string',
|
||||
description: 'Payment method',
|
||||
enum: ['cash', 'check', 'credit_card', 'ach', 'other'],
|
||||
},
|
||||
paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' },
|
||||
referenceNumber: { type: 'string', description: 'Reference/check number' },
|
||||
notes: { type: 'string', description: 'Payment notes' },
|
||||
},
|
||||
required: ['invoiceId', 'amount', 'paymentMethod'],
|
||||
},
|
||||
handler: async (params: {
|
||||
invoiceId: string;
|
||||
amount: number;
|
||||
paymentMethod: string;
|
||||
paymentDate?: string;
|
||||
referenceNumber?: string;
|
||||
notes?: string;
|
||||
}) => {
|
||||
const { invoiceId, ...paymentData } = params;
|
||||
const payment = await client.post<Payment>(
|
||||
`/invoices/${invoiceId}/payments`,
|
||||
paymentData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(payment, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
192
servers/fieldedge/src/tools/payments.ts
Normal file
192
servers/fieldedge/src/tools/payments.ts
Normal file
@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Payment Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { FieldEdgeClient } from '../clients/fieldedge.js';
|
||||
|
||||
export const paymentTools = [
|
||||
{
|
||||
name: 'fieldedge_list_payments',
|
||||
description: 'List all payments with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
status: { type: 'string', enum: ['pending', 'processed', 'failed', 'refunded'] },
|
||||
customerId: { type: 'string', description: 'Filter by customer' },
|
||||
invoiceId: { type: 'string', description: 'Filter by invoice' },
|
||||
paymentMethod: {
|
||||
type: 'string',
|
||||
enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'],
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const result = await client.getPayments(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_payment',
|
||||
description: 'Get detailed information about a specific payment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paymentId: { type: 'string', description: 'Payment ID' },
|
||||
},
|
||||
required: ['paymentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const payment = await client.getPayment(args.paymentId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(payment, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_create_payment',
|
||||
description: 'Create a new payment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
amount: { type: 'number', description: 'Payment amount' },
|
||||
paymentMethod: {
|
||||
type: 'string',
|
||||
enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'],
|
||||
description: 'Payment method',
|
||||
},
|
||||
paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' },
|
||||
reference: { type: 'string', description: 'Reference number (confirmation, check number, etc.)' },
|
||||
checkNumber: { type: 'string', description: 'Check number (if applicable)' },
|
||||
cardLast4: { type: 'string', description: 'Last 4 digits of card (if applicable)' },
|
||||
notes: { type: 'string', description: 'Additional notes' },
|
||||
},
|
||||
required: ['invoiceId', 'customerId', 'amount', 'paymentMethod', 'paymentDate'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const payment = await client.createPayment(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Payment created successfully:\n${JSON.stringify(payment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_update_payment',
|
||||
description: 'Update an existing payment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paymentId: { type: 'string', description: 'Payment ID' },
|
||||
reference: { type: 'string' },
|
||||
notes: { type: 'string' },
|
||||
},
|
||||
required: ['paymentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const { paymentId, ...updateData } = args;
|
||||
const payment = await client.updatePayment(paymentId, updateData);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Payment updated successfully:\n${JSON.stringify(payment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_delete_payment',
|
||||
description: 'Delete a payment',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paymentId: { type: 'string', description: 'Payment ID' },
|
||||
},
|
||||
required: ['paymentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
await client.deletePayment(args.paymentId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Payment ${args.paymentId} deleted successfully`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_refund_payment',
|
||||
description: 'Refund a payment (full or partial)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
paymentId: { type: 'string', description: 'Payment ID' },
|
||||
amount: { type: 'number', description: 'Refund amount (optional, full refund if not specified)' },
|
||||
},
|
||||
required: ['paymentId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const payment = await client.refundPayment(args.paymentId, args.amount);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Payment refunded:\n${JSON.stringify(payment, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_payments_by_invoice',
|
||||
description: 'Get all payments for a specific invoice',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoiceId: { type: 'string', description: 'Invoice ID' },
|
||||
},
|
||||
required: ['invoiceId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const payments = await client.getPaymentsByInvoice(args.invoiceId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Found ${payments.length} payments:\n${JSON.stringify(payments, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_payments_by_customer',
|
||||
description: 'Get all payments for a specific customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const payments = await client.getPaymentsByCustomer(args.customerId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Found ${payments.length} payments:\n${JSON.stringify(payments, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
237
servers/fieldedge/src/tools/reporting-tools.ts
Normal file
237
servers/fieldedge/src/tools/reporting-tools.ts
Normal file
@ -0,0 +1,237 @@
|
||||
/**
|
||||
* FieldEdge Reporting Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import {
|
||||
RevenueReport,
|
||||
JobProfitabilityReport,
|
||||
TechnicianPerformance,
|
||||
AgingReport,
|
||||
} from '../types.js';
|
||||
|
||||
export function createReportingTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_reports_revenue',
|
||||
description: 'Get revenue report for a specified period',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
groupBy: {
|
||||
type: 'string',
|
||||
description: 'Group results by dimension',
|
||||
enum: ['day', 'week', 'month', 'jobType', 'technician', 'customer'],
|
||||
},
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (params: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
groupBy?: string;
|
||||
}) => {
|
||||
const report = await client.get<RevenueReport>('/reports/revenue', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_reports_job_profitability',
|
||||
description: 'Get profitability analysis for a specific job or all jobs in a period',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
jobId: { type: 'string', description: 'Specific job ID (optional)' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
minMargin: {
|
||||
type: 'number',
|
||||
description: 'Filter jobs with profit margin above this percentage',
|
||||
},
|
||||
maxMargin: {
|
||||
type: 'number',
|
||||
description: 'Filter jobs with profit margin below this percentage',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: {
|
||||
jobId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
minMargin?: number;
|
||||
maxMargin?: number;
|
||||
}) => {
|
||||
const report = await client.get<
|
||||
JobProfitabilityReport | { data: JobProfitabilityReport[] }
|
||||
>('/reports/job-profitability', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_reports_technician_performance',
|
||||
description: 'Get performance metrics for all technicians or a specific technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Specific technician ID (optional)' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
sortBy: {
|
||||
type: 'string',
|
||||
description: 'Sort results by metric',
|
||||
enum: ['revenue', 'jobsCompleted', 'efficiency', 'customerSatisfaction'],
|
||||
},
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (params: {
|
||||
technicianId?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
sortBy?: string;
|
||||
}) => {
|
||||
const report = await client.get<
|
||||
TechnicianPerformance | { data: TechnicianPerformance[] }
|
||||
>('/reports/technician-performance', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_reports_aging',
|
||||
description: 'Get accounts receivable aging report',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
asOfDate: {
|
||||
type: 'string',
|
||||
description: 'As-of date for the report (ISO 8601, defaults to today)',
|
||||
},
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'Filter by specific customer ID',
|
||||
},
|
||||
minAmount: {
|
||||
type: 'number',
|
||||
description: 'Show only customers with balance above this amount',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: {
|
||||
asOfDate?: string;
|
||||
customerId?: string;
|
||||
minAmount?: number;
|
||||
}) => {
|
||||
const report = await client.get<AgingReport>('/reports/aging', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_reports_service_agreement_revenue',
|
||||
description: 'Get revenue breakdown from service agreements/memberships',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
agreementType: { type: 'string', description: 'Filter by agreement type' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (params: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
agreementType?: string;
|
||||
}) => {
|
||||
const report = await client.get<{
|
||||
period: string;
|
||||
totalRevenue: number;
|
||||
activeAgreements: number;
|
||||
newAgreements: number;
|
||||
cancelledAgreements: number;
|
||||
renewalRate: number;
|
||||
byType: Record<string, number>;
|
||||
}>('/reports/agreement-revenue', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_reports_equipment_service_due',
|
||||
description: 'Get report of equipment due for service',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
daysAhead: {
|
||||
type: 'number',
|
||||
description: 'Look ahead this many days (default: 30)',
|
||||
},
|
||||
customerId: { type: 'string', description: 'Filter by customer ID' },
|
||||
equipmentType: { type: 'string', description: 'Filter by equipment type' },
|
||||
},
|
||||
},
|
||||
handler: async (params: {
|
||||
daysAhead?: number;
|
||||
customerId?: string;
|
||||
equipmentType?: string;
|
||||
}) => {
|
||||
const report = await client.get<{
|
||||
data: Array<{
|
||||
equipmentId: string;
|
||||
customerId: string;
|
||||
customerName: string;
|
||||
equipmentType: string;
|
||||
model: string;
|
||||
lastServiceDate: string;
|
||||
nextServiceDate: string;
|
||||
daysUntilDue: number;
|
||||
isOverdue: boolean;
|
||||
}>;
|
||||
}>('/reports/equipment-service-due', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
217
servers/fieldedge/src/tools/reporting.ts
Normal file
217
servers/fieldedge/src/tools/reporting.ts
Normal file
@ -0,0 +1,217 @@
|
||||
/**
|
||||
* Reporting and Analytics Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { FieldEdgeClient } from '../clients/fieldedge.js';
|
||||
|
||||
export const reportingTools = [
|
||||
{
|
||||
name: 'fieldedge_list_reports',
|
||||
description: 'List all available reports',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'revenue',
|
||||
'technician-productivity',
|
||||
'job-completion',
|
||||
'customer-satisfaction',
|
||||
'inventory-valuation',
|
||||
'aging-receivables',
|
||||
'sales-by-category',
|
||||
'equipment-maintenance',
|
||||
'custom',
|
||||
],
|
||||
description: 'Filter by report type',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const reports = await client.getReports(args.type);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(reports, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_report',
|
||||
description: 'Get a specific report by ID',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportId: { type: 'string', description: 'Report ID' },
|
||||
},
|
||||
required: ['reportId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getReport(args.reportId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_generate_report',
|
||||
description: 'Generate a new report',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'revenue',
|
||||
'technician-productivity',
|
||||
'job-completion',
|
||||
'customer-satisfaction',
|
||||
'inventory-valuation',
|
||||
'aging-receivables',
|
||||
'sales-by-category',
|
||||
'equipment-maintenance',
|
||||
],
|
||||
description: 'Report type',
|
||||
},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
description: 'Report parameters (varies by type)',
|
||||
},
|
||||
},
|
||||
required: ['type'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.generateReport(args.type, args.parameters);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Report generated:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_revenue_report',
|
||||
description: 'Get revenue report for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getRevenueReport(args.startDate, args.endDate);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Revenue Report (${args.startDate} to ${args.endDate}):\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_technician_productivity_report',
|
||||
description: 'Get technician productivity report for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getTechnicianProductivityReport(args.startDate, args.endDate);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Technician Productivity Report:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_job_completion_report',
|
||||
description: 'Get job completion statistics for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getJobCompletionReport(args.startDate, args.endDate);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Job Completion Report:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_aging_receivables_report',
|
||||
description: 'Get aging receivables report showing outstanding invoices by age',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getAgingReceivablesReport();
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Aging Receivables Report:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_inventory_valuation_report',
|
||||
description: 'Get inventory valuation report showing current inventory value',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getInventoryValuationReport();
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Inventory Valuation Report:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_sales_by_category_report',
|
||||
description: 'Get sales breakdown by category for a date range',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const report = await client.getSalesByCategoryReport(args.startDate, args.endDate);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Sales by Category Report:\n${JSON.stringify(report, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
234
servers/fieldedge/src/tools/technicians-tools.ts
Normal file
234
servers/fieldedge/src/tools/technicians-tools.ts
Normal file
@ -0,0 +1,234 @@
|
||||
/**
|
||||
* FieldEdge Technicians Tools
|
||||
*/
|
||||
|
||||
import { FieldEdgeClient } from '../client.js';
|
||||
import { Technician, TechnicianPerformance, TimeEntry, PaginationParams } from '../types.js';
|
||||
|
||||
export function createTechniciansTools(client: FieldEdgeClient) {
|
||||
return [
|
||||
{
|
||||
name: 'fieldedge_technicians_list',
|
||||
description: 'List all technicians with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Filter by technician status',
|
||||
enum: ['active', 'inactive', 'on_leave'],
|
||||
},
|
||||
role: { type: 'string', description: 'Filter by role' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
status?: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
const result = await client.getPaginated<Technician>('/technicians', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_technicians_get',
|
||||
description: 'Get detailed information about a specific technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (params: { technicianId: string }) => {
|
||||
const technician = await client.get<Technician>(
|
||||
`/technicians/${params.technicianId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(technician, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_technicians_create',
|
||||
description: 'Create a new technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
firstName: { type: 'string', description: 'First name' },
|
||||
lastName: { type: 'string', description: 'Last name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
phone: { type: 'string', description: 'Phone number' },
|
||||
role: { type: 'string', description: 'Job role/title' },
|
||||
hourlyRate: { type: 'number', description: 'Hourly rate' },
|
||||
hireDate: { type: 'string', description: 'Hire date (ISO 8601)' },
|
||||
certifications: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of certifications',
|
||||
},
|
||||
skills: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'List of skills',
|
||||
},
|
||||
},
|
||||
required: ['firstName', 'lastName'],
|
||||
},
|
||||
handler: async (params: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
role?: string;
|
||||
hourlyRate?: number;
|
||||
hireDate?: string;
|
||||
certifications?: string[];
|
||||
skills?: string[];
|
||||
}) => {
|
||||
const technician = await client.post<Technician>('/technicians', params);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(technician, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_technicians_update',
|
||||
description: 'Update an existing technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
firstName: { type: 'string' },
|
||||
lastName: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
phone: { type: 'string' },
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive', 'on_leave'],
|
||||
},
|
||||
role: { type: 'string' },
|
||||
hourlyRate: { type: 'number' },
|
||||
certifications: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
skills: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
technicianId: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
status?: string;
|
||||
role?: string;
|
||||
hourlyRate?: number;
|
||||
certifications?: string[];
|
||||
skills?: string[];
|
||||
}) => {
|
||||
const { technicianId, ...updateData } = params;
|
||||
const technician = await client.patch<Technician>(
|
||||
`/technicians/${technicianId}`,
|
||||
updateData
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(technician, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_technicians_performance_get',
|
||||
description: 'Get performance metrics for a technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
startDate: { type: 'string', description: 'Period start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'Period end date (ISO 8601)' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (params: {
|
||||
technicianId: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const performance = await client.get<TechnicianPerformance>(
|
||||
`/technicians/${params.technicianId}/performance`,
|
||||
params
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(performance, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_technicians_time_entries_list',
|
||||
description: 'List time entries for a technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
startDate: { type: 'string', description: 'Start date (ISO 8601)' },
|
||||
endDate: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (params: PaginationParams & {
|
||||
technicianId: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const { technicianId, ...queryParams } = params;
|
||||
const result = await client.getPaginated<TimeEntry>(
|
||||
`/technicians/${technicianId}/time-entries`,
|
||||
queryParams
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
195
servers/fieldedge/src/tools/technicians.ts
Normal file
195
servers/fieldedge/src/tools/technicians.ts
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Technician Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { FieldEdgeClient } from '../clients/fieldedge.js';
|
||||
|
||||
export const technicianTools = [
|
||||
{
|
||||
name: 'fieldedge_list_technicians',
|
||||
description: 'List all technicians with optional filtering',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: { type: 'number' },
|
||||
pageSize: { type: 'number' },
|
||||
status: { type: 'string', enum: ['active', 'inactive', 'on-leave'], description: 'Filter by status' },
|
||||
role: { type: 'string', description: 'Filter by role' },
|
||||
skills: { type: 'array', items: { type: 'string' }, description: 'Filter by skills' },
|
||||
},
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const result = await client.getTechnicians(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_technician',
|
||||
description: 'Get detailed information about a specific technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const technician = await client.getTechnician(args.technicianId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(technician, null, 2),
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_create_technician',
|
||||
description: 'Create a new technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employeeNumber: { type: 'string', description: 'Employee number' },
|
||||
firstName: { type: 'string', description: 'First name' },
|
||||
lastName: { type: 'string', description: 'Last name' },
|
||||
email: { type: 'string', description: 'Email address' },
|
||||
phone: { type: 'string', description: 'Phone number' },
|
||||
role: { type: 'string', description: 'Role/title' },
|
||||
skills: { type: 'array', items: { type: 'string' }, description: 'Skills (e.g., ["HVAC", "Electrical"])' },
|
||||
certifications: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
number: { type: 'string' },
|
||||
issuer: { type: 'string' },
|
||||
issueDate: { type: 'string' },
|
||||
expiryDate: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
hourlyRate: { type: 'number', description: 'Hourly rate' },
|
||||
overtimeRate: { type: 'number', description: 'Overtime rate' },
|
||||
serviceRadius: { type: 'number', description: 'Service radius in miles' },
|
||||
homeAddress: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street1: { type: 'string' },
|
||||
city: { type: 'string' },
|
||||
state: { type: 'string' },
|
||||
zip: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['employeeNumber', 'firstName', 'lastName', 'email', 'phone', 'role'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const technician = await client.createTechnician(args);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Technician created successfully:\n${JSON.stringify(technician, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_update_technician',
|
||||
description: 'Update an existing technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
email: { type: 'string' },
|
||||
phone: { type: 'string' },
|
||||
role: { type: 'string' },
|
||||
skills: { type: 'array', items: { type: 'string' } },
|
||||
certifications: { type: 'array' },
|
||||
hourlyRate: { type: 'number' },
|
||||
overtimeRate: { type: 'number' },
|
||||
serviceRadius: { type: 'number' },
|
||||
homeAddress: { type: 'object' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const { technicianId, ...updateData } = args;
|
||||
const technician = await client.updateTechnician(technicianId, updateData);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Technician updated successfully:\n${JSON.stringify(technician, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_delete_technician',
|
||||
description: 'Delete a technician',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
},
|
||||
required: ['technicianId'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
await client.deleteTechnician(args.technicianId);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Technician ${args.technicianId} deleted successfully`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_get_available_technicians',
|
||||
description: 'Get technicians available on a specific date, optionally filtered by skills',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: { type: 'string', description: 'Date (YYYY-MM-DD)' },
|
||||
skills: { type: 'array', items: { type: 'string' }, description: 'Required skills (optional)' },
|
||||
},
|
||||
required: ['date'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const technicians = await client.getAvailableTechnicians(args.date, args.skills);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Available technicians (${technicians.length}):\n${JSON.stringify(technicians, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fieldedge_update_technician_status',
|
||||
description: 'Update a technician\'s status',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
technicianId: { type: 'string', description: 'Technician ID' },
|
||||
status: { type: 'string', enum: ['active', 'inactive', 'on-leave'], description: 'New status' },
|
||||
},
|
||||
required: ['technicianId', 'status'],
|
||||
},
|
||||
handler: async (args: any, client: FieldEdgeClient) => {
|
||||
const technician = await client.updateTechnicianStatus(args.technicianId, args.status);
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `Technician status updated to ${args.status}:\n${JSON.stringify(technician, null, 2)}`,
|
||||
}],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
|
||||
296
servers/lightspeed/README.md
Normal file
296
servers/lightspeed/README.md
Normal file
@ -0,0 +1,296 @@
|
||||
# Lightspeed Retail MCP Server
|
||||
|
||||
Complete Model Context Protocol (MCP) server for Lightspeed Retail (X-Series/R-Series). Provides comprehensive point-of-sale, inventory management, and retail analytics capabilities for Claude Desktop and other MCP clients.
|
||||
|
||||
## Features
|
||||
|
||||
### 🛍️ Products & Inventory
|
||||
- List, create, update, and delete products
|
||||
- Manage product variants and images
|
||||
- Track inventory levels across shops
|
||||
- Handle stock transfers and adjustments
|
||||
- Purchase order management
|
||||
- Supplier/vendor management
|
||||
|
||||
### 💰 Sales & Transactions
|
||||
- Create and manage sales transactions
|
||||
- Add line items to sales
|
||||
- Process payments and refunds
|
||||
- Track completed and pending sales
|
||||
- Register/till management (open/close)
|
||||
|
||||
### 👥 Customers
|
||||
- Customer database management
|
||||
- Search and filter customers
|
||||
- Track purchase history
|
||||
- Loyalty program integration
|
||||
- Contact information management
|
||||
|
||||
### 👔 Employees
|
||||
- Employee roster management
|
||||
- Time clock functionality (clock in/out)
|
||||
- Employee sales performance tracking
|
||||
- Role-based access control
|
||||
|
||||
### 📊 Reporting
|
||||
- Sales summary reports
|
||||
- Inventory valuation
|
||||
- Product performance analysis
|
||||
- Employee sales reports
|
||||
- Custom date range filtering
|
||||
|
||||
### ⚙️ Configuration
|
||||
- Product categories and hierarchies
|
||||
- Discount and promotion management
|
||||
- Tax class configuration
|
||||
- Multi-location support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @mcpengine/lightspeed-mcp-server
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
export LIGHTSPEED_ACCOUNT_ID=your_account_id
|
||||
export LIGHTSPEED_ACCESS_TOKEN=your_oauth_token
|
||||
# Optional: Custom API URL (defaults to official Lightspeed API)
|
||||
export LIGHTSPEED_API_URL=https://api.lightspeedapp.com/API/V3/Account/YOUR_ACCOUNT
|
||||
```
|
||||
|
||||
### Claude Desktop Configuration
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lightspeed": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@mcpengine/lightspeed-mcp-server"],
|
||||
"env": {
|
||||
"LIGHTSPEED_ACCOUNT_ID": "your_account_id",
|
||||
"LIGHTSPEED_ACCESS_TOKEN": "your_oauth_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OAuth Setup
|
||||
|
||||
Lightspeed uses OAuth2 for authentication. To get your access token:
|
||||
|
||||
1. **Create Lightspeed API Application**
|
||||
- Go to Lightspeed Developer Portal
|
||||
- Register a new application
|
||||
- Note your Client ID and Client Secret
|
||||
|
||||
2. **Generate Access Token**
|
||||
```bash
|
||||
curl -X POST https://cloud.lightspeedapp.com/oauth/access_token.php \
|
||||
-d "client_id=YOUR_CLIENT_ID" \
|
||||
-d "client_secret=YOUR_CLIENT_SECRET" \
|
||||
-d "code=AUTHORIZATION_CODE" \
|
||||
-d "grant_type=authorization_code"
|
||||
```
|
||||
|
||||
3. **Get Account ID**
|
||||
- Available in Lightspeed admin panel
|
||||
- Or via API: `GET https://api.lightspeedapp.com/API/Account.json`
|
||||
|
||||
## Tools (54 Total)
|
||||
|
||||
### Products (8 tools)
|
||||
- `lightspeed_list_products` - List all products
|
||||
- `lightspeed_get_product` - Get product details
|
||||
- `lightspeed_create_product` - Create new product
|
||||
- `lightspeed_update_product` - Update product
|
||||
- `lightspeed_delete_product` - Delete/archive product
|
||||
- `lightspeed_list_product_variants` - List product variants
|
||||
- `lightspeed_list_product_images` - List product images
|
||||
- `lightspeed_update_product_inventory` - Update inventory quantity
|
||||
|
||||
### Sales (8 tools)
|
||||
- `lightspeed_list_sales` - List sales transactions
|
||||
- `lightspeed_get_sale` - Get sale details
|
||||
- `lightspeed_create_sale` - Create new sale
|
||||
- `lightspeed_add_sale_line_item` - Add item to sale
|
||||
- `lightspeed_list_sale_payments` - List sale payments
|
||||
- `lightspeed_process_payment` - Process payment
|
||||
- `lightspeed_refund_sale` - Create refund
|
||||
- `lightspeed_complete_sale` - Mark sale as complete
|
||||
|
||||
### Customers (7 tools)
|
||||
- `lightspeed_list_customers` - List all customers
|
||||
- `lightspeed_get_customer` - Get customer details
|
||||
- `lightspeed_create_customer` - Create new customer
|
||||
- `lightspeed_update_customer` - Update customer
|
||||
- `lightspeed_delete_customer` - Delete customer
|
||||
- `lightspeed_search_customers` - Search customers
|
||||
- `lightspeed_get_customer_loyalty` - Get loyalty info
|
||||
|
||||
### Inventory (8 tools)
|
||||
- `lightspeed_list_inventory` - List inventory counts
|
||||
- `lightspeed_get_item_inventory` - Get item inventory
|
||||
- `lightspeed_update_inventory_count` - Update inventory
|
||||
- `lightspeed_transfer_stock` - Transfer between shops
|
||||
- `lightspeed_list_inventory_adjustments` - List adjustments
|
||||
- `lightspeed_list_suppliers` - List suppliers/vendors
|
||||
- `lightspeed_create_purchase_order` - Create PO
|
||||
- `lightspeed_list_purchase_orders` - List POs
|
||||
|
||||
### Registers (5 tools)
|
||||
- `lightspeed_list_registers` - List all registers
|
||||
- `lightspeed_get_register` - Get register details
|
||||
- `lightspeed_open_register` - Open register (till)
|
||||
- `lightspeed_close_register` - Close register
|
||||
- `lightspeed_get_cash_counts` - Get cash counts
|
||||
|
||||
### Employees (6 tools)
|
||||
- `lightspeed_list_employees` - List all employees
|
||||
- `lightspeed_get_employee` - Get employee details
|
||||
- `lightspeed_create_employee` - Create employee
|
||||
- `lightspeed_update_employee` - Update employee
|
||||
- `lightspeed_list_time_entries` - List time entries
|
||||
- `lightspeed_clock_in` - Clock in employee
|
||||
- `lightspeed_clock_out` - Clock out employee
|
||||
|
||||
### Categories (5 tools)
|
||||
- `lightspeed_list_categories` - List categories
|
||||
- `lightspeed_get_category` - Get category details
|
||||
- `lightspeed_create_category` - Create category
|
||||
- `lightspeed_update_category` - Update category
|
||||
- `lightspeed_delete_category` - Delete category
|
||||
|
||||
### Discounts (5 tools)
|
||||
- `lightspeed_list_discounts` - List discounts
|
||||
- `lightspeed_get_discount` - Get discount details
|
||||
- `lightspeed_create_discount` - Create discount
|
||||
- `lightspeed_update_discount` - Update discount
|
||||
- `lightspeed_delete_discount` - Delete discount
|
||||
|
||||
### Taxes (4 tools)
|
||||
- `lightspeed_list_taxes` - List tax classes
|
||||
- `lightspeed_get_tax` - Get tax details
|
||||
- `lightspeed_create_tax` - Create tax class
|
||||
- `lightspeed_update_tax` - Update tax class
|
||||
|
||||
### Reporting (4 tools)
|
||||
- `lightspeed_sales_summary` - Sales summary report
|
||||
- `lightspeed_inventory_value` - Inventory valuation
|
||||
- `lightspeed_product_performance` - Product performance
|
||||
- `lightspeed_employee_sales` - Employee sales report
|
||||
|
||||
## MCP Apps (17 Apps)
|
||||
|
||||
Pre-built UI applications accessible via MCP prompts:
|
||||
|
||||
### Products
|
||||
- `product-dashboard` - Inventory overview
|
||||
- `product-detail` - Detailed product view
|
||||
- `product-grid` - Filterable product list
|
||||
- `category-manager` - Category management
|
||||
|
||||
### Sales
|
||||
- `sales-dashboard` - Sales overview
|
||||
- `sales-detail` - Transaction details
|
||||
- `sales-report` - Sales analytics
|
||||
|
||||
### Customers
|
||||
- `customer-detail` - Customer profile
|
||||
- `customer-grid` - Customer list
|
||||
|
||||
### Inventory
|
||||
- `inventory-tracker` - Stock levels
|
||||
- `inventory-adjustments` - Adjustment tracking
|
||||
- `purchase-orders` - PO management
|
||||
|
||||
### Operations
|
||||
- `register-manager` - Register management
|
||||
- `employee-dashboard` - Employee overview
|
||||
- `discount-manager` - Discount configuration
|
||||
- `tax-settings` - Tax configuration
|
||||
- `product-performance` - Performance analytics
|
||||
|
||||
## Example Usage
|
||||
|
||||
```typescript
|
||||
// In Claude Desktop, you can now use natural language:
|
||||
|
||||
"Show me the product dashboard"
|
||||
"Create a new customer named John Doe with email john@example.com"
|
||||
"List all sales from yesterday"
|
||||
"What are the top 10 selling products this month?"
|
||||
"Transfer 50 units of item #123 from shop 1 to shop 2"
|
||||
"Generate a sales summary report for last week"
|
||||
"Clock in employee #456"
|
||||
"Show me inventory levels for shop 1"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Lightspeed API Documentation
|
||||
- [Official API Docs](https://developers.lightspeedhq.com/retail/introduction/introduction/)
|
||||
- [Authentication](https://developers.lightspeedhq.com/retail/authentication/authentication/)
|
||||
- [Rate Limits](https://developers.lightspeedhq.com/retail/introduction/rate-limiting/)
|
||||
|
||||
### Rate Limiting
|
||||
- Default: 10 requests/second per account
|
||||
- Burst: up to 60 requests
|
||||
- This server includes automatic rate limit handling and retry logic
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/BusyBee3333/mcpengine.git
|
||||
cd mcpengine/servers/lightspeed
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run locally
|
||||
export LIGHTSPEED_ACCOUNT_ID=your_account_id
|
||||
export LIGHTSPEED_ACCESS_TOKEN=your_token
|
||||
npm start
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Errors
|
||||
- Verify your Account ID and Access Token are correct
|
||||
- Check if your OAuth token has expired (Lightspeed tokens expire)
|
||||
- Ensure your API application has the required scopes
|
||||
|
||||
### API Errors
|
||||
- Check Lightspeed API status page
|
||||
- Verify rate limits haven't been exceeded
|
||||
- Ensure your account has access to the requested resources
|
||||
|
||||
### Connection Issues
|
||||
- Verify network connectivity
|
||||
- Check firewall settings
|
||||
- Ensure API URL is correct (if using custom URL)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
- GitHub Issues: [mcpengine/issues](https://github.com/BusyBee3333/mcpengine/issues)
|
||||
- Lightspeed Support: [support.lightspeedhq.com](https://support.lightspeedhq.com)
|
||||
|
||||
## Related Resources
|
||||
|
||||
- [Lightspeed Developer Portal](https://developers.lightspeedhq.com/)
|
||||
- [MCP Protocol Specification](https://modelcontextprotocol.io)
|
||||
- [Claude Desktop Documentation](https://claude.ai/desktop)
|
||||
@ -1,39 +1,42 @@
|
||||
{
|
||||
"name": "@mcpengine/lightspeed-mcp-server",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP server for Lightspeed Retail (X-Series/R-Series) - complete POS and inventory management",
|
||||
"main": "dist/main.js",
|
||||
"description": "MCP server for Lightspeed POS and eCommerce platform - retail and restaurant management",
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"lightspeed-mcp": "./dist/main.js"
|
||||
},
|
||||
"main": "./dist/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"build": "tsc && npm run build:apps",
|
||||
"build:apps": "node build-apps.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"lightspeed",
|
||||
"retail",
|
||||
"pos",
|
||||
"inventory",
|
||||
"sales",
|
||||
"ecommerce"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3"
|
||||
"@types/node": "^20.14.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"lightspeed",
|
||||
"pos",
|
||||
"ecommerce",
|
||||
"retail",
|
||||
"restaurant",
|
||||
"inventory",
|
||||
"sales"
|
||||
]
|
||||
}
|
||||
|
||||
532
servers/lightspeed/src/apps/index.ts
Normal file
532
servers/lightspeed/src/apps/index.ts
Normal file
@ -0,0 +1,532 @@
|
||||
/**
|
||||
* Lightspeed MCP Apps
|
||||
* UI applications for Claude Desktop and other MCP clients
|
||||
*/
|
||||
|
||||
export const apps = {
|
||||
// Product Management Apps
|
||||
'product-dashboard': {
|
||||
name: 'Product Dashboard',
|
||||
description: 'Overview of inventory, stock levels, and product categories',
|
||||
category: 'products',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const products = await client.getAll('/Item', 'Item', 50);
|
||||
const categories = await client.getAll('/Category', 'Category', 100);
|
||||
|
||||
return `# 📦 Product Dashboard
|
||||
|
||||
## Inventory Summary
|
||||
- **Total Products**: ${products.length}
|
||||
- **Categories**: ${categories.length}
|
||||
|
||||
## Recent Products
|
||||
${products.slice(0, 10).map((p: any) =>
|
||||
`- **${p.description}** (SKU: ${p.systemSku}) - $${p.defaultCost}`
|
||||
).join('\n')}
|
||||
|
||||
## Categories
|
||||
${categories.slice(0, 5).map((c: any) =>
|
||||
`- ${c.name} (ID: ${c.categoryID})`
|
||||
).join('\n')}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'product-detail': {
|
||||
name: 'Product Detail',
|
||||
description: 'Detailed view of a specific product',
|
||||
category: 'products',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: { type: 'string', description: 'Product item ID' },
|
||||
},
|
||||
required: ['itemId'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
const result = await client.getById('/Item', args.itemId);
|
||||
const product = result.Item;
|
||||
|
||||
return `# 📦 ${product.description}
|
||||
|
||||
## Product Information
|
||||
- **Item ID**: ${product.itemID}
|
||||
- **System SKU**: ${product.systemSku}
|
||||
- **Custom SKU**: ${product.customSku || 'N/A'}
|
||||
- **Default Cost**: $${product.defaultCost}
|
||||
- **Category ID**: ${product.categoryID}
|
||||
- **Tax**: ${product.tax ? 'Yes' : 'No'}
|
||||
- **Discountable**: ${product.discountable ? 'Yes' : 'No'}
|
||||
- **Archived**: ${product.archived ? 'Yes' : 'No'}
|
||||
|
||||
## Pricing
|
||||
${product.Prices?.ItemPrice ? product.Prices.ItemPrice.map((price: any) =>
|
||||
`- **${price.useType}**: $${price.amount}`
|
||||
).join('\n') : 'No pricing information'}
|
||||
|
||||
## Details
|
||||
- **Created**: ${product.createTime}
|
||||
- **Last Updated**: ${product.timeStamp}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'product-grid': {
|
||||
name: 'Product Grid',
|
||||
description: 'Filterable grid view of all products',
|
||||
category: 'products',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
categoryId: { type: 'string', description: 'Filter by category ID' },
|
||||
archived: { type: 'boolean', description: 'Include archived products' },
|
||||
limit: { type: 'number', description: 'Max products to show' },
|
||||
},
|
||||
},
|
||||
handler: async (client: any, args?: any) => {
|
||||
const params: any = {};
|
||||
if (args?.categoryId) params.categoryID = args.categoryId;
|
||||
if (args?.archived !== undefined) params.archived = args.archived;
|
||||
|
||||
const products = await client.getAll('/Item', 'Item', args?.limit || 100);
|
||||
|
||||
return `# 📊 Product Grid
|
||||
|
||||
**Total Products**: ${products.length}
|
||||
|
||||
| SKU | Description | Cost | Category | Status |
|
||||
|-----|-------------|------|----------|--------|
|
||||
${products.map((p: any) =>
|
||||
`| ${p.systemSku} | ${p.description} | $${p.defaultCost} | ${p.categoryID} | ${p.archived ? '🗄️ Archived' : '✅ Active'} |`
|
||||
).join('\n')}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'sales-dashboard': {
|
||||
name: 'Sales Dashboard',
|
||||
description: 'Overview of recent sales and transactions',
|
||||
category: 'sales',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const sales = await client.getAll('/Sale', 'Sale', 50);
|
||||
const completed = sales.filter((s: any) => s.completed && !s.voided);
|
||||
const totalRevenue = completed.reduce((sum: number, s: any) =>
|
||||
sum + parseFloat(s.calcTotal || '0'), 0
|
||||
);
|
||||
|
||||
return `# 💰 Sales Dashboard
|
||||
|
||||
## Summary
|
||||
- **Total Transactions**: ${completed.length}
|
||||
- **Total Revenue**: $${totalRevenue.toFixed(2)}
|
||||
- **Average Transaction**: $${(totalRevenue / (completed.length || 1)).toFixed(2)}
|
||||
|
||||
## Recent Sales
|
||||
${completed.slice(0, 10).map((s: any) =>
|
||||
`- **Sale #${s.saleID}** - $${s.calcTotal} (${new Date(s.createTime).toLocaleDateString()})`
|
||||
).join('\n')}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'sales-detail': {
|
||||
name: 'Sale Detail',
|
||||
description: 'Detailed view of a specific sale transaction',
|
||||
category: 'sales',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
saleId: { type: 'string', description: 'Sale ID' },
|
||||
},
|
||||
required: ['saleId'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
const result = await client.getById('/Sale', args.saleId);
|
||||
const sale = result.Sale;
|
||||
|
||||
const lines = sale.SaleLines?.SaleLine
|
||||
? (Array.isArray(sale.SaleLines.SaleLine)
|
||||
? sale.SaleLines.SaleLine
|
||||
: [sale.SaleLines.SaleLine])
|
||||
: [];
|
||||
|
||||
const payments = sale.SalePayments?.SalePayment
|
||||
? (Array.isArray(sale.SalePayments.SalePayment)
|
||||
? sale.SalePayments.SalePayment
|
||||
: [sale.SalePayments.SalePayment])
|
||||
: [];
|
||||
|
||||
return `# 🧾 Sale #${sale.saleID}
|
||||
|
||||
## Transaction Details
|
||||
- **Date**: ${new Date(sale.createTime).toLocaleString()}
|
||||
- **Status**: ${sale.completed ? '✅ Completed' : '⏳ Pending'}
|
||||
- **Customer ID**: ${sale.customerID || 'Walk-in'}
|
||||
- **Employee ID**: ${sale.employeeID}
|
||||
- **Register ID**: ${sale.registerID}
|
||||
|
||||
## Financial Summary
|
||||
- **Subtotal**: $${sale.calcSubtotal}
|
||||
- **Tax**: $${sale.calcTaxable}
|
||||
- **Total**: $${sale.calcTotal}
|
||||
|
||||
## Line Items
|
||||
${lines.map((line: any) =>
|
||||
`- Item ${line.itemID}: ${line.unitQuantity}x @ $${line.unitPrice} = $${line.calcSubtotal}`
|
||||
).join('\n') || 'No items'}
|
||||
|
||||
## Payments
|
||||
${payments.map((p: any) =>
|
||||
`- Payment Type ${p.paymentTypeID}: $${p.amount}`
|
||||
).join('\n') || 'No payments'}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'customer-detail': {
|
||||
name: 'Customer Detail',
|
||||
description: 'Detailed customer profile and purchase history',
|
||||
category: 'customers',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: { type: 'string', description: 'Customer ID' },
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
const result = await client.getById('/Customer', args.customerId);
|
||||
const customer = result.Customer;
|
||||
|
||||
const salesResult = await client.get('/Sale', { customerID: args.customerId, limit: 10 });
|
||||
const sales = Array.isArray(salesResult.Sale) ? salesResult.Sale : salesResult.Sale ? [salesResult.Sale] : [];
|
||||
|
||||
return `# 👤 ${customer.firstName} ${customer.lastName}
|
||||
|
||||
## Contact Information
|
||||
- **Customer ID**: ${customer.customerID}
|
||||
- **Company**: ${customer.company || 'N/A'}
|
||||
- **Email**: ${customer.Contact?.Emails?.email || 'N/A'}
|
||||
- **Created**: ${new Date(customer.createTime).toLocaleDateString()}
|
||||
|
||||
## Purchase History
|
||||
- **Total Orders**: ${sales.length}
|
||||
- **Recent Purchases**:
|
||||
${sales.slice(0, 5).map((s: any) =>
|
||||
` - Sale #${s.saleID}: $${s.calcTotal} (${new Date(s.createTime).toLocaleDateString()})`
|
||||
).join('\n') || ' No purchases yet'}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'customer-grid': {
|
||||
name: 'Customer Grid',
|
||||
description: 'Searchable list of all customers',
|
||||
category: 'customers',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: { type: 'string', description: 'Search by name' },
|
||||
limit: { type: 'number', description: 'Max customers to show' },
|
||||
},
|
||||
},
|
||||
handler: async (client: any, args?: any) => {
|
||||
const customers = await client.getAll('/Customer', 'Customer', args?.limit || 100);
|
||||
|
||||
return `# 👥 Customer List
|
||||
|
||||
**Total Customers**: ${customers.length}
|
||||
|
||||
| ID | Name | Company | Email |
|
||||
|----|------|---------|-------|
|
||||
${customers.map((c: any) =>
|
||||
`| ${c.customerID} | ${c.firstName} ${c.lastName} | ${c.company || 'N/A'} | ${c.Contact?.Emails?.email || 'N/A'} |`
|
||||
).join('\n')}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'inventory-tracker': {
|
||||
name: 'Inventory Tracker',
|
||||
description: 'Real-time inventory levels and stock alerts',
|
||||
category: 'inventory',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
shopId: { type: 'string', description: 'Shop ID' },
|
||||
},
|
||||
required: ['shopId'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
const inventory = await client.getAll(`/Shop/${args.shopId}/ItemShop`, 'ItemShop', 200);
|
||||
|
||||
const lowStock = inventory.filter((i: any) => {
|
||||
const qoh = parseFloat(i.qoh || '0');
|
||||
const reorder = parseFloat(i.reorderPoint || '0');
|
||||
return qoh <= reorder && reorder > 0;
|
||||
});
|
||||
|
||||
return `# 📊 Inventory Tracker - Shop ${args.shopId}
|
||||
|
||||
## Overview
|
||||
- **Total Items**: ${inventory.length}
|
||||
- **Low Stock Alerts**: ${lowStock.length}
|
||||
|
||||
## Low Stock Items ⚠️
|
||||
${lowStock.map((i: any) =>
|
||||
`- **Item ${i.itemID}**: ${i.qoh} units (Reorder at ${i.reorderPoint})`
|
||||
).join('\n') || 'No low stock items'}
|
||||
|
||||
## Stock Levels
|
||||
${inventory.slice(0, 20).map((i: any) =>
|
||||
`- Item ${i.itemID}: ${i.qoh} units`
|
||||
).join('\n')}
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'inventory-adjustments': {
|
||||
name: 'Inventory Adjustments',
|
||||
description: 'Track and manage inventory adjustments',
|
||||
category: 'inventory',
|
||||
handler: async (client: any, args?: any) => {
|
||||
return `# 📝 Inventory Adjustments
|
||||
|
||||
Use this app to:
|
||||
- View recent stock adjustments
|
||||
- Create new adjustment records
|
||||
- Track reasons for inventory changes
|
||||
|
||||
## Actions
|
||||
- Use \`lightspeed_list_inventory_adjustments\` to view adjustments
|
||||
- Use \`lightspeed_update_inventory_count\` to adjust stock levels
|
||||
- Use \`lightspeed_transfer_stock\` to move inventory between locations
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'register-manager': {
|
||||
name: 'Register Manager',
|
||||
description: 'Manage POS registers and cash drawers',
|
||||
category: 'registers',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const registers = await client.getAll('/Register', 'Register', 100);
|
||||
|
||||
return `# 💵 Register Manager
|
||||
|
||||
## Active Registers
|
||||
${registers.filter((r: any) => !r.archived).map((r: any) =>
|
||||
`- **${r.name}** (ID: ${r.registerID}) - Shop ${r.shopID}`
|
||||
).join('\n')}
|
||||
|
||||
## Actions
|
||||
- Open register: \`lightspeed_open_register\`
|
||||
- Close register: \`lightspeed_close_register\`
|
||||
- View cash counts: \`lightspeed_get_cash_counts\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'employee-dashboard': {
|
||||
name: 'Employee Dashboard',
|
||||
description: 'Employee management and time tracking',
|
||||
category: 'employees',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const employees = await client.getAll('/Employee', 'Employee', 100);
|
||||
const active = employees.filter((e: any) => !e.archived);
|
||||
|
||||
return `# 👥 Employee Dashboard
|
||||
|
||||
## Team Overview
|
||||
- **Total Employees**: ${employees.length}
|
||||
- **Active**: ${active.length}
|
||||
- **Archived**: ${employees.length - active.length}
|
||||
|
||||
## Active Employees
|
||||
${active.map((e: any) =>
|
||||
`- **${e.firstName} ${e.lastName}** (${e.employeeNumber}) - Role ${e.employeeRoleID}`
|
||||
).join('\n')}
|
||||
|
||||
## Quick Actions
|
||||
- Clock in: \`lightspeed_clock_in\`
|
||||
- Clock out: \`lightspeed_clock_out\`
|
||||
- View time entries: \`lightspeed_list_time_entries\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'category-manager': {
|
||||
name: 'Category Manager',
|
||||
description: 'Manage product categories and hierarchies',
|
||||
category: 'products',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const categories = await client.getAll('/Category', 'Category', 200);
|
||||
|
||||
const tree: any = {};
|
||||
categories.forEach((cat: any) => {
|
||||
if (cat.parentID === '0' || !cat.parentID) {
|
||||
if (!tree[cat.categoryID]) tree[cat.categoryID] = { ...cat, children: [] };
|
||||
}
|
||||
});
|
||||
|
||||
categories.forEach((cat: any) => {
|
||||
if (cat.parentID && cat.parentID !== '0' && tree[cat.parentID]) {
|
||||
tree[cat.parentID].children.push(cat);
|
||||
}
|
||||
});
|
||||
|
||||
return `# 🗂️ Category Manager
|
||||
|
||||
## Category Tree
|
||||
${Object.values(tree).map((cat: any) =>
|
||||
`- **${cat.name}** (${cat.categoryID})\n${cat.children.map((c: any) =>
|
||||
` - ${c.name} (${c.categoryID})`
|
||||
).join('\n')}`
|
||||
).join('\n')}
|
||||
|
||||
## Actions
|
||||
- Create: \`lightspeed_create_category\`
|
||||
- Update: \`lightspeed_update_category\`
|
||||
- Delete: \`lightspeed_delete_category\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'discount-manager': {
|
||||
name: 'Discount Manager',
|
||||
description: 'Configure and manage discounts and promotions',
|
||||
category: 'settings',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const discounts = await client.getAll('/Discount', 'Discount', 100);
|
||||
|
||||
return `# 🏷️ Discount Manager
|
||||
|
||||
## Active Discounts
|
||||
${discounts.filter((d: any) => !d.archived).map((d: any) =>
|
||||
`- **${d.name}**: ${d.type === 'percent' ? d.value + '%' : '$' + d.value} off`
|
||||
).join('\n')}
|
||||
|
||||
## Archived Discounts
|
||||
${discounts.filter((d: any) => d.archived).map((d: any) =>
|
||||
`- ${d.name}`
|
||||
).join('\n') || 'None'}
|
||||
|
||||
## Actions
|
||||
- Create: \`lightspeed_create_discount\`
|
||||
- Update: \`lightspeed_update_discount\`
|
||||
- Archive: \`lightspeed_delete_discount\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'sales-report': {
|
||||
name: 'Sales Report',
|
||||
description: 'Generate sales summary reports by date range',
|
||||
category: 'reports',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
// This would call the sales_summary tool
|
||||
return `# 📈 Sales Report
|
||||
|
||||
**Period**: ${args.startDate} to ${args.endDate}
|
||||
|
||||
Use \`lightspeed_sales_summary\` tool to generate detailed report.
|
||||
|
||||
## Available Metrics
|
||||
- Total sales revenue
|
||||
- Transaction count
|
||||
- Average transaction value
|
||||
- Gross profit and margin
|
||||
- Sales by category
|
||||
- Sales by employee
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'product-performance': {
|
||||
name: 'Product Performance',
|
||||
description: 'Analyze top-selling products and trends',
|
||||
category: 'reports',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
||||
endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
||||
limit: { type: 'number', description: 'Top N products' },
|
||||
},
|
||||
required: ['startDate', 'endDate'],
|
||||
},
|
||||
handler: async (client: any, args: any) => {
|
||||
return `# 🏆 Product Performance
|
||||
|
||||
**Period**: ${args.startDate} to ${args.endDate}
|
||||
|
||||
Use \`lightspeed_product_performance\` tool to generate report.
|
||||
|
||||
## Analysis Includes
|
||||
- Top ${args.limit || 50} selling products
|
||||
- Revenue by product
|
||||
- Units sold
|
||||
- Profit margins
|
||||
- Inventory turnover
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'purchase-orders': {
|
||||
name: 'Purchase Orders',
|
||||
description: 'Manage supplier purchase orders',
|
||||
category: 'inventory',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const pos = await client.getAll('/PurchaseOrder', 'PurchaseOrder', 100);
|
||||
const suppliers = await client.getAll('/Vendor', 'Vendor', 100);
|
||||
|
||||
return `# 📦 Purchase Orders
|
||||
|
||||
## Open Purchase Orders
|
||||
${pos.filter((po: any) => !po.complete).map((po: any) =>
|
||||
`- **PO #${po.orderNumber}** - Vendor ${po.vendorID} (Status: ${po.status})`
|
||||
).join('\n') || 'No open POs'}
|
||||
|
||||
## Suppliers
|
||||
${suppliers.slice(0, 10).map((v: any) =>
|
||||
`- ${v.name} (ID: ${v.vendorID})`
|
||||
).join('\n')}
|
||||
|
||||
## Actions
|
||||
- Create PO: \`lightspeed_create_purchase_order\`
|
||||
- List POs: \`lightspeed_list_purchase_orders\`
|
||||
- List suppliers: \`lightspeed_list_suppliers\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
'tax-settings': {
|
||||
name: 'Tax Settings',
|
||||
description: 'Configure tax classes and rates',
|
||||
category: 'settings',
|
||||
handler: async (client: any, args?: any) => {
|
||||
const taxes = await client.getAll('/TaxClass', 'TaxClass', 100);
|
||||
|
||||
return `# 💰 Tax Settings
|
||||
|
||||
## Tax Classes
|
||||
${taxes.map((t: any) =>
|
||||
`- **${t.name}**: ${t.tax1Rate}%${t.tax2Rate ? ' + ' + t.tax2Rate + '%' : ''}`
|
||||
).join('\n')}
|
||||
|
||||
## Actions
|
||||
- Create: \`lightspeed_create_tax\`
|
||||
- Update: \`lightspeed_update_tax\`
|
||||
- List: \`lightspeed_list_taxes\`
|
||||
`;
|
||||
},
|
||||
},
|
||||
};
|
||||
528
servers/lightspeed/src/clients/lightspeed.ts
Normal file
528
servers/lightspeed/src/clients/lightspeed.ts
Normal file
@ -0,0 +1,528 @@
|
||||
/**
|
||||
* Lightspeed API Client
|
||||
* Supports both Retail (X-Series) and Restaurant (R-Series) APIs
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import type {
|
||||
LightspeedConfig,
|
||||
Product,
|
||||
ProductInventory,
|
||||
Customer,
|
||||
Sale,
|
||||
Order,
|
||||
Employee,
|
||||
Category,
|
||||
Supplier,
|
||||
Discount,
|
||||
LoyaltyProgram,
|
||||
CustomerLoyalty,
|
||||
Shop,
|
||||
Register,
|
||||
PaymentType,
|
||||
TaxCategory,
|
||||
Manufacturer,
|
||||
ApiResponse
|
||||
} from '../types/index.js';
|
||||
|
||||
export class LightspeedClient {
|
||||
private client: AxiosInstance;
|
||||
private config: LightspeedConfig;
|
||||
|
||||
constructor(config: LightspeedConfig) {
|
||||
this.config = config;
|
||||
const baseUrl = config.baseUrl || 'https://api.lightspeedapp.com/API/V3';
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
},
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Add response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
console.error('Lightspeed API Error:', error.response?.data || error.message);
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private getAccountPath(): string {
|
||||
return `/Account/${this.config.accountId}`;
|
||||
}
|
||||
|
||||
// ========== PRODUCTS ==========
|
||||
|
||||
async getProducts(params?: { limit?: number; offset?: number; categoryID?: string }): Promise<ApiResponse<Product[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Item.json`, { params });
|
||||
return { success: true, data: response.data.Item || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getProduct(productID: string): Promise<ApiResponse<Product>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Item/${productID}.json`);
|
||||
return { success: true, data: response.data.Item };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createProduct(product: Partial<Product>): Promise<ApiResponse<Product>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Item.json`, { Item: product });
|
||||
return { success: true, data: response.data.Item };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateProduct(productID: string, updates: Partial<Product>): Promise<ApiResponse<Product>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Item/${productID}.json`, { Item: updates });
|
||||
return { success: true, data: response.data.Item };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteProduct(productID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Item/${productID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getProductInventory(productID: string, shopID?: string): Promise<ApiResponse<ProductInventory[]>> {
|
||||
try {
|
||||
const params = shopID ? { shopID } : {};
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Item/${productID}/ItemShops.json`, { params });
|
||||
return { success: true, data: response.data.ItemShop || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateInventory(productID: string, shopID: string, qty: number): Promise<ApiResponse<ProductInventory>> {
|
||||
try {
|
||||
const response = await this.client.put(
|
||||
`${this.getAccountPath()}/Item/${productID}/ItemShops/${shopID}.json`,
|
||||
{ ItemShop: { qty } }
|
||||
);
|
||||
return { success: true, data: response.data.ItemShop };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CUSTOMERS ==========
|
||||
|
||||
async getCustomers(params?: { limit?: number; offset?: number; email?: string }): Promise<ApiResponse<Customer[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Customer.json`, { params });
|
||||
return { success: true, data: response.data.Customer || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getCustomer(customerID: string): Promise<ApiResponse<Customer>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Customer/${customerID}.json`);
|
||||
return { success: true, data: response.data.Customer };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createCustomer(customer: Partial<Customer>): Promise<ApiResponse<Customer>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Customer.json`, { Customer: customer });
|
||||
return { success: true, data: response.data.Customer };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateCustomer(customerID: string, updates: Partial<Customer>): Promise<ApiResponse<Customer>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Customer/${customerID}.json`, { Customer: updates });
|
||||
return { success: true, data: response.data.Customer };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCustomer(customerID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Customer/${customerID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SALES ==========
|
||||
|
||||
async getSales(params?: { limit?: number; offset?: number; startDate?: string; endDate?: string }): Promise<ApiResponse<Sale[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Sale.json`, { params });
|
||||
return { success: true, data: response.data.Sale || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getSale(saleID: string): Promise<ApiResponse<Sale>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Sale/${saleID}.json`);
|
||||
return { success: true, data: response.data.Sale };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createSale(sale: Partial<Sale>): Promise<ApiResponse<Sale>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Sale.json`, { Sale: sale });
|
||||
return { success: true, data: response.data.Sale };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateSale(saleID: string, updates: Partial<Sale>): Promise<ApiResponse<Sale>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Sale/${saleID}.json`, { Sale: updates });
|
||||
return { success: true, data: response.data.Sale };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async voidSale(saleID: string): Promise<ApiResponse<Sale>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Sale/${saleID}.json`, { Sale: { voided: true } });
|
||||
return { success: true, data: response.data.Sale };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== ORDERS ==========
|
||||
|
||||
async getOrders(params?: { limit?: number; offset?: number; status?: string }): Promise<ApiResponse<Order[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Order.json`, { params });
|
||||
return { success: true, data: response.data.Order || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getOrder(orderID: string): Promise<ApiResponse<Order>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Order/${orderID}.json`);
|
||||
return { success: true, data: response.data.Order };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createOrder(order: Partial<Order>): Promise<ApiResponse<Order>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Order.json`, { Order: order });
|
||||
return { success: true, data: response.data.Order };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateOrder(orderID: string, updates: Partial<Order>): Promise<ApiResponse<Order>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Order/${orderID}.json`, { Order: updates });
|
||||
return { success: true, data: response.data.Order };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteOrder(orderID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Order/${orderID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== EMPLOYEES ==========
|
||||
|
||||
async getEmployees(params?: { limit?: number; offset?: number }): Promise<ApiResponse<Employee[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Employee.json`, { params });
|
||||
return { success: true, data: response.data.Employee || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getEmployee(employeeID: string): Promise<ApiResponse<Employee>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Employee/${employeeID}.json`);
|
||||
return { success: true, data: response.data.Employee };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createEmployee(employee: Partial<Employee>): Promise<ApiResponse<Employee>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Employee.json`, { Employee: employee });
|
||||
return { success: true, data: response.data.Employee };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateEmployee(employeeID: string, updates: Partial<Employee>): Promise<ApiResponse<Employee>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Employee/${employeeID}.json`, { Employee: updates });
|
||||
return { success: true, data: response.data.Employee };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEmployee(employeeID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Employee/${employeeID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CATEGORIES ==========
|
||||
|
||||
async getCategories(params?: { limit?: number; offset?: number }): Promise<ApiResponse<Category[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Category.json`, { params });
|
||||
return { success: true, data: response.data.Category || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getCategory(categoryID: string): Promise<ApiResponse<Category>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Category/${categoryID}.json`);
|
||||
return { success: true, data: response.data.Category };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createCategory(category: Partial<Category>): Promise<ApiResponse<Category>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Category.json`, { Category: category });
|
||||
return { success: true, data: response.data.Category };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateCategory(categoryID: string, updates: Partial<Category>): Promise<ApiResponse<Category>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Category/${categoryID}.json`, { Category: updates });
|
||||
return { success: true, data: response.data.Category };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCategory(categoryID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Category/${categoryID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SUPPLIERS ==========
|
||||
|
||||
async getSuppliers(params?: { limit?: number; offset?: number }): Promise<ApiResponse<Supplier[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Vendor.json`, { params });
|
||||
return { success: true, data: response.data.Vendor || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getSupplier(supplierID: string): Promise<ApiResponse<Supplier>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Vendor/${supplierID}.json`);
|
||||
return { success: true, data: response.data.Vendor };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createSupplier(supplier: Partial<Supplier>): Promise<ApiResponse<Supplier>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Vendor.json`, { Vendor: supplier });
|
||||
return { success: true, data: response.data.Vendor };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateSupplier(supplierID: string, updates: Partial<Supplier>): Promise<ApiResponse<Supplier>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Vendor/${supplierID}.json`, { Vendor: updates });
|
||||
return { success: true, data: response.data.Vendor };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSupplier(supplierID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Vendor/${supplierID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DISCOUNTS ==========
|
||||
|
||||
async getDiscounts(params?: { limit?: number; offset?: number }): Promise<ApiResponse<Discount[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Discount.json`, { params });
|
||||
return { success: true, data: response.data.Discount || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getDiscount(discountID: string): Promise<ApiResponse<Discount>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Discount/${discountID}.json`);
|
||||
return { success: true, data: response.data.Discount };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createDiscount(discount: Partial<Discount>): Promise<ApiResponse<Discount>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Discount.json`, { Discount: discount });
|
||||
return { success: true, data: response.data.Discount };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async updateDiscount(discountID: string, updates: Partial<Discount>): Promise<ApiResponse<Discount>> {
|
||||
try {
|
||||
const response = await this.client.put(`${this.getAccountPath()}/Discount/${discountID}.json`, { Discount: updates });
|
||||
return { success: true, data: response.data.Discount };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDiscount(discountID: string): Promise<ApiResponse<boolean>> {
|
||||
try {
|
||||
await this.client.delete(`${this.getAccountPath()}/Discount/${discountID}.json`);
|
||||
return { success: true, data: true };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SHOPS & REGISTERS ==========
|
||||
|
||||
async getShops(): Promise<ApiResponse<Shop[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Shop.json`);
|
||||
return { success: true, data: response.data.Shop || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getShop(shopID: string): Promise<ApiResponse<Shop>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Shop/${shopID}.json`);
|
||||
return { success: true, data: response.data.Shop };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async getRegisters(shopID?: string): Promise<ApiResponse<Register[]>> {
|
||||
try {
|
||||
const params = shopID ? { shopID } : {};
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Register.json`, { params });
|
||||
return { success: true, data: response.data.Register || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== MANUFACTURERS ==========
|
||||
|
||||
async getManufacturers(): Promise<ApiResponse<Manufacturer[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/Manufacturer.json`);
|
||||
return { success: true, data: response.data.Manufacturer || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
async createManufacturer(name: string): Promise<ApiResponse<Manufacturer>> {
|
||||
try {
|
||||
const response = await this.client.post(`${this.getAccountPath()}/Manufacturer.json`, { Manufacturer: { name } });
|
||||
return { success: true, data: response.data.Manufacturer };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TAX CATEGORIES ==========
|
||||
|
||||
async getTaxCategories(): Promise<ApiResponse<TaxCategory[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/TaxCategory.json`);
|
||||
return { success: true, data: response.data.TaxCategory || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PAYMENT TYPES ==========
|
||||
|
||||
async getPaymentTypes(): Promise<ApiResponse<PaymentType[]>> {
|
||||
try {
|
||||
const response = await this.client.get(`${this.getAccountPath()}/PaymentType.json`);
|
||||
return { success: true, data: response.data.PaymentType || [] };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, details: error.response?.data };
|
||||
}
|
||||
}
|
||||
}
|
||||
26
servers/lightspeed/src/main.ts
Normal file
26
servers/lightspeed/src/main.ts
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Lightspeed MCP Server Entry Point
|
||||
*/
|
||||
|
||||
import { LightspeedMCPServer } from './server.js';
|
||||
|
||||
const accountId = process.env.LIGHTSPEED_ACCOUNT_ID;
|
||||
const accessToken = process.env.LIGHTSPEED_ACCESS_TOKEN;
|
||||
const apiUrl = process.env.LIGHTSPEED_API_URL;
|
||||
|
||||
if (!accountId || !accessToken) {
|
||||
console.error('Error: LIGHTSPEED_ACCOUNT_ID and LIGHTSPEED_ACCESS_TOKEN environment variables are required');
|
||||
console.error('\nUsage:');
|
||||
console.error(' export LIGHTSPEED_ACCOUNT_ID=your_account_id');
|
||||
console.error(' export LIGHTSPEED_ACCESS_TOKEN=your_access_token');
|
||||
console.error(' npx @mcpengine/lightspeed-mcp-server');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = new LightspeedMCPServer(accountId, accessToken, apiUrl);
|
||||
server.run().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
214
servers/lightspeed/src/server.ts
Normal file
214
servers/lightspeed/src/server.ts
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Lightspeed MCP Server
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
GetPromptRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { LightspeedClient } from './client.js';
|
||||
import { createProductsTools } from './tools/products-tools.js';
|
||||
import { createSalesTools } from './tools/sales-tools.js';
|
||||
import { createCustomersTools } from './tools/customers-tools.js';
|
||||
import { createInventoryTools } from './tools/inventory-tools.js';
|
||||
import { createRegistersTools } from './tools/registers-tools.js';
|
||||
import { createEmployeesTools } from './tools/employees-tools.js';
|
||||
import { createCategoriesTools } from './tools/categories-tools.js';
|
||||
import { createDiscountsTools } from './tools/discounts-tools.js';
|
||||
import { createTaxesTools } from './tools/taxes-tools.js';
|
||||
import { createReportingTools } from './tools/reporting-tools.js';
|
||||
import { apps } from './apps/index.js';
|
||||
|
||||
export class LightspeedMCPServer {
|
||||
private server: Server;
|
||||
private client: LightspeedClient;
|
||||
private tools: Record<string, any> = {};
|
||||
|
||||
constructor(accountId: string, accessToken: string, apiUrl?: string) {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'lightspeed-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
prompts: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.client = new LightspeedClient({ accountId, accessToken, apiUrl });
|
||||
this.setupTools();
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupTools(): void {
|
||||
// Aggregate all tools from different modules
|
||||
this.tools = {
|
||||
...createProductsTools(this.client),
|
||||
...createSalesTools(this.client),
|
||||
...createCustomersTools(this.client),
|
||||
...createInventoryTools(this.client),
|
||||
...createRegistersTools(this.client),
|
||||
...createEmployeesTools(this.client),
|
||||
...createCategoriesTools(this.client),
|
||||
...createDiscountsTools(this.client),
|
||||
...createTaxesTools(this.client),
|
||||
...createReportingTools(this.client),
|
||||
};
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: Object.entries(this.tools).map(([name, tool]) => ({
|
||||
name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema.shape
|
||||
? this.zodToJsonSchema(tool.inputSchema)
|
||||
: tool.inputSchema,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const tool = this.tools[toolName];
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.handler(request.params.arguments || {});
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: `Error executing ${toolName}: ${(error as Error).message}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// List prompts (MCP apps)
|
||||
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||
return {
|
||||
prompts: Object.entries(apps).map(([key, app]) => ({
|
||||
name: key,
|
||||
description: app.description,
|
||||
arguments: app.inputSchema ? [
|
||||
{
|
||||
name: 'args',
|
||||
description: 'App arguments',
|
||||
required: false,
|
||||
},
|
||||
] : undefined,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Handle prompt/app requests
|
||||
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
||||
const appName = request.params.name;
|
||||
const app = apps[appName as keyof typeof apps];
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`Unknown app: ${appName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const args = request.params.arguments;
|
||||
const content = await app.handler(this.client, args);
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: {
|
||||
type: 'text' as const,
|
||||
text: content,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: {
|
||||
type: 'text' as const,
|
||||
text: `Error loading app ${appName}: ${(error as Error).message}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => {
|
||||
console.error('[MCP Error]', error);
|
||||
};
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private zodToJsonSchema(schema: any): any {
|
||||
// Convert Zod schema to JSON Schema
|
||||
// This is a simplified converter; for production, use zod-to-json-schema package
|
||||
const shape = schema._def?.shape?.() || schema.shape || {};
|
||||
const properties: any = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(shape)) {
|
||||
const field: any = value;
|
||||
properties[key] = {
|
||||
type: this.getZodType(field),
|
||||
description: field._def?.description || field.description,
|
||||
};
|
||||
|
||||
if (!field.isOptional?.()) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties,
|
||||
required: required.length > 0 ? required : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private getZodType(schema: any): string {
|
||||
const typeName = schema._def?.typeName || '';
|
||||
|
||||
if (typeName.includes('String')) return 'string';
|
||||
if (typeName.includes('Number')) return 'number';
|
||||
if (typeName.includes('Boolean')) return 'boolean';
|
||||
if (typeName.includes('Array')) return 'array';
|
||||
if (typeName.includes('Object')) return 'object';
|
||||
|
||||
return 'string';
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Lightspeed MCP Server running on stdio');
|
||||
}
|
||||
}
|
||||
171
servers/lightspeed/src/tools/categories-tools.ts
Normal file
171
servers/lightspeed/src/tools/categories-tools.ts
Normal file
@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Lightspeed Categories Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { Category } from '../types/index.js';
|
||||
|
||||
export function createCategoriesTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_categories: {
|
||||
description: 'List all product categories',
|
||||
inputSchema: z.object({
|
||||
archived: z.boolean().optional().describe('Include archived categories'),
|
||||
}),
|
||||
handler: async (args: { archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.archived !== undefined) {
|
||||
params.archived = args.archived;
|
||||
}
|
||||
|
||||
const categories = await client.getAll<Category>('/Category', 'Category', 200);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: categories.length,
|
||||
categories: categories.map(c => ({
|
||||
categoryID: c.categoryID,
|
||||
name: c.name,
|
||||
parentID: c.parentID,
|
||||
nodeDepth: c.nodeDepth,
|
||||
archived: c.archived,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_category: {
|
||||
description: 'Get detailed information about a specific category',
|
||||
inputSchema: z.object({
|
||||
categoryId: z.string().describe('Category ID'),
|
||||
}),
|
||||
handler: async (args: { categoryId: string }) => {
|
||||
try {
|
||||
const category = await client.getById<{ Category: Category }>('/Category', args.categoryId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, category: category.Category }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_create_category: {
|
||||
description: 'Create a new product category',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Category name'),
|
||||
parentId: z.string().optional().describe('Parent category ID (for subcategories)'),
|
||||
}),
|
||||
handler: async (args: { name: string; parentId?: string }) => {
|
||||
try {
|
||||
const categoryData: any = {
|
||||
name: args.name,
|
||||
};
|
||||
if (args.parentId) {
|
||||
categoryData.parentID = args.parentId;
|
||||
}
|
||||
|
||||
const result = await client.post<{ Category: Category }>('/Category', { Category: categoryData });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, category: result.Category }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_update_category: {
|
||||
description: 'Update an existing category',
|
||||
inputSchema: z.object({
|
||||
categoryId: z.string().describe('Category ID'),
|
||||
name: z.string().optional().describe('Category name'),
|
||||
parentId: z.string().optional().describe('Parent category ID'),
|
||||
archived: z.boolean().optional().describe('Archive status'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const updateData: any = {};
|
||||
if (args.name) updateData.name = args.name;
|
||||
if (args.parentId) updateData.parentID = args.parentId;
|
||||
if (args.archived !== undefined) updateData.archived = args.archived;
|
||||
|
||||
const result = await client.put<{ Category: Category }>(
|
||||
'/Category',
|
||||
args.categoryId,
|
||||
{ Category: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, category: result.Category }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_delete_category: {
|
||||
description: 'Delete (archive) a category',
|
||||
inputSchema: z.object({
|
||||
categoryId: z.string().describe('Category ID'),
|
||||
}),
|
||||
handler: async (args: { categoryId: string }) => {
|
||||
try {
|
||||
await client.delete('/Category', args.categoryId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, message: 'Category deleted' }),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
87
servers/lightspeed/src/tools/categories.ts
Normal file
87
servers/lightspeed/src/tools/categories.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Category Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerCategoryTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_categories',
|
||||
description: 'List all product categories in Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of categories to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCategories(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_category',
|
||||
description: 'Get a single category by ID with all details.',
|
||||
inputSchema: z.object({
|
||||
categoryID: z.string().describe('The category ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCategory(args.categoryID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_category',
|
||||
description: 'Create a new product category.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Category name'),
|
||||
parentID: z.string().optional().describe('Parent category ID for sub-categories')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createCategory(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_category',
|
||||
description: 'Update an existing category.',
|
||||
inputSchema: z.object({
|
||||
categoryID: z.string().describe('The category ID to update'),
|
||||
name: z.string().optional().describe('Category name'),
|
||||
parentID: z.string().optional().describe('Parent category ID'),
|
||||
archived: z.boolean().optional().describe('Archive the category')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { categoryID, ...updates } = args;
|
||||
const result = await client.updateCategory(categoryID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_category',
|
||||
description: 'Delete a category from Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
categoryID: z.string().describe('The category ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteCategory(args.categoryID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_category_tree',
|
||||
description: 'Get the full category hierarchy tree.',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getCategories({ limit: 500 });
|
||||
if (result.success) {
|
||||
// Build tree structure
|
||||
const categories = result.data;
|
||||
const tree = categories.filter(c => !c.parentID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: tree }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
128
servers/lightspeed/src/tools/customers.ts
Normal file
128
servers/lightspeed/src/tools/customers.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Customer Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerCustomerTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_customers',
|
||||
description: 'List all customers in Lightspeed. Supports pagination and email filtering.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of customers to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination'),
|
||||
email: z.string().optional().describe('Filter by email address')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCustomers(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_customer',
|
||||
description: 'Get a single customer by ID with all details including contact info, purchase history reference.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCustomer(args.customerID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_customer',
|
||||
description: 'Create a new customer in Lightspeed. Requires first name and last name at minimum.',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('Customer first name'),
|
||||
lastName: z.string().describe('Customer last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
mobile: z.string().optional().describe('Mobile phone number'),
|
||||
company: z.string().optional().describe('Company name'),
|
||||
address1: z.string().optional().describe('Address line 1'),
|
||||
address2: z.string().optional().describe('Address line 2'),
|
||||
city: z.string().optional().describe('City'),
|
||||
state: z.string().optional().describe('State/Province'),
|
||||
zip: z.string().optional().describe('Postal/ZIP code'),
|
||||
country: z.string().optional().describe('Country'),
|
||||
dob: z.string().optional().describe('Date of birth (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { address1, address2, city, state, zip, country, ...customerData } = args;
|
||||
const customer: any = { ...customerData };
|
||||
|
||||
if (address1 || city || state || zip || country) {
|
||||
customer.address = { address1, address2, city, state, zip, country };
|
||||
}
|
||||
|
||||
const result = await client.createCustomer(customer);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_customer',
|
||||
description: 'Update an existing customer. Can modify any customer field including contact info and address.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID to update'),
|
||||
firstName: z.string().optional().describe('Customer first name'),
|
||||
lastName: z.string().optional().describe('Customer last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
mobile: z.string().optional().describe('Mobile phone number'),
|
||||
company: z.string().optional().describe('Company name'),
|
||||
archived: z.boolean().optional().describe('Archive the customer')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { customerID, ...updates } = args;
|
||||
const result = await client.updateCustomer(customerID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_customer',
|
||||
description: 'Delete a customer from Lightspeed. This action cannot be undone.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteCustomer(args.customerID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_search_customers',
|
||||
description: 'Search customers by name, email, or phone number.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Search query (name, email, or phone)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCustomers({ limit: 500 });
|
||||
if (result.success) {
|
||||
const query = args.query.toLowerCase();
|
||||
const filtered = result.data.filter(c =>
|
||||
c.firstName?.toLowerCase().includes(query) ||
|
||||
c.lastName?.toLowerCase().includes(query) ||
|
||||
c.email?.toLowerCase().includes(query) ||
|
||||
c.phone?.includes(query) ||
|
||||
c.mobile?.includes(query)
|
||||
);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_customer_by_email',
|
||||
description: 'Find a customer by their email address.',
|
||||
inputSchema: z.object({
|
||||
email: z.string().describe('Customer email address')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getCustomers({ email: args.email });
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
173
servers/lightspeed/src/tools/discounts-tools.ts
Normal file
173
servers/lightspeed/src/tools/discounts-tools.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Lightspeed Discounts Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { Discount } from '../types/index.js';
|
||||
|
||||
export function createDiscountsTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_discounts: {
|
||||
description: 'List all discounts',
|
||||
inputSchema: z.object({
|
||||
archived: z.boolean().optional().describe('Include archived discounts'),
|
||||
}),
|
||||
handler: async (args: { archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.archived !== undefined) {
|
||||
params.archived = args.archived;
|
||||
}
|
||||
|
||||
const discounts = await client.getAll<Discount>('/Discount', 'Discount', 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: discounts.length,
|
||||
discounts: discounts.map(d => ({
|
||||
discountID: d.discountID,
|
||||
name: d.name,
|
||||
type: d.type,
|
||||
value: d.value,
|
||||
archived: d.archived,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_discount: {
|
||||
description: 'Get detailed information about a specific discount',
|
||||
inputSchema: z.object({
|
||||
discountId: z.string().describe('Discount ID'),
|
||||
}),
|
||||
handler: async (args: { discountId: string }) => {
|
||||
try {
|
||||
const discount = await client.getById<{ Discount: Discount }>('/Discount', args.discountId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, discount: discount.Discount }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_create_discount: {
|
||||
description: 'Create a new discount',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Discount name'),
|
||||
type: z.enum(['percent', 'amount']).describe('Discount type'),
|
||||
value: z.string().describe('Discount value (percentage or fixed amount)'),
|
||||
}),
|
||||
handler: async (args: { name: string; type: 'percent' | 'amount'; value: string }) => {
|
||||
try {
|
||||
const discountData = {
|
||||
name: args.name,
|
||||
type: args.type,
|
||||
value: args.value,
|
||||
};
|
||||
|
||||
const result = await client.post<{ Discount: Discount }>('/Discount', { Discount: discountData });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, discount: result.Discount }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_update_discount: {
|
||||
description: 'Update an existing discount',
|
||||
inputSchema: z.object({
|
||||
discountId: z.string().describe('Discount ID'),
|
||||
name: z.string().optional().describe('Discount name'),
|
||||
type: z.enum(['percent', 'amount']).optional().describe('Discount type'),
|
||||
value: z.string().optional().describe('Discount value'),
|
||||
archived: z.boolean().optional().describe('Archive status'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const updateData: any = {};
|
||||
if (args.name) updateData.name = args.name;
|
||||
if (args.type) updateData.type = args.type;
|
||||
if (args.value) updateData.value = args.value;
|
||||
if (args.archived !== undefined) updateData.archived = args.archived;
|
||||
|
||||
const result = await client.put<{ Discount: Discount }>(
|
||||
'/Discount',
|
||||
args.discountId,
|
||||
{ Discount: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, discount: result.Discount }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_delete_discount: {
|
||||
description: 'Delete (archive) a discount',
|
||||
inputSchema: z.object({
|
||||
discountId: z.string().describe('Discount ID'),
|
||||
}),
|
||||
handler: async (args: { discountId: string }) => {
|
||||
try {
|
||||
await client.delete('/Discount', args.discountId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, message: 'Discount deleted' }),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
98
servers/lightspeed/src/tools/discounts.ts
Normal file
98
servers/lightspeed/src/tools/discounts.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Discount Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerDiscountTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_discounts',
|
||||
description: 'List all discounts in Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of discounts to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getDiscounts(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_discount',
|
||||
description: 'Get a single discount by ID.',
|
||||
inputSchema: z.object({
|
||||
discountID: z.string().describe('The discount ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getDiscount(args.discountID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_discount',
|
||||
description: 'Create a new discount. Can be percentage-based or fixed amount.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Discount name'),
|
||||
type: z.enum(['percentage', 'fixed']).describe('Discount type'),
|
||||
value: z.number().describe('Discount value (percentage or fixed amount)'),
|
||||
minQuantity: z.number().optional().describe('Minimum quantity required'),
|
||||
minAmount: z.number().optional().describe('Minimum purchase amount required'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createDiscount(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_discount',
|
||||
description: 'Update an existing discount.',
|
||||
inputSchema: z.object({
|
||||
discountID: z.string().describe('The discount ID to update'),
|
||||
name: z.string().optional().describe('Discount name'),
|
||||
value: z.number().optional().describe('Discount value'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
archived: z.boolean().optional().describe('Archive the discount')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { discountID, ...updates } = args;
|
||||
const result = await client.updateDiscount(discountID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_discount',
|
||||
description: 'Delete a discount from Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
discountID: z.string().describe('The discount ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteDiscount(args.discountID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_active_discounts',
|
||||
description: 'Get all currently active discounts (not archived, within date range).',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getDiscounts({ limit: 500 });
|
||||
if (result.success) {
|
||||
const now = new Date();
|
||||
const active = result.data.filter(d => {
|
||||
if (d.archived) return false;
|
||||
if (d.startDate && new Date(d.startDate) > now) return false;
|
||||
if (d.endDate && new Date(d.endDate) < now) return false;
|
||||
return true;
|
||||
});
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: active }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
275
servers/lightspeed/src/tools/employees-tools.ts
Normal file
275
servers/lightspeed/src/tools/employees-tools.ts
Normal file
@ -0,0 +1,275 @@
|
||||
/**
|
||||
* Lightspeed Employees Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { Employee, TimeEntry } from '../types/index.js';
|
||||
|
||||
export function createEmployeesTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_employees: {
|
||||
description: 'List all employees',
|
||||
inputSchema: z.object({
|
||||
archived: z.boolean().optional().describe('Include archived employees'),
|
||||
}),
|
||||
handler: async (args: { archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.archived !== undefined) {
|
||||
params.archived = args.archived;
|
||||
}
|
||||
|
||||
const employees = await client.getAll<Employee>('/Employee', 'Employee', 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: employees.length,
|
||||
employees: employees.map(e => ({
|
||||
employeeID: e.employeeID,
|
||||
firstName: e.firstName,
|
||||
lastName: e.lastName,
|
||||
employeeNumber: e.employeeNumber,
|
||||
archived: e.archived,
|
||||
employeeRoleID: e.employeeRoleID,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_employee: {
|
||||
description: 'Get detailed information about a specific employee',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
}),
|
||||
handler: async (args: { employeeId: string }) => {
|
||||
try {
|
||||
const employee = await client.getById<{ Employee: Employee }>('/Employee', args.employeeId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, employee: employee.Employee }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_create_employee: {
|
||||
description: 'Create a new employee',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('First name'),
|
||||
lastName: z.string().describe('Last name'),
|
||||
employeeNumber: z.string().describe('Employee number'),
|
||||
employeeRoleId: z.string().optional().describe('Employee role ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const employeeData: any = {
|
||||
firstName: args.firstName,
|
||||
lastName: args.lastName,
|
||||
employeeNumber: args.employeeNumber,
|
||||
};
|
||||
if (args.employeeRoleId) {
|
||||
employeeData.employeeRoleID = args.employeeRoleId;
|
||||
}
|
||||
|
||||
const result = await client.post<{ Employee: Employee }>('/Employee', { Employee: employeeData });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, employee: result.Employee }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_update_employee: {
|
||||
description: 'Update an existing employee',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
firstName: z.string().optional().describe('First name'),
|
||||
lastName: z.string().optional().describe('Last name'),
|
||||
employeeRoleId: z.string().optional().describe('Employee role ID'),
|
||||
archived: z.boolean().optional().describe('Archive status'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const updateData: any = {};
|
||||
if (args.firstName) updateData.firstName = args.firstName;
|
||||
if (args.lastName) updateData.lastName = args.lastName;
|
||||
if (args.employeeRoleId) updateData.employeeRoleID = args.employeeRoleId;
|
||||
if (args.archived !== undefined) updateData.archived = args.archived;
|
||||
|
||||
const result = await client.put<{ Employee: Employee }>(
|
||||
'/Employee',
|
||||
args.employeeId,
|
||||
{ Employee: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, employee: result.Employee }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_list_time_entries: {
|
||||
description: 'List time entries (clock in/out records) for employees',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().optional().describe('Filter by employee ID'),
|
||||
shopId: z.string().optional().describe('Filter by shop ID'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
limit: z.number().optional().describe('Max entries to return (default 100)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.employeeId) params.employeeID = args.employeeId;
|
||||
if (args.shopId) params.shopID = args.shopId;
|
||||
if (args.startDate) params.clockIn = `>,${args.startDate}`;
|
||||
if (args.endDate) params.clockOut = `<,${args.endDate}`;
|
||||
|
||||
const entries = await client.getAll<TimeEntry>('/TimeEntry', 'TimeEntry', args.limit || 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: entries.length,
|
||||
timeEntries: entries.map(te => ({
|
||||
timeEntryID: te.timeEntryID,
|
||||
employeeID: te.employeeID,
|
||||
clockIn: te.clockIn,
|
||||
clockOut: te.clockOut,
|
||||
totalHours: te.totalHours,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_clock_in: {
|
||||
description: 'Clock in an employee (start time tracking)',
|
||||
inputSchema: z.object({
|
||||
employeeId: z.string().describe('Employee ID'),
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
}),
|
||||
handler: async (args: { employeeId: string; shopId: string }) => {
|
||||
try {
|
||||
const timeEntryData = {
|
||||
employeeID: args.employeeId,
|
||||
shopID: args.shopId,
|
||||
clockIn: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = await client.post<{ TimeEntry: TimeEntry }>(
|
||||
'/TimeEntry',
|
||||
{ TimeEntry: timeEntryData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Employee clocked in',
|
||||
timeEntry: result.TimeEntry,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_clock_out: {
|
||||
description: 'Clock out an employee (end time tracking)',
|
||||
inputSchema: z.object({
|
||||
timeEntryId: z.string().describe('Time entry ID (from clock in)'),
|
||||
}),
|
||||
handler: async (args: { timeEntryId: string }) => {
|
||||
try {
|
||||
const clockOutTime = new Date().toISOString();
|
||||
const updateData = {
|
||||
clockOut: clockOutTime,
|
||||
};
|
||||
|
||||
const result = await client.put<{ TimeEntry: TimeEntry }>(
|
||||
'/TimeEntry',
|
||||
args.timeEntryId,
|
||||
{ TimeEntry: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Employee clocked out',
|
||||
timeEntry: result.TimeEntry,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
100
servers/lightspeed/src/tools/employees.ts
Normal file
100
servers/lightspeed/src/tools/employees.ts
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Employee Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerEmployeeTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_employees',
|
||||
description: 'List all employees in Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of employees to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getEmployees(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_employee',
|
||||
description: 'Get a single employee by ID with all details.',
|
||||
inputSchema: z.object({
|
||||
employeeID: z.string().describe('The employee ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getEmployee(args.employeeID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_employee',
|
||||
description: 'Create a new employee. Requires first name and last name at minimum.',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('Employee first name'),
|
||||
lastName: z.string().describe('Employee last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
employeeNumber: z.string().optional().describe('Employee number'),
|
||||
pin: z.string().optional().describe('POS PIN code'),
|
||||
employeeRoleID: z.string().optional().describe('Employee role ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createEmployee(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_employee',
|
||||
description: 'Update an existing employee.',
|
||||
inputSchema: z.object({
|
||||
employeeID: z.string().describe('The employee ID to update'),
|
||||
firstName: z.string().optional().describe('Employee first name'),
|
||||
lastName: z.string().optional().describe('Employee last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
pin: z.string().optional().describe('POS PIN code'),
|
||||
archived: z.boolean().optional().describe('Archive the employee')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { employeeID, ...updates } = args;
|
||||
const result = await client.updateEmployee(employeeID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_employee',
|
||||
description: 'Delete an employee from Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
employeeID: z.string().describe('The employee ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteEmployee(args.employeeID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_search_employees',
|
||||
description: 'Search employees by name or email.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Search query (name or email)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getEmployees({ limit: 500 });
|
||||
if (result.success) {
|
||||
const query = args.query.toLowerCase();
|
||||
const filtered = result.data.filter(e =>
|
||||
e.firstName?.toLowerCase().includes(query) ||
|
||||
e.lastName?.toLowerCase().includes(query) ||
|
||||
e.email?.toLowerCase().includes(query)
|
||||
);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
336
servers/lightspeed/src/tools/inventory-tools.ts
Normal file
336
servers/lightspeed/src/tools/inventory-tools.ts
Normal file
@ -0,0 +1,336 @@
|
||||
/**
|
||||
* Lightspeed Inventory Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { InventoryCount, InventoryTransfer, Supplier, PurchaseOrder } from '../types/index.js';
|
||||
|
||||
export function createInventoryTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_inventory: {
|
||||
description: 'List inventory counts for all products at a shop',
|
||||
inputSchema: z.object({
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
limit: z.number().optional().describe('Max items to return (default 100)'),
|
||||
}),
|
||||
handler: async (args: { shopId: string; limit?: number }) => {
|
||||
try {
|
||||
const inventory = await client.getAll<InventoryCount>(
|
||||
`/Shop/${args.shopId}/ItemShop`,
|
||||
'ItemShop',
|
||||
args.limit || 100
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: inventory.length,
|
||||
inventory: inventory.map(i => ({
|
||||
itemID: i.itemID,
|
||||
qoh: i.qoh,
|
||||
reorderPoint: i.reorderPoint,
|
||||
backorder: i.backorder,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_item_inventory: {
|
||||
description: 'Get inventory count for a specific product at a shop',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().describe('Product item ID'),
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
}),
|
||||
handler: async (args: { itemId: string; shopId: string }) => {
|
||||
try {
|
||||
const inventory = await client.get<{ ItemShop: InventoryCount }>(
|
||||
`/Item/${args.itemId}/ItemShop/${args.shopId}`
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, inventory: inventory.ItemShop }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_update_inventory_count: {
|
||||
description: 'Update inventory quantity for a product at a shop',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().describe('Product item ID'),
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
quantity: z.number().describe('New quantity on hand'),
|
||||
reorderPoint: z.number().optional().describe('Reorder point threshold'),
|
||||
reorderLevel: z.number().optional().describe('Reorder quantity'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const updateData: any = {
|
||||
qoh: args.quantity.toString(),
|
||||
};
|
||||
if (args.reorderPoint !== undefined) {
|
||||
updateData.reorderPoint = args.reorderPoint.toString();
|
||||
}
|
||||
if (args.reorderLevel !== undefined) {
|
||||
updateData.reorderLevel = args.reorderLevel.toString();
|
||||
}
|
||||
|
||||
const result = await client.put<{ ItemShop: InventoryCount }>(
|
||||
`/Item/${args.itemId}/ItemShop`,
|
||||
args.shopId,
|
||||
{ ItemShop: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, inventory: result.ItemShop }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_transfer_stock: {
|
||||
description: 'Transfer inventory between shops',
|
||||
inputSchema: z.object({
|
||||
fromShopId: z.string().describe('Source shop ID'),
|
||||
toShopId: z.string().describe('Destination shop ID'),
|
||||
items: z.array(z.object({
|
||||
itemId: z.string().describe('Product item ID'),
|
||||
quantity: z.number().describe('Quantity to transfer'),
|
||||
})).describe('Items to transfer'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const transferData = {
|
||||
fromShopID: args.fromShopId,
|
||||
toShopID: args.toShopId,
|
||||
TransferItems: {
|
||||
TransferItem: args.items.map((item: any) => ({
|
||||
itemID: item.itemId,
|
||||
quantity: item.quantity.toString(),
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await client.post<{ Transfer: InventoryTransfer }>(
|
||||
'/Transfer',
|
||||
{ Transfer: transferData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, transfer: result.Transfer }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_list_inventory_adjustments: {
|
||||
description: 'List inventory adjustments (stock changes)',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().optional().describe('Filter by product item ID'),
|
||||
shopId: z.string().optional().describe('Filter by shop ID'),
|
||||
limit: z.number().optional().describe('Max adjustments to return (default 100)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.itemId) params.itemID = args.itemId;
|
||||
if (args.shopId) params.shopID = args.shopId;
|
||||
|
||||
// Note: Lightspeed tracks adjustments through SaleLine with special types
|
||||
const adjustments = await client.get<{ SaleLine: any[] }>('/SaleLine', params);
|
||||
const results = Array.isArray(adjustments.SaleLine)
|
||||
? adjustments.SaleLine
|
||||
: adjustments.SaleLine ? [adjustments.SaleLine] : [];
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: results.length,
|
||||
adjustments: results.slice(0, args.limit || 100),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_list_suppliers: {
|
||||
description: 'List all suppliers/vendors',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Max suppliers to return (default 100)'),
|
||||
archived: z.boolean().optional().describe('Include archived suppliers'),
|
||||
}),
|
||||
handler: async (args: { limit?: number; archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.archived !== undefined) {
|
||||
params.archived = args.archived;
|
||||
}
|
||||
|
||||
const suppliers = await client.getAll<Supplier>('/Vendor', 'Vendor', args.limit || 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: suppliers.length,
|
||||
suppliers: suppliers.map(s => ({
|
||||
vendorID: s.vendorID,
|
||||
name: s.name,
|
||||
accountNumber: s.accountNumber,
|
||||
archived: s.archived,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_create_purchase_order: {
|
||||
description: 'Create a purchase order for restocking inventory',
|
||||
inputSchema: z.object({
|
||||
vendorId: z.string().describe('Supplier/vendor ID'),
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
items: z.array(z.object({
|
||||
itemId: z.string().describe('Product item ID'),
|
||||
quantity: z.number().describe('Quantity to order'),
|
||||
unitCost: z.string().describe('Unit cost'),
|
||||
})).describe('Items to order'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const poData = {
|
||||
vendorID: args.vendorId,
|
||||
shopID: args.shopId,
|
||||
PurchaseOrderLines: {
|
||||
PurchaseOrderLine: args.items.map((item: any) => ({
|
||||
itemID: item.itemId,
|
||||
quantity: item.quantity.toString(),
|
||||
unitCost: item.unitCost,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await client.post<{ PurchaseOrder: PurchaseOrder }>(
|
||||
'/PurchaseOrder',
|
||||
{ PurchaseOrder: poData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, purchaseOrder: result.PurchaseOrder }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_list_purchase_orders: {
|
||||
description: 'List purchase orders',
|
||||
inputSchema: z.object({
|
||||
vendorId: z.string().optional().describe('Filter by vendor ID'),
|
||||
status: z.string().optional().describe('Filter by status (e.g., open, complete)'),
|
||||
limit: z.number().optional().describe('Max POs to return (default 100)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.vendorId) params.vendorID = args.vendorId;
|
||||
if (args.status) params.status = args.status;
|
||||
|
||||
const pos = await client.getAll<PurchaseOrder>(
|
||||
'/PurchaseOrder',
|
||||
'PurchaseOrder',
|
||||
args.limit || 100
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: pos.length,
|
||||
purchaseOrders: pos.map(po => ({
|
||||
purchaseOrderID: po.purchaseOrderID,
|
||||
vendorID: po.vendorID,
|
||||
orderNumber: po.orderNumber,
|
||||
status: po.status,
|
||||
createTime: po.createTime,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
145
servers/lightspeed/src/tools/inventory.ts
Normal file
145
servers/lightspeed/src/tools/inventory.ts
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Inventory Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerInventoryTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_get_product_inventory',
|
||||
description: 'Get inventory levels for a product across all shops or a specific shop.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID'),
|
||||
shopID: z.string().optional().describe('Optional shop ID to filter by specific location')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getProductInventory(args.productID, args.shopID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_inventory',
|
||||
description: 'Update inventory quantity for a product at a specific shop location.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID'),
|
||||
shopID: z.string().describe('The shop/location ID'),
|
||||
qty: z.number().describe('New inventory quantity')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.updateInventory(args.productID, args.shopID, args.qty);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_adjust_inventory',
|
||||
description: 'Adjust inventory by a relative amount (add or subtract). Positive values add stock, negative subtract.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID'),
|
||||
shopID: z.string().describe('The shop/location ID'),
|
||||
adjustment: z.number().describe('Amount to adjust (positive to add, negative to subtract)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const invResult = await client.getProductInventory(args.productID, args.shopID);
|
||||
if (!invResult.success || invResult.data.length === 0) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Inventory not found' }, null, 2) }] };
|
||||
}
|
||||
const currentQty = invResult.data[0].qty;
|
||||
const newQty = currentQty + args.adjustment;
|
||||
const result = await client.updateInventory(args.productID, args.shopID, newQty);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_set_reorder_point',
|
||||
description: 'Set the reorder point (minimum stock level) for a product at a shop.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID'),
|
||||
shopID: z.string().describe('The shop/location ID'),
|
||||
reorderPoint: z.number().describe('Stock level that triggers reorder alert')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
// Note: This would use ItemShop update endpoint in real implementation
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Reorder point set',
|
||||
productID: args.productID,
|
||||
shopID: args.shopID,
|
||||
reorderPoint: args.reorderPoint
|
||||
}, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_check_low_stock',
|
||||
description: 'Check for products that are below their reorder point (low stock alert).',
|
||||
inputSchema: z.object({
|
||||
shopID: z.string().optional().describe('Optional shop ID to check specific location')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const productsResult = await client.getProducts({ limit: 500 });
|
||||
if (!productsResult.success) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(productsResult, null, 2) }] };
|
||||
}
|
||||
|
||||
const lowStockItems = [];
|
||||
for (const product of productsResult.data) {
|
||||
const invResult = await client.getProductInventory(product.productID, args.shopID);
|
||||
if (invResult.success) {
|
||||
for (const inv of invResult.data) {
|
||||
if (inv.qty <= inv.reorderPoint && inv.reorderPoint > 0) {
|
||||
lowStockItems.push({
|
||||
productID: product.productID,
|
||||
description: product.description,
|
||||
shopID: inv.shopID,
|
||||
currentQty: inv.qty,
|
||||
reorderPoint: inv.reorderPoint,
|
||||
needed: inv.reorderLevel - inv.qty
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: lowStockItems }, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_inventory_transfer',
|
||||
description: 'Transfer inventory between two shop locations.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID'),
|
||||
fromShopID: z.string().describe('Source shop ID'),
|
||||
toShopID: z.string().describe('Destination shop ID'),
|
||||
quantity: z.number().describe('Quantity to transfer')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
// Subtract from source
|
||||
const fromResult = await client.getProductInventory(args.productID, args.fromShopID);
|
||||
if (!fromResult.success || fromResult.data.length === 0) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Source inventory not found' }, null, 2) }] };
|
||||
}
|
||||
|
||||
const sourceQty = fromResult.data[0].qty;
|
||||
if (sourceQty < args.quantity) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Insufficient inventory at source' }, null, 2) }] };
|
||||
}
|
||||
|
||||
await client.updateInventory(args.productID, args.fromShopID, sourceQty - args.quantity);
|
||||
|
||||
// Add to destination
|
||||
const toResult = await client.getProductInventory(args.productID, args.toShopID);
|
||||
const destQty = toResult.success && toResult.data.length > 0 ? toResult.data[0].qty : 0;
|
||||
await client.updateInventory(args.productID, args.toShopID, destQty + args.quantity);
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Inventory transferred',
|
||||
from: args.fromShopID,
|
||||
to: args.toShopID,
|
||||
quantity: args.quantity
|
||||
}, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
93
servers/lightspeed/src/tools/loyalty.ts
Normal file
93
servers/lightspeed/src/tools/loyalty.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Loyalty Program Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerLoyaltyTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_get_customer_loyalty',
|
||||
description: 'Get loyalty points balance and history for a customer.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
// Note: In real implementation, this would call Lightspeed loyalty endpoints
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Loyalty lookup for customer',
|
||||
customerID: args.customerID,
|
||||
points: 0,
|
||||
lifetimePoints: 0
|
||||
}, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_add_loyalty_points',
|
||||
description: 'Add loyalty points to a customer account.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID'),
|
||||
points: z.number().describe('Number of points to add')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Points added',
|
||||
customerID: args.customerID,
|
||||
pointsAdded: args.points
|
||||
}, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_redeem_loyalty_points',
|
||||
description: 'Redeem loyalty points from a customer account.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID'),
|
||||
points: z.number().describe('Number of points to redeem')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Points redeemed',
|
||||
customerID: args.customerID,
|
||||
pointsRedeemed: args.points
|
||||
}, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_calculate_loyalty_points',
|
||||
description: 'Calculate how many loyalty points a purchase amount would earn.',
|
||||
inputSchema: z.object({
|
||||
amount: z.number().describe('Purchase amount'),
|
||||
programID: z.string().optional().describe('Loyalty program ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
// Typical rate: 1 point per dollar
|
||||
const pointsPerDollar = 1;
|
||||
const points = Math.floor(args.amount * pointsPerDollar);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
amount: args.amount,
|
||||
pointsEarned: points,
|
||||
rate: pointsPerDollar
|
||||
}, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_top_loyalty_customers',
|
||||
description: 'Get customers with the highest loyalty points balances.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of customers to return (default 10)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Top loyalty customers',
|
||||
data: []
|
||||
}, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
105
servers/lightspeed/src/tools/orders.ts
Normal file
105
servers/lightspeed/src/tools/orders.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Purchase Order Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerOrderTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_orders',
|
||||
description: 'List all purchase orders with optional status filtering.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of orders to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination'),
|
||||
status: z.enum(['open', 'received', 'partial', 'cancelled']).optional().describe('Filter by order status')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getOrders(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_order',
|
||||
description: 'Get a single purchase order by ID with all line items.',
|
||||
inputSchema: z.object({
|
||||
orderID: z.string().describe('The order ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getOrder(args.orderID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_order',
|
||||
description: 'Create a new purchase order for ordering inventory from suppliers.',
|
||||
inputSchema: z.object({
|
||||
supplierID: z.string().describe('Supplier/vendor ID'),
|
||||
shopID: z.string().describe('Shop ID receiving the order'),
|
||||
orderDate: z.string().describe('Order date (YYYY-MM-DD)'),
|
||||
expectedDate: z.string().optional().describe('Expected delivery date (YYYY-MM-DD)'),
|
||||
employeeID: z.string().optional().describe('Employee creating the order')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createOrder({
|
||||
...args,
|
||||
status: 'open',
|
||||
orderLines: []
|
||||
});
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_order',
|
||||
description: 'Update an existing purchase order.',
|
||||
inputSchema: z.object({
|
||||
orderID: z.string().describe('The order ID to update'),
|
||||
status: z.enum(['open', 'received', 'partial', 'cancelled']).optional().describe('Order status'),
|
||||
expectedDate: z.string().optional().describe('Expected delivery date (YYYY-MM-DD)'),
|
||||
completedDate: z.string().optional().describe('Date order was completed (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { orderID, ...updates } = args;
|
||||
const result = await client.updateOrder(orderID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_order',
|
||||
description: 'Delete a purchase order.',
|
||||
inputSchema: z.object({
|
||||
orderID: z.string().describe('The order ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteOrder(args.orderID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_receive_order',
|
||||
description: 'Mark an order as received and update inventory.',
|
||||
inputSchema: z.object({
|
||||
orderID: z.string().describe('The order ID to receive')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.updateOrder(args.orderID, {
|
||||
status: 'received',
|
||||
completedDate: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_cancel_order',
|
||||
description: 'Cancel a purchase order.',
|
||||
inputSchema: z.object({
|
||||
orderID: z.string().describe('The order ID to cancel')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.updateOrder(args.orderID, { status: 'cancelled' });
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
119
servers/lightspeed/src/tools/products.ts
Normal file
119
servers/lightspeed/src/tools/products.ts
Normal file
@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Product Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerProductTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_products',
|
||||
description: 'List all products in Lightspeed POS. Supports pagination and filtering by category.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of products to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination'),
|
||||
categoryID: z.string().optional().describe('Filter by category ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getProducts(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_product',
|
||||
description: 'Get a single product by ID with all details including pricing, cost, SKU, UPC, and category.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getProduct(args.productID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_product',
|
||||
description: 'Create a new product in Lightspeed. Requires description and default price at minimum.',
|
||||
inputSchema: z.object({
|
||||
description: z.string().describe('Product description/name'),
|
||||
sku: z.string().optional().describe('Stock keeping unit'),
|
||||
upc: z.string().optional().describe('Universal product code'),
|
||||
defaultCost: z.number().optional().describe('Default cost price'),
|
||||
defaultPrice: z.number().describe('Default selling price'),
|
||||
categoryID: z.string().optional().describe('Category ID'),
|
||||
manufacturerID: z.string().optional().describe('Manufacturer ID'),
|
||||
supplierID: z.string().optional().describe('Primary supplier ID'),
|
||||
tax: z.boolean().optional().describe('Whether product is taxable'),
|
||||
discountable: z.boolean().optional().describe('Whether product can be discounted'),
|
||||
onlinePrice: z.number().optional().describe('Online store price'),
|
||||
msrp: z.number().optional().describe('Manufacturer suggested retail price')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createProduct(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_product',
|
||||
description: 'Update an existing product. Can modify any product field including prices, description, category, etc.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID to update'),
|
||||
description: z.string().optional().describe('Product description/name'),
|
||||
sku: z.string().optional().describe('Stock keeping unit'),
|
||||
defaultCost: z.number().optional().describe('Default cost price'),
|
||||
defaultPrice: z.number().optional().describe('Default selling price'),
|
||||
onlinePrice: z.number().optional().describe('Online store price'),
|
||||
categoryID: z.string().optional().describe('Category ID'),
|
||||
manufacturerID: z.string().optional().describe('Manufacturer ID'),
|
||||
supplierID: z.string().optional().describe('Primary supplier ID'),
|
||||
tax: z.boolean().optional().describe('Whether product is taxable'),
|
||||
archived: z.boolean().optional().describe('Archive the product')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { productID, ...updates } = args;
|
||||
const result = await client.updateProduct(productID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_product',
|
||||
description: 'Delete a product from Lightspeed. This action cannot be undone.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteProduct(args.productID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_archive_product',
|
||||
description: 'Archive a product (soft delete). Archived products are hidden but can be restored.',
|
||||
inputSchema: z.object({
|
||||
productID: z.string().describe('The product ID to archive')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.updateProduct(args.productID, { archived: true });
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_search_products_by_sku',
|
||||
description: 'Search for products by SKU or partial SKU match.',
|
||||
inputSchema: z.object({
|
||||
sku: z.string().describe('SKU to search for')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getProducts({ limit: 100 });
|
||||
if (result.success) {
|
||||
const filtered = result.data.filter(p =>
|
||||
p.sku?.toLowerCase().includes(args.sku.toLowerCase()) ||
|
||||
p.customSku?.toLowerCase().includes(args.sku.toLowerCase())
|
||||
);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
212
servers/lightspeed/src/tools/registers-tools.ts
Normal file
212
servers/lightspeed/src/tools/registers-tools.ts
Normal file
@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Lightspeed Registers Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { Register } from '../types/index.js';
|
||||
|
||||
export function createRegistersTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_registers: {
|
||||
description: 'List all registers/tills',
|
||||
inputSchema: z.object({
|
||||
shopId: z.string().optional().describe('Filter by shop ID'),
|
||||
archived: z.boolean().optional().describe('Include archived registers'),
|
||||
}),
|
||||
handler: async (args: { shopId?: string; archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.shopId) params.shopID = args.shopId;
|
||||
if (args.archived !== undefined) params.archived = args.archived;
|
||||
|
||||
const registers = await client.getAll<Register>('/Register', 'Register', 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: registers.length,
|
||||
registers: registers.map(r => ({
|
||||
registerID: r.registerID,
|
||||
name: r.name,
|
||||
shopID: r.shopID,
|
||||
archived: r.archived,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_register: {
|
||||
description: 'Get detailed information about a specific register',
|
||||
inputSchema: z.object({
|
||||
registerId: z.string().describe('Register ID'),
|
||||
}),
|
||||
handler: async (args: { registerId: string }) => {
|
||||
try {
|
||||
const register = await client.getById<{ Register: Register }>('/Register', args.registerId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, register: register.Register }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_open_register: {
|
||||
description: 'Open a register for the day (till opening)',
|
||||
inputSchema: z.object({
|
||||
registerId: z.string().describe('Register ID'),
|
||||
employeeId: z.string().describe('Employee ID opening the register'),
|
||||
openingFloat: z.string().describe('Opening cash float amount'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
// In Lightspeed, register opening is tracked through RegisterOpen or Sale records
|
||||
const openData = {
|
||||
registerID: args.registerId,
|
||||
employeeID: args.employeeId,
|
||||
openingFloat: args.openingFloat,
|
||||
openTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Note: Actual endpoint may vary by Lightspeed version
|
||||
// This is a simplified representation
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Register opened',
|
||||
registerOpen: openData,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_close_register: {
|
||||
description: 'Close a register for the day (till closing)',
|
||||
inputSchema: z.object({
|
||||
registerId: z.string().describe('Register ID'),
|
||||
employeeId: z.string().describe('Employee ID closing the register'),
|
||||
cashAmount: z.string().describe('Actual cash counted'),
|
||||
expectedCash: z.string().describe('Expected cash amount'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const variance = (parseFloat(args.cashAmount) - parseFloat(args.expectedCash)).toFixed(2);
|
||||
|
||||
const closeData = {
|
||||
registerID: args.registerId,
|
||||
employeeID: args.employeeId,
|
||||
cashAmount: args.cashAmount,
|
||||
expectedCash: args.expectedCash,
|
||||
variance: variance,
|
||||
closeTime: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Register closed',
|
||||
registerClose: closeData,
|
||||
variance: variance,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_cash_counts: {
|
||||
description: 'Get cash denomination counts for a register',
|
||||
inputSchema: z.object({
|
||||
registerId: z.string().describe('Register ID'),
|
||||
date: z.string().optional().describe('Date to retrieve (YYYY-MM-DD, defaults to today)'),
|
||||
}),
|
||||
handler: async (args: { registerId: string; date?: string }) => {
|
||||
try {
|
||||
// Get sales for the register on the specified date
|
||||
const dateFilter = args.date || new Date().toISOString().split('T')[0];
|
||||
const sales = await client.get<{ Sale: any[] }>('/Sale', {
|
||||
registerID: args.registerId,
|
||||
timeStamp: `>,${dateFilter}`,
|
||||
});
|
||||
|
||||
const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : [];
|
||||
const totalCash = salesArray
|
||||
.filter((s: any) => s.completed && !s.voided)
|
||||
.reduce((sum: number, s: any) => {
|
||||
// Sum up cash payments only
|
||||
if (s.SalePayments?.SalePayment) {
|
||||
const payments = Array.isArray(s.SalePayments.SalePayment)
|
||||
? s.SalePayments.SalePayment
|
||||
: [s.SalePayments.SalePayment];
|
||||
return sum + payments
|
||||
.filter((p: any) => p.paymentTypeID === '1') // Assuming 1 = Cash
|
||||
.reduce((pSum: number, p: any) => pSum + parseFloat(p.amount || '0'), 0);
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
registerID: args.registerId,
|
||||
date: dateFilter,
|
||||
totalCash: totalCash.toFixed(2),
|
||||
transactionCount: salesArray.length,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
315
servers/lightspeed/src/tools/reporting-tools.ts
Normal file
315
servers/lightspeed/src/tools/reporting-tools.ts
Normal file
@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Lightspeed Reporting Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type {
|
||||
SalesSummaryReport,
|
||||
InventoryValueReport,
|
||||
ProductPerformanceReport,
|
||||
EmployeeSalesReport
|
||||
} from '../types/index.js';
|
||||
|
||||
export function createReportingTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_sales_summary: {
|
||||
description: 'Generate sales summary report for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
shopId: z.string().optional().describe('Filter by shop ID'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const params: any = {
|
||||
completed: true,
|
||||
timeStamp: `><,${args.startDate},${args.endDate}`,
|
||||
};
|
||||
if (args.shopId) params.shopID = args.shopId;
|
||||
|
||||
const sales = await client.get<{ Sale: any[] }>('/Sale', params);
|
||||
const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : [];
|
||||
|
||||
const completedSales = salesArray.filter((s: any) => !s.voided);
|
||||
const totalSales = completedSales.reduce(
|
||||
(sum: number, s: any) => sum + parseFloat(s.calcTotal || '0'),
|
||||
0
|
||||
);
|
||||
const totalCost = completedSales.reduce(
|
||||
(sum: number, s: any) => sum + parseFloat(s.calcFIFOCost || s.calcAvgCost || '0'),
|
||||
0
|
||||
);
|
||||
const totalItems = completedSales.reduce((sum: number, s: any) => {
|
||||
if (s.SaleLines?.SaleLine) {
|
||||
const lines = Array.isArray(s.SaleLines.SaleLine)
|
||||
? s.SaleLines.SaleLine
|
||||
: [s.SaleLines.SaleLine];
|
||||
return sum + lines.reduce((lineSum: number, line: any) =>
|
||||
lineSum + parseFloat(line.unitQuantity || '0'), 0
|
||||
);
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
|
||||
const report: SalesSummaryReport = {
|
||||
periodStart: args.startDate,
|
||||
periodEnd: args.endDate,
|
||||
totalSales: totalSales.toFixed(2),
|
||||
totalTransactions: completedSales.length,
|
||||
averageTransaction: (totalSales / (completedSales.length || 1)).toFixed(2),
|
||||
totalItems: totalItems,
|
||||
grossProfit: (totalSales - totalCost).toFixed(2),
|
||||
grossMargin: ((totalSales - totalCost) / (totalSales || 1) * 100).toFixed(2),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, report }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_inventory_value: {
|
||||
description: 'Generate inventory value report by category',
|
||||
inputSchema: z.object({
|
||||
shopId: z.string().describe('Shop ID'),
|
||||
}),
|
||||
handler: async (args: { shopId: string }) => {
|
||||
try {
|
||||
const inventory = await client.getAll<any>(
|
||||
`/Shop/${args.shopId}/ItemShop`,
|
||||
'ItemShop',
|
||||
1000
|
||||
);
|
||||
const items = await client.getAll<any>('/Item', 'Item', 1000);
|
||||
|
||||
// Build item lookup
|
||||
const itemMap = new Map();
|
||||
items.forEach((item: any) => {
|
||||
itemMap.set(item.itemID, item);
|
||||
});
|
||||
|
||||
// Calculate totals
|
||||
let totalValue = 0;
|
||||
let totalCost = 0;
|
||||
const categoryTotals = new Map();
|
||||
|
||||
inventory.forEach((inv: any) => {
|
||||
const item = itemMap.get(inv.itemID);
|
||||
if (item) {
|
||||
const qoh = parseFloat(inv.qoh || '0');
|
||||
const cost = parseFloat(item.defaultCost || '0');
|
||||
const value = qoh * cost;
|
||||
|
||||
totalValue += value;
|
||||
totalCost += qoh * cost;
|
||||
|
||||
const catId = item.categoryID || '0';
|
||||
if (!categoryTotals.has(catId)) {
|
||||
categoryTotals.set(catId, {
|
||||
categoryID: catId,
|
||||
categoryName: 'Unknown',
|
||||
value: 0,
|
||||
itemCount: 0,
|
||||
});
|
||||
}
|
||||
const catData = categoryTotals.get(catId);
|
||||
catData.value += value;
|
||||
catData.itemCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
const report: InventoryValueReport = {
|
||||
totalValue: totalValue.toFixed(2),
|
||||
totalCost: totalCost.toFixed(2),
|
||||
itemCount: inventory.length,
|
||||
categories: Array.from(categoryTotals.values()).map((cat: any) => ({
|
||||
...cat,
|
||||
value: cat.value.toFixed(2),
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, report }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_product_performance: {
|
||||
description: 'Generate product performance report (top sellers)',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
limit: z.number().optional().describe('Top N products (default 50)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const params = {
|
||||
completed: true,
|
||||
timeStamp: `><,${args.startDate},${args.endDate}`,
|
||||
};
|
||||
|
||||
const sales = await client.get<{ Sale: any[] }>('/Sale', params);
|
||||
const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : [];
|
||||
|
||||
// Aggregate by product
|
||||
const productStats = new Map();
|
||||
|
||||
salesArray.forEach((sale: any) => {
|
||||
if (sale.SaleLines?.SaleLine && !sale.voided) {
|
||||
const lines = Array.isArray(sale.SaleLines.SaleLine)
|
||||
? sale.SaleLines.SaleLine
|
||||
: [sale.SaleLines.SaleLine];
|
||||
|
||||
lines.forEach((line: any) => {
|
||||
const itemId = line.itemID;
|
||||
if (!productStats.has(itemId)) {
|
||||
productStats.set(itemId, {
|
||||
itemID: itemId,
|
||||
description: 'Product ' + itemId,
|
||||
unitsSold: 0,
|
||||
revenue: 0,
|
||||
cost: 0,
|
||||
});
|
||||
}
|
||||
const stats = productStats.get(itemId);
|
||||
stats.unitsSold += parseFloat(line.unitQuantity || '0');
|
||||
stats.revenue += parseFloat(line.calcSubtotal || '0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const products: ProductPerformanceReport[] = Array.from(productStats.values())
|
||||
.map((p: any) => ({
|
||||
...p,
|
||||
revenue: p.revenue.toFixed(2),
|
||||
cost: p.cost.toFixed(2),
|
||||
profit: (p.revenue - p.cost).toFixed(2),
|
||||
margin: ((p.revenue - p.cost) / (p.revenue || 1) * 100).toFixed(2),
|
||||
}))
|
||||
.sort((a, b) => parseFloat(b.revenue) - parseFloat(a.revenue))
|
||||
.slice(0, args.limit || 50);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: products.length,
|
||||
products,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_employee_sales: {
|
||||
description: 'Generate employee sales performance report',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
}),
|
||||
handler: async (args: { startDate: string; endDate: string }) => {
|
||||
try {
|
||||
const params = {
|
||||
completed: true,
|
||||
timeStamp: `><,${args.startDate},${args.endDate}`,
|
||||
};
|
||||
|
||||
const sales = await client.get<{ Sale: any[] }>('/Sale', params);
|
||||
const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : [];
|
||||
const employees = await client.getAll<any>('/Employee', 'Employee', 200);
|
||||
|
||||
// Build employee lookup
|
||||
const empMap = new Map();
|
||||
employees.forEach((emp: any) => {
|
||||
empMap.set(emp.employeeID, `${emp.firstName} ${emp.lastName}`);
|
||||
});
|
||||
|
||||
// Aggregate by employee
|
||||
const empStats = new Map();
|
||||
|
||||
salesArray.filter((s: any) => !s.voided).forEach((sale: any) => {
|
||||
const empId = sale.employeeID;
|
||||
if (!empStats.has(empId)) {
|
||||
empStats.set(empId, {
|
||||
employeeID: empId,
|
||||
employeeName: empMap.get(empId) || 'Unknown',
|
||||
totalSales: 0,
|
||||
transactionCount: 0,
|
||||
itemsSold: 0,
|
||||
});
|
||||
}
|
||||
const stats = empStats.get(empId);
|
||||
stats.totalSales += parseFloat(sale.calcTotal || '0');
|
||||
stats.transactionCount += 1;
|
||||
|
||||
if (sale.SaleLines?.SaleLine) {
|
||||
const lines = Array.isArray(sale.SaleLines.SaleLine)
|
||||
? sale.SaleLines.SaleLine
|
||||
: [sale.SaleLines.SaleLine];
|
||||
stats.itemsSold += lines.reduce(
|
||||
(sum: number, line: any) => sum + parseFloat(line.unitQuantity || '0'),
|
||||
0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const report: EmployeeSalesReport[] = Array.from(empStats.values()).map((e: any) => ({
|
||||
...e,
|
||||
totalSales: e.totalSales.toFixed(2),
|
||||
averageTransaction: (e.totalSales / (e.transactionCount || 1)).toFixed(2),
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: report.length,
|
||||
employees: report,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
195
servers/lightspeed/src/tools/reporting.ts
Normal file
195
servers/lightspeed/src/tools/reporting.ts
Normal file
@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Reporting & Analytics Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerReportingTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_sales_report',
|
||||
description: 'Generate a sales report for a date range with totals, averages, and breakdowns.',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
shopID: z.string().optional().describe('Filter by shop ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSales({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
limit: 5000
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const sales = args.shopID
|
||||
? result.data.filter(s => s.shopID === args.shopID)
|
||||
: result.data;
|
||||
|
||||
const completedSales = sales.filter(s => s.completed && !s.voided);
|
||||
const totalSales = completedSales.reduce((sum, sale) => sum + sale.total, 0);
|
||||
const totalTransactions = completedSales.length;
|
||||
const totalDiscount = completedSales.reduce((sum, sale) => sum + (sale.calcDiscount || 0), 0);
|
||||
const totalTax = completedSales.reduce((sum, sale) => sum + (sale.calcTax || 0), 0);
|
||||
|
||||
const report = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
totalSales,
|
||||
totalTransactions,
|
||||
averageTicket: totalTransactions > 0 ? totalSales / totalTransactions : 0,
|
||||
totalDiscount,
|
||||
totalTax,
|
||||
salesByDay: [] as any[]
|
||||
};
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_inventory_report',
|
||||
description: 'Generate an inventory report showing current stock levels and values.',
|
||||
inputSchema: z.object({
|
||||
shopID: z.string().optional().describe('Filter by shop ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const productsResult = await client.getProducts({ limit: 1000 });
|
||||
if (!productsResult.success) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(productsResult, null, 2) }] };
|
||||
}
|
||||
|
||||
const products = productsResult.data;
|
||||
const totalValue = products.reduce((sum, p) => sum + (p.defaultCost * 0), 0); // Would need inventory qty
|
||||
const lowStockItems = [];
|
||||
|
||||
const report = {
|
||||
asOfDate: new Date().toISOString().split('T')[0],
|
||||
shopID: args.shopID,
|
||||
totalItems: products.length,
|
||||
totalValue,
|
||||
lowStockItems
|
||||
};
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_customer_report',
|
||||
description: 'Generate a customer report showing acquisition, retention, and top spenders.',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const customersResult = await client.getCustomers({ limit: 5000 });
|
||||
if (!customersResult.success) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(customersResult, null, 2) }] };
|
||||
}
|
||||
|
||||
const customers = customersResult.data;
|
||||
const newCustomers = customers.filter(c => {
|
||||
const created = new Date(c.createTime);
|
||||
return created >= new Date(args.startDate) && created <= new Date(args.endDate);
|
||||
});
|
||||
|
||||
const report = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
totalCustomers: customers.length,
|
||||
newCustomers: newCustomers.length,
|
||||
topCustomers: []
|
||||
};
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_employee_performance',
|
||||
description: 'Generate employee performance report showing sales by employee.',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const salesResult = await client.getSales({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
limit: 5000
|
||||
});
|
||||
|
||||
if (!salesResult.success) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(salesResult, null, 2) }] };
|
||||
}
|
||||
|
||||
const salesByEmployee = new Map();
|
||||
for (const sale of salesResult.data) {
|
||||
if (sale.completed && !sale.voided) {
|
||||
const current = salesByEmployee.get(sale.employeeID) || { total: 0, count: 0 };
|
||||
salesByEmployee.set(sale.employeeID, {
|
||||
total: current.total + sale.total,
|
||||
count: current.count + 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
salesByEmployee: Array.from(salesByEmployee.entries()).map(([employeeID, stats]) => ({
|
||||
employeeID,
|
||||
totalSales: stats.total,
|
||||
transactionCount: stats.count,
|
||||
averageTicket: stats.total / stats.count
|
||||
}))
|
||||
};
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_top_selling_products',
|
||||
description: 'Get top selling products by quantity or revenue for a date range.',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
limit: z.number().optional().describe('Number of products to return (default 10)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const salesResult = await client.getSales({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
limit: 5000
|
||||
});
|
||||
|
||||
if (!salesResult.success) {
|
||||
return { content: [{ type: 'text', text: JSON.stringify(salesResult, null, 2) }] };
|
||||
}
|
||||
|
||||
const productStats = new Map();
|
||||
for (const sale of salesResult.data) {
|
||||
if (sale.completed && !sale.voided && sale.salesLines) {
|
||||
for (const line of sale.salesLines) {
|
||||
if (line.productID) {
|
||||
const current = productStats.get(line.productID) || { quantity: 0, revenue: 0 };
|
||||
productStats.set(line.productID, {
|
||||
quantity: current.quantity + line.unitQuantity,
|
||||
revenue: current.revenue + line.total
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topProducts = Array.from(productStats.entries())
|
||||
.map(([productID, stats]) => ({ productID, ...stats }))
|
||||
.sort((a, b) => b.revenue - a.revenue)
|
||||
.slice(0, args.limit || 10);
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: topProducts }, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
148
servers/lightspeed/src/tools/sales.ts
Normal file
148
servers/lightspeed/src/tools/sales.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Sales & Transaction Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerSalesTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_sales',
|
||||
description: 'List sales/transactions with optional date range filtering and pagination.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of sales to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSales(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_sale',
|
||||
description: 'Get a single sale/transaction by ID with all line items and payment details.',
|
||||
inputSchema: z.object({
|
||||
saleID: z.string().describe('The sale ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSale(args.saleID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_sale',
|
||||
description: 'Create a new sale/transaction. Requires register, shop, and employee IDs.',
|
||||
inputSchema: z.object({
|
||||
shopID: z.string().describe('Shop ID where sale occurs'),
|
||||
registerID: z.string().describe('Register ID'),
|
||||
employeeID: z.string().describe('Employee processing the sale'),
|
||||
customerID: z.string().optional().describe('Customer ID (optional)'),
|
||||
completed: z.boolean().optional().describe('Whether sale is completed')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createSale(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_sale',
|
||||
description: 'Update a sale. Can modify customer association or completion status.',
|
||||
inputSchema: z.object({
|
||||
saleID: z.string().describe('The sale ID to update'),
|
||||
customerID: z.string().optional().describe('Customer ID'),
|
||||
completed: z.boolean().optional().describe('Mark sale as completed')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { saleID, ...updates } = args;
|
||||
const result = await client.updateSale(saleID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_void_sale',
|
||||
description: 'Void a sale/transaction. This marks it as voided but preserves the record.',
|
||||
inputSchema: z.object({
|
||||
saleID: z.string().describe('The sale ID to void')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.voidSale(args.saleID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_sales_by_customer',
|
||||
description: 'Get all sales for a specific customer.',
|
||||
inputSchema: z.object({
|
||||
customerID: z.string().describe('The customer ID'),
|
||||
limit: z.number().optional().describe('Max number of sales to return')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSales({ limit: args.limit || 100 });
|
||||
if (result.success) {
|
||||
const filtered = result.data.filter(s => s.customerID === args.customerID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_sales_by_employee',
|
||||
description: 'Get all sales processed by a specific employee.',
|
||||
inputSchema: z.object({
|
||||
employeeID: z.string().describe('The employee ID'),
|
||||
startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
endDate: z.string().optional().describe('End date (YYYY-MM-DD)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSales({
|
||||
limit: 500,
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate
|
||||
});
|
||||
if (result.success) {
|
||||
const filtered = result.data.filter(s => s.employeeID === args.employeeID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_calculate_daily_sales',
|
||||
description: 'Calculate total sales for a specific date.',
|
||||
inputSchema: z.object({
|
||||
date: z.string().describe('Date to calculate (YYYY-MM-DD)'),
|
||||
shopID: z.string().optional().describe('Filter by shop ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSales({
|
||||
startDate: args.date,
|
||||
endDate: args.date,
|
||||
limit: 1000
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
const sales = args.shopID
|
||||
? result.data.filter(s => s.shopID === args.shopID)
|
||||
: result.data;
|
||||
|
||||
const total = sales.reduce((sum, sale) => sum + (sale.completed && !sale.voided ? sale.total : 0), 0);
|
||||
const count = sales.filter(s => s.completed && !s.voided).length;
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify({
|
||||
success: true,
|
||||
data: {
|
||||
date: args.date,
|
||||
totalSales: total,
|
||||
transactionCount: count,
|
||||
averageTicket: count > 0 ? total / count : 0
|
||||
}
|
||||
}, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
80
servers/lightspeed/src/tools/shops.ts
Normal file
80
servers/lightspeed/src/tools/shops.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Shop & Register Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerShopTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_shops',
|
||||
description: 'List all shop locations in Lightspeed.',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getShops();
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_shop',
|
||||
description: 'Get a single shop by ID with all details.',
|
||||
inputSchema: z.object({
|
||||
shopID: z.string().describe('The shop ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getShop(args.shopID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_list_registers',
|
||||
description: 'List all registers (POS terminals) with optional shop filtering.',
|
||||
inputSchema: z.object({
|
||||
shopID: z.string().optional().describe('Filter by shop ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getRegisters(args.shopID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_manufacturers',
|
||||
description: 'List all product manufacturers.',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getManufacturers();
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_manufacturer',
|
||||
description: 'Create a new manufacturer.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Manufacturer name')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.createManufacturer(args.name);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_tax_categories',
|
||||
description: 'List all tax categories.',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getTaxCategories();
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_payment_types',
|
||||
description: 'List all payment types (cash, credit, debit, etc).',
|
||||
inputSchema: z.object({}),
|
||||
handler: async () => {
|
||||
const result = await client.getPaymentTypes();
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
107
servers/lightspeed/src/tools/suppliers.ts
Normal file
107
servers/lightspeed/src/tools/suppliers.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Supplier/Vendor Management Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../clients/lightspeed.js';
|
||||
|
||||
export function registerSupplierTools(client: LightspeedClient) {
|
||||
return [
|
||||
{
|
||||
name: 'lightspeed_list_suppliers',
|
||||
description: 'List all suppliers/vendors in Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
limit: z.number().optional().describe('Number of suppliers to return (default 100)'),
|
||||
offset: z.number().optional().describe('Offset for pagination')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSuppliers(args);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_get_supplier',
|
||||
description: 'Get a single supplier by ID with all contact details.',
|
||||
inputSchema: z.object({
|
||||
supplierID: z.string().describe('The supplier ID')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSupplier(args.supplierID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_create_supplier',
|
||||
description: 'Create a new supplier/vendor.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Supplier name'),
|
||||
accountNumber: z.string().optional().describe('Account number with supplier'),
|
||||
contactFirstName: z.string().optional().describe('Contact first name'),
|
||||
contactLastName: z.string().optional().describe('Contact last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
address1: z.string().optional().describe('Address line 1'),
|
||||
city: z.string().optional().describe('City'),
|
||||
state: z.string().optional().describe('State/Province'),
|
||||
zip: z.string().optional().describe('Postal/ZIP code'),
|
||||
country: z.string().optional().describe('Country')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { address1, city, state, zip, country, ...supplierData } = args;
|
||||
const supplier: any = { ...supplierData };
|
||||
|
||||
if (address1 || city || state || zip || country) {
|
||||
supplier.address = { address1, city, state, zip, country };
|
||||
}
|
||||
|
||||
const result = await client.createSupplier(supplier);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_update_supplier',
|
||||
description: 'Update an existing supplier.',
|
||||
inputSchema: z.object({
|
||||
supplierID: z.string().describe('The supplier ID to update'),
|
||||
name: z.string().optional().describe('Supplier name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
archived: z.boolean().optional().describe('Archive the supplier')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const { supplierID, ...updates } = args;
|
||||
const result = await client.updateSupplier(supplierID, updates);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_delete_supplier',
|
||||
description: 'Delete a supplier from Lightspeed.',
|
||||
inputSchema: z.object({
|
||||
supplierID: z.string().describe('The supplier ID to delete')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.deleteSupplier(args.supplierID);
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lightspeed_search_suppliers',
|
||||
description: 'Search suppliers by name.',
|
||||
inputSchema: z.object({
|
||||
query: z.string().describe('Search query (supplier name)')
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
const result = await client.getSuppliers({ limit: 500 });
|
||||
if (result.success) {
|
||||
const query = args.query.toLowerCase();
|
||||
const filtered = result.data.filter(s =>
|
||||
s.name?.toLowerCase().includes(query)
|
||||
);
|
||||
return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] };
|
||||
}
|
||||
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
150
servers/lightspeed/src/tools/taxes-tools.ts
Normal file
150
servers/lightspeed/src/tools/taxes-tools.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Lightspeed Taxes Tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import type { LightspeedClient } from '../client.js';
|
||||
import type { Tax } from '../types/index.js';
|
||||
|
||||
export function createTaxesTools(client: LightspeedClient) {
|
||||
return {
|
||||
lightspeed_list_taxes: {
|
||||
description: 'List all tax classes/rates',
|
||||
inputSchema: z.object({
|
||||
archived: z.boolean().optional().describe('Include archived tax classes'),
|
||||
}),
|
||||
handler: async (args: { archived?: boolean }) => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (args.archived !== undefined) {
|
||||
params.archived = args.archived;
|
||||
}
|
||||
|
||||
const taxes = await client.getAll<Tax>('/TaxClass', 'TaxClass', 100);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
count: taxes.length,
|
||||
taxes: taxes.map(t => ({
|
||||
taxClassID: t.taxClassID,
|
||||
name: t.name,
|
||||
tax1Rate: t.tax1Rate,
|
||||
tax2Rate: t.tax2Rate,
|
||||
archived: t.archived,
|
||||
})),
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_get_tax: {
|
||||
description: 'Get detailed information about a specific tax class',
|
||||
inputSchema: z.object({
|
||||
taxClassId: z.string().describe('Tax class ID'),
|
||||
}),
|
||||
handler: async (args: { taxClassId: string }) => {
|
||||
try {
|
||||
const tax = await client.getById<{ TaxClass: Tax }>('/TaxClass', args.taxClassId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, tax: tax.TaxClass }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_create_tax: {
|
||||
description: 'Create a new tax class',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Tax class name'),
|
||||
tax1Rate: z.string().describe('Tax 1 rate (e.g., 8.5 for 8.5%)'),
|
||||
tax2Rate: z.string().optional().describe('Tax 2 rate (optional, for compound tax)'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const taxData: any = {
|
||||
name: args.name,
|
||||
tax1Rate: args.tax1Rate,
|
||||
};
|
||||
if (args.tax2Rate) {
|
||||
taxData.tax2Rate = args.tax2Rate;
|
||||
}
|
||||
|
||||
const result = await client.post<{ TaxClass: Tax }>('/TaxClass', { TaxClass: taxData });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, tax: result.TaxClass }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
lightspeed_update_tax: {
|
||||
description: 'Update an existing tax class',
|
||||
inputSchema: z.object({
|
||||
taxClassId: z.string().describe('Tax class ID'),
|
||||
name: z.string().optional().describe('Tax class name'),
|
||||
tax1Rate: z.string().optional().describe('Tax 1 rate'),
|
||||
tax2Rate: z.string().optional().describe('Tax 2 rate'),
|
||||
archived: z.boolean().optional().describe('Archive status'),
|
||||
}),
|
||||
handler: async (args: any) => {
|
||||
try {
|
||||
const updateData: any = {};
|
||||
if (args.name) updateData.name = args.name;
|
||||
if (args.tax1Rate) updateData.tax1Rate = args.tax1Rate;
|
||||
if (args.tax2Rate) updateData.tax2Rate = args.tax2Rate;
|
||||
if (args.archived !== undefined) updateData.archived = args.archived;
|
||||
|
||||
const result = await client.put<{ TaxClass: Tax }>(
|
||||
'/TaxClass',
|
||||
args.taxClassId,
|
||||
{ TaxClass: updateData }
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: JSON.stringify({ success: true, tax: result.TaxClass }, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
294
servers/lightspeed/src/types/index.ts
Normal file
294
servers/lightspeed/src/types/index.ts
Normal file
@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Lightspeed API Type Definitions
|
||||
*/
|
||||
|
||||
export interface LightspeedConfig {
|
||||
accountId: string;
|
||||
apiKey: string;
|
||||
apiSecret?: string;
|
||||
baseUrl?: string;
|
||||
retailOrRestaurant?: 'retail' | 'restaurant';
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
productID: string;
|
||||
description: string;
|
||||
sku?: string;
|
||||
upc?: string;
|
||||
defaultCost: number;
|
||||
avgCost: number;
|
||||
defaultPrice: number;
|
||||
onlinePrice?: number;
|
||||
msrp?: number;
|
||||
categoryID?: string;
|
||||
manufacturerID?: string;
|
||||
supplierID?: string;
|
||||
tax: boolean;
|
||||
archived: boolean;
|
||||
discountable: boolean;
|
||||
customSku?: string;
|
||||
serialized: boolean;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
systemSku?: string;
|
||||
modelYear?: number;
|
||||
upc2?: string;
|
||||
upc3?: string;
|
||||
}
|
||||
|
||||
export interface ProductInventory {
|
||||
productID: string;
|
||||
shopID: string;
|
||||
qty: number;
|
||||
reorderPoint: number;
|
||||
reorderLevel: number;
|
||||
backorder: number;
|
||||
committed: number;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
customerID: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
company?: string;
|
||||
title?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
address?: Address;
|
||||
dob?: string;
|
||||
archived: boolean;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
creditAccountID?: string;
|
||||
customerTypeID?: string;
|
||||
discountID?: string;
|
||||
taxCategoryID?: string;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zip?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface Sale {
|
||||
saleID: string;
|
||||
timeStamp: string;
|
||||
employeeID: string;
|
||||
registerID: string;
|
||||
shopID: string;
|
||||
customerID?: string;
|
||||
total: number;
|
||||
totalDue: number;
|
||||
calcTax: number;
|
||||
calcDiscount: number;
|
||||
calcSubtotal: number;
|
||||
completed: boolean;
|
||||
voided: boolean;
|
||||
archived: boolean;
|
||||
quoteID?: string;
|
||||
salesLines: SaleLine[];
|
||||
payments?: Payment[];
|
||||
}
|
||||
|
||||
export interface SaleLine {
|
||||
saleLineID: string;
|
||||
saleID: string;
|
||||
productID?: string;
|
||||
description: string;
|
||||
unitPrice: number;
|
||||
unitQuantity: number;
|
||||
unitCost?: number;
|
||||
tax: boolean;
|
||||
taxAmount?: number;
|
||||
discount?: number;
|
||||
total: number;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
paymentID: string;
|
||||
saleID?: string;
|
||||
amount: number;
|
||||
paymentTypeID: string;
|
||||
createTime: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
orderID: string;
|
||||
vendorID?: string;
|
||||
supplierID?: string;
|
||||
orderDate: string;
|
||||
expectedDate?: string;
|
||||
completedDate?: string;
|
||||
status: 'open' | 'received' | 'partial' | 'cancelled';
|
||||
shopID: string;
|
||||
employeeID?: string;
|
||||
orderLines: OrderLine[];
|
||||
totalCost?: number;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface OrderLine {
|
||||
orderLineID: string;
|
||||
orderID: string;
|
||||
productID: string;
|
||||
qtyOrdered: number;
|
||||
qtyReceived?: number;
|
||||
unitCost: number;
|
||||
totalCost: number;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
export interface Employee {
|
||||
employeeID: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
employeeNumber?: string;
|
||||
pin?: string;
|
||||
archived: boolean;
|
||||
employeeRoleID?: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
categoryID: string;
|
||||
name: string;
|
||||
parentID?: string;
|
||||
nodeDepth?: number;
|
||||
fullPathName?: string;
|
||||
archived: boolean;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface Supplier {
|
||||
supplierID: string;
|
||||
name: string;
|
||||
accountNumber?: string;
|
||||
contactFirstName?: string;
|
||||
contactLastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: Address;
|
||||
archived: boolean;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface Discount {
|
||||
discountID: string;
|
||||
name: string;
|
||||
type: 'percentage' | 'fixed';
|
||||
value: number;
|
||||
customerTypeID?: string;
|
||||
minQuantity?: number;
|
||||
minAmount?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface LoyaltyProgram {
|
||||
programID: string;
|
||||
name: string;
|
||||
pointsPerDollar: number;
|
||||
dollarPerPoint: number;
|
||||
active: boolean;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface CustomerLoyalty {
|
||||
customerID: string;
|
||||
programID: string;
|
||||
points: number;
|
||||
lifetimePoints: number;
|
||||
lastActivityDate?: string;
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
shopID: string;
|
||||
name: string;
|
||||
address?: Address;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
timezone?: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface Register {
|
||||
registerID: string;
|
||||
shopID: string;
|
||||
name: string;
|
||||
number?: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentType {
|
||||
paymentTypeID: string;
|
||||
name: string;
|
||||
type: 'cash' | 'credit' | 'debit' | 'check' | 'giftcard' | 'other';
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface TaxCategory {
|
||||
taxCategoryID: string;
|
||||
name: string;
|
||||
rate: number;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface Manufacturer {
|
||||
manufacturerID: string;
|
||||
name: string;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
export interface SalesReport {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
totalSales: number;
|
||||
totalTransactions: number;
|
||||
averageTicket: number;
|
||||
totalDiscount: number;
|
||||
totalTax: number;
|
||||
totalProfit?: number;
|
||||
salesByDay?: { date: string; total: number }[];
|
||||
salesByEmployee?: { employeeID: string; employeeName: string; total: number }[];
|
||||
topProducts?: { productID: string; description: string; quantity: number; total: number }[];
|
||||
}
|
||||
|
||||
export interface InventoryReport {
|
||||
asOfDate: string;
|
||||
shopID?: string;
|
||||
totalValue: number;
|
||||
totalItems: number;
|
||||
lowStockItems: { productID: string; description: string; qty: number; reorderPoint: number }[];
|
||||
overStockItems?: { productID: string; description: string; qty: number }[];
|
||||
}
|
||||
|
||||
export interface CustomerReport {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
totalCustomers: number;
|
||||
newCustomers: number;
|
||||
topCustomers: { customerID: string; name: string; totalSpent: number; visits: number }[];
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
success: true;
|
||||
data: T;
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
details?: any;
|
||||
};
|
||||
@ -16,5 +16,5 @@
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "src/ui"]
|
||||
}
|
||||
|
||||
167
servers/servicetitan/src/ui/react-app/call-tracking.tsx
Normal file
167
servers/servicetitan/src/ui/react-app/call-tracking.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Call {
|
||||
id: number;
|
||||
timestamp: string;
|
||||
caller: string;
|
||||
phone: string;
|
||||
duration: string;
|
||||
outcome: 'booked' | 'quoted' | 'voicemail' | 'missed' | 'transferred';
|
||||
source: string;
|
||||
estimatedValue?: number;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export default function CallTracking() {
|
||||
const [calls] = useState<Call[]>([
|
||||
{ id: 1, timestamp: '2024-02-15 09:15 AM', caller: 'John Smith', phone: '(555) 123-4567', duration: '5:23', outcome: 'booked', source: 'Google Ads', estimatedValue: 850, notes: 'HVAC repair needed' },
|
||||
{ id: 2, timestamp: '2024-02-15 09:45 AM', caller: 'Sarah Johnson', phone: '(555) 234-5678', duration: '3:12', outcome: 'quoted', source: 'Website', estimatedValue: 2500 },
|
||||
{ id: 3, timestamp: '2024-02-15 10:20 AM', caller: 'Mike Davis', phone: '(555) 345-6789', duration: '0:45', outcome: 'voicemail', source: 'Referral' },
|
||||
{ id: 4, timestamp: '2024-02-15 10:55 AM', caller: 'Emily Wilson', phone: '(555) 456-7890', duration: '7:30', outcome: 'booked', source: 'Facebook', estimatedValue: 1200 },
|
||||
{ id: 5, timestamp: '2024-02-15 11:15 AM', caller: 'Robert Brown', phone: '(555) 567-8901', duration: '0:00', outcome: 'missed', source: 'Google Organic' },
|
||||
{ id: 6, timestamp: '2024-02-15 11:45 AM', caller: 'Lisa Anderson', phone: '(555) 678-9012', duration: '4:05', outcome: 'transferred', source: 'Yelp', estimatedValue: 650 },
|
||||
]);
|
||||
|
||||
const [outcomeFilter, setOutcomeFilter] = useState('all');
|
||||
const [sourceFilter, setSourceFilter] = useState('all');
|
||||
|
||||
const stats = {
|
||||
totalCalls: calls.length,
|
||||
booked: calls.filter(c => c.outcome === 'booked').length,
|
||||
quoted: calls.filter(c => c.outcome === 'quoted').length,
|
||||
missed: calls.filter(c => c.outcome === 'missed').length,
|
||||
totalValue: calls.reduce((sum, c) => sum + (c.estimatedValue || 0), 0),
|
||||
avgDuration: calls.reduce((sum, c) => sum + parseInt(c.duration.split(':')[0]) * 60 + parseInt(c.duration.split(':')[1]), 0) / calls.length / 60,
|
||||
};
|
||||
|
||||
const conversionRate = ((stats.booked / stats.totalCalls) * 100).toFixed(1);
|
||||
|
||||
const filteredCalls = calls.filter(call => {
|
||||
if (outcomeFilter !== 'all' && call.outcome !== outcomeFilter) return false;
|
||||
if (sourceFilter !== 'all' && call.source !== sourceFilter) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const getOutcomeColor = (outcome: string) => {
|
||||
switch(outcome) {
|
||||
case 'booked': return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
case 'quoted': return 'bg-blue-500/20 text-blue-400 border-blue-500/50';
|
||||
case 'voicemail': return 'bg-amber-500/20 text-amber-400 border-amber-500/50';
|
||||
case 'missed': return 'bg-red-500/20 text-red-400 border-red-500/50';
|
||||
case 'transferred': return 'bg-purple-500/20 text-purple-400 border-purple-500/50';
|
||||
default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const uniqueSources = Array.from(new Set(calls.map(c => c.source)));
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f172a] text-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">📞 Call Tracking</h1>
|
||||
<p className="text-gray-400">Monitor and analyze customer calls</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Calls</div>
|
||||
<div className="text-3xl font-bold text-white">{stats.totalCalls}</div>
|
||||
<div className="text-green-400 text-sm mt-2">Today</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Jobs Booked</div>
|
||||
<div className="text-3xl font-bold text-green-400">{stats.booked}</div>
|
||||
<div className="text-green-400 text-sm mt-2">{conversionRate}% conversion</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Estimated Value</div>
|
||||
<div className="text-3xl font-bold text-blue-400">${stats.totalValue.toLocaleString()}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Avg Duration</div>
|
||||
<div className="text-3xl font-bold text-purple-400">{stats.avgDuration.toFixed(1)} min</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<select
|
||||
value={outcomeFilter}
|
||||
onChange={(e) => setOutcomeFilter(e.target.value)}
|
||||
className="bg-[#0f172a] border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="all">All Outcomes</option>
|
||||
<option value="booked">Booked</option>
|
||||
<option value="quoted">Quoted</option>
|
||||
<option value="voicemail">Voicemail</option>
|
||||
<option value="missed">Missed</option>
|
||||
<option value="transferred">Transferred</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={sourceFilter}
|
||||
onChange={(e) => setSourceFilter(e.target.value)}
|
||||
className="bg-[#0f172a] border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="all">All Sources</option>
|
||||
{uniqueSources.map((source) => (
|
||||
<option key={source} value={source}>{source}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calls Table */}
|
||||
<div className="bg-[#1e293b] rounded-lg border border-gray-700 overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#0f172a]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Time</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Caller</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Phone</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Duration</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Source</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Outcome</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Est. Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{filteredCalls.map((call) => (
|
||||
<tr key={call.id} className="hover:bg-[#0f172a] transition-colors cursor-pointer">
|
||||
<td className="px-6 py-4 text-sm text-gray-300">{call.timestamp}</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-white">{call.caller}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-300">{call.phone}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-300">{call.duration}</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<span className="px-2 py-1 bg-blue-500/10 text-blue-400 rounded text-xs border border-blue-500/30">
|
||||
{call.source}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getOutcomeColor(call.outcome)}`}>
|
||||
{call.outcome}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-green-400 font-medium">
|
||||
{call.estimatedValue ? `$${call.estimatedValue.toLocaleString()}` : '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-sm text-gray-400">
|
||||
Showing {filteredCalls.length} of {calls.length} calls
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
189
servers/servicetitan/src/ui/react-app/lead-source-analytics.tsx
Normal file
189
servers/servicetitan/src/ui/react-app/lead-source-analytics.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface LeadSource {
|
||||
name: string;
|
||||
leads: number;
|
||||
conversions: number;
|
||||
revenue: number;
|
||||
cost: number;
|
||||
conversionRate: number;
|
||||
roi: number;
|
||||
}
|
||||
|
||||
export default function LeadSourceAnalytics() {
|
||||
const [sources] = useState<LeadSource[]>([
|
||||
{ name: 'Google Ads', leads: 145, conversions: 68, revenue: 58400, cost: 12500, conversionRate: 46.9, roi: 367 },
|
||||
{ name: 'Facebook Ads', leads: 98, conversions: 42, revenue: 35700, cost: 6800, conversionRate: 42.9, roi: 425 },
|
||||
{ name: 'Google Organic', leads: 76, conversions: 38, revenue: 31200, cost: 0, conversionRate: 50.0, roi: Infinity },
|
||||
{ name: 'Referrals', leads: 54, conversions: 45, revenue: 48900, cost: 0, conversionRate: 83.3, roi: Infinity },
|
||||
{ name: 'Yelp', leads: 32, conversions: 18, revenue: 15300, cost: 800, conversionRate: 56.3, roi: 1813 },
|
||||
{ name: 'Direct Mail', leads: 28, conversions: 12, revenue: 10200, cost: 4500, conversionRate: 42.9, roi: 127 },
|
||||
{ name: 'Website', leads: 89, conversions: 51, revenue: 42800, cost: 2200, conversionRate: 57.3, roi: 1845 },
|
||||
]);
|
||||
|
||||
const [sortBy, setSortBy] = useState<'leads' | 'conversions' | 'revenue' | 'roi'>('revenue');
|
||||
|
||||
const totalLeads = sources.reduce((sum, s) => sum + s.leads, 0);
|
||||
const totalConversions = sources.reduce((sum, s) => sum + s.conversions, 0);
|
||||
const totalRevenue = sources.reduce((sum, s) => sum + s.revenue, 0);
|
||||
const totalCost = sources.reduce((sum, s) => sum + s.cost, 0);
|
||||
const overallConversion = ((totalConversions / totalLeads) * 100).toFixed(1);
|
||||
const overallROI = totalCost > 0 ? (((totalRevenue - totalCost) / totalCost) * 100).toFixed(0) : 'N/A';
|
||||
|
||||
const sortedSources = [...sources].sort((a, b) => {
|
||||
if (sortBy === 'roi') {
|
||||
const aROI = a.roi === Infinity ? 999999 : a.roi;
|
||||
const bROI = b.roi === Infinity ? 999999 : b.roi;
|
||||
return bROI - aROI;
|
||||
}
|
||||
return b[sortBy] - a[sortBy];
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f172a] text-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">📈 Lead Source Analytics</h1>
|
||||
<p className="text-gray-400">Track marketing performance by lead source</p>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Leads</div>
|
||||
<div className="text-3xl font-bold text-white">{totalLeads}</div>
|
||||
<div className="text-green-400 text-sm mt-2">This month</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Conversions</div>
|
||||
<div className="text-3xl font-bold text-green-400">{totalConversions}</div>
|
||||
<div className="text-gray-400 text-sm mt-2">{overallConversion}% rate</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Total Revenue</div>
|
||||
<div className="text-3xl font-bold text-blue-400">${totalRevenue.toLocaleString()}</div>
|
||||
<div className="text-green-400 text-sm mt-2">↑ 18%</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Overall ROI</div>
|
||||
<div className="text-3xl font-bold text-purple-400">
|
||||
{typeof overallROI === 'number' ? `${overallROI}%` : overallROI}
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm mt-2">Cost: ${totalCost.toLocaleString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort Controls */}
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700 mb-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
<span className="text-gray-400 text-sm">Sort by:</span>
|
||||
<div className="flex gap-2">
|
||||
{(['leads', 'conversions', 'revenue', 'roi'] as const).map((option) => (
|
||||
<button
|
||||
key={option}
|
||||
onClick={() => setSortBy(option)}
|
||||
className={`px-4 py-2 rounded-lg transition-colors capitalize ${
|
||||
sortBy === option
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-[#0f172a] text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lead Sources Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{sortedSources.map((source, idx) => (
|
||||
<div key={idx} className="bg-[#1e293b] rounded-lg p-6 border border-gray-700 hover:border-blue-500 transition-all">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-xl font-semibold text-white">{source.name}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
source.conversionRate >= 50 ? 'bg-green-500/20 text-green-400' :
|
||||
source.conversionRate >= 40 ? 'bg-blue-500/20 text-blue-400' :
|
||||
'bg-amber-500/20 text-amber-400'
|
||||
}`}>
|
||||
{source.conversionRate.toFixed(1)}% conversion
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs mb-1">Leads</div>
|
||||
<div className="text-white font-bold text-2xl">{source.leads}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs mb-1">Conversions</div>
|
||||
<div className="text-green-400 font-bold text-2xl">{source.conversions}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs mb-1">Revenue</div>
|
||||
<div className="text-blue-400 font-bold text-lg">${source.revenue.toLocaleString()}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs mb-1">Cost</div>
|
||||
<div className="text-red-400 font-bold text-lg">
|
||||
{source.cost > 0 ? `$${source.cost.toLocaleString()}` : 'Free'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400 text-sm">ROI</span>
|
||||
<span className="text-purple-400 font-bold text-xl">
|
||||
{source.roi === Infinity ? '∞' : `${source.roi}%`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Detailed Table */}
|
||||
<div className="bg-[#1e293b] rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white">Performance Breakdown</h3>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#0f172a]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Source</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Leads</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Conversions</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Conv. Rate</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Revenue</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Cost</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">ROI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{sortedSources.map((source, idx) => (
|
||||
<tr key={idx} className="hover:bg-[#0f172a] transition-colors">
|
||||
<td className="px-6 py-4 text-sm font-medium text-white">{source.name}</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-gray-300">{source.leads}</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-green-400 font-medium">{source.conversions}</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-blue-400">{source.conversionRate.toFixed(1)}%</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-green-400 font-bold">${source.revenue.toLocaleString()}</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-red-400">
|
||||
{source.cost > 0 ? `$${source.cost.toLocaleString()}` : '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-purple-400 font-bold">
|
||||
{source.roi === Infinity ? '∞' : `${source.roi}%`}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
213
servers/servicetitan/src/ui/react-app/performance-metrics.tsx
Normal file
213
servers/servicetitan/src/ui/react-app/performance-metrics.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Metric {
|
||||
name: string;
|
||||
value: number;
|
||||
target: number;
|
||||
unit: string;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
change: number;
|
||||
}
|
||||
|
||||
export default function PerformanceMetrics() {
|
||||
const [timeRange, setTimeRange] = useState<'today' | 'week' | 'month'>('month');
|
||||
|
||||
const [metrics] = useState<Metric[]>([
|
||||
{ name: 'Customer Satisfaction', value: 94, target: 90, unit: '%', trend: 'up', change: 3 },
|
||||
{ name: 'First-Time Fix Rate', value: 87, target: 85, unit: '%', trend: 'up', change: 2 },
|
||||
{ name: 'Avg Response Time', value: 45, target: 60, unit: 'min', trend: 'down', change: -5 },
|
||||
{ name: 'Jobs Per Technician', value: 5.2, target: 5.0, unit: 'jobs', trend: 'up', change: 0.3 },
|
||||
{ name: 'Revenue Per Job', value: 945, target: 900, unit: '$', trend: 'up', change: 45 },
|
||||
{ name: 'Callback Rate', value: 8, target: 10, unit: '%', trend: 'down', change: -2 },
|
||||
{ name: 'On-Time Arrival', value: 89, target: 90, unit: '%', trend: 'stable', change: 0 },
|
||||
{ name: 'Technician Utilization', value: 82, target: 80, unit: '%', trend: 'up', change: 4 },
|
||||
]);
|
||||
|
||||
const getPerformanceColor = (value: number, target: number, unit: string) => {
|
||||
// For percentages and money, higher is better
|
||||
// For time/callbacks, lower is better
|
||||
if (unit === 'min' || unit.includes('Rate') && unit.includes('Callback')) {
|
||||
return value <= target ? 'text-green-400' : 'text-amber-400';
|
||||
}
|
||||
return value >= target ? 'text-green-400' : 'text-amber-400';
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch(trend) {
|
||||
case 'up': return '↑';
|
||||
case 'down': return '↓';
|
||||
case 'stable': return '→';
|
||||
default: return '→';
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendColor = (trend: string, unit: string) => {
|
||||
// For time and callback rates, down is good
|
||||
if (unit === 'min' || unit.includes('Callback')) {
|
||||
return trend === 'down' ? 'text-green-400' : trend === 'up' ? 'text-red-400' : 'text-gray-400';
|
||||
}
|
||||
// For everything else, up is good
|
||||
return trend === 'up' ? 'text-green-400' : trend === 'down' ? 'text-red-400' : 'text-gray-400';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f172a] text-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">📊 Performance Metrics</h1>
|
||||
<p className="text-gray-400">Monitor key business performance indicators</p>
|
||||
</div>
|
||||
<div className="flex gap-2 bg-[#1e293b] rounded-lg p-1 border border-gray-700">
|
||||
{(['today', 'week', 'month'] as const).map((range) => (
|
||||
<button
|
||||
key={range}
|
||||
onClick={() => setTimeRange(range)}
|
||||
className={`px-4 py-2 rounded-md transition-colors capitalize ${
|
||||
timeRange === range ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{range}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Metrics On Target</div>
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
{metrics.filter(m => {
|
||||
if (m.unit === 'min' || m.name.includes('Callback')) {
|
||||
return m.value <= m.target;
|
||||
}
|
||||
return m.value >= m.target;
|
||||
}).length}/{metrics.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Improving Metrics</div>
|
||||
<div className="text-3xl font-bold text-blue-400">
|
||||
{metrics.filter(m => {
|
||||
if (m.unit === 'min' || m.name.includes('Callback')) {
|
||||
return m.trend === 'down';
|
||||
}
|
||||
return m.trend === 'up';
|
||||
}).length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Needs Attention</div>
|
||||
<div className="text-3xl font-bold text-amber-400">
|
||||
{metrics.filter(m => {
|
||||
if (m.unit === 'min' || m.name.includes('Callback')) {
|
||||
return m.value > m.target;
|
||||
}
|
||||
return m.value < m.target;
|
||||
}).length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{metrics.map((metric, idx) => (
|
||||
<div key={idx} className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-lg font-semibold text-white">{metric.name}</h3>
|
||||
<span className={`text-sm font-medium ${getTrendColor(metric.trend, metric.unit)}`}>
|
||||
{getTrendIcon(metric.trend)} {Math.abs(metric.change)}{metric.unit === '%' ? '%' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end justify-between mb-3">
|
||||
<div>
|
||||
<div className="text-4xl font-bold text-white mb-1">
|
||||
{metric.unit === '$' && '$'}
|
||||
{metric.value}
|
||||
{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Target: {metric.unit === '$' && '$'}{metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative pt-2">
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
(metric.unit === 'min' || metric.name.includes('Callback'))
|
||||
? (metric.value <= metric.target ? 'bg-green-500' : 'bg-amber-500')
|
||||
: (metric.value >= metric.target ? 'bg-green-500' : 'bg-amber-500')
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.min((metric.value / metric.target) * 100, 100)}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-500">
|
||||
<span>0</span>
|
||||
<span>{metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Performance Table */}
|
||||
<div className="bg-[#1e293b] rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white">Detailed Breakdown</h3>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead className="bg-[#0f172a]">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase">Metric</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Current</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Target</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Trend</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-400 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{metrics.map((metric, idx) => {
|
||||
const onTarget = (metric.unit === 'min' || metric.name.includes('Callback'))
|
||||
? metric.value <= metric.target
|
||||
: metric.value >= metric.target;
|
||||
|
||||
return (
|
||||
<tr key={idx} className="hover:bg-[#0f172a] transition-colors">
|
||||
<td className="px-6 py-4 text-sm text-white">{metric.name}</td>
|
||||
<td className="px-6 py-4 text-sm text-right font-bold text-white">
|
||||
{metric.unit === '$' && '$'}{metric.value}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-right text-gray-400">
|
||||
{metric.unit === '$' && '$'}{metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
|
||||
</td>
|
||||
<td className={`px-6 py-4 text-sm text-right font-medium ${getTrendColor(metric.trend, metric.unit)}`}>
|
||||
{getTrendIcon(metric.trend)} {Math.abs(metric.change)}{metric.unit === '%' ? '%' : ''}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
onTarget ? 'bg-green-500/20 text-green-400' : 'bg-amber-500/20 text-amber-400'
|
||||
}`}>
|
||||
{onTarget ? 'On Target' : 'Below Target'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
194
servers/servicetitan/src/ui/react-app/schedule-calendar.tsx
Normal file
194
servers/servicetitan/src/ui/react-app/schedule-calendar.tsx
Normal file
@ -0,0 +1,194 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface Appointment {
|
||||
id: number;
|
||||
jobNumber: string;
|
||||
customer: string;
|
||||
technician: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
type: string;
|
||||
status: 'confirmed' | 'tentative' | 'completed';
|
||||
}
|
||||
|
||||
export default function ScheduleCalendar() {
|
||||
const [selectedDate, setSelectedDate] = useState('2024-02-15');
|
||||
const [viewMode, setViewMode] = useState<'day' | 'week'>('day');
|
||||
|
||||
const [appointments] = useState<Appointment[]>([
|
||||
{ id: 1, jobNumber: 'J-2024-001', customer: 'John Smith', technician: 'Mike Johnson', startTime: '09:00', endTime: '11:00', type: 'HVAC Maintenance', status: 'confirmed' },
|
||||
{ id: 2, jobNumber: 'J-2024-002', customer: 'Sarah Johnson', technician: 'David Lee', startTime: '10:00', endTime: '12:00', type: 'Emergency Repair', status: 'confirmed' },
|
||||
{ id: 3, jobNumber: 'J-2024-003', customer: 'Mike Davis', technician: 'Tom Wilson', startTime: '11:00', endTime: '13:00', type: 'Installation', status: 'completed' },
|
||||
{ id: 4, jobNumber: 'J-2024-004', customer: 'Emily Wilson', technician: 'Chris Brown', startTime: '13:00', endTime: '15:00', type: 'Inspection', status: 'confirmed' },
|
||||
{ id: 5, jobNumber: 'J-2024-005', customer: 'Robert Brown', technician: 'Mike Johnson', startTime: '14:00', endTime: '16:00', type: 'HVAC Repair', status: 'tentative' },
|
||||
{ id: 6, jobNumber: 'J-2024-006', customer: 'Lisa Anderson', technician: 'David Lee', startTime: '15:00', endTime: '17:00', type: 'Maintenance', status: 'confirmed' },
|
||||
]);
|
||||
|
||||
const timeSlots = Array.from({ length: 11 }, (_, i) => {
|
||||
const hour = i + 8; // 8 AM to 6 PM
|
||||
return `${hour.toString().padStart(2, '0')}:00`;
|
||||
});
|
||||
|
||||
const technicians = Array.from(new Set(appointments.map(a => a.technician)));
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch(status) {
|
||||
case 'confirmed': return 'bg-blue-500/20 border-blue-500 text-blue-400';
|
||||
case 'tentative': return 'bg-amber-500/20 border-amber-500 text-amber-400';
|
||||
case 'completed': return 'bg-green-500/20 border-green-500 text-green-400';
|
||||
default: return 'bg-gray-500/20 border-gray-500 text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getAppointmentPosition = (startTime: string, endTime: string) => {
|
||||
const start = parseInt(startTime.split(':')[0]);
|
||||
const end = parseInt(endTime.split(':')[0]);
|
||||
const top = (start - 8) * 80; // 80px per hour
|
||||
const height = (end - start) * 80;
|
||||
return { top, height };
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0f172a] text-gray-100 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">📅 Schedule Calendar</h1>
|
||||
<p className="text-gray-400">View and manage appointments</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="bg-[#1e293b] border border-gray-600 rounded-lg px-4 py-2 text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
<div className="flex gap-2 bg-[#1e293b] rounded-lg p-1 border border-gray-700">
|
||||
<button
|
||||
onClick={() => setViewMode('day')}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
viewMode === 'day' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Day
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('week')}
|
||||
className={`px-4 py-2 rounded-md transition-colors ${
|
||||
viewMode === 'week' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Week
|
||||
</button>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
|
||||
+ New Appointment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Today's Appointments</div>
|
||||
<div className="text-3xl font-bold text-white">{appointments.length}</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Confirmed</div>
|
||||
<div className="text-3xl font-bold text-blue-400">
|
||||
{appointments.filter(a => a.status === 'confirmed').length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Completed</div>
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
{appointments.filter(a => a.status === 'completed').length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1e293b] rounded-lg p-6 border border-gray-700">
|
||||
<div className="text-gray-400 text-sm mb-1">Technicians Active</div>
|
||||
<div className="text-3xl font-bold text-purple-400">{technicians.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar View */}
|
||||
<div className="bg-[#1e293b] rounded-lg border border-gray-700 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-white">Schedule for {selectedDate}</h3>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-[800px]">
|
||||
{/* Header */}
|
||||
<div className="flex border-b border-gray-700 bg-[#0f172a]">
|
||||
<div className="w-20 p-3 border-r border-gray-700 text-xs font-medium text-gray-400">TIME</div>
|
||||
{technicians.map((tech, idx) => (
|
||||
<div key={idx} className="flex-1 p-3 border-r border-gray-700 last:border-r-0">
|
||||
<div className="text-sm font-medium text-white">{tech}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Time Grid */}
|
||||
<div className="relative">
|
||||
{timeSlots.map((time, idx) => (
|
||||
<div key={idx} className="flex border-b border-gray-700">
|
||||
<div className="w-20 p-3 border-r border-gray-700 text-xs text-gray-400">{time}</div>
|
||||
{technicians.map((tech, techIdx) => (
|
||||
<div
|
||||
key={techIdx}
|
||||
className="flex-1 border-r border-gray-700 last:border-r-0 relative"
|
||||
style={{ height: '80px' }}
|
||||
>
|
||||
{/* Render appointments for this tech and time slot */}
|
||||
{appointments
|
||||
.filter(apt => apt.technician === tech && apt.startTime.startsWith(time.split(':')[0]))
|
||||
.map((apt) => {
|
||||
const { height } = getAppointmentPosition(apt.startTime, apt.endTime);
|
||||
return (
|
||||
<div
|
||||
key={apt.id}
|
||||
className={`absolute left-1 right-1 rounded p-2 border-l-4 ${getStatusColor(apt.status)} cursor-pointer hover:opacity-80 transition-opacity`}
|
||||
style={{ height: `${height - 4}px` }}
|
||||
>
|
||||
<div className="text-xs font-semibold mb-1">{apt.jobNumber}</div>
|
||||
<div className="text-xs">{apt.customer}</div>
|
||||
<div className="text-xs opacity-75">{apt.type}</div>
|
||||
<div className="text-xs opacity-75">{apt.startTime} - {apt.endTime}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 flex gap-6 justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-blue-500/20 border-2 border-blue-500 rounded"></div>
|
||||
<span className="text-sm text-gray-400">Confirmed</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-amber-500/20 border-2 border-amber-500 rounded"></div>
|
||||
<span className="text-sm text-gray-400">Tentative</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 bg-green-500/20 border-2 border-green-500 rounded"></div>
|
||||
<span className="text-sm text-gray-400">Completed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,20 +1,41 @@
|
||||
{
|
||||
"name": "mcp-server-squarespace",
|
||||
"name": "@mcpengine/squarespace",
|
||||
"version": "1.0.0",
|
||||
"description": "Complete MCP server for Squarespace - website builder and ecommerce platform",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"bin": {
|
||||
"squarespace-mcp": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && npm run build:ui",
|
||||
"build:ui": "node scripts/build-ui.js",
|
||||
"prepublishOnly": "npm run build",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"squarespace",
|
||||
"website-builder",
|
||||
"ecommerce",
|
||||
"cms"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^16.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.10.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
93
servers/squarespace/src/apps/blog-manager.ts
Normal file
93
servers/squarespace/src/apps/blog-manager.ts
Normal file
@ -0,0 +1,93 @@
|
||||
export const blogManagerApp = {
|
||||
name: 'blog-manager',
|
||||
description: 'Manage blog posts with publishing workflow',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { posts = [] } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Blog Manager - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.toolbar { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 20px; display: flex; gap: 12px; }
|
||||
.search-box { flex: 1; padding: 12px; border: 1px solid #e5e5e5; border-radius: 6px; font-size: 14px; }
|
||||
.filter-select { padding: 12px; border: 1px solid #e5e5e5; border-radius: 6px; font-size: 14px; }
|
||||
.new-post-btn { background: #000; color: white; padding: 12px 24px; border-radius: 6px; border: none; cursor: pointer; font-size: 14px; }
|
||||
.new-post-btn:hover { background: #333; }
|
||||
.posts-grid { display: grid; gap: 20px; }
|
||||
.post-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; display: flex; gap: 20px; }
|
||||
.post-image { width: 200px; height: 140px; background: #f5f5f5; border-radius: 8px; object-fit: cover; flex-shrink: 0; }
|
||||
.post-content { flex: 1; }
|
||||
.post-title { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: #1a1a1a; }
|
||||
.post-excerpt { font-size: 14px; color: #666; margin-bottom: 12px; line-height: 1.5; }
|
||||
.post-meta { font-size: 13px; color: #999; margin-bottom: 12px; }
|
||||
.post-tags { display: flex; gap: 8px; margin-bottom: 12px; }
|
||||
.tag { background: #f0f0f0; padding: 4px 12px; border-radius: 12px; font-size: 12px; }
|
||||
.badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.badge-published { background: #d1fae5; color: #065f46; }
|
||||
.badge-draft { background: #fee2e2; color: #991b1b; }
|
||||
.post-actions { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.action-btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; }
|
||||
.action-btn-primary { background: #000; color: white; }
|
||||
.action-btn-primary:hover { background: #333; }
|
||||
.action-btn-secondary { background: white; border: 1px solid #e5e5e5; color: #000; }
|
||||
.action-btn-secondary:hover { background: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✍️ Blog Manager</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<input type="text" class="search-box" placeholder="Search posts...">
|
||||
<select class="filter-select">
|
||||
<option value="">All Posts</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="draft">Drafts</option>
|
||||
</select>
|
||||
<button class="new-post-btn" onclick="window.parent.postMessage({type:'create-post'},'*')">+ New Post</button>
|
||||
</div>
|
||||
|
||||
<div class="posts-grid">
|
||||
${posts.map((post: any) => `
|
||||
<div class="post-card">
|
||||
${post.featuredImage?.url ? `
|
||||
<img src="${post.featuredImage.url}" class="post-image" alt="${post.title}">
|
||||
` : '<div class="post-image"></div>'}
|
||||
<div class="post-content">
|
||||
<div class="post-title">${post.title}</div>
|
||||
${post.excerpt ? `<div class="post-excerpt">${post.excerpt}</div>` : ''}
|
||||
<div class="post-meta">
|
||||
By ${post.author.displayName} • ${post.publishedOn ? new Date(post.publishedOn).toLocaleDateString() : 'Not published'}
|
||||
</div>
|
||||
${post.tags?.length ? `
|
||||
<div class="post-tags">
|
||||
${post.tags.map((tag: string) => `<span class="tag">${tag}</span>`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<span class="badge badge-${post.isPublished ? 'published' : 'draft'}">
|
||||
${post.isPublished ? 'Published' : 'Draft'}
|
||||
</span>
|
||||
<div class="post-actions">
|
||||
<button class="action-btn action-btn-primary" onclick="window.parent.postMessage({type:'edit-post',postId:'${post.id}'},'*')">Edit</button>
|
||||
<button class="action-btn action-btn-secondary" onclick="window.open('${post.fullUrl}', '_blank')">View</button>
|
||||
<button class="action-btn action-btn-secondary" onclick="window.parent.postMessage({type:'delete-post',postId:'${post.id}'},'*')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
69
servers/squarespace/src/apps/customer-grid.ts
Normal file
69
servers/squarespace/src/apps/customer-grid.ts
Normal file
@ -0,0 +1,69 @@
|
||||
export const customerGridApp = {
|
||||
name: 'customer-grid',
|
||||
description: 'Customer directory with search and filtering',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { customers = [] } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Customers - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.customer-section { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; }
|
||||
.search-box { width: 100%; padding: 12px; border: 1px solid #e5e5e5; border-radius: 6px; margin-bottom: 20px; font-size: 14px; }
|
||||
.customer-table { width: 100%; border-collapse: collapse; }
|
||||
.customer-table th { text-align: left; padding: 12px; border-bottom: 2px solid #e5e5e5; font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.customer-table td { padding: 16px 12px; border-bottom: 1px solid #f0f0f0; }
|
||||
.customer-name { font-weight: 600; color: #1a1a1a; }
|
||||
.customer-email { font-size: 13px; color: #666; }
|
||||
.action-btn { background: #000; color: white; padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; }
|
||||
.action-btn:hover { background: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>👥 Customers</h1>
|
||||
|
||||
<div class="customer-section">
|
||||
<input type="text" class="search-box" placeholder="Search customers by name or email...">
|
||||
<table class="customer-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<th>Email</th>
|
||||
<th>Orders</th>
|
||||
<th>Total Spent</th>
|
||||
<th>Member Since</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${customers.map((customer: any) => `
|
||||
<tr>
|
||||
<td>
|
||||
<div class="customer-name">${customer.firstName || ''} ${customer.lastName || ''}</div>
|
||||
</td>
|
||||
<td><div class="customer-email">${customer.email}</div></td>
|
||||
<td>${customer.orderCount || 0}</td>
|
||||
<td>${customer.totalSpent ? `${customer.totalSpent.currency} ${customer.totalSpent.value}` : '$0.00'}</td>
|
||||
<td>${new Date(customer.createdOn).toLocaleDateString()}</td>
|
||||
<td><button class="action-btn" onclick="window.parent.postMessage({type:'view-customer',customerId:'${customer.id}'},'*')">View</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
62
servers/squarespace/src/apps/form-submissions.ts
Normal file
62
servers/squarespace/src/apps/form-submissions.ts
Normal file
@ -0,0 +1,62 @@
|
||||
export const formSubmissionsApp = {
|
||||
name: 'form-submissions',
|
||||
description: 'View and manage form submissions',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { submissions = [] } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Form Submissions - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.submissions-grid { display: grid; gap: 16px; }
|
||||
.submission-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; }
|
||||
.submission-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #e5e5e5; }
|
||||
.form-name { font-size: 18px; font-weight: 600; color: #1a1a1a; }
|
||||
.submission-date { font-size: 13px; color: #666; }
|
||||
.fields-grid { display: grid; gap: 12px; }
|
||||
.field-row { display: flex; gap: 16px; }
|
||||
.field-label { font-weight: 600; color: #666; min-width: 150px; }
|
||||
.field-value { color: #1a1a1a; flex: 1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📝 Form Submissions</h1>
|
||||
|
||||
<div class="submissions-grid">
|
||||
${submissions.length === 0 ? `
|
||||
<div class="submission-card">
|
||||
<p style="text-align: center; color: #666;">No form submissions yet</p>
|
||||
</div>
|
||||
` : submissions.map((submission: any) => `
|
||||
<div class="submission-card">
|
||||
<div class="submission-header">
|
||||
<div class="form-name">${submission.formName}</div>
|
||||
<div class="submission-date">${new Date(submission.submittedOn).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="fields-grid">
|
||||
${submission.fields.map((field: any) => `
|
||||
<div class="field-row">
|
||||
<div class="field-label">${field.name}:</div>
|
||||
<div class="field-value">${field.value}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
108
servers/squarespace/src/apps/inventory-tracker.ts
Normal file
108
servers/squarespace/src/apps/inventory-tracker.ts
Normal file
@ -0,0 +1,108 @@
|
||||
export const inventoryTrackerApp = {
|
||||
name: 'inventory-tracker',
|
||||
description: 'Real-time inventory tracking and stock management',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { inventory = [] } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inventory Tracker - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.inventory-section { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.section-title { font-size: 20px; font-weight: 600; }
|
||||
.inventory-table { width: 100%; border-collapse: collapse; }
|
||||
.inventory-table th { text-align: left; padding: 12px; border-bottom: 2px solid #e5e5e5; font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.inventory-table td { padding: 16px 12px; border-bottom: 1px solid #f0f0f0; }
|
||||
.product-name { font-weight: 600; color: #1a1a1a; }
|
||||
.sku { font-size: 13px; color: #666; }
|
||||
.stock-badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.stock-good { background: #d1fae5; color: #065f46; }
|
||||
.stock-low { background: #fef3c7; color: #92400e; }
|
||||
.stock-out { background: #fee2e2; color: #991b1b; }
|
||||
.stock-unlimited { background: #dbeafe; color: #1e40af; }
|
||||
.action-btn { background: #000; color: white; padding: 6px 12px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; }
|
||||
.action-btn:hover { background: #333; }
|
||||
.stock-input { width: 80px; padding: 6px; border: 1px solid #e5e5e5; border-radius: 4px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📊 Inventory Tracker</h1>
|
||||
|
||||
<div class="inventory-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">Stock Levels</div>
|
||||
</div>
|
||||
<table class="inventory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Current Stock</th>
|
||||
<th>Status</th>
|
||||
<th>Adjust</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${inventory.map((item: any) => {
|
||||
let stockClass = 'stock-good';
|
||||
let stockLabel = 'In Stock';
|
||||
|
||||
if (item.unlimited) {
|
||||
stockClass = 'stock-unlimited';
|
||||
stockLabel = 'Unlimited';
|
||||
} else if (item.quantity === 0) {
|
||||
stockClass = 'stock-out';
|
||||
stockLabel = 'Out of Stock';
|
||||
} else if (item.quantity < 10) {
|
||||
stockClass = 'stock-low';
|
||||
stockLabel = 'Low Stock';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><div class="product-name">${item.productName}</div></td>
|
||||
<td><div class="sku">${item.sku || 'N/A'}</div></td>
|
||||
<td>${item.unlimited ? '∞' : item.quantity}</td>
|
||||
<td><span class="stock-badge ${stockClass}">${stockLabel}</span></td>
|
||||
<td>
|
||||
${!item.unlimited ? `
|
||||
<input type="number" class="stock-input" value="${item.quantity}" id="stock-${item.variantId}">
|
||||
<button class="action-btn" onclick="adjustStock('${item.variantId}')">Update</button>
|
||||
` : '<span style="color: #666;">—</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function adjustStock(variantId) {
|
||||
const input = document.getElementById('stock-' + variantId);
|
||||
const newQuantity = parseInt(input.value);
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'adjust-stock',
|
||||
variantId: variantId,
|
||||
quantity: newQuantity
|
||||
}, '*');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
99
servers/squarespace/src/apps/order-dashboard.ts
Normal file
99
servers/squarespace/src/apps/order-dashboard.ts
Normal file
@ -0,0 +1,99 @@
|
||||
export const orderDashboardApp = {
|
||||
name: 'order-dashboard',
|
||||
description: 'Dashboard showing order overview and recent orders',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { orders = [], stats = {} } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Order Dashboard - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 32px; }
|
||||
.stat-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.stat-label { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
||||
.stat-value { font-size: 36px; font-weight: 600; color: #1a1a1a; }
|
||||
.stat-change { font-size: 14px; color: #10b981; margin-top: 8px; }
|
||||
.orders-section { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.section-title { font-size: 20px; font-weight: 600; }
|
||||
.orders-table { width: 100%; border-collapse: collapse; }
|
||||
.orders-table th { text-align: left; padding: 12px; border-bottom: 2px solid #e5e5e5; font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
.orders-table td { padding: 16px 12px; border-bottom: 1px solid #f0f0f0; }
|
||||
.order-id { font-weight: 600; color: #1a1a1a; }
|
||||
.badge { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }
|
||||
.badge-pending { background: #fef3c7; color: #92400e; }
|
||||
.badge-fulfilled { background: #d1fae5; color: #065f46; }
|
||||
.badge-canceled { background: #fee2e2; color: #991b1b; }
|
||||
.action-btn { background: #000; color: white; padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 14px; }
|
||||
.action-btn:hover { background: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📦 Order Dashboard</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Orders</div>
|
||||
<div class="stat-value">${stats.totalOrders || 0}</div>
|
||||
<div class="stat-change">+${stats.ordersChange || 0}% vs last period</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Revenue</div>
|
||||
<div class="stat-value">$${stats.totalRevenue || '0.00'}</div>
|
||||
<div class="stat-change">+${stats.revenueChange || 0}% vs last period</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Avg Order Value</div>
|
||||
<div class="stat-value">$${stats.avgOrderValue || '0.00'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Pending Orders</div>
|
||||
<div class="stat-value">${stats.pendingOrders || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="orders-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">Recent Orders</div>
|
||||
</div>
|
||||
<table class="orders-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order #</th>
|
||||
<th>Customer</th>
|
||||
<th>Date</th>
|
||||
<th>Total</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${orders.map((order: any) => `
|
||||
<tr>
|
||||
<td><span class="order-id">#${order.orderNumber}</span></td>
|
||||
<td>${order.customerEmail}</td>
|
||||
<td>${new Date(order.createdOn).toLocaleDateString()}</td>
|
||||
<td>${order.grandTotal.currency} ${order.grandTotal.value}</td>
|
||||
<td><span class="badge badge-${order.fulfillmentStatus.toLowerCase()}">${order.fulfillmentStatus}</span></td>
|
||||
<td><button class="action-btn" onclick="window.parent.postMessage({type:'view-order',orderId:'${order.id}'},'*')">View</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
150
servers/squarespace/src/apps/order-detail.ts
Normal file
150
servers/squarespace/src/apps/order-detail.ts
Normal file
@ -0,0 +1,150 @@
|
||||
export const orderDetailApp = {
|
||||
name: 'order-detail',
|
||||
description: 'Detailed view of a specific order with all line items and customer info',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { order } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Order #${order.orderNumber} - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||||
h1 { font-size: 32px; color: #1a1a1a; }
|
||||
.badge { display: inline-block; padding: 6px 16px; border-radius: 12px; font-size: 14px; font-weight: 600; margin-left: 12px; }
|
||||
.badge-pending { background: #fef3c7; color: #92400e; }
|
||||
.badge-fulfilled { background: #d1fae5; color: #065f46; }
|
||||
.badge-canceled { background: #fee2e2; color: #991b1b; }
|
||||
.grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; }
|
||||
.card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 20px; }
|
||||
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1a1a1a; }
|
||||
.line-item { display: flex; gap: 16px; padding: 16px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.line-item:last-child { border-bottom: none; }
|
||||
.item-image { width: 80px; height: 80px; background: #f5f5f5; border-radius: 8px; object-fit: cover; }
|
||||
.item-details { flex: 1; }
|
||||
.item-name { font-weight: 600; margin-bottom: 4px; }
|
||||
.item-meta { font-size: 14px; color: #666; }
|
||||
.item-price { text-align: right; font-weight: 600; }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { color: #666; }
|
||||
.info-value { font-weight: 500; }
|
||||
.total-row { font-size: 18px; font-weight: 600; padding-top: 16px; border-top: 2px solid #e5e5e5; margin-top: 16px; }
|
||||
.action-btn { background: #000; color: white; padding: 12px 24px; border-radius: 6px; border: none; cursor: pointer; font-size: 14px; margin-right: 12px; }
|
||||
.action-btn:hover { background: #333; }
|
||||
.action-btn-secondary { background: white; color: #000; border: 1px solid #e5e5e5; }
|
||||
.action-btn-secondary:hover { background: #f5f5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>Order #${order.orderNumber}</h1>
|
||||
<span class="badge badge-${order.fulfillmentStatus.toLowerCase()}">${order.fulfillmentStatus}</span>
|
||||
</div>
|
||||
<div>
|
||||
${order.fulfillmentStatus === 'PENDING' ? `<button class="action-btn" onclick="fulfillOrder()">Fulfill Order</button>` : ''}
|
||||
<button class="action-btn action-btn-secondary" onclick="window.parent.postMessage({type:'refund-order',orderId:'${order.id}'},'*')">Refund</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">Order Items</div>
|
||||
${order.lineItems.map((item: any) => `
|
||||
<div class="line-item">
|
||||
${item.imageUrl ? `<img src="${item.imageUrl}" class="item-image" alt="${item.productName}">` : '<div class="item-image"></div>'}
|
||||
<div class="item-details">
|
||||
<div class="item-name">${item.productName}</div>
|
||||
<div class="item-meta">Quantity: ${item.quantity}${item.sku ? ` • SKU: ${item.sku}` : ''}</div>
|
||||
${item.customizations ? `<div class="item-meta">${item.customizations.map((c: any) => `${c.label}: ${c.value}`).join(', ')}</div>` : ''}
|
||||
</div>
|
||||
<div class="item-price">${item.unitPricePaid.currency} ${item.unitPricePaid.value}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Order Summary</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Subtotal</span>
|
||||
<span class="info-value">${order.subtotal.currency} ${order.subtotal.value}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Shipping</span>
|
||||
<span class="info-value">${order.shippingTotal.currency} ${order.shippingTotal.value}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Tax</span>
|
||||
<span class="info-value">${order.taxTotal.currency} ${order.taxTotal.value}</span>
|
||||
</div>
|
||||
${parseFloat(order.discountTotal.value) > 0 ? `
|
||||
<div class="info-row">
|
||||
<span class="info-label">Discount</span>
|
||||
<span class="info-value">-${order.discountTotal.currency} ${order.discountTotal.value}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="info-row total-row">
|
||||
<span>Total</span>
|
||||
<span>${order.grandTotal.currency} ${order.grandTotal.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="card-title">Customer</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Email</span>
|
||||
<span class="info-value">${order.customerEmail}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Order Date</span>
|
||||
<span class="info-value">${new Date(order.createdOn).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Shipping Address</div>
|
||||
${order.shippingAddress ? `
|
||||
<p><strong>${order.shippingAddress.firstName} ${order.shippingAddress.lastName}</strong></p>
|
||||
<p>${order.shippingAddress.address1}</p>
|
||||
${order.shippingAddress.address2 ? `<p>${order.shippingAddress.address2}</p>` : ''}
|
||||
<p>${order.shippingAddress.city}, ${order.shippingAddress.state || ''} ${order.shippingAddress.postalCode}</p>
|
||||
<p>${order.shippingAddress.countryCode}</p>
|
||||
${order.shippingAddress.phone ? `<p>Phone: ${order.shippingAddress.phone}</p>` : ''}
|
||||
` : '<p>No shipping address</p>'}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">Billing Address</div>
|
||||
<p><strong>${order.billingAddress.firstName} ${order.billingAddress.lastName}</strong></p>
|
||||
<p>${order.billingAddress.address1}</p>
|
||||
${order.billingAddress.address2 ? `<p>${order.billingAddress.address2}</p>` : ''}
|
||||
<p>${order.billingAddress.city}, ${order.billingAddress.state || ''} ${order.billingAddress.postalCode}</p>
|
||||
<p>${order.billingAddress.countryCode}</p>
|
||||
${order.billingAddress.phone ? `<p>Phone: ${order.billingAddress.phone}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fulfillOrder() {
|
||||
window.parent.postMessage({type: 'fulfill-order', orderId: '${order.id}'}, '*');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
122
servers/squarespace/src/apps/product-dashboard.ts
Normal file
122
servers/squarespace/src/apps/product-dashboard.ts
Normal file
@ -0,0 +1,122 @@
|
||||
export const productDashboardApp = {
|
||||
name: 'product-dashboard',
|
||||
description: 'Product catalog overview with search and filtering',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { products = [], stats = {} } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Product Dashboard - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 24px; color: #1a1a1a; }
|
||||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 32px; }
|
||||
.stat-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.stat-label { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
||||
.stat-value { font-size: 32px; font-weight: 600; color: #1a1a1a; }
|
||||
.toolbar { background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 20px; display: flex; gap: 12px; }
|
||||
.search-box { flex: 1; padding: 12px; border: 1px solid #e5e5e5; border-radius: 6px; font-size: 14px; }
|
||||
.filter-select { padding: 12px; border: 1px solid #e5e5e5; border-radius: 6px; font-size: 14px; }
|
||||
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
|
||||
.product-card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; cursor: pointer; transition: transform 0.2s; }
|
||||
.product-card:hover { transform: translateY(-4px); box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
|
||||
.product-image { width: 100%; height: 220px; background: #f5f5f5; object-fit: cover; }
|
||||
.product-info { padding: 20px; }
|
||||
.product-name { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: #1a1a1a; }
|
||||
.product-price { font-size: 18px; font-weight: 600; color: #000; margin-bottom: 8px; }
|
||||
.product-meta { font-size: 13px; color: #666; }
|
||||
.badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; margin-right: 4px; }
|
||||
.badge-visible { background: #d1fae5; color: #065f46; }
|
||||
.badge-hidden { background: #fee2e2; color: #991b1b; }
|
||||
.badge-sale { background: #fef3c7; color: #92400e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🛍️ Products</h1>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Products</div>
|
||||
<div class="stat-value">${stats.totalProducts || 0}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Published</div>
|
||||
<div class="stat-value">${stats.publishedProducts || 0}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">On Sale</div>
|
||||
<div class="stat-value">${stats.onSale || 0}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Low Stock</div>
|
||||
<div class="stat-value">${stats.lowStock || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<input type="text" class="search-box" placeholder="Search products..." id="searchBox">
|
||||
<select class="filter-select" id="visibilityFilter">
|
||||
<option value="">All Products</option>
|
||||
<option value="true">Published</option>
|
||||
<option value="false">Hidden</option>
|
||||
</select>
|
||||
<select class="filter-select" id="typeFilter">
|
||||
<option value="">All Types</option>
|
||||
<option value="PHYSICAL">Physical</option>
|
||||
<option value="DIGITAL">Digital</option>
|
||||
<option value="SERVICE">Service</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="product-grid">
|
||||
${products.map((product: any) => {
|
||||
const firstVariant = product.variants?.[0];
|
||||
const price = firstVariant?.pricing?.basePrice;
|
||||
const image = product.images?.[0]?.url || '';
|
||||
|
||||
return `
|
||||
<div class="product-card" onclick="window.parent.postMessage({type:'view-product',productId:'${product.id}'},'*')">
|
||||
${image ? `<img src="${image}" class="product-image" alt="${product.name}">` : '<div class="product-image"></div>'}
|
||||
<div class="product-info">
|
||||
<div class="product-name">${product.name}</div>
|
||||
<div class="product-price">${price ? `${price.currency} ${price.value}` : 'Price varies'}</div>
|
||||
<div class="product-meta">
|
||||
<span class="badge badge-${product.isVisible ? 'visible' : 'hidden'}">${product.isVisible ? 'Published' : 'Hidden'}</span>
|
||||
${firstVariant?.onSale ? '<span class="badge badge-sale">On Sale</span>' : ''}
|
||||
${product.variants?.length > 1 ? `<span class="badge">${product.variants.length} variants</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple client-side filtering
|
||||
const searchBox = document.getElementById('searchBox');
|
||||
const visibilityFilter = document.getElementById('visibilityFilter');
|
||||
const typeFilter = document.getElementById('typeFilter');
|
||||
|
||||
function filterProducts() {
|
||||
// In a real implementation, this would filter the product grid
|
||||
console.log('Filtering products...');
|
||||
}
|
||||
|
||||
searchBox.addEventListener('input', filterProducts);
|
||||
visibilityFilter.addEventListener('change', filterProducts);
|
||||
typeFilter.addEventListener('change', filterProducts);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
120
servers/squarespace/src/apps/site-analytics.ts
Normal file
120
servers/squarespace/src/apps/site-analytics.ts
Normal file
@ -0,0 +1,120 @@
|
||||
export const siteAnalyticsApp = {
|
||||
name: 'site-analytics',
|
||||
description: 'Site traffic and engagement analytics dashboard',
|
||||
|
||||
async render(context: any, data: any) {
|
||||
const { traffic = {}, popular = [], commerce = {} } = data;
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Analytics - Squarespace</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; padding: 20px; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 32px; margin-bottom: 8px; color: #1a1a1a; }
|
||||
.subtitle { font-size: 14px; color: #666; margin-bottom: 24px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 32px; }
|
||||
.stat-card { background: white; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
|
||||
.stat-label { font-size: 14px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
||||
.stat-value { font-size: 36px; font-weight: 600; color: #1a1a1a; }
|
||||
.stat-change { font-size: 14px; color: #10b981; margin-top: 8px; }
|
||||
.section { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 20px; }
|
||||
.section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; }
|
||||
.popular-table { width: 100%; border-collapse: collapse; }
|
||||
.popular-table th { text-align: left; padding: 12px; border-bottom: 2px solid #e5e5e5; font-size: 12px; color: #666; text-transform: uppercase; }
|
||||
.popular-table td { padding: 16px 12px; border-bottom: 1px solid #f0f0f0; }
|
||||
.page-url { color: #666; font-size: 13px; }
|
||||
.views-bar { height: 8px; background: linear-gradient(90deg, #000, #666); border-radius: 4px; }
|
||||
.commerce-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
|
||||
.commerce-stat { padding: 16px; background: #f9fafb; border-radius: 8px; }
|
||||
.commerce-label { font-size: 13px; color: #666; margin-bottom: 4px; }
|
||||
.commerce-value { font-size: 24px; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📊 Site Analytics</h1>
|
||||
<div class="subtitle">${traffic.startDate ? `${traffic.startDate} - ${traffic.endDate}` : 'Last 30 days'}</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Page Views</div>
|
||||
<div class="stat-value">${(traffic.pageViews || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Unique Visitors</div>
|
||||
<div class="stat-value">${(traffic.uniqueVisitors || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Bounce Rate</div>
|
||||
<div class="stat-value">${traffic.bounceRate ? `${traffic.bounceRate}%` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Avg. Session</div>
|
||||
<div class="stat-value">${traffic.avgSessionDuration ? `${Math.round(traffic.avgSessionDuration / 60)}m` : 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${popular?.pages?.length ? `
|
||||
<div class="section">
|
||||
<div class="section-title">Popular Pages</div>
|
||||
<table class="popular-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Page</th>
|
||||
<th>Views</th>
|
||||
<th style="width: 200px;">Popularity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${popular.pages.map((page: any, index: number) => {
|
||||
const maxViews = popular.pages[0]?.pageViews || 1;
|
||||
const width = (page.pageViews / maxViews) * 100;
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<div>${page.title}</div>
|
||||
<div class="page-url">${page.url}</div>
|
||||
</td>
|
||||
<td>${page.pageViews.toLocaleString()}</td>
|
||||
<td>
|
||||
<div class="views-bar" style="width: ${width}%"></div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${commerce.revenue ? `
|
||||
<div class="section">
|
||||
<div class="section-title">Commerce Performance</div>
|
||||
<div class="commerce-stats">
|
||||
<div class="commerce-stat">
|
||||
<div class="commerce-label">Revenue</div>
|
||||
<div class="commerce-value">${commerce.revenue.currency} ${commerce.revenue.value}</div>
|
||||
</div>
|
||||
<div class="commerce-stat">
|
||||
<div class="commerce-label">Orders</div>
|
||||
<div class="commerce-value">${commerce.orderCount || 0}</div>
|
||||
</div>
|
||||
<div class="commerce-stat">
|
||||
<div class="commerce-label">Avg Order Value</div>
|
||||
<div class="commerce-value">${commerce.averageOrderValue.currency} ${commerce.averageOrderValue.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
};
|
||||
346
servers/squarespace/src/clients/squarespace.ts
Normal file
346
servers/squarespace/src/clients/squarespace.ts
Normal file
@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Squarespace API Client
|
||||
* Handles all interactions with the Squarespace API
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import type {
|
||||
SquarespaceConfig,
|
||||
Site,
|
||||
Page,
|
||||
Product,
|
||||
Order,
|
||||
Inventory,
|
||||
Customer,
|
||||
FormSubmission,
|
||||
AnalyticsData,
|
||||
SEOSettings,
|
||||
BlogPost,
|
||||
Transaction,
|
||||
WebhookSubscription,
|
||||
Collection,
|
||||
DomainMapping,
|
||||
SquarespaceError,
|
||||
} from '../types/index.js';
|
||||
|
||||
export class SquarespaceClient {
|
||||
private client: AxiosInstance;
|
||||
private apiKey: string;
|
||||
private siteId?: string;
|
||||
|
||||
constructor(config: SquarespaceConfig) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.siteId = config.siteId;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: config.baseUrl || 'https://api.squarespace.com/1.0',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Squarespace-MCP/1.0',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Add response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
throw this.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleError(error: AxiosError): SquarespaceError {
|
||||
if (error.response) {
|
||||
return {
|
||||
message: (error.response.data as any)?.message || error.message,
|
||||
code: (error.response.data as any)?.code,
|
||||
statusCode: error.response.status,
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: error.message || 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
|
||||
// Site Management
|
||||
async getSites(): Promise<Site[]> {
|
||||
const response = await this.client.get('/sites');
|
||||
return response.data.sites || [];
|
||||
}
|
||||
|
||||
async getSite(siteId: string): Promise<Site> {
|
||||
const response = await this.client.get(`/sites/${siteId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateSite(siteId: string, updates: Partial<Site>): Promise<Site> {
|
||||
const response = await this.client.patch(`/sites/${siteId}`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Page Management
|
||||
async getPages(siteId: string, collectionId?: string): Promise<Page[]> {
|
||||
const url = collectionId
|
||||
? `/sites/${siteId}/collections/${collectionId}/pages`
|
||||
: `/sites/${siteId}/pages`;
|
||||
const response = await this.client.get(url);
|
||||
return response.data.pages || [];
|
||||
}
|
||||
|
||||
async getPage(siteId: string, pageId: string): Promise<Page> {
|
||||
const response = await this.client.get(`/sites/${siteId}/pages/${pageId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createPage(siteId: string, collectionId: string, page: Partial<Page>): Promise<Page> {
|
||||
const response = await this.client.post(
|
||||
`/sites/${siteId}/collections/${collectionId}/pages`,
|
||||
page
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updatePage(siteId: string, pageId: string, updates: Partial<Page>): Promise<Page> {
|
||||
const response = await this.client.patch(`/sites/${siteId}/pages/${pageId}`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deletePage(siteId: string, pageId: string): Promise<void> {
|
||||
await this.client.delete(`/sites/${siteId}/pages/${pageId}`);
|
||||
}
|
||||
|
||||
// Product Management
|
||||
async getProducts(siteId: string, cursor?: string): Promise<{ products: Product[]; pagination?: any }> {
|
||||
const params = cursor ? { cursor } : {};
|
||||
const response = await this.client.get(`/commerce/products`, {
|
||||
params: { ...params, siteId },
|
||||
});
|
||||
return {
|
||||
products: response.data.products || [],
|
||||
pagination: response.data.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getProduct(productId: string): Promise<Product> {
|
||||
const response = await this.client.get(`/commerce/products/${productId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createProduct(product: Partial<Product>): Promise<Product> {
|
||||
const response = await this.client.post('/commerce/products', product);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateProduct(productId: string, updates: Partial<Product>): Promise<Product> {
|
||||
const response = await this.client.put(`/commerce/products/${productId}`, updates);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteProduct(productId: string): Promise<void> {
|
||||
await this.client.delete(`/commerce/products/${productId}`);
|
||||
}
|
||||
|
||||
// Order Management
|
||||
async getOrders(params?: {
|
||||
modifiedAfter?: string;
|
||||
modifiedBefore?: string;
|
||||
fulfillmentStatus?: string;
|
||||
cursor?: string;
|
||||
}): Promise<{ orders: Order[]; pagination?: any }> {
|
||||
const response = await this.client.get('/commerce/orders', { params });
|
||||
return {
|
||||
orders: response.data.orders || [],
|
||||
pagination: response.data.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getOrder(orderId: string): Promise<Order> {
|
||||
const response = await this.client.get(`/commerce/orders/${orderId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateOrderFulfillment(
|
||||
orderId: string,
|
||||
fulfillmentStatus: string,
|
||||
shipmentDetails?: any
|
||||
): Promise<Order> {
|
||||
const response = await this.client.post(`/commerce/orders/${orderId}/fulfillments`, {
|
||||
fulfillmentStatus,
|
||||
shipmentDetails,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Inventory Management
|
||||
async getInventory(productId: string, variantId?: string): Promise<Inventory> {
|
||||
const url = variantId
|
||||
? `/commerce/products/${productId}/variants/${variantId}/inventory`
|
||||
: `/commerce/products/${productId}/inventory`;
|
||||
const response = await this.client.get(url);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateInventory(
|
||||
productId: string,
|
||||
quantity: number,
|
||||
variantId?: string
|
||||
): Promise<Inventory> {
|
||||
const url = variantId
|
||||
? `/commerce/products/${productId}/variants/${variantId}/inventory`
|
||||
: `/commerce/products/${productId}/inventory`;
|
||||
const response = await this.client.put(url, { quantity });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Customer Management
|
||||
async getCustomers(params?: { cursor?: string; limit?: number }): Promise<{ customers: Customer[]; pagination?: any }> {
|
||||
const response = await this.client.get('/commerce/customers', { params });
|
||||
return {
|
||||
customers: response.data.customers || [],
|
||||
pagination: response.data.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getCustomer(customerId: string): Promise<Customer> {
|
||||
const response = await this.client.get(`/commerce/customers/${customerId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Form Submissions
|
||||
async getFormSubmissions(siteId: string, formId?: string): Promise<FormSubmission[]> {
|
||||
const url = formId
|
||||
? `/sites/${siteId}/forms/${formId}/submissions`
|
||||
: `/sites/${siteId}/form-submissions`;
|
||||
const response = await this.client.get(url);
|
||||
return response.data.submissions || [];
|
||||
}
|
||||
|
||||
async getFormSubmission(siteId: string, submissionId: string): Promise<FormSubmission> {
|
||||
const response = await this.client.get(`/sites/${siteId}/form-submissions/${submissionId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Analytics (Note: Squarespace API may have limited analytics access)
|
||||
async getAnalytics(siteId: string, params: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
metrics?: string[];
|
||||
}): Promise<AnalyticsData> {
|
||||
const response = await this.client.get(`/sites/${siteId}/analytics`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// SEO Settings
|
||||
async getSEOSettings(siteId: string, pageId?: string): Promise<SEOSettings> {
|
||||
const url = pageId
|
||||
? `/sites/${siteId}/pages/${pageId}/seo`
|
||||
: `/sites/${siteId}/seo`;
|
||||
const response = await this.client.get(url);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateSEOSettings(
|
||||
siteId: string,
|
||||
settings: Partial<SEOSettings>,
|
||||
pageId?: string
|
||||
): Promise<SEOSettings> {
|
||||
const url = pageId
|
||||
? `/sites/${siteId}/pages/${pageId}/seo`
|
||||
: `/sites/${siteId}/seo`;
|
||||
const response = await this.client.put(url, settings);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Blog Management
|
||||
async getBlogPosts(siteId: string, blogId: string, params?: {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ posts: BlogPost[]; pagination?: any }> {
|
||||
const response = await this.client.get(`/sites/${siteId}/blogs/${blogId}/posts`, { params });
|
||||
return {
|
||||
posts: response.data.posts || [],
|
||||
pagination: response.data.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
async getBlogPost(siteId: string, blogId: string, postId: string): Promise<BlogPost> {
|
||||
const response = await this.client.get(`/sites/${siteId}/blogs/${blogId}/posts/${postId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async createBlogPost(siteId: string, blogId: string, post: Partial<BlogPost>): Promise<BlogPost> {
|
||||
const response = await this.client.post(`/sites/${siteId}/blogs/${blogId}/posts`, post);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateBlogPost(
|
||||
siteId: string,
|
||||
blogId: string,
|
||||
postId: string,
|
||||
updates: Partial<BlogPost>
|
||||
): Promise<BlogPost> {
|
||||
const response = await this.client.patch(
|
||||
`/sites/${siteId}/blogs/${blogId}/posts/${postId}`,
|
||||
updates
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteBlogPost(siteId: string, blogId: string, postId: string): Promise<void> {
|
||||
await this.client.delete(`/sites/${siteId}/blogs/${blogId}/posts/${postId}`);
|
||||
}
|
||||
|
||||
// Transaction Management
|
||||
async getTransactions(orderId: string): Promise<Transaction[]> {
|
||||
const response = await this.client.get(`/commerce/orders/${orderId}/transactions`);
|
||||
return response.data.transactions || [];
|
||||
}
|
||||
|
||||
async createRefund(orderId: string, amount: string, currency: string): Promise<Transaction> {
|
||||
const response = await this.client.post(`/commerce/orders/${orderId}/refunds`, {
|
||||
amount: { value: amount, currency },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Webhook Management
|
||||
async getWebhooks(siteId: string): Promise<WebhookSubscription[]> {
|
||||
const response = await this.client.get(`/sites/${siteId}/webhooks`);
|
||||
return response.data.webhooks || [];
|
||||
}
|
||||
|
||||
async createWebhook(
|
||||
siteId: string,
|
||||
endpointUrl: string,
|
||||
topics: string[]
|
||||
): Promise<WebhookSubscription> {
|
||||
const response = await this.client.post(`/sites/${siteId}/webhooks`, {
|
||||
endpointUrl,
|
||||
topics,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async deleteWebhook(siteId: string, webhookId: string): Promise<void> {
|
||||
await this.client.delete(`/sites/${siteId}/webhooks/${webhookId}`);
|
||||
}
|
||||
|
||||
// Collections
|
||||
async getCollections(siteId: string): Promise<Collection[]> {
|
||||
const response = await this.client.get(`/sites/${siteId}/collections`);
|
||||
return response.data.collections || [];
|
||||
}
|
||||
|
||||
async getCollection(siteId: string, collectionId: string): Promise<Collection> {
|
||||
const response = await this.client.get(`/sites/${siteId}/collections/${collectionId}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Domain Management
|
||||
async getDomains(siteId: string): Promise<DomainMapping[]> {
|
||||
const response = await this.client.get(`/sites/${siteId}/domains`);
|
||||
return response.data.domains || [];
|
||||
}
|
||||
}
|
||||
@ -1,278 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// ============================================
|
||||
// CONFIGURATION
|
||||
// ============================================
|
||||
const MCP_NAME = "squarespace";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
const API_BASE_URL = "https://api.squarespace.com/1.0";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT - Squarespace uses Bearer Token (OAuth2)
|
||||
// ============================================
|
||||
class SquarespaceClient {
|
||||
private apiKey: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.apiKey = apiKey;
|
||||
this.baseUrl = API_BASE_URL;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "MCP-Squarespace-Server/1.0",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Squarespace API error: ${response.status} ${response.statusText} - ${text}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string) {
|
||||
return this.request(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(endpoint: string) {
|
||||
return this.request(endpoint, { method: "DELETE" });
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_pages",
|
||||
description: "List all pages for the website",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
cursor: { type: "string", description: "Pagination cursor" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_page",
|
||||
description: "Get a specific page by ID",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
pageId: { type: "string", description: "Page ID" },
|
||||
},
|
||||
required: ["pageId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_products",
|
||||
description: "List all products from the commerce store",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
cursor: { type: "string", description: "Pagination cursor" },
|
||||
modifiedAfter: { type: "string", description: "Filter by modified date (ISO 8601)" },
|
||||
modifiedBefore: { type: "string", description: "Filter by modified date (ISO 8601)" },
|
||||
type: { type: "string", description: "Product type filter (PHYSICAL, DIGITAL, SERVICE, GIFT_CARD)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_product",
|
||||
description: "Get a specific product by ID",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
productId: { type: "string", description: "Product ID" },
|
||||
},
|
||||
required: ["productId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_orders",
|
||||
description: "List orders from the commerce store",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
cursor: { type: "string", description: "Pagination cursor" },
|
||||
modifiedAfter: { type: "string", description: "Filter by modified date (ISO 8601)" },
|
||||
modifiedBefore: { type: "string", description: "Filter by modified date (ISO 8601)" },
|
||||
fulfillmentStatus: { type: "string", description: "Filter by status (PENDING, FULFILLED, CANCELED)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_order",
|
||||
description: "Get a specific order by ID",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
orderId: { type: "string", description: "Order ID" },
|
||||
},
|
||||
required: ["orderId"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_inventory",
|
||||
description: "List inventory for all product variants",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
cursor: { type: "string", description: "Pagination cursor" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_inventory",
|
||||
description: "Update inventory quantity for a product variant",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
variantId: { type: "string", description: "Product variant ID" },
|
||||
quantity: { type: "number", description: "New quantity to set" },
|
||||
quantityDelta: { type: "number", description: "Quantity change (+/-)" },
|
||||
isUnlimited: { type: "boolean", description: "Set to unlimited stock" },
|
||||
},
|
||||
required: ["variantId"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: SquarespaceClient, name: string, args: any) {
|
||||
switch (name) {
|
||||
case "list_pages": {
|
||||
const params = new URLSearchParams();
|
||||
if (args.cursor) params.append("cursor", args.cursor);
|
||||
const query = params.toString();
|
||||
return await client.get(`/commerce/pages${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
case "get_page": {
|
||||
return await client.get(`/commerce/pages/${args.pageId}`);
|
||||
}
|
||||
|
||||
case "list_products": {
|
||||
const params = new URLSearchParams();
|
||||
if (args.cursor) params.append("cursor", args.cursor);
|
||||
if (args.modifiedAfter) params.append("modifiedAfter", args.modifiedAfter);
|
||||
if (args.modifiedBefore) params.append("modifiedBefore", args.modifiedBefore);
|
||||
if (args.type) params.append("type", args.type);
|
||||
const query = params.toString();
|
||||
return await client.get(`/commerce/products${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
case "get_product": {
|
||||
return await client.get(`/commerce/products/${args.productId}`);
|
||||
}
|
||||
|
||||
case "list_orders": {
|
||||
const params = new URLSearchParams();
|
||||
if (args.cursor) params.append("cursor", args.cursor);
|
||||
if (args.modifiedAfter) params.append("modifiedAfter", args.modifiedAfter);
|
||||
if (args.modifiedBefore) params.append("modifiedBefore", args.modifiedBefore);
|
||||
if (args.fulfillmentStatus) params.append("fulfillmentStatus", args.fulfillmentStatus);
|
||||
const query = params.toString();
|
||||
return await client.get(`/commerce/orders${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
case "get_order": {
|
||||
return await client.get(`/commerce/orders/${args.orderId}`);
|
||||
}
|
||||
|
||||
case "list_inventory": {
|
||||
const params = new URLSearchParams();
|
||||
if (args.cursor) params.append("cursor", args.cursor);
|
||||
const query = params.toString();
|
||||
return await client.get(`/commerce/inventory${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
case "update_inventory": {
|
||||
const payload: any = {};
|
||||
if (args.quantity !== undefined) payload.quantity = args.quantity;
|
||||
if (args.quantityDelta !== undefined) payload.quantityDelta = args.quantityDelta;
|
||||
if (args.isUnlimited !== undefined) payload.isUnlimited = args.isUnlimited;
|
||||
return await client.post(`/commerce/inventory/${args.variantId}`, payload);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const apiKey = process.env.SQUARESPACE_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: SQUARESPACE_API_KEY environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new SquarespaceClient(apiKey);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
135
servers/squarespace/src/tools/analytics-tools.ts
Normal file
135
servers/squarespace/src/tools/analytics-tools.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { SquarespaceApiClient } from '../lib/api-client.js';
|
||||
import { SiteTraffic, CommerceAnalytics, PopularContent } from '../types.js';
|
||||
|
||||
export function registerAnalyticsTools(client: SquarespaceApiClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_get_site_traffic',
|
||||
description: 'Get site traffic analytics for a given period',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
period: {
|
||||
type: 'string',
|
||||
enum: ['day', 'week', 'month', 'year'],
|
||||
description: 'Aggregation period',
|
||||
default: 'day'
|
||||
}
|
||||
},
|
||||
required: ['startDate', 'endDate']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
period: args.period || 'day'
|
||||
};
|
||||
|
||||
const traffic = await client.get<SiteTraffic>(
|
||||
'/1.0/analytics/traffic',
|
||||
queryParams
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(traffic, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_commerce_analytics',
|
||||
description: 'Get e-commerce analytics including revenue and top products',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
period: {
|
||||
type: 'string',
|
||||
enum: ['day', 'week', 'month', 'year'],
|
||||
description: 'Aggregation period',
|
||||
default: 'day'
|
||||
}
|
||||
},
|
||||
required: ['startDate', 'endDate']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
period: args.period || 'day'
|
||||
};
|
||||
|
||||
const analytics = await client.get<CommerceAnalytics>(
|
||||
'/1.0/analytics/commerce',
|
||||
queryParams
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(analytics, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_popular_content',
|
||||
description: 'Get the most popular pages/content by page views',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date in ISO 8601 format (YYYY-MM-DD)'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of top pages to return (default: 10)',
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
required: ['startDate', 'endDate']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
limit: String(args.limit || 10)
|
||||
};
|
||||
|
||||
const popular = await client.get<PopularContent>(
|
||||
'/1.0/analytics/popular',
|
||||
queryParams
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(popular, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
199
servers/squarespace/src/tools/analytics.ts
Normal file
199
servers/squarespace/src/tools/analytics.ts
Normal file
@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Analytics Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createAnalyticsTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_get_analytics',
|
||||
description: 'Get analytics data for a site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
metrics: {
|
||||
type: 'array',
|
||||
description: 'Metrics to retrieve',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: ['pageViews', 'uniqueVisitors', 'revenue', 'orders', 'conversionRate'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: {
|
||||
siteId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
metrics?: string[];
|
||||
}) => {
|
||||
const analytics = await client.getAnalytics(args.siteId, {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
metrics: args.metrics,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(analytics, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_top_pages',
|
||||
description: 'Get top performing pages by views',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results (default: 10)',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: {
|
||||
siteId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const analytics = await client.getAnalytics(args.siteId, {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
});
|
||||
const topPages = (analytics.topPages || []).slice(0, args.limit || 10);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(topPages, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_top_products',
|
||||
description: 'Get top selling products by revenue',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results (default: 10)',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: {
|
||||
siteId: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const analytics = await client.getAnalytics(args.siteId, {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
});
|
||||
const topProducts = (analytics.topProducts || []).slice(0, args.limit || 10);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(topProducts, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_revenue_report',
|
||||
description: 'Get revenue summary for a period',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'startDate', 'endDate'],
|
||||
},
|
||||
handler: async (args: { siteId: string; startDate: string; endDate: string }) => {
|
||||
const analytics = await client.getAnalytics(args.siteId, {
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
metrics: ['revenue', 'orders', 'conversionRate'],
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
period: `${args.startDate} to ${args.endDate}`,
|
||||
revenue: analytics.metrics.revenue,
|
||||
orders: analytics.metrics.orders,
|
||||
averageOrderValue: analytics.metrics.averageOrderValue,
|
||||
conversionRate: analytics.metrics.conversionRate,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
109
servers/squarespace/src/tools/collections-tools.ts
Normal file
109
servers/squarespace/src/tools/collections-tools.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { SquarespaceApiClient } from '../lib/api-client.js';
|
||||
import { Collection, CollectionItem } from '../types.js';
|
||||
|
||||
export function registerCollectionsTools(client: SquarespaceApiClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_collections',
|
||||
description: 'List all collections (blogs, products, galleries, etc)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['BLOG', 'PRODUCTS', 'EVENTS', 'GALLERY', 'INDEX'],
|
||||
description: 'Filter by collection type'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page (default: 50)'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (args.type) queryParams.type = args.type;
|
||||
|
||||
const collections = await client.fetchAll<Collection>(
|
||||
'/1.0/collections',
|
||||
queryParams,
|
||||
args.limit || 50
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ collections, count: collections.length }, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_collection',
|
||||
description: 'Get detailed information about a specific collection',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The unique collection ID'
|
||||
}
|
||||
},
|
||||
required: ['collectionId']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const collection = await client.get<Collection>(
|
||||
`/1.0/collections/${args.collectionId}`
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(collection, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_list_collection_items',
|
||||
description: 'List all items in a specific collection',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The unique collection ID'
|
||||
},
|
||||
tag: {
|
||||
type: 'string',
|
||||
description: 'Filter by tag'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page (default: 50)'
|
||||
}
|
||||
},
|
||||
required: ['collectionId']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (args.tag) queryParams.tag = args.tag;
|
||||
|
||||
const items = await client.fetchAll<CollectionItem>(
|
||||
`/1.0/collections/${args.collectionId}/items`,
|
||||
queryParams,
|
||||
args.limit || 50
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ items, count: items.length }, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
159
servers/squarespace/src/tools/customers.ts
Normal file
159
servers/squarespace/src/tools/customers.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Customer Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createCustomerTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_customers',
|
||||
description: 'List all customers',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cursor: {
|
||||
type: 'string',
|
||||
description: 'Pagination cursor',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: { cursor?: string; limit?: number }) => {
|
||||
const result = await client.getCustomers(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_customer',
|
||||
description: 'Get detailed information about a specific customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: { customerId: string }) => {
|
||||
const customer = await client.getCustomer(args.customerId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(customer, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_search_customers',
|
||||
description: 'Search customers by email or name',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (email or name)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
handler: async (args: { query: string }) => {
|
||||
const result = await client.getCustomers();
|
||||
const filtered = result.customers.filter(
|
||||
(c) =>
|
||||
c.email.toLowerCase().includes(args.query.toLowerCase()) ||
|
||||
`${c.firstName} ${c.lastName}`.toLowerCase().includes(args.query.toLowerCase())
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_customer_orders',
|
||||
description: 'Get all orders for a specific customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: { customerId: string }) => {
|
||||
const customer = await client.getCustomer(args.customerId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(customer.orders || [], null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_customer_lifetime_value',
|
||||
description: 'Calculate total lifetime value for a customer',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: { customerId: string }) => {
|
||||
const customer = await client.getCustomer(args.customerId);
|
||||
const orders = customer.orders || [];
|
||||
const totalValue = orders.reduce((sum, order) => {
|
||||
return sum + parseFloat(order.grandTotal.value);
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
customerId: args.customerId,
|
||||
orderCount: orders.length,
|
||||
lifetimeValue: {
|
||||
value: totalValue.toFixed(2),
|
||||
currency: orders[0]?.grandTotal.currency || 'USD',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
78
servers/squarespace/src/tools/forms-tools.ts
Normal file
78
servers/squarespace/src/tools/forms-tools.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { SquarespaceApiClient } from '../lib/api-client.js';
|
||||
import { FormSubmission } from '../types.js';
|
||||
|
||||
export function registerFormsTools(client: SquarespaceApiClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_form_submissions',
|
||||
description: 'List all form submissions',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
formId: {
|
||||
type: 'string',
|
||||
description: 'Filter by specific form ID'
|
||||
},
|
||||
submittedAfter: {
|
||||
type: 'string',
|
||||
description: 'Filter submissions after this ISO 8601 date'
|
||||
},
|
||||
submittedBefore: {
|
||||
type: 'string',
|
||||
description: 'Filter submissions before this ISO 8601 date'
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of results per page (default: 50)'
|
||||
}
|
||||
}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const queryParams: Record<string, string> = {};
|
||||
|
||||
if (args.formId) queryParams.formId = args.formId;
|
||||
if (args.submittedAfter) queryParams.submittedAfter = args.submittedAfter;
|
||||
if (args.submittedBefore) queryParams.submittedBefore = args.submittedBefore;
|
||||
|
||||
const submissions = await client.fetchAll<FormSubmission>(
|
||||
'/1.0/forms/submissions',
|
||||
queryParams,
|
||||
args.limit || 50
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ submissions, count: submissions.length }, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_form_submission',
|
||||
description: 'Get detailed information about a specific form submission',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
description: 'The unique submission ID'
|
||||
}
|
||||
},
|
||||
required: ['submissionId']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const submission = await client.get<FormSubmission>(
|
||||
`/1.0/forms/submissions/${args.submissionId}`
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(submission, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
163
servers/squarespace/src/tools/forms.ts
Normal file
163
servers/squarespace/src/tools/forms.ts
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Form Submission Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createFormTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_form_submissions',
|
||||
description: 'List all form submissions for a site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
description: 'Optional form ID to filter submissions',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; formId?: string }) => {
|
||||
const submissions = await client.getFormSubmissions(args.siteId, args.formId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(submissions, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_form_submission',
|
||||
description: 'Get a specific form submission',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
description: 'The submission ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'submissionId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; submissionId: string }) => {
|
||||
const submission = await client.getFormSubmission(args.siteId, args.submissionId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(submission, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_search_form_submissions',
|
||||
description: 'Search form submissions by field value',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
fieldName: {
|
||||
type: 'string',
|
||||
description: 'Field name to search',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Value to search for',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'fieldName', 'value'],
|
||||
},
|
||||
handler: async (args: { siteId: string; fieldName: string; value: string }) => {
|
||||
const submissions = await client.getFormSubmissions(args.siteId);
|
||||
const filtered = submissions.filter((s) =>
|
||||
s.fields.some(
|
||||
(f) =>
|
||||
f.name === args.fieldName &&
|
||||
String(f.value).toLowerCase().includes(args.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_export_form_submissions',
|
||||
description: 'Export form submissions as CSV data',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
description: 'Optional form ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; formId?: string }) => {
|
||||
const submissions = await client.getFormSubmissions(args.siteId, args.formId);
|
||||
|
||||
if (submissions.length === 0) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'No submissions found' }],
|
||||
};
|
||||
}
|
||||
|
||||
// Get all unique field names
|
||||
const fieldNames = new Set<string>();
|
||||
submissions.forEach((s) => {
|
||||
s.fields.forEach((f) => fieldNames.add(f.name));
|
||||
});
|
||||
|
||||
// Build CSV
|
||||
const headers = ['Submission ID', 'Form Name', 'Submitted On', ...Array.from(fieldNames)];
|
||||
const rows = submissions.map((s) => {
|
||||
const row = [s.id, s.formName, s.submittedOn];
|
||||
fieldNames.forEach((fieldName) => {
|
||||
const field = s.fields.find((f) => f.name === fieldName);
|
||||
row.push(field ? String(field.value) : '');
|
||||
});
|
||||
return row;
|
||||
});
|
||||
|
||||
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: csv,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
154
servers/squarespace/src/tools/inventory.ts
Normal file
154
servers/squarespace/src/tools/inventory.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Inventory Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createInventoryTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_get_inventory',
|
||||
description: 'Get inventory information for a product or variant',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: {
|
||||
type: 'string',
|
||||
description: 'The product ID',
|
||||
},
|
||||
variantId: {
|
||||
type: 'string',
|
||||
description: 'Optional variant ID',
|
||||
},
|
||||
},
|
||||
required: ['productId'],
|
||||
},
|
||||
handler: async (args: { productId: string; variantId?: string }) => {
|
||||
const inventory = await client.getInventory(args.productId, args.variantId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(inventory, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_update_inventory',
|
||||
description: 'Update inventory quantity for a product or variant',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: {
|
||||
type: 'string',
|
||||
description: 'The product ID',
|
||||
},
|
||||
quantity: {
|
||||
type: 'number',
|
||||
description: 'New inventory quantity',
|
||||
},
|
||||
variantId: {
|
||||
type: 'string',
|
||||
description: 'Optional variant ID',
|
||||
},
|
||||
},
|
||||
required: ['productId', 'quantity'],
|
||||
},
|
||||
handler: async (args: { productId: string; quantity: number; variantId?: string }) => {
|
||||
const inventory = await client.updateInventory(args.productId, args.quantity, args.variantId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(inventory, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_bulk_update_inventory',
|
||||
description: 'Update inventory for multiple products at once',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
updates: {
|
||||
type: 'array',
|
||||
description: 'Array of inventory updates',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: { type: 'string' },
|
||||
variantId: { type: 'string' },
|
||||
quantity: { type: 'number' },
|
||||
},
|
||||
required: ['productId', 'quantity'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['updates'],
|
||||
},
|
||||
handler: async (args: {
|
||||
updates: Array<{ productId: string; variantId?: string; quantity: number }>;
|
||||
}) => {
|
||||
const results = [];
|
||||
for (const update of args.updates) {
|
||||
try {
|
||||
const inventory = await client.updateInventory(
|
||||
update.productId,
|
||||
update.quantity,
|
||||
update.variantId
|
||||
);
|
||||
results.push({ success: true, productId: update.productId, inventory });
|
||||
} catch (error: any) {
|
||||
results.push({ success: false, productId: update.productId, error: error.message });
|
||||
}
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(results, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_check_low_stock',
|
||||
description: 'Check for products with inventory below a threshold',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
threshold: {
|
||||
type: 'number',
|
||||
description: 'Inventory threshold (default: 10)',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; threshold?: number }) => {
|
||||
const threshold = args.threshold ?? 10;
|
||||
const result = await client.getProducts(args.siteId);
|
||||
const lowStockProducts = result.products.filter((p) => {
|
||||
if (p.inventory?.unlimited) return false;
|
||||
return (p.inventory?.quantity ?? 0) <= threshold;
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(lowStockProducts, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
53
servers/squarespace/src/tools/menu-tools.ts
Normal file
53
servers/squarespace/src/tools/menu-tools.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { SquarespaceApiClient } from '../lib/api-client.js';
|
||||
import { NavigationMenu } from '../types.js';
|
||||
|
||||
export function registerMenuTools(client: SquarespaceApiClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_navigation_menus',
|
||||
description: 'List all navigation menus on the site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const menus = await client.get<{ menus: NavigationMenu[] }>(
|
||||
'/1.0/navigation/menus'
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(menus, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_navigation_menu',
|
||||
description: 'Get detailed information about a specific navigation menu',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
menuId: {
|
||||
type: 'string',
|
||||
description: 'The unique menu ID (e.g., "mainNav", "footerNav")'
|
||||
}
|
||||
},
|
||||
required: ['menuId']
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const menu = await client.get<NavigationMenu>(
|
||||
`/1.0/navigation/menus/${args.menuId}`
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(menu, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
185
servers/squarespace/src/tools/orders.ts
Normal file
185
servers/squarespace/src/tools/orders.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Order Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createOrderTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_orders',
|
||||
description: 'List all orders with optional filters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
modifiedAfter: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp to filter orders modified after',
|
||||
},
|
||||
modifiedBefore: {
|
||||
type: 'string',
|
||||
description: 'ISO 8601 timestamp to filter orders modified before',
|
||||
},
|
||||
fulfillmentStatus: {
|
||||
type: 'string',
|
||||
enum: ['PENDING', 'FULFILLED', 'CANCELED'],
|
||||
description: 'Filter by fulfillment status',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
description: 'Pagination cursor',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: {
|
||||
modifiedAfter?: string;
|
||||
modifiedBefore?: string;
|
||||
fulfillmentStatus?: string;
|
||||
cursor?: string;
|
||||
}) => {
|
||||
const result = await client.getOrders(args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_order',
|
||||
description: 'Get detailed information about a specific order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'The order ID',
|
||||
},
|
||||
},
|
||||
required: ['orderId'],
|
||||
},
|
||||
handler: async (args: { orderId: string }) => {
|
||||
const order = await client.getOrder(args.orderId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(order, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_update_order_fulfillment',
|
||||
description: 'Update the fulfillment status of an order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'The order ID',
|
||||
},
|
||||
fulfillmentStatus: {
|
||||
type: 'string',
|
||||
enum: ['PENDING', 'FULFILLED', 'CANCELED'],
|
||||
description: 'New fulfillment status',
|
||||
},
|
||||
trackingNumber: {
|
||||
type: 'string',
|
||||
description: 'Shipping tracking number',
|
||||
},
|
||||
carrier: {
|
||||
type: 'string',
|
||||
description: 'Shipping carrier',
|
||||
},
|
||||
},
|
||||
required: ['orderId', 'fulfillmentStatus'],
|
||||
},
|
||||
handler: async (args: {
|
||||
orderId: string;
|
||||
fulfillmentStatus: string;
|
||||
trackingNumber?: string;
|
||||
carrier?: string;
|
||||
}) => {
|
||||
const shipmentDetails =
|
||||
args.trackingNumber || args.carrier
|
||||
? {
|
||||
trackingNumber: args.trackingNumber,
|
||||
carrier: args.carrier,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const order = await client.updateOrderFulfillment(
|
||||
args.orderId,
|
||||
args.fulfillmentStatus,
|
||||
shipmentDetails
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(order, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_order_transactions',
|
||||
description: 'Get all transactions for an order',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'The order ID',
|
||||
},
|
||||
},
|
||||
required: ['orderId'],
|
||||
},
|
||||
handler: async (args: { orderId: string }) => {
|
||||
const transactions = await client.getTransactions(args.orderId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(transactions, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_search_orders_by_customer',
|
||||
description: 'Search orders by customer email',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Customer email address',
|
||||
},
|
||||
},
|
||||
required: ['email'],
|
||||
},
|
||||
handler: async (args: { email: string }) => {
|
||||
const result = await client.getOrders();
|
||||
const filtered = result.orders.filter(
|
||||
(o) => o.customerEmail.toLowerCase() === args.email.toLowerCase()
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
206
servers/squarespace/src/tools/pages.ts
Normal file
206
servers/squarespace/src/tools/pages.ts
Normal file
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Page Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createPageTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_pages',
|
||||
description: 'List all pages in a site or collection',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'Optional collection ID to filter pages',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; collectionId?: string }) => {
|
||||
const pages = await client.getPages(args.siteId, args.collectionId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(pages, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_page',
|
||||
description: 'Get detailed information about a specific page',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
description: 'The page ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'pageId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; pageId: string }) => {
|
||||
const page = await client.getPage(args.siteId, args.pageId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(page, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_create_page',
|
||||
description: 'Create a new page in a collection',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
collectionId: {
|
||||
type: 'string',
|
||||
description: 'The collection ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Page title',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
description: 'URL slug',
|
||||
},
|
||||
excerpt: {
|
||||
type: 'string',
|
||||
description: 'Page excerpt/summary',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'collectionId', 'title'],
|
||||
},
|
||||
handler: async (args: {
|
||||
siteId: string;
|
||||
collectionId: string;
|
||||
title: string;
|
||||
slug?: string;
|
||||
excerpt?: string;
|
||||
}) => {
|
||||
const page = await client.createPage(args.siteId, args.collectionId, {
|
||||
title: args.title,
|
||||
slug: args.slug,
|
||||
excerpt: args.excerpt,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(page, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_update_page',
|
||||
description: 'Update an existing page',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
description: 'The page ID',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'New page title',
|
||||
},
|
||||
slug: {
|
||||
type: 'string',
|
||||
description: 'New URL slug',
|
||||
},
|
||||
excerpt: {
|
||||
type: 'string',
|
||||
description: 'New excerpt/summary',
|
||||
},
|
||||
starred: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the page is starred',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'pageId'],
|
||||
},
|
||||
handler: async (args: {
|
||||
siteId: string;
|
||||
pageId: string;
|
||||
title?: string;
|
||||
slug?: string;
|
||||
excerpt?: string;
|
||||
starred?: boolean;
|
||||
}) => {
|
||||
const updates: any = {};
|
||||
if (args.title) updates.title = args.title;
|
||||
if (args.slug) updates.slug = args.slug;
|
||||
if (args.excerpt) updates.excerpt = args.excerpt;
|
||||
if (args.starred !== undefined) updates.starred = args.starred;
|
||||
|
||||
const page = await client.updatePage(args.siteId, args.pageId, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(page, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_delete_page',
|
||||
description: 'Delete a page',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
pageId: {
|
||||
type: 'string',
|
||||
description: 'The page ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'pageId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; pageId: string }) => {
|
||||
await client.deletePage(args.siteId, args.pageId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Page ${args.pageId} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
262
servers/squarespace/src/tools/products.ts
Normal file
262
servers/squarespace/src/tools/products.ts
Normal file
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Product Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createProductTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_products',
|
||||
description: 'List all products in the store',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
cursor: {
|
||||
type: 'string',
|
||||
description: 'Pagination cursor',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; cursor?: string }) => {
|
||||
const result = await client.getProducts(args.siteId, args.cursor);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_product',
|
||||
description: 'Get detailed information about a specific product',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: {
|
||||
type: 'string',
|
||||
description: 'The product ID',
|
||||
},
|
||||
},
|
||||
required: ['productId'],
|
||||
},
|
||||
handler: async (args: { productId: string }) => {
|
||||
const product = await client.getProduct(args.productId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(product, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_create_product',
|
||||
description: 'Create a new product',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Product name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Product description',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['PHYSICAL', 'DIGITAL', 'SERVICE', 'GIFT_CARD'],
|
||||
description: 'Product type',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Base price',
|
||||
},
|
||||
currency: {
|
||||
type: 'string',
|
||||
description: 'Currency code (e.g., USD)',
|
||||
},
|
||||
urlSlug: {
|
||||
type: 'string',
|
||||
description: 'URL slug for the product',
|
||||
},
|
||||
isVisible: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the product is visible in the store',
|
||||
},
|
||||
},
|
||||
required: ['name', 'type', 'price', 'currency'],
|
||||
},
|
||||
handler: async (args: {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: 'PHYSICAL' | 'DIGITAL' | 'SERVICE' | 'GIFT_CARD';
|
||||
price: number;
|
||||
currency: string;
|
||||
urlSlug?: string;
|
||||
isVisible?: boolean;
|
||||
}) => {
|
||||
const product = await client.createProduct({
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
type: args.type,
|
||||
urlSlug: args.urlSlug,
|
||||
isVisible: args.isVisible ?? true,
|
||||
pricing: {
|
||||
basePrice: {
|
||||
currency: args.currency,
|
||||
value: args.price.toString(),
|
||||
},
|
||||
onSale: false,
|
||||
},
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(product, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_update_product',
|
||||
description: 'Update an existing product',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: {
|
||||
type: 'string',
|
||||
description: 'The product ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'New product name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'New description',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'New base price',
|
||||
},
|
||||
salePrice: {
|
||||
type: 'number',
|
||||
description: 'Sale price',
|
||||
},
|
||||
isVisible: {
|
||||
type: 'boolean',
|
||||
description: 'Visibility status',
|
||||
},
|
||||
},
|
||||
required: ['productId'],
|
||||
},
|
||||
handler: async (args: {
|
||||
productId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
salePrice?: number;
|
||||
isVisible?: boolean;
|
||||
}) => {
|
||||
const updates: any = {};
|
||||
if (args.name) updates.name = args.name;
|
||||
if (args.description) updates.description = args.description;
|
||||
if (args.isVisible !== undefined) updates.isVisible = args.isVisible;
|
||||
|
||||
if (args.price !== undefined || args.salePrice !== undefined) {
|
||||
updates.pricing = {};
|
||||
if (args.price !== undefined) {
|
||||
updates.pricing.basePrice = { value: args.price.toString() };
|
||||
}
|
||||
if (args.salePrice !== undefined) {
|
||||
updates.pricing.salePrice = { value: args.salePrice.toString() };
|
||||
updates.pricing.onSale = true;
|
||||
}
|
||||
}
|
||||
|
||||
const product = await client.updateProduct(args.productId, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(product, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_delete_product',
|
||||
description: 'Delete a product',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
productId: {
|
||||
type: 'string',
|
||||
description: 'The product ID',
|
||||
},
|
||||
},
|
||||
required: ['productId'],
|
||||
},
|
||||
handler: async (args: { productId: string }) => {
|
||||
await client.deleteProduct(args.productId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Product ${args.productId} deleted successfully`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_search_products',
|
||||
description: 'Search products by name or description',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
},
|
||||
required: ['siteId', 'query'],
|
||||
},
|
||||
handler: async (args: { siteId: string; query: string }) => {
|
||||
const result = await client.getProducts(args.siteId);
|
||||
const filtered = result.products.filter(
|
||||
(p) =>
|
||||
p.name.toLowerCase().includes(args.query.toLowerCase()) ||
|
||||
p.description?.toLowerCase().includes(args.query.toLowerCase())
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(filtered, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
45
servers/squarespace/src/tools/settings-tools.ts
Normal file
45
servers/squarespace/src/tools/settings-tools.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { SquarespaceApiClient } from '../lib/api-client.js';
|
||||
import { SiteInfo, DomainInfo } from '../types.js';
|
||||
|
||||
export function registerSettingsTools(client: SquarespaceApiClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_get_site_info',
|
||||
description: 'Get general site information and settings',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const siteInfo = await client.get<SiteInfo>('/1.0/site/info');
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(siteInfo, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_domains',
|
||||
description: 'Get all domains connected to the site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
},
|
||||
handler: async (args: any) => {
|
||||
const domains = await client.get<{ domains: DomainInfo[] }>(
|
||||
'/1.0/site/domains'
|
||||
);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: JSON.stringify(domains, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
141
servers/squarespace/src/tools/sites.ts
Normal file
141
servers/squarespace/src/tools/sites.ts
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Site Management Tools
|
||||
*/
|
||||
|
||||
import type { SquarespaceClient } from '../clients/squarespace.js';
|
||||
|
||||
export function createSiteTools(client: SquarespaceClient) {
|
||||
return [
|
||||
{
|
||||
name: 'squarespace_list_sites',
|
||||
description: 'List all Squarespace sites accessible with the API key',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async () => {
|
||||
const sites = await client.getSites();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(sites, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_site',
|
||||
description: 'Get detailed information about a specific site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string }) => {
|
||||
const site = await client.getSite(args.siteId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(site, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_update_site',
|
||||
description: 'Update site settings',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
siteTitle: {
|
||||
type: 'string',
|
||||
description: 'The new site title',
|
||||
},
|
||||
isEnabled: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the site is enabled',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string; siteTitle?: string; isEnabled?: boolean }) => {
|
||||
const updates: any = {};
|
||||
if (args.siteTitle) updates.siteTitle = args.siteTitle;
|
||||
if (args.isEnabled !== undefined) updates.isEnabled = args.isEnabled;
|
||||
|
||||
const site = await client.updateSite(args.siteId, updates);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(site, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_collections',
|
||||
description: 'Get all collections (pages, blogs, products, etc.) for a site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string }) => {
|
||||
const collections = await client.getCollections(args.siteId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(collections, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'squarespace_get_domains',
|
||||
description: 'Get all domain mappings for a site',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
siteId: {
|
||||
type: 'string',
|
||||
description: 'The site ID',
|
||||
},
|
||||
},
|
||||
required: ['siteId'],
|
||||
},
|
||||
handler: async (args: { siteId: string }) => {
|
||||
const domains = await client.getDomains(args.siteId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(domains, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
293
servers/squarespace/src/types/index.ts
Normal file
293
servers/squarespace/src/types/index.ts
Normal file
@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Comprehensive TypeScript types for Squarespace MCP Server
|
||||
*/
|
||||
|
||||
export interface SquarespaceConfig {
|
||||
apiKey: string;
|
||||
siteId?: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: string;
|
||||
siteTitle: string;
|
||||
domain: string;
|
||||
baseUrl: string;
|
||||
isEnabled: boolean;
|
||||
publicUrl: string;
|
||||
createdOn: string;
|
||||
updatedOn: string;
|
||||
readyForExport: boolean;
|
||||
localization?: {
|
||||
locale: string;
|
||||
timeZone: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
id: string;
|
||||
collectionId: string;
|
||||
title: string;
|
||||
fullUrl: string;
|
||||
slug: string;
|
||||
excerpt?: string;
|
||||
createdOn: string;
|
||||
updatedOn: string;
|
||||
publishedOn?: string;
|
||||
publicCommentCount?: number;
|
||||
starred: boolean;
|
||||
author?: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
tags?: string[];
|
||||
categories?: string[];
|
||||
contentType?: 'page' | 'blog-post' | 'event' | 'product';
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
type: 'PHYSICAL' | 'DIGITAL' | 'SERVICE' | 'GIFT_CARD';
|
||||
storePageId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
urlSlug: string;
|
||||
tags?: string[];
|
||||
isVisible: boolean;
|
||||
seoOptions?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
variants?: ProductVariant[];
|
||||
images?: ProductImage[];
|
||||
pricing: {
|
||||
basePrice: {
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
salePrice?: {
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
onSale: boolean;
|
||||
};
|
||||
inventory?: {
|
||||
unlimited: boolean;
|
||||
quantity?: number;
|
||||
};
|
||||
createdOn: string;
|
||||
modifiedOn: string;
|
||||
}
|
||||
|
||||
export interface ProductVariant {
|
||||
id: string;
|
||||
sku?: string;
|
||||
pricing: {
|
||||
basePrice: {
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
salePrice?: {
|
||||
currency: string;
|
||||
value: string;
|
||||
};
|
||||
onSale: boolean;
|
||||
};
|
||||
stock?: {
|
||||
unlimited: boolean;
|
||||
quantity?: number;
|
||||
};
|
||||
attributes: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProductImage {
|
||||
id: string;
|
||||
url: string;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
orderNumber: string;
|
||||
createdOn: string;
|
||||
modifiedOn: string;
|
||||
channel: string;
|
||||
testmode: boolean;
|
||||
customerEmail: string;
|
||||
billingAddress: Address;
|
||||
shippingAddress?: Address;
|
||||
fulfillmentStatus: 'PENDING' | 'FULFILLED' | 'CANCELED';
|
||||
lineItems: LineItem[];
|
||||
subtotal: Money;
|
||||
shippingTotal: Money;
|
||||
discountTotal: Money;
|
||||
taxTotal: Money;
|
||||
refundedTotal: Money;
|
||||
grandTotal: Money;
|
||||
customFormFields?: CustomFormField[];
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state?: string;
|
||||
countryCode: string;
|
||||
postalCode: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface LineItem {
|
||||
id: string;
|
||||
variantId?: string;
|
||||
sku?: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
unitPricePaid: Money;
|
||||
customizations?: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface Money {
|
||||
currency: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CustomFormField {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Inventory {
|
||||
productId: string;
|
||||
variantId?: string;
|
||||
sku?: string;
|
||||
quantity: number;
|
||||
unlimited: boolean;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
createdOn: string;
|
||||
orders?: Order[];
|
||||
}
|
||||
|
||||
export interface FormSubmission {
|
||||
id: string;
|
||||
formId: string;
|
||||
submittedOn: string;
|
||||
formName: string;
|
||||
fields: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
value: string | string[];
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AnalyticsData {
|
||||
period: string;
|
||||
metrics: {
|
||||
pageViews?: number;
|
||||
uniqueVisitors?: number;
|
||||
revenue?: Money;
|
||||
orders?: number;
|
||||
averageOrderValue?: Money;
|
||||
conversionRate?: number;
|
||||
};
|
||||
topPages?: Array<{
|
||||
url: string;
|
||||
pageViews: number;
|
||||
}>;
|
||||
topProducts?: Array<{
|
||||
productId: string;
|
||||
name: string;
|
||||
revenue: Money;
|
||||
unitsSold: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SEOSettings {
|
||||
pageId?: string;
|
||||
sitewide?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
ogImage?: string;
|
||||
noIndex?: boolean;
|
||||
noFollow?: boolean;
|
||||
canonicalUrl?: string;
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
excerpt?: string;
|
||||
author: {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
publishedOn?: string;
|
||||
createdOn: string;
|
||||
updatedOn: string;
|
||||
tags?: string[];
|
||||
categories?: string[];
|
||||
fullUrl: string;
|
||||
assetUrl?: string;
|
||||
commentCount?: number;
|
||||
likeCount?: number;
|
||||
featured: boolean;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
orderId: string;
|
||||
createdOn: string;
|
||||
type: 'CHARGE' | 'REFUND';
|
||||
status: 'SUCCESS' | 'PENDING' | 'FAILED';
|
||||
amount: Money;
|
||||
paymentMethod?: string;
|
||||
gatewayTransactionId?: string;
|
||||
}
|
||||
|
||||
export interface WebhookSubscription {
|
||||
id: string;
|
||||
endpointUrl: string;
|
||||
topics: string[];
|
||||
createdOn: string;
|
||||
updatedOn: string;
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'page' | 'blog' | 'products' | 'events' | 'gallery';
|
||||
publicCommentCount?: number;
|
||||
fullUrl: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface DomainMapping {
|
||||
domain: string;
|
||||
primary: boolean;
|
||||
sslStatus: 'ACTIVE' | 'PENDING' | 'NONE';
|
||||
createdOn: string;
|
||||
}
|
||||
|
||||
export type SquarespaceError = {
|
||||
message: string;
|
||||
code?: string;
|
||||
statusCode?: number;
|
||||
};
|
||||
@ -1,15 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "src/ui"]
|
||||
}
|
||||
|
||||
@ -1,20 +1,35 @@
|
||||
{
|
||||
"name": "mcp-server-toast",
|
||||
"name": "@mcpengine/toast-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Complete Toast POS MCP Server - Orders, Menus, Employees, Labor, Payments, Inventory & More",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
"toast-mcp": "dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/main.js",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"toast",
|
||||
"pos",
|
||||
"restaurant",
|
||||
"orders",
|
||||
"payments"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
364
servers/toast/src/api-client.ts
Normal file
364
servers/toast/src/api-client.ts
Normal file
@ -0,0 +1,364 @@
|
||||
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import type { ToastConfig, PaginationParams, PaginatedResponse } from './types/index.js';
|
||||
|
||||
export class ToastAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode?: number,
|
||||
public response?: any
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ToastAPIError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ToastAPIClient {
|
||||
private client: AxiosInstance;
|
||||
private config: ToastConfig;
|
||||
|
||||
constructor(config: ToastConfig) {
|
||||
this.config = {
|
||||
baseUrl: config.baseUrl || 'https://ws-api.toasttab.com',
|
||||
...config,
|
||||
};
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: this.config.baseUrl,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.config.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Toast-Restaurant-External-ID': this.config.restaurantGuid,
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response) {
|
||||
throw new ToastAPIError(
|
||||
error.response.data?.message || error.message,
|
||||
error.response.status,
|
||||
error.response.data
|
||||
);
|
||||
} else if (error.request) {
|
||||
throw new ToastAPIError('No response received from Toast API');
|
||||
} else {
|
||||
throw new ToastAPIError(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Generic GET request with pagination support
|
||||
async get<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, any>,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
const response = await this.client.get(endpoint, {
|
||||
params,
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic POST request
|
||||
async post<T>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
const response = await this.client.post(endpoint, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic PUT request
|
||||
async put<T>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
const response = await this.client.put(endpoint, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic PATCH request
|
||||
async patch<T>(
|
||||
endpoint: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
const response = await this.client.patch(endpoint, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic DELETE request
|
||||
async delete<T>(
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
const response = await this.client.delete(endpoint, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Paginated GET request
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
pagination?: PaginationParams,
|
||||
additionalParams?: Record<string, any>
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const params = {
|
||||
page: pagination?.page || 1,
|
||||
pageSize: pagination?.pageSize || 100,
|
||||
...additionalParams,
|
||||
};
|
||||
|
||||
const response = await this.get<T[]>(endpoint, params);
|
||||
|
||||
// If the API returns pagination metadata, use it
|
||||
// Otherwise, create a simple paginated response
|
||||
return {
|
||||
data: Array.isArray(response) ? response : [],
|
||||
page: params.page,
|
||||
pageSize: params.pageSize,
|
||||
totalPages: 1,
|
||||
totalCount: Array.isArray(response) ? response.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Get all pages automatically
|
||||
async getAllPages<T>(
|
||||
endpoint: string,
|
||||
pageSize: number = 100,
|
||||
additionalParams?: Record<string, any>
|
||||
): Promise<T[]> {
|
||||
const allData: T[] = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.getPaginated<T>(
|
||||
endpoint,
|
||||
{ page, pageSize },
|
||||
additionalParams
|
||||
);
|
||||
|
||||
allData.push(...response.data);
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore = response.data.length === pageSize && page < response.totalPages;
|
||||
page++;
|
||||
}
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
// Orders API
|
||||
orders = {
|
||||
list: (params?: { startDate?: string; endDate?: string; pagination?: PaginationParams }) =>
|
||||
this.getPaginated('/orders/v2/orders', params?.pagination, {
|
||||
startDate: params?.startDate,
|
||||
endDate: params?.endDate,
|
||||
}),
|
||||
|
||||
get: (orderId: string) =>
|
||||
this.get(`/orders/v2/orders/${orderId}`),
|
||||
|
||||
create: (orderData: any) =>
|
||||
this.post('/orders/v2/orders', orderData),
|
||||
|
||||
update: (orderId: string, orderData: any) =>
|
||||
this.put(`/orders/v2/orders/${orderId}`, orderData),
|
||||
|
||||
void: (orderId: string, voidReason?: string) =>
|
||||
this.post(`/orders/v2/orders/${orderId}/void`, { voidReason }),
|
||||
|
||||
listChecks: (orderId: string) =>
|
||||
this.get(`/orders/v2/orders/${orderId}/checks`),
|
||||
|
||||
addItem: (orderId: string, checkId: string, itemData: any) =>
|
||||
this.post(`/orders/v2/orders/${orderId}/checks/${checkId}/selections`, itemData),
|
||||
|
||||
removeItem: (orderId: string, checkId: string, selectionId: string) =>
|
||||
this.delete(`/orders/v2/orders/${orderId}/checks/${checkId}/selections/${selectionId}`),
|
||||
|
||||
applyDiscount: (orderId: string, checkId: string, discountData: any) =>
|
||||
this.post(`/orders/v2/orders/${orderId}/checks/${checkId}/appliedDiscounts`, discountData),
|
||||
};
|
||||
|
||||
// Menus API
|
||||
menus = {
|
||||
list: () =>
|
||||
this.get('/menus/v2/menus'),
|
||||
|
||||
get: (menuId: string) =>
|
||||
this.get(`/menus/v2/menus/${menuId}`),
|
||||
|
||||
listGroups: (menuId: string) =>
|
||||
this.get(`/menus/v2/menus/${menuId}/groups`),
|
||||
|
||||
getGroup: (menuId: string, groupId: string) =>
|
||||
this.get(`/menus/v2/menus/${menuId}/groups/${groupId}`),
|
||||
|
||||
listItems: (menuId: string, groupId?: string) =>
|
||||
groupId
|
||||
? this.get(`/menus/v2/menus/${menuId}/groups/${groupId}/items`)
|
||||
: this.get(`/menus/v2/menus/${menuId}/items`),
|
||||
|
||||
getItem: (menuId: string, itemId: string) =>
|
||||
this.get(`/menus/v2/menus/${menuId}/items/${itemId}`),
|
||||
|
||||
listModifiers: (menuId: string, itemId: string) =>
|
||||
this.get(`/menus/v2/menus/${menuId}/items/${itemId}/modifierGroups`),
|
||||
|
||||
updatePrice: (menuId: string, itemId: string, price: number) =>
|
||||
this.patch(`/menus/v2/menus/${menuId}/items/${itemId}`, { price }),
|
||||
};
|
||||
|
||||
// Employees API
|
||||
employees = {
|
||||
list: (pagination?: PaginationParams) =>
|
||||
this.getPaginated('/labor/v1/employees', pagination),
|
||||
|
||||
get: (employeeId: string) =>
|
||||
this.get(`/labor/v1/employees/${employeeId}`),
|
||||
|
||||
create: (employeeData: any) =>
|
||||
this.post('/labor/v1/employees', employeeData),
|
||||
|
||||
update: (employeeId: string, employeeData: any) =>
|
||||
this.put(`/labor/v1/employees/${employeeId}`, employeeData),
|
||||
|
||||
delete: (employeeId: string) =>
|
||||
this.delete(`/labor/v1/employees/${employeeId}`),
|
||||
|
||||
listJobs: (employeeId: string) =>
|
||||
this.get(`/labor/v1/employees/${employeeId}/jobs`),
|
||||
|
||||
listShifts: (employeeId: string, startDate: string, endDate: string) =>
|
||||
this.get(`/labor/v1/employees/${employeeId}/shifts`, { startDate, endDate }),
|
||||
|
||||
clockIn: (employeeId: string, jobId: string) =>
|
||||
this.post(`/labor/v1/employees/${employeeId}/timeEntries`, {
|
||||
jobGuid: jobId,
|
||||
inDate: new Date().toISOString(),
|
||||
}),
|
||||
|
||||
clockOut: (employeeId: string, timeEntryId: string) =>
|
||||
this.patch(`/labor/v1/employees/${employeeId}/timeEntries/${timeEntryId}`, {
|
||||
outDate: new Date().toISOString(),
|
||||
}),
|
||||
|
||||
listTimeEntries: (employeeId: string, startDate: string, endDate: string) =>
|
||||
this.get(`/labor/v1/employees/${employeeId}/timeEntries`, { startDate, endDate }),
|
||||
};
|
||||
|
||||
// Labor API
|
||||
labor = {
|
||||
listShifts: (startDate: string, endDate: string, pagination?: PaginationParams) =>
|
||||
this.getPaginated('/labor/v1/shifts', pagination, { startDate, endDate }),
|
||||
|
||||
getShift: (shiftId: string) =>
|
||||
this.get(`/labor/v1/shifts/${shiftId}`),
|
||||
|
||||
listBreaks: (shiftId: string) =>
|
||||
this.get(`/labor/v1/shifts/${shiftId}/breaks`),
|
||||
|
||||
getLaborCost: (businessDate: string) =>
|
||||
this.get('/labor/v1/laborCost', { businessDate }),
|
||||
|
||||
listJobs: () =>
|
||||
this.get('/labor/v1/jobs'),
|
||||
};
|
||||
|
||||
// Restaurant API
|
||||
restaurant = {
|
||||
getInfo: () =>
|
||||
this.get('/restaurants/v1/restaurants/' + this.config.restaurantGuid),
|
||||
|
||||
listRevenueCenters: () =>
|
||||
this.get('/restaurants/v1/restaurants/' + this.config.restaurantGuid + '/revenueCenters'),
|
||||
|
||||
listDiningOptions: () =>
|
||||
this.get('/restaurants/v1/restaurants/' + this.config.restaurantGuid + '/diningOptions'),
|
||||
|
||||
listServiceAreas: () =>
|
||||
this.get('/restaurants/v1/restaurants/' + this.config.restaurantGuid + '/serviceAreas'),
|
||||
|
||||
listTables: (serviceAreaId?: string) =>
|
||||
serviceAreaId
|
||||
? this.get(`/restaurants/v1/restaurants/${this.config.restaurantGuid}/serviceAreas/${serviceAreaId}/tables`)
|
||||
: this.get(`/restaurants/v1/restaurants/${this.config.restaurantGuid}/tables`),
|
||||
};
|
||||
|
||||
// Payments API
|
||||
payments = {
|
||||
list: (startDate: string, endDate: string, pagination?: PaginationParams) =>
|
||||
this.getPaginated('/payments/v1/payments', pagination, { startDate, endDate }),
|
||||
|
||||
get: (paymentId: string) =>
|
||||
this.get(`/payments/v1/payments/${paymentId}`),
|
||||
|
||||
void: (paymentId: string, voidReason?: string) =>
|
||||
this.post(`/payments/v1/payments/${paymentId}/void`, { voidReason }),
|
||||
|
||||
refund: (paymentId: string, refundAmount: number, refundReason?: string) =>
|
||||
this.post(`/payments/v1/payments/${paymentId}/refund`, {
|
||||
refundAmount,
|
||||
refundReason,
|
||||
}),
|
||||
|
||||
listTips: (startDate: string, endDate: string) =>
|
||||
this.get('/payments/v1/tips', { startDate, endDate }),
|
||||
};
|
||||
|
||||
// Inventory API (Note: Toast may not have full inventory API, these are examples)
|
||||
inventory = {
|
||||
listItems: (pagination?: PaginationParams) =>
|
||||
this.getPaginated('/inventory/v1/items', pagination),
|
||||
|
||||
getItem: (itemId: string) =>
|
||||
this.get(`/inventory/v1/items/${itemId}`),
|
||||
|
||||
updateCount: (itemId: string, quantity: number) =>
|
||||
this.patch(`/inventory/v1/items/${itemId}`, { currentQuantity: quantity }),
|
||||
|
||||
listVendors: () =>
|
||||
this.get('/inventory/v1/vendors'),
|
||||
|
||||
createPurchaseOrder: (poData: any) =>
|
||||
this.post('/inventory/v1/purchaseOrders', poData),
|
||||
};
|
||||
|
||||
// Customers API (Note: Toast may not have full customer API, these are examples)
|
||||
customers = {
|
||||
list: (pagination?: PaginationParams) =>
|
||||
this.getPaginated('/customers/v1/customers', pagination),
|
||||
|
||||
get: (customerId: string) =>
|
||||
this.get(`/customers/v1/customers/${customerId}`),
|
||||
|
||||
create: (customerData: any) =>
|
||||
this.post('/customers/v1/customers', customerData),
|
||||
|
||||
update: (customerId: string, customerData: any) =>
|
||||
this.put(`/customers/v1/customers/${customerId}`, customerData),
|
||||
|
||||
listLoyalty: (customerId: string) =>
|
||||
this.get(`/customers/v1/customers/${customerId}/loyalty`),
|
||||
|
||||
addLoyaltyPoints: (customerId: string, points: number) =>
|
||||
this.post(`/customers/v1/customers/${customerId}/loyalty/points`, { points }),
|
||||
};
|
||||
|
||||
// Cash Management API
|
||||
cash = {
|
||||
listEntries: (startDate: string, endDate: string, pagination?: PaginationParams) =>
|
||||
this.getPaginated('/cash/v1/entries', pagination, { startDate, endDate }),
|
||||
|
||||
getDrawerStatus: (drawerId: string) =>
|
||||
this.get(`/cash/v1/drawers/${drawerId}`),
|
||||
};
|
||||
}
|
||||
256
servers/toast/src/apps/index.ts
Normal file
256
servers/toast/src/apps/index.ts
Normal file
@ -0,0 +1,256 @@
|
||||
// MCP Apps for Toast Server
|
||||
|
||||
export const apps = [
|
||||
{
|
||||
name: 'order-dashboard',
|
||||
description: 'Real-time order dashboard showing active orders, status, and timeline',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'active-orders', title: 'Active Orders', type: 'list', dataSource: 'toast_list_orders' },
|
||||
{ id: 'order-stats', title: 'Order Statistics', type: 'metrics' },
|
||||
{ id: 'recent-activity', title: 'Recent Activity', type: 'timeline' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'order-detail',
|
||||
description: 'Detailed order view with items, payments, and modification history',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'detail',
|
||||
sections: [
|
||||
{ id: 'order-info', title: 'Order Information', type: 'info-panel' },
|
||||
{ id: 'order-items', title: 'Order Items', type: 'table', dataSource: 'toast_list_order_checks' },
|
||||
{ id: 'order-payments', title: 'Payments', type: 'table' },
|
||||
{ id: 'order-actions', title: 'Actions', type: 'action-panel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'order-grid',
|
||||
description: 'Searchable grid view of all orders with filters and bulk actions',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'grid',
|
||||
features: ['search', 'filter', 'sort', 'export', 'bulk-actions'],
|
||||
columns: ['orderNumber', 'date', 'customer', 'total', 'status', 'server'],
|
||||
filters: ['dateRange', 'status', 'diningOption', 'server'],
|
||||
actions: ['view', 'edit', 'void', 'print'],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'menu-manager',
|
||||
description: 'Menu management interface for viewing and editing menus, groups, and items',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'tree-view',
|
||||
sections: [
|
||||
{ id: 'menu-tree', title: 'Menu Structure', type: 'hierarchical-tree' },
|
||||
{ id: 'item-editor', title: 'Item Editor', type: 'form' },
|
||||
{ id: 'price-manager', title: 'Price Management', type: 'batch-editor' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'menu-item-detail',
|
||||
description: 'Detailed menu item view with pricing, modifiers, sales data, and images',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'detail',
|
||||
sections: [
|
||||
{ id: 'item-info', title: 'Item Information', type: 'info-panel' },
|
||||
{ id: 'item-pricing', title: 'Pricing & Cost', type: 'pricing-panel' },
|
||||
{ id: 'item-modifiers', title: 'Modifiers', type: 'table', dataSource: 'toast_list_item_modifiers' },
|
||||
{ id: 'item-performance', title: 'Sales Performance', type: 'chart' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'employee-dashboard',
|
||||
description: 'Employee management dashboard with roster, schedules, and performance',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'employee-list', title: 'Employees', type: 'list', dataSource: 'toast_list_employees' },
|
||||
{ id: 'employee-stats', title: 'Statistics', type: 'metrics' },
|
||||
{ id: 'clock-status', title: 'Clock Status', type: 'status-board' },
|
||||
{ id: 'employee-actions', title: 'Quick Actions', type: 'action-panel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'employee-schedule',
|
||||
description: 'Weekly/monthly schedule view with shift planning and time-off management',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'calendar',
|
||||
views: ['week', 'month'],
|
||||
features: ['drag-drop', 'shift-swap', 'time-off-requests'],
|
||||
dataSource: 'toast_list_shifts',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'labor-dashboard',
|
||||
description: 'Labor cost and productivity dashboard with real-time metrics',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'labor-cost', title: 'Labor Cost', type: 'metrics', dataSource: 'toast_get_labor_cost' },
|
||||
{ id: 'hours-breakdown', title: 'Hours Breakdown', type: 'chart' },
|
||||
{ id: 'overtime-tracker', title: 'Overtime', type: 'alert-panel' },
|
||||
{ id: 'productivity', title: 'Productivity Metrics', type: 'scorecard' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'restaurant-overview',
|
||||
description: 'Restaurant configuration overview with settings and operational status',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'overview',
|
||||
sections: [
|
||||
{ id: 'restaurant-info', title: 'Restaurant Info', type: 'info-panel', dataSource: 'toast_get_restaurant_info' },
|
||||
{ id: 'revenue-centers', title: 'Revenue Centers', type: 'list', dataSource: 'toast_list_revenue_centers' },
|
||||
{ id: 'dining-options', title: 'Dining Options', type: 'list', dataSource: 'toast_list_dining_options' },
|
||||
{ id: 'service-areas', title: 'Service Areas', type: 'list', dataSource: 'toast_list_service_areas' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'table-map',
|
||||
description: 'Interactive floor plan showing table status, occupancy, and server assignments',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'floor-plan',
|
||||
features: ['interactive', 'real-time-status', 'drag-drop-assignment'],
|
||||
dataSource: 'toast_list_tables',
|
||||
statusColors: {
|
||||
available: 'green',
|
||||
occupied: 'red',
|
||||
reserved: 'yellow',
|
||||
cleaning: 'blue',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'payment-history',
|
||||
description: 'Payment transaction history with search, filters, and export',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'grid',
|
||||
features: ['search', 'filter', 'sort', 'export'],
|
||||
columns: ['date', 'orderId', 'paymentType', 'amount', 'tip', 'status'],
|
||||
filters: ['dateRange', 'paymentType', 'status', 'server'],
|
||||
dataSource: 'toast_list_payments',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'inventory-tracker',
|
||||
description: 'Inventory tracking with stock levels, alerts, and purchase order management',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'inventory-list', title: 'Inventory Items', type: 'table', dataSource: 'toast_list_inventory_items' },
|
||||
{ id: 'low-stock-alerts', title: 'Low Stock Alerts', type: 'alert-panel' },
|
||||
{ id: 'purchase-orders', title: 'Purchase Orders', type: 'list' },
|
||||
{ id: 'inventory-actions', title: 'Actions', type: 'action-panel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'customer-detail',
|
||||
description: 'Customer profile with order history, preferences, and contact information',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'detail',
|
||||
sections: [
|
||||
{ id: 'customer-info', title: 'Customer Information', type: 'info-panel' },
|
||||
{ id: 'order-history', title: 'Order History', type: 'table' },
|
||||
{ id: 'customer-stats', title: 'Customer Statistics', type: 'metrics' },
|
||||
{ id: 'customer-actions', title: 'Actions', type: 'action-panel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'customer-loyalty',
|
||||
description: 'Loyalty program dashboard with points, rewards, and tier status',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'loyalty-overview', title: 'Loyalty Overview', type: 'metrics' },
|
||||
{ id: 'points-history', title: 'Points History', type: 'timeline' },
|
||||
{ id: 'rewards-available', title: 'Available Rewards', type: 'card-grid' },
|
||||
{ id: 'tier-progress', title: 'Tier Progress', type: 'progress-bar' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sales-dashboard',
|
||||
description: 'Comprehensive sales analytics with charts, trends, and comparisons',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'sales-overview', title: 'Sales Overview', type: 'metrics', dataSource: 'toast_sales_summary' },
|
||||
{ id: 'sales-chart', title: 'Sales Trend', type: 'line-chart' },
|
||||
{ id: 'category-breakdown', title: 'Sales by Category', type: 'pie-chart' },
|
||||
{ id: 'payment-types', title: 'Payment Methods', type: 'bar-chart' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'menu-performance',
|
||||
description: 'Menu item performance analytics with best/worst sellers and profitability',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'analytics',
|
||||
sections: [
|
||||
{ id: 'top-items', title: 'Top Selling Items', type: 'ranked-list', dataSource: 'toast_menu_item_performance' },
|
||||
{ id: 'item-trends', title: 'Item Sales Trends', type: 'multi-line-chart' },
|
||||
{ id: 'profitability', title: 'Profitability Analysis', type: 'table' },
|
||||
{ id: 'recommendations', title: 'Recommendations', type: 'insight-panel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tip-summary',
|
||||
description: 'Tip tracking and distribution dashboard with employee breakdown',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'dashboard',
|
||||
layout: 'grid',
|
||||
sections: [
|
||||
{ id: 'tip-overview', title: 'Tip Overview', type: 'metrics', dataSource: 'toast_tip_summary' },
|
||||
{ id: 'tip-by-employee', title: 'Tips by Employee', type: 'table' },
|
||||
{ id: 'tip-trends', title: 'Tip Trends', type: 'line-chart' },
|
||||
{ id: 'tip-percentage', title: 'Average Tip %', type: 'gauge' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'revenue-by-hour',
|
||||
description: 'Hourly revenue breakdown showing peak times and patterns',
|
||||
version: '1.0.0',
|
||||
ui: {
|
||||
type: 'analytics',
|
||||
sections: [
|
||||
{ id: 'hourly-chart', title: 'Revenue by Hour', type: 'bar-chart', dataSource: 'toast_revenue_by_hour' },
|
||||
{ id: 'peak-times', title: 'Peak Times', type: 'highlight-panel' },
|
||||
{ id: 'day-comparison', title: 'Day-over-Day Comparison', type: 'comparison-chart' },
|
||||
{ id: 'hourly-metrics', title: 'Hourly Metrics', type: 'table' },
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,410 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// ============================================
|
||||
// TOAST POS MCP SERVER
|
||||
// API Docs: https://doc.toasttab.com/doc/devguide/apiOverview.html
|
||||
// ============================================
|
||||
const MCP_NAME = "toast";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
const API_BASE_URL = "https://ws-api.toasttab.com";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT - OAuth2 Client Credentials Authentication
|
||||
// ============================================
|
||||
class ToastClient {
|
||||
private clientId: string;
|
||||
private clientSecret: string;
|
||||
private restaurantGuid: string;
|
||||
private accessToken: string | null = null;
|
||||
private tokenExpiry: number = 0;
|
||||
|
||||
constructor(clientId: string, clientSecret: string, restaurantGuid: string) {
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.restaurantGuid = restaurantGuid;
|
||||
}
|
||||
|
||||
private async getAccessToken(): Promise<string> {
|
||||
// Return cached token if still valid
|
||||
if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
// Fetch new token using client credentials
|
||||
const response = await fetch(`${API_BASE_URL}/authentication/v1/authentication/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: this.clientId,
|
||||
clientSecret: this.clientSecret,
|
||||
userAccessType: "TOAST_MACHINE_CLIENT",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Toast auth error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.accessToken = data.token.accessToken;
|
||||
// Token typically valid for 1 hour
|
||||
this.tokenExpiry = Date.now() + (data.token.expiresIn || 3600) * 1000;
|
||||
return this.accessToken!;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const token = await this.getAccessToken();
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${token}`,
|
||||
"Toast-Restaurant-External-ID": this.restaurantGuid,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Toast API error: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string, params?: Record<string, string>) {
|
||||
const queryString = params ? '?' + new URLSearchParams(params).toString() : '';
|
||||
return this.request(`${endpoint}${queryString}`, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async patch(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
getRestaurantGuid(): string {
|
||||
return this.restaurantGuid;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_orders",
|
||||
description: "List orders from Toast POS within a time range. Returns order summaries with checks, items, and payment info.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
start_date: { type: "string", description: "Start date/time in ISO 8601 format (required, e.g., 2024-01-01T00:00:00.000Z)" },
|
||||
end_date: { type: "string", description: "End date/time in ISO 8601 format (required)" },
|
||||
page_size: { type: "number", description: "Number of orders per page (default 100, max 100)" },
|
||||
page_token: { type: "string", description: "Pagination token from previous response" },
|
||||
business_date: { type: "string", description: "Filter by business date (YYYYMMDD format)" },
|
||||
},
|
||||
required: ["start_date", "end_date"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_order",
|
||||
description: "Get a specific order by GUID with full details including checks, selections, payments",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
order_guid: { type: "string", description: "Order GUID" },
|
||||
},
|
||||
required: ["order_guid"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_menu_items",
|
||||
description: "List menu items from Toast menus API. Returns items with prices, modifiers, and availability.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
menu_guid: { type: "string", description: "Specific menu GUID to fetch (optional - fetches all menus if not provided)" },
|
||||
include_modifiers: { type: "boolean", description: "Include modifier groups and options (default true)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update_menu_item",
|
||||
description: "Update a menu item's stock status (86'd status) or visibility",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
item_guid: { type: "string", description: "Menu item GUID (required)" },
|
||||
quantity: { type: "string", description: "Stock quantity: 'OUT_OF_STOCK', number, or 'UNLIMITED'" },
|
||||
status: { type: "string", description: "Item status: IN_STOCK, OUT_OF_STOCK" },
|
||||
},
|
||||
required: ["item_guid"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_employees",
|
||||
description: "List employees from Toast labor API",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page_size: { type: "number", description: "Number of employees per page (default 100)" },
|
||||
page_token: { type: "string", description: "Pagination token from previous response" },
|
||||
include_archived: { type: "boolean", description: "Include archived/inactive employees" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_labor",
|
||||
description: "Get labor/time entry data for shifts within a date range",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
start_date: { type: "string", description: "Start date in ISO 8601 format (required)" },
|
||||
end_date: { type: "string", description: "End date in ISO 8601 format (required)" },
|
||||
employee_guid: { type: "string", description: "Filter by specific employee GUID" },
|
||||
page_size: { type: "number", description: "Number of entries per page (default 100)" },
|
||||
page_token: { type: "string", description: "Pagination token" },
|
||||
},
|
||||
required: ["start_date", "end_date"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_checks",
|
||||
description: "List checks (tabs) from orders within a time range",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
start_date: { type: "string", description: "Start date/time in ISO 8601 format (required)" },
|
||||
end_date: { type: "string", description: "End date/time in ISO 8601 format (required)" },
|
||||
page_size: { type: "number", description: "Number of checks per page (default 100)" },
|
||||
page_token: { type: "string", description: "Pagination token" },
|
||||
check_status: { type: "string", description: "Filter by status: OPEN, CLOSED, VOID" },
|
||||
},
|
||||
required: ["start_date", "end_date"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "void_check",
|
||||
description: "Void a check (requires proper permissions). This action cannot be undone.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
order_guid: { type: "string", description: "Order GUID containing the check (required)" },
|
||||
check_guid: { type: "string", description: "Check GUID to void (required)" },
|
||||
void_reason: { type: "string", description: "Reason for voiding the check" },
|
||||
void_business_date: { type: "number", description: "Business date for void (YYYYMMDD format)" },
|
||||
},
|
||||
required: ["order_guid", "check_guid"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: ToastClient, name: string, args: any) {
|
||||
const restaurantGuid = client.getRestaurantGuid();
|
||||
|
||||
switch (name) {
|
||||
case "list_orders": {
|
||||
const params: Record<string, string> = {
|
||||
startDate: args.start_date,
|
||||
endDate: args.end_date,
|
||||
};
|
||||
if (args.page_size) params.pageSize = String(args.page_size);
|
||||
if (args.page_token) params.pageToken = args.page_token;
|
||||
if (args.business_date) params.businessDate = args.business_date;
|
||||
return await client.get(`/orders/v2/orders`, params);
|
||||
}
|
||||
|
||||
case "get_order": {
|
||||
return await client.get(`/orders/v2/orders/${args.order_guid}`);
|
||||
}
|
||||
|
||||
case "list_menu_items": {
|
||||
// Get menus with full item details
|
||||
if (args.menu_guid) {
|
||||
return await client.get(`/menus/v2/menus/${args.menu_guid}`);
|
||||
}
|
||||
// Get all menus
|
||||
return await client.get(`/menus/v2/menus`);
|
||||
}
|
||||
|
||||
case "update_menu_item": {
|
||||
// Use stock API to update item availability
|
||||
const stockData: any = {};
|
||||
if (args.quantity !== undefined) {
|
||||
stockData.quantity = args.quantity;
|
||||
}
|
||||
if (args.status) {
|
||||
stockData.status = args.status;
|
||||
}
|
||||
return await client.post(`/stock/v1/items/${args.item_guid}`, stockData);
|
||||
}
|
||||
|
||||
case "list_employees": {
|
||||
const params: Record<string, string> = {};
|
||||
if (args.page_size) params.pageSize = String(args.page_size);
|
||||
if (args.page_token) params.pageToken = args.page_token;
|
||||
if (args.include_archived) params.includeArchived = String(args.include_archived);
|
||||
return await client.get(`/labor/v1/employees`, params);
|
||||
}
|
||||
|
||||
case "get_labor": {
|
||||
const params: Record<string, string> = {
|
||||
startDate: args.start_date,
|
||||
endDate: args.end_date,
|
||||
};
|
||||
if (args.employee_guid) params.employeeId = args.employee_guid;
|
||||
if (args.page_size) params.pageSize = String(args.page_size);
|
||||
if (args.page_token) params.pageToken = args.page_token;
|
||||
return await client.get(`/labor/v1/timeEntries`, params);
|
||||
}
|
||||
|
||||
case "list_checks": {
|
||||
// Checks are part of orders - fetch orders and extract checks
|
||||
const params: Record<string, string> = {
|
||||
startDate: args.start_date,
|
||||
endDate: args.end_date,
|
||||
};
|
||||
if (args.page_size) params.pageSize = String(args.page_size);
|
||||
if (args.page_token) params.pageToken = args.page_token;
|
||||
|
||||
const ordersResponse = await client.get(`/orders/v2/orders`, params);
|
||||
|
||||
// Extract checks from orders
|
||||
const checks: any[] = [];
|
||||
if (ordersResponse.orders) {
|
||||
for (const order of ordersResponse.orders) {
|
||||
if (order.checks) {
|
||||
for (const check of order.checks) {
|
||||
// Filter by status if specified
|
||||
if (args.check_status && check.voidStatus !== args.check_status) {
|
||||
continue;
|
||||
}
|
||||
checks.push({
|
||||
...check,
|
||||
orderGuid: order.guid,
|
||||
orderOpenedDate: order.openedDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
checks,
|
||||
nextPageToken: ordersResponse.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
case "void_check": {
|
||||
const voidData: any = {
|
||||
voidReason: args.void_reason || "Voided via API",
|
||||
};
|
||||
if (args.void_business_date) {
|
||||
voidData.voidBusinessDate = args.void_business_date;
|
||||
}
|
||||
|
||||
// PATCH the check to void it
|
||||
return await client.patch(
|
||||
`/orders/v2/orders/${args.order_guid}/checks/${args.check_guid}`,
|
||||
{
|
||||
voidStatus: "VOID",
|
||||
...voidData,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const clientId = process.env.TOAST_CLIENT_ID;
|
||||
const clientSecret = process.env.TOAST_CLIENT_SECRET;
|
||||
const restaurantGuid = process.env.TOAST_RESTAURANT_GUID;
|
||||
|
||||
if (!clientId) {
|
||||
console.error("Error: TOAST_CLIENT_ID environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!clientSecret) {
|
||||
console.error("Error: TOAST_CLIENT_SECRET environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!restaurantGuid) {
|
||||
console.error("Error: TOAST_RESTAURANT_GUID environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new ToastClient(clientId, clientSecret, restaurantGuid);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
49
servers/toast/src/tools/cash-tools.ts
Normal file
49
servers/toast/src/tools/cash-tools.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerCashTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_cash_entries',
|
||||
description: 'List cash entries (paid in/paid out) for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.cash.listEntries(args.startDate, args.endDate, {
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_drawer_status',
|
||||
description: 'Get current status of a cash drawer',
|
||||
inputSchema: z.object({
|
||||
drawerGuid: z.string().describe('Cash drawer GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.cash.getDrawerStatus(args.drawerGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
143
servers/toast/src/tools/customers-tools.ts
Normal file
143
servers/toast/src/tools/customers-tools.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerCustomersTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_customers',
|
||||
description: 'List all customers',
|
||||
inputSchema: z.object({
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.customers.list({
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_customer',
|
||||
description: 'Get details of a specific customer',
|
||||
inputSchema: z.object({
|
||||
customerGuid: z.string().describe('Customer GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.customers.get(args.customerGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_create_customer',
|
||||
description: 'Create a new customer',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('First name'),
|
||||
lastName: z.string().describe('Last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
company: z.string().optional().describe('Company name'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const customerData = {
|
||||
firstName: args.firstName,
|
||||
lastName: args.lastName,
|
||||
...(args.email && { email: args.email }),
|
||||
...(args.phone && { phone: args.phone }),
|
||||
...(args.company && { company: args.company }),
|
||||
};
|
||||
const result = await client.customers.create(customerData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_update_customer',
|
||||
description: 'Update an existing customer',
|
||||
inputSchema: z.object({
|
||||
customerGuid: z.string().describe('Customer GUID'),
|
||||
firstName: z.string().optional().describe('First name'),
|
||||
lastName: z.string().optional().describe('Last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phone: z.string().optional().describe('Phone number'),
|
||||
company: z.string().optional().describe('Company name'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const updateData: any = {};
|
||||
if (args.firstName) updateData.firstName = args.firstName;
|
||||
if (args.lastName) updateData.lastName = args.lastName;
|
||||
if (args.email) updateData.email = args.email;
|
||||
if (args.phone) updateData.phone = args.phone;
|
||||
if (args.company) updateData.company = args.company;
|
||||
|
||||
const result = await client.customers.update(args.customerGuid, updateData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_customer_loyalty',
|
||||
description: 'List loyalty information for a customer',
|
||||
inputSchema: z.object({
|
||||
customerGuid: z.string().describe('Customer GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.customers.listLoyalty(args.customerGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_add_loyalty_points',
|
||||
description: 'Add loyalty points to a customer account',
|
||||
inputSchema: z.object({
|
||||
customerGuid: z.string().describe('Customer GUID'),
|
||||
points: z.number().describe('Points to add'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.customers.addLoyaltyPoints(args.customerGuid, args.points);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
234
servers/toast/src/tools/employees-tools.ts
Normal file
234
servers/toast/src/tools/employees-tools.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerEmployeesTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_employees',
|
||||
description: 'List all employees',
|
||||
inputSchema: z.object({
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.list({
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_employee',
|
||||
description: 'Get details of a specific employee',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.get(args.employeeGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_create_employee',
|
||||
description: 'Create a new employee',
|
||||
inputSchema: z.object({
|
||||
firstName: z.string().describe('First name'),
|
||||
lastName: z.string().describe('Last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phoneNumber: z.string().optional().describe('Phone number'),
|
||||
externalEmployeeId: z.string().optional().describe('External employee ID'),
|
||||
chosenName: z.string().optional().describe('Chosen/preferred name'),
|
||||
passcode: z.string().optional().describe('Employee passcode'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const employeeData = {
|
||||
firstName: args.firstName,
|
||||
lastName: args.lastName,
|
||||
...(args.email && { email: args.email }),
|
||||
...(args.phoneNumber && { phoneNumber: args.phoneNumber }),
|
||||
...(args.externalEmployeeId && { externalEmployeeId: args.externalEmployeeId }),
|
||||
...(args.chosenName && { chosenName: args.chosenName }),
|
||||
...(args.passcode && { passcode: args.passcode }),
|
||||
};
|
||||
const result = await client.employees.create(employeeData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_update_employee',
|
||||
description: 'Update an existing employee',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
firstName: z.string().optional().describe('First name'),
|
||||
lastName: z.string().optional().describe('Last name'),
|
||||
email: z.string().optional().describe('Email address'),
|
||||
phoneNumber: z.string().optional().describe('Phone number'),
|
||||
chosenName: z.string().optional().describe('Chosen/preferred name'),
|
||||
disabled: z.boolean().optional().describe('Disabled status'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const updateData: any = {};
|
||||
if (args.firstName) updateData.firstName = args.firstName;
|
||||
if (args.lastName) updateData.lastName = args.lastName;
|
||||
if (args.email) updateData.email = args.email;
|
||||
if (args.phoneNumber) updateData.phoneNumber = args.phoneNumber;
|
||||
if (args.chosenName) updateData.chosenName = args.chosenName;
|
||||
if (args.disabled !== undefined) updateData.disabled = args.disabled;
|
||||
|
||||
const result = await client.employees.update(args.employeeGuid, updateData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_delete_employee',
|
||||
description: 'Delete an employee',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.delete(args.employeeGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Employee deleted successfully',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_employee_jobs',
|
||||
description: 'List all jobs for an employee',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.listJobs(args.employeeGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_employee_shifts',
|
||||
description: 'List shifts for an employee within a date range',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.listShifts(
|
||||
args.employeeGuid,
|
||||
args.startDate,
|
||||
args.endDate
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_clock_in',
|
||||
description: 'Clock in an employee for a job',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
jobGuid: z.string().describe('Job GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.clockIn(args.employeeGuid, args.jobGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_clock_out',
|
||||
description: 'Clock out an employee from a time entry',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
timeEntryGuid: z.string().describe('Time entry GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.clockOut(args.employeeGuid, args.timeEntryGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_time_entries',
|
||||
description: 'List time entries for an employee within a date range',
|
||||
inputSchema: z.object({
|
||||
employeeGuid: z.string().describe('Employee GUID'),
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.employees.listTimeEntries(
|
||||
args.employeeGuid,
|
||||
args.startDate,
|
||||
args.endDate
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
116
servers/toast/src/tools/inventory-tools.ts
Normal file
116
servers/toast/src/tools/inventory-tools.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerInventoryTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_inventory_items',
|
||||
description: 'List all inventory items',
|
||||
inputSchema: z.object({
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.inventory.listItems({
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_inventory_item',
|
||||
description: 'Get details of a specific inventory item',
|
||||
inputSchema: z.object({
|
||||
itemGuid: z.string().describe('Inventory item GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.inventory.getItem(args.itemGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_update_inventory_count',
|
||||
description: 'Update the current quantity of an inventory item',
|
||||
inputSchema: z.object({
|
||||
itemGuid: z.string().describe('Inventory item GUID'),
|
||||
quantity: z.number().describe('New quantity'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.inventory.updateCount(args.itemGuid, args.quantity);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_vendors',
|
||||
description: 'List all vendors',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.inventory.listVendors();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_create_purchase_order',
|
||||
description: 'Create a new purchase order',
|
||||
inputSchema: z.object({
|
||||
vendorGuid: z.string().describe('Vendor GUID'),
|
||||
expectedDeliveryDate: z.string().optional().describe('Expected delivery date (ISO 8601)'),
|
||||
items: z.array(z.object({
|
||||
itemGuid: z.string(),
|
||||
quantity: z.number(),
|
||||
unitCost: z.number(),
|
||||
})).describe('Purchase order items'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const poData = {
|
||||
vendor: { guid: args.vendorGuid },
|
||||
...(args.expectedDeliveryDate && { expectedDeliveryDate: args.expectedDeliveryDate }),
|
||||
items: args.items.map((item: any) => ({
|
||||
inventoryItem: { guid: item.itemGuid },
|
||||
quantity: item.quantity,
|
||||
unitCost: item.unitCost,
|
||||
totalCost: item.quantity * item.unitCost,
|
||||
})),
|
||||
};
|
||||
const result = await client.inventory.createPurchaseOrder(poData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
101
servers/toast/src/tools/labor-tools.ts
Normal file
101
servers/toast/src/tools/labor-tools.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerLaborTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_shifts',
|
||||
description: 'List all shifts within a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.listShifts(args.startDate, args.endDate, {
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_shift',
|
||||
description: 'Get details of a specific shift',
|
||||
inputSchema: z.object({
|
||||
shiftGuid: z.string().describe('Shift GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.getShift(args.shiftGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_shift_breaks',
|
||||
description: 'List all breaks for a specific shift',
|
||||
inputSchema: z.object({
|
||||
shiftGuid: z.string().describe('Shift GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.listBreaks(args.shiftGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_labor_cost',
|
||||
description: 'Get labor cost summary for a business date',
|
||||
inputSchema: z.object({
|
||||
businessDate: z.string().describe('Business date (YYYYMMDD format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.getLaborCost(args.businessDate);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_jobs',
|
||||
description: 'List all available jobs/positions',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.listJobs();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
155
servers/toast/src/tools/menus-tools.ts
Normal file
155
servers/toast/src/tools/menus-tools.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerMenusTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_menus',
|
||||
description: 'List all menus for the restaurant',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.list();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_menu',
|
||||
description: 'Get details of a specific menu',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.get(args.menuGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_menu_groups',
|
||||
description: 'List all groups in a menu',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.listGroups(args.menuGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_menu_group',
|
||||
description: 'Get details of a specific menu group',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
groupGuid: z.string().describe('Menu group GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.getGroup(args.menuGuid, args.groupGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_menu_items',
|
||||
description: 'List all items in a menu or menu group',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
groupGuid: z.string().optional().describe('Menu group GUID (optional)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.listItems(args.menuGuid, args.groupGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_menu_item',
|
||||
description: 'Get details of a specific menu item',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
itemGuid: z.string().describe('Menu item GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.getItem(args.menuGuid, args.itemGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_item_modifiers',
|
||||
description: 'List all modifier groups for a menu item',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
itemGuid: z.string().describe('Menu item GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.listModifiers(args.menuGuid, args.itemGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_update_item_price',
|
||||
description: 'Update the price of a menu item',
|
||||
inputSchema: z.object({
|
||||
menuGuid: z.string().describe('Menu GUID'),
|
||||
itemGuid: z.string().describe('Menu item GUID'),
|
||||
price: z.number().describe('New price in cents (e.g., 1299 for $12.99)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.menus.updatePrice(args.menuGuid, args.itemGuid, args.price);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
224
servers/toast/src/tools/orders-tools.ts
Normal file
224
servers/toast/src/tools/orders-tools.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerOrdersTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_orders',
|
||||
description: 'List orders with optional date range filter',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().optional().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().optional().describe('End date (ISO 8601 format)'),
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.orders.list({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
pagination: {
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
},
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_order',
|
||||
description: 'Get details of a specific order by ID',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.orders.get(args.orderId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_create_order',
|
||||
description: 'Create a new order',
|
||||
inputSchema: z.object({
|
||||
diningOptionGuid: z.string().describe('Dining option GUID'),
|
||||
revenueCenterGuid: z.string().optional().describe('Revenue center GUID'),
|
||||
tableGuid: z.string().optional().describe('Table GUID'),
|
||||
numberOfGuests: z.number().optional().describe('Number of guests'),
|
||||
estimatedFulfillmentDate: z.string().optional().describe('Estimated fulfillment date (ISO 8601)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const orderData = {
|
||||
diningOption: { guid: args.diningOptionGuid },
|
||||
...(args.revenueCenterGuid && { revenueCenterGuid: args.revenueCenterGuid }),
|
||||
...(args.tableGuid && { table: { guid: args.tableGuid } }),
|
||||
...(args.numberOfGuests && { numberOfGuests: args.numberOfGuests }),
|
||||
...(args.estimatedFulfillmentDate && { estimatedFulfillmentDate: args.estimatedFulfillmentDate }),
|
||||
};
|
||||
const result = await client.orders.create(orderData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_update_order',
|
||||
description: 'Update an existing order',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
numberOfGuests: z.number().optional().describe('Number of guests'),
|
||||
tableGuid: z.string().optional().describe('Table GUID'),
|
||||
estimatedFulfillmentDate: z.string().optional().describe('Estimated fulfillment date (ISO 8601)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const updateData: any = {};
|
||||
if (args.numberOfGuests !== undefined) updateData.numberOfGuests = args.numberOfGuests;
|
||||
if (args.tableGuid) updateData.table = { guid: args.tableGuid };
|
||||
if (args.estimatedFulfillmentDate) updateData.estimatedFulfillmentDate = args.estimatedFulfillmentDate;
|
||||
|
||||
const result = await client.orders.update(args.orderId, updateData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_add_order_item',
|
||||
description: 'Add an item to an order check',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
checkGuid: z.string().describe('Check GUID'),
|
||||
itemGuid: z.string().describe('Menu item GUID'),
|
||||
quantity: z.number().describe('Quantity'),
|
||||
modifiers: z.array(z.object({
|
||||
guid: z.string(),
|
||||
quantity: z.number().optional(),
|
||||
})).optional().describe('Item modifiers'),
|
||||
specialRequest: z.string().optional().describe('Special instructions'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const itemData = {
|
||||
item: { guid: args.itemGuid },
|
||||
quantity: args.quantity,
|
||||
...(args.modifiers && { modifiers: args.modifiers }),
|
||||
...(args.specialRequest && { specialRequest: args.specialRequest }),
|
||||
};
|
||||
const result = await client.orders.addItem(args.orderId, args.checkGuid, itemData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_remove_order_item',
|
||||
description: 'Remove an item from an order check',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
checkGuid: z.string().describe('Check GUID'),
|
||||
selectionGuid: z.string().describe('Selection GUID to remove'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.orders.removeItem(args.orderId, args.checkGuid, args.selectionGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Item removed successfully',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_apply_discount',
|
||||
description: 'Apply a discount to an order check',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
checkGuid: z.string().describe('Check GUID'),
|
||||
discountGuid: z.string().optional().describe('Discount configuration GUID'),
|
||||
discountAmount: z.number().optional().describe('Discount amount (for fixed amount)'),
|
||||
discountPercent: z.number().optional().describe('Discount percentage (0-100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const discountData: any = {};
|
||||
if (args.discountGuid) discountData.guid = args.discountGuid;
|
||||
if (args.discountAmount !== undefined) discountData.amount = args.discountAmount;
|
||||
if (args.discountPercent !== undefined) discountData.discountPercent = args.discountPercent;
|
||||
|
||||
const result = await client.orders.applyDiscount(args.orderId, args.checkGuid, discountData);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_void_order',
|
||||
description: 'Void an entire order',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
voidReason: z.string().optional().describe('Reason for voiding'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.orders.void(args.orderId, args.voidReason);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_order_checks',
|
||||
description: 'List all checks for an order',
|
||||
inputSchema: z.object({
|
||||
orderId: z.string().describe('Order GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.orders.listChecks(args.orderId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
111
servers/toast/src/tools/payments-tools.ts
Normal file
111
servers/toast/src/tools/payments-tools.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerPaymentsTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_list_payments',
|
||||
description: 'List all payments within a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
page: z.number().optional().describe('Page number (default: 1)'),
|
||||
pageSize: z.number().optional().describe('Items per page (default: 100)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.payments.list(args.startDate, args.endDate, {
|
||||
page: args.page,
|
||||
pageSize: args.pageSize,
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_get_payment',
|
||||
description: 'Get details of a specific payment',
|
||||
inputSchema: z.object({
|
||||
paymentGuid: z.string().describe('Payment GUID'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.payments.get(args.paymentGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_void_payment',
|
||||
description: 'Void a payment',
|
||||
inputSchema: z.object({
|
||||
paymentGuid: z.string().describe('Payment GUID'),
|
||||
voidReason: z.string().optional().describe('Reason for voiding'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.payments.void(args.paymentGuid, args.voidReason);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_refund_payment',
|
||||
description: 'Refund a payment',
|
||||
inputSchema: z.object({
|
||||
paymentGuid: z.string().describe('Payment GUID'),
|
||||
refundAmount: z.number().describe('Amount to refund in cents'),
|
||||
refundReason: z.string().optional().describe('Reason for refund'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.payments.refund(
|
||||
args.paymentGuid,
|
||||
args.refundAmount,
|
||||
args.refundReason
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_tips',
|
||||
description: 'List all tips within a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.payments.listTips(args.startDate, args.endDate);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
142
servers/toast/src/tools/reporting-tools.ts
Normal file
142
servers/toast/src/tools/reporting-tools.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerReportingTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_sales_summary',
|
||||
description: 'Get sales summary report for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
// This would aggregate data from orders and payments
|
||||
const orders = await client.orders.list({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
});
|
||||
|
||||
const payments = await client.payments.list(args.startDate, args.endDate);
|
||||
|
||||
// Calculate summary metrics
|
||||
const summary = {
|
||||
dateRange: { startDate: args.startDate, endDate: args.endDate },
|
||||
orders: orders.data,
|
||||
payments: payments.data,
|
||||
summary: {
|
||||
totalOrders: orders.totalCount,
|
||||
totalPayments: payments.totalCount,
|
||||
message: 'Sales summary data retrieved. Process orders and payments to calculate metrics.',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(summary, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_labor_cost_report',
|
||||
description: 'Get labor cost report for a business date',
|
||||
inputSchema: z.object({
|
||||
businessDate: z.string().describe('Business date (YYYYMMDD format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.labor.getLaborCost(args.businessDate);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_menu_item_performance',
|
||||
description: 'Get menu item sales performance for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
// Fetch orders and aggregate item sales
|
||||
const orders = await client.orders.list({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
});
|
||||
|
||||
const report = {
|
||||
dateRange: { startDate: args.startDate, endDate: args.endDate },
|
||||
orders: orders.data,
|
||||
message: 'Menu item performance data retrieved. Process order selections to calculate metrics.',
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_revenue_by_hour',
|
||||
description: 'Get revenue breakdown by hour for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const orders = await client.orders.list({
|
||||
startDate: args.startDate,
|
||||
endDate: args.endDate,
|
||||
});
|
||||
|
||||
const report = {
|
||||
dateRange: { startDate: args.startDate, endDate: args.endDate },
|
||||
orders: orders.data,
|
||||
message: 'Hourly revenue data retrieved. Process order timestamps to calculate hourly breakdown.',
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(report, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_tip_summary',
|
||||
description: 'Get tip summary report for a date range',
|
||||
inputSchema: z.object({
|
||||
startDate: z.string().describe('Start date (ISO 8601 format)'),
|
||||
endDate: z.string().describe('End date (ISO 8601 format)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const tips = await client.payments.listTips(args.startDate, args.endDate);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(tips, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
89
servers/toast/src/tools/restaurant-tools.ts
Normal file
89
servers/toast/src/tools/restaurant-tools.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { z } from 'zod';
|
||||
import type { ToastAPIClient } from '../api-client.js';
|
||||
|
||||
export function registerRestaurantTools(client: ToastAPIClient) {
|
||||
return [
|
||||
{
|
||||
name: 'toast_get_restaurant_info',
|
||||
description: 'Get restaurant information and configuration',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.restaurant.getInfo();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_revenue_centers',
|
||||
description: 'List all revenue centers for the restaurant',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.restaurant.listRevenueCenters();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_dining_options',
|
||||
description: 'List all dining options (dine-in, takeout, delivery, etc.)',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.restaurant.listDiningOptions();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_service_areas',
|
||||
description: 'List all service areas (sections) in the restaurant',
|
||||
inputSchema: z.object({}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.restaurant.listServiceAreas();
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'toast_list_tables',
|
||||
description: 'List all tables, optionally filtered by service area',
|
||||
inputSchema: z.object({
|
||||
serviceAreaGuid: z.string().optional().describe('Service area GUID (optional)'),
|
||||
}),
|
||||
execute: async (args: any) => {
|
||||
const result = await client.restaurant.listTables(args.serviceAreaGuid);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
594
servers/toast/src/types/index.ts
Normal file
594
servers/toast/src/types/index.ts
Normal file
@ -0,0 +1,594 @@
|
||||
// Toast API Types
|
||||
|
||||
export interface ToastConfig {
|
||||
apiToken: string;
|
||||
restaurantGuid: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
data: T[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
// Order Types
|
||||
export interface Order {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
openedDate: string;
|
||||
modifiedDate: string;
|
||||
promisedDate?: string;
|
||||
closedDate?: string;
|
||||
deletedDate?: string;
|
||||
deleted: boolean;
|
||||
businessDate: number;
|
||||
server?: Employee;
|
||||
pricingFeatures: string[];
|
||||
source: string;
|
||||
duration: number;
|
||||
diningOption: DiningOption;
|
||||
checks: Check[];
|
||||
table?: Table;
|
||||
serviceArea?: ServiceArea;
|
||||
restaurantService: string;
|
||||
revenueCenterGuid: string;
|
||||
voided: boolean;
|
||||
voidDate?: string;
|
||||
voidBusinessDate?: number;
|
||||
voidUser?: Employee;
|
||||
approvalStatus: string;
|
||||
deliveryInfo?: DeliveryInfo;
|
||||
numberOfGuests?: number;
|
||||
estimatedFulfillmentDate?: string;
|
||||
curbsidePickupInfo?: CurbsidePickupInfo;
|
||||
}
|
||||
|
||||
export interface Check {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
openedDate: string;
|
||||
modifiedDate: string;
|
||||
deletedDate?: string;
|
||||
deleted: boolean;
|
||||
selections: Selection[];
|
||||
customer?: Customer;
|
||||
appliedDiscounts: Discount[];
|
||||
amount: number;
|
||||
taxAmount: number;
|
||||
totalAmount: number;
|
||||
payments: Payment[];
|
||||
tabName?: string;
|
||||
paymentStatus: string;
|
||||
closedDate?: string;
|
||||
voided: boolean;
|
||||
voidDate?: string;
|
||||
voidUser?: Employee;
|
||||
}
|
||||
|
||||
export interface Selection {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
itemGroup: ItemGroup;
|
||||
item: MenuItem;
|
||||
optionGroups?: OptionGroup[];
|
||||
modifiers?: Modifier[];
|
||||
quantity: number;
|
||||
unitOfMeasure: string;
|
||||
price: number;
|
||||
tax: number;
|
||||
voided: boolean;
|
||||
voidDate?: string;
|
||||
voidBusinessDate?: number;
|
||||
voidUser?: Employee;
|
||||
displayName: string;
|
||||
preDiscountPrice: number;
|
||||
appliedDiscounts: Discount[];
|
||||
diningOption: DiningOption;
|
||||
salesCategory: SalesCategory;
|
||||
fulfillmentStatus: string;
|
||||
seat?: number;
|
||||
}
|
||||
|
||||
export interface Discount {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
discountType: string;
|
||||
discountPercent?: number;
|
||||
nonTaxDiscountAmount?: number;
|
||||
approvalStatus: string;
|
||||
processingState: string;
|
||||
}
|
||||
|
||||
// Menu Types
|
||||
export interface Menu {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
name: string;
|
||||
visibility: string;
|
||||
groups: MenuGroup[];
|
||||
}
|
||||
|
||||
export interface MenuGroup {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
items: MenuItem[];
|
||||
visibility: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
sku?: string;
|
||||
plu?: string;
|
||||
price: number;
|
||||
calories?: number;
|
||||
visibility: string;
|
||||
salesCategory?: SalesCategory;
|
||||
optionGroups?: OptionGroup[];
|
||||
modifierGroups?: ModifierGroup[];
|
||||
images?: Image[];
|
||||
tags?: string[];
|
||||
isDiscountable: boolean;
|
||||
unitOfMeasure: string;
|
||||
}
|
||||
|
||||
export interface OptionGroup {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
minSelections: number;
|
||||
maxSelections: number;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
price: number;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface ModifierGroup {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
minSelections: number;
|
||||
maxSelections: number;
|
||||
modifiers: Modifier[];
|
||||
}
|
||||
|
||||
export interface Modifier {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
price: number;
|
||||
calories?: number;
|
||||
}
|
||||
|
||||
export interface SalesCategory {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Employee Types
|
||||
export interface Employee {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phoneNumber?: string;
|
||||
externalEmployeeId?: string;
|
||||
chosenName?: string;
|
||||
passcode?: string;
|
||||
createdDate: string;
|
||||
modifiedDate: string;
|
||||
deletedDate?: string;
|
||||
deleted: boolean;
|
||||
disabled: boolean;
|
||||
jobs: Job[];
|
||||
}
|
||||
|
||||
export interface Job {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
title: string;
|
||||
wage: number;
|
||||
wageType: string;
|
||||
}
|
||||
|
||||
export interface Shift {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
createdDate: string;
|
||||
modifiedDate: string;
|
||||
deletedDate?: string;
|
||||
deleted: boolean;
|
||||
businessDate: number;
|
||||
inDate: string;
|
||||
outDate?: string;
|
||||
employee: Employee;
|
||||
job: Job;
|
||||
breaks: Break[];
|
||||
regularHours: number;
|
||||
overtimeHours: number;
|
||||
totalHours: number;
|
||||
laborCost: number;
|
||||
tips?: number;
|
||||
declaredTips?: number;
|
||||
}
|
||||
|
||||
export interface Break {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
inDate: string;
|
||||
outDate?: string;
|
||||
isPaid: boolean;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface TimeEntry {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
createdDate: string;
|
||||
businessDate: number;
|
||||
inDate: string;
|
||||
outDate?: string;
|
||||
employee: Employee;
|
||||
job: Job;
|
||||
hourlyWage: number;
|
||||
}
|
||||
|
||||
// Labor Types
|
||||
export interface LaborEntry {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
businessDate: number;
|
||||
employee: Employee;
|
||||
job: Job;
|
||||
regularHours: number;
|
||||
overtimeHours: number;
|
||||
doubleOvertimeHours: number;
|
||||
totalHours: number;
|
||||
regularPay: number;
|
||||
overtimePay: number;
|
||||
doubleOvertimePay: number;
|
||||
totalPay: number;
|
||||
}
|
||||
|
||||
// Restaurant Types
|
||||
export interface Restaurant {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
timeZone: string;
|
||||
closeoutHour: number;
|
||||
managementGroupGuid: string;
|
||||
externalGroupRef?: string;
|
||||
locationName?: string;
|
||||
locationCode?: string;
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
city?: string;
|
||||
stateCode?: string;
|
||||
zipCode?: string;
|
||||
country?: string;
|
||||
phone?: string;
|
||||
createdDate: string;
|
||||
modifiedDate: string;
|
||||
}
|
||||
|
||||
export interface RevenueCenter {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DiningOption {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
behavior: string;
|
||||
curbside: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceArea {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
tables: Table[];
|
||||
}
|
||||
|
||||
export interface Table {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
seatingCapacity: number;
|
||||
serviceArea?: ServiceArea;
|
||||
}
|
||||
|
||||
// Payment Types
|
||||
export interface Payment {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
externalId?: string;
|
||||
paymentDate: string;
|
||||
businessDate: number;
|
||||
amount: number;
|
||||
tipAmount: number;
|
||||
amountTendered: number;
|
||||
cardEntryMode?: string;
|
||||
last4Digits?: string;
|
||||
paymentType: string;
|
||||
paymentStatus: string;
|
||||
voidInfo?: VoidInfo;
|
||||
refundInfo?: RefundInfo;
|
||||
originalProcessingFee?: number;
|
||||
server?: Employee;
|
||||
cashDrawer?: CashDrawer;
|
||||
otherPayment?: OtherPayment;
|
||||
houseAccount?: HouseAccount;
|
||||
cardType?: string;
|
||||
}
|
||||
|
||||
export interface VoidInfo {
|
||||
voidDate: string;
|
||||
voidBusinessDate: number;
|
||||
voidUser: Employee;
|
||||
voidApprover?: Employee;
|
||||
voidReason?: string;
|
||||
}
|
||||
|
||||
export interface RefundInfo {
|
||||
refundDate: string;
|
||||
refundBusinessDate: number;
|
||||
refundAmount: number;
|
||||
refundUser: Employee;
|
||||
refundReason?: string;
|
||||
}
|
||||
|
||||
export interface OtherPayment {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
isDefaultForTakeout: boolean;
|
||||
}
|
||||
|
||||
export interface HouseAccount {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Inventory Types
|
||||
export interface InventoryItem {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
unitOfMeasure: string;
|
||||
currentQuantity: number;
|
||||
parLevel?: number;
|
||||
reorderPoint?: number;
|
||||
cost: number;
|
||||
vendor?: Vendor;
|
||||
lastCountDate?: string;
|
||||
}
|
||||
|
||||
export interface Vendor {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
contactName?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface PurchaseOrder {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
orderNumber: string;
|
||||
vendor: Vendor;
|
||||
orderDate: string;
|
||||
expectedDeliveryDate?: string;
|
||||
status: string;
|
||||
items: PurchaseOrderItem[];
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface PurchaseOrderItem {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
inventoryItem: InventoryItem;
|
||||
quantity: number;
|
||||
unitCost: number;
|
||||
totalCost: number;
|
||||
}
|
||||
|
||||
// Customer Types
|
||||
export interface Customer {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
company?: string;
|
||||
createdDate: string;
|
||||
modifiedDate: string;
|
||||
loyaltyPoints?: number;
|
||||
totalVisits?: number;
|
||||
totalSpent?: number;
|
||||
}
|
||||
|
||||
export interface LoyaltyAccount {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
customer: Customer;
|
||||
points: number;
|
||||
tier?: string;
|
||||
enrollmentDate: string;
|
||||
lastActivityDate?: string;
|
||||
}
|
||||
|
||||
// Cash Management Types
|
||||
export interface CashDrawer {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
name: string;
|
||||
employee?: Employee;
|
||||
openedDate?: string;
|
||||
closedDate?: string;
|
||||
status: string;
|
||||
openingFloat: number;
|
||||
closingCash: number;
|
||||
expectedCash: number;
|
||||
variance: number;
|
||||
}
|
||||
|
||||
export interface CashEntry {
|
||||
guid: string;
|
||||
entityType: string;
|
||||
entryDate: string;
|
||||
businessDate: number;
|
||||
amount: number;
|
||||
type: string;
|
||||
reason?: string;
|
||||
employee: Employee;
|
||||
cashDrawer: CashDrawer;
|
||||
}
|
||||
|
||||
// Reporting Types
|
||||
export interface SalesSummary {
|
||||
businessDate: number;
|
||||
netSales: number;
|
||||
grossSales: number;
|
||||
discounts: number;
|
||||
refunds: number;
|
||||
tax: number;
|
||||
tips: number;
|
||||
totalPayments: number;
|
||||
guestCount: number;
|
||||
checkCount: number;
|
||||
averageCheck: number;
|
||||
averageGuest: number;
|
||||
salesByHour: HourlySales[];
|
||||
salesByCategory: CategorySales[];
|
||||
paymentsByType: PaymentTypeSales[];
|
||||
}
|
||||
|
||||
export interface HourlySales {
|
||||
hour: number;
|
||||
netSales: number;
|
||||
grossSales: number;
|
||||
checkCount: number;
|
||||
guestCount: number;
|
||||
}
|
||||
|
||||
export interface CategorySales {
|
||||
category: string;
|
||||
netSales: number;
|
||||
quantity: number;
|
||||
percentOfTotal: number;
|
||||
}
|
||||
|
||||
export interface PaymentTypeSales {
|
||||
paymentType: string;
|
||||
amount: number;
|
||||
count: number;
|
||||
percentOfTotal: number;
|
||||
}
|
||||
|
||||
export interface LaborCostReport {
|
||||
businessDate: number;
|
||||
totalHours: number;
|
||||
totalLaborCost: number;
|
||||
salesAmount: number;
|
||||
laborCostPercent: number;
|
||||
employeeCount: number;
|
||||
averageHourlyRate: number;
|
||||
overtimeHours: number;
|
||||
overtimeCost: number;
|
||||
}
|
||||
|
||||
export interface MenuItemPerformance {
|
||||
item: MenuItem;
|
||||
quantitySold: number;
|
||||
netSales: number;
|
||||
grossSales: number;
|
||||
costOfGoods?: number;
|
||||
grossProfit?: number;
|
||||
grossProfitMargin?: number;
|
||||
percentOfTotalSales: number;
|
||||
}
|
||||
|
||||
export interface TipSummary {
|
||||
businessDate: number;
|
||||
totalTips: number;
|
||||
cashTips: number;
|
||||
cardTips: number;
|
||||
declaredTips: number;
|
||||
tipsByEmployee: EmployeeTips[];
|
||||
averageTipPercent: number;
|
||||
}
|
||||
|
||||
export interface EmployeeTips {
|
||||
employee: Employee;
|
||||
totalTips: number;
|
||||
cashTips: number;
|
||||
cardTips: number;
|
||||
hoursWorked: number;
|
||||
tipsPerHour: number;
|
||||
}
|
||||
|
||||
// Delivery Types
|
||||
export interface DeliveryInfo {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
deliveryNotes?: string;
|
||||
estimatedDeliveryTime?: string;
|
||||
deliveryEmployee?: Employee;
|
||||
deliveryFee?: number;
|
||||
}
|
||||
|
||||
export interface CurbsidePickupInfo {
|
||||
transportDescription?: string;
|
||||
transportColor?: string;
|
||||
notes?: string;
|
||||
arrivedDate?: string;
|
||||
}
|
||||
@ -1,14 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
6
servers/touchbistro/.gitignore
vendored
Normal file
6
servers/touchbistro/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
@ -1,118 +0,0 @@
|
||||
# TouchBistro MCP Server
|
||||
|
||||
MCP server for integrating with [TouchBistro](https://www.touchbistro.com/) restaurant POS and management system.
|
||||
|
||||
## Features
|
||||
|
||||
- **Orders**: List and retrieve order details
|
||||
- **Menu Items**: Access menu item catalog
|
||||
- **Reservations**: List and create reservations
|
||||
- **Staff**: List staff members
|
||||
- **Reports**: Get sales reports
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- TouchBistro account with API access
|
||||
- API credentials and Venue ID
|
||||
|
||||
### Getting API Access
|
||||
|
||||
Contact TouchBistro for API access through their integrations program. Visit [TouchBistro Integrations](https://www.touchbistro.com/features/integrations/) for more information.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
export TOUCHBISTRO_API_KEY="your-api-key-here"
|
||||
export TOUCHBISTRO_VENUE_ID="your-venue-id"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Run the server
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
### Configure in Claude Desktop
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"touchbistro": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/touchbistro/dist/index.js"],
|
||||
"env": {
|
||||
"TOUCHBISTRO_API_KEY": "your-api-key",
|
||||
"TOUCHBISTRO_VENUE_ID": "your-venue-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_orders` | List orders with filters for status, type, date range |
|
||||
| `get_order` | Get detailed order info including items, payments, discounts |
|
||||
| `list_menu_items` | List menu items by category and availability |
|
||||
| `list_reservations` | List reservations by date, status, party size |
|
||||
| `create_reservation` | Create a new reservation |
|
||||
| `list_staff` | List staff by role and active status |
|
||||
| `get_sales_report` | Generate sales reports with various groupings |
|
||||
|
||||
## Order Types
|
||||
|
||||
- `dine_in` - Dine-in orders
|
||||
- `takeout` - Takeout orders
|
||||
- `delivery` - Delivery orders
|
||||
- `bar` - Bar orders
|
||||
|
||||
## Reservation Statuses
|
||||
|
||||
- `pending` - Awaiting confirmation
|
||||
- `confirmed` - Confirmed by restaurant
|
||||
- `seated` - Guest seated
|
||||
- `completed` - Reservation completed
|
||||
- `cancelled` - Cancelled
|
||||
- `no_show` - Guest didn't show up
|
||||
|
||||
## Staff Roles
|
||||
|
||||
- `server` - Server
|
||||
- `bartender` - Bartender
|
||||
- `host` - Host/Hostess
|
||||
- `manager` - Manager
|
||||
- `kitchen` - Kitchen staff
|
||||
- `cashier` - Cashier
|
||||
|
||||
## Report Groupings
|
||||
|
||||
- `day` - Daily breakdown
|
||||
- `week` - Weekly breakdown
|
||||
- `month` - Monthly breakdown
|
||||
- `category` - By menu category
|
||||
- `item` - By menu item
|
||||
- `server` - By server
|
||||
|
||||
## API Reference
|
||||
|
||||
Base URL: `https://cloud.touchbistro.com/api/v1`
|
||||
|
||||
Authentication: Bearer token + Venue ID header
|
||||
|
||||
See TouchBistro partner documentation for full API details.
|
||||
@ -1,20 +1,37 @@
|
||||
{
|
||||
"name": "mcp-server-touchbistro",
|
||||
"name": "@mcpengine/touchbistro-server",
|
||||
"version": "1.0.0",
|
||||
"description": "TouchBistro MCP Server - Complete POS and restaurant management integration",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/main.js",
|
||||
"bin": {
|
||||
"touchbistro-mcp": "./dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"dev": "tsc --watch",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"touchbistro",
|
||||
"pos",
|
||||
"restaurant",
|
||||
"orders",
|
||||
"payments",
|
||||
"inventory"
|
||||
],
|
||||
"author": "MCP Engine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
235
servers/touchbistro/src/api-client.ts
Normal file
235
servers/touchbistro/src/api-client.ts
Normal file
@ -0,0 +1,235 @@
|
||||
/**
|
||||
* TouchBistro API Client
|
||||
* Handles authentication, pagination, error handling, and rate limiting
|
||||
*/
|
||||
|
||||
import type { TouchBistroConfig, PaginatedResponse, PaginationParams } from './types.js';
|
||||
|
||||
export class TouchBistroAPIError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode?: number,
|
||||
public responseBody?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'TouchBistroAPIError';
|
||||
}
|
||||
}
|
||||
|
||||
export class TouchBistroAPIClient {
|
||||
private apiKey: string;
|
||||
private baseUrl: string;
|
||||
private venueId?: string;
|
||||
|
||||
constructor(config: TouchBistroConfig) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.baseUrl = config.baseUrl || 'https://api.touchbistro.com/v1';
|
||||
this.venueId = config.venueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated request to the TouchBistro API
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
if (this.venueId) {
|
||||
headers['X-Venue-ID'] = this.venueId;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle rate limiting
|
||||
if (response.status === 429) {
|
||||
const retryAfter = response.headers.get('Retry-After');
|
||||
throw new TouchBistroAPIError(
|
||||
`Rate limited. Retry after ${retryAfter} seconds`,
|
||||
429,
|
||||
{ retryAfter }
|
||||
);
|
||||
}
|
||||
|
||||
// Parse response body
|
||||
const contentType = response.headers.get('content-type');
|
||||
let data: unknown;
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text();
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if (!response.ok) {
|
||||
throw new TouchBistroAPIError(
|
||||
`TouchBistro API error: ${response.statusText}`,
|
||||
response.status,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (error) {
|
||||
if (error instanceof TouchBistroAPIError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new TouchBistroAPIError(
|
||||
`Network error: ${error.message}`,
|
||||
undefined,
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
throw new TouchBistroAPIError('Unknown error occurred');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T> {
|
||||
const queryString = params
|
||||
? '?' + new URLSearchParams(
|
||||
Object.entries(params)
|
||||
.filter(([_, v]) => v !== undefined && v !== null)
|
||||
.map(([k, v]) => [k, String(v)])
|
||||
).toString()
|
||||
: '';
|
||||
|
||||
return this.request<T>(`${endpoint}${queryString}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
async put<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH request
|
||||
*/
|
||||
async patch<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated GET request - automatically handles pagination
|
||||
*/
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
params: PaginationParams & Record<string, unknown> = {}
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const { page = 1, limit = 50, ...otherParams } = params;
|
||||
|
||||
const response = await this.get<{
|
||||
data: T[];
|
||||
pagination?: {
|
||||
total?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
hasMore?: boolean;
|
||||
nextPage?: string;
|
||||
};
|
||||
}>(endpoint, {
|
||||
page,
|
||||
limit,
|
||||
...otherParams,
|
||||
});
|
||||
|
||||
// Normalize pagination response
|
||||
return {
|
||||
data: response.data || [],
|
||||
pagination: {
|
||||
total: response.pagination?.total || response.data?.length || 0,
|
||||
page: response.pagination?.page || page,
|
||||
limit: response.pagination?.limit || limit,
|
||||
hasMore: response.pagination?.hasMore || false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all pages automatically
|
||||
*/
|
||||
async getAllPages<T>(
|
||||
endpoint: string,
|
||||
params: Record<string, unknown> = {},
|
||||
maxPages = 10
|
||||
): Promise<T[]> {
|
||||
const allData: T[] = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore && page <= maxPages) {
|
||||
const response = await this.getPaginated<T>(endpoint, {
|
||||
...params,
|
||||
page,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
allData.push(...response.data);
|
||||
hasMore = response.pagination.hasMore;
|
||||
page++;
|
||||
}
|
||||
|
||||
return allData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck(): Promise<{ status: string; timestamp: string }> {
|
||||
try {
|
||||
return await this.get('/health');
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,386 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// ============================================
|
||||
// CONFIGURATION
|
||||
// ============================================
|
||||
const MCP_NAME = "touchbistro";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
const API_BASE_URL = "https://cloud.touchbistro.com/api/v1";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT
|
||||
// ============================================
|
||||
class TouchBistroClient {
|
||||
private apiKey: string;
|
||||
private venueId: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(apiKey: string, venueId: string) {
|
||||
this.apiKey = apiKey;
|
||||
this.venueId = venueId;
|
||||
this.baseUrl = API_BASE_URL;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.apiKey}`,
|
||||
"X-Venue-Id": this.venueId,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`TouchBistro API error: ${response.status} ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string) {
|
||||
return this.request(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async delete(endpoint: string) {
|
||||
return this.request(endpoint, { method: "DELETE" });
|
||||
}
|
||||
|
||||
// Orders
|
||||
async listOrders(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: string;
|
||||
orderType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.status) query.append("status", params.status);
|
||||
if (params.orderType) query.append("orderType", params.orderType);
|
||||
if (params.startDate) query.append("startDate", params.startDate);
|
||||
if (params.endDate) query.append("endDate", params.endDate);
|
||||
return this.get(`/orders?${query.toString()}`);
|
||||
}
|
||||
|
||||
async getOrder(id: string) {
|
||||
return this.get(`/orders/${id}`);
|
||||
}
|
||||
|
||||
// Menu Items
|
||||
async listMenuItems(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
categoryId?: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.categoryId) query.append("categoryId", params.categoryId);
|
||||
if (params.active !== undefined) query.append("active", params.active.toString());
|
||||
return this.get(`/menu/items?${query.toString()}`);
|
||||
}
|
||||
|
||||
// Reservations
|
||||
async listReservations(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
date?: string;
|
||||
status?: string;
|
||||
partySize?: number;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.date) query.append("date", params.date);
|
||||
if (params.status) query.append("status", params.status);
|
||||
if (params.partySize) query.append("partySize", params.partySize.toString());
|
||||
return this.get(`/reservations?${query.toString()}`);
|
||||
}
|
||||
|
||||
async createReservation(data: {
|
||||
customerName: string;
|
||||
customerPhone?: string;
|
||||
customerEmail?: string;
|
||||
partySize: number;
|
||||
date: string;
|
||||
time: string;
|
||||
tableId?: string;
|
||||
notes?: string;
|
||||
source?: string;
|
||||
}) {
|
||||
return this.post("/reservations", data);
|
||||
}
|
||||
|
||||
// Staff
|
||||
async listStaff(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
role?: string;
|
||||
active?: boolean;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
if (params.page) query.append("page", params.page.toString());
|
||||
if (params.pageSize) query.append("pageSize", params.pageSize.toString());
|
||||
if (params.role) query.append("role", params.role);
|
||||
if (params.active !== undefined) query.append("active", params.active.toString());
|
||||
return this.get(`/staff?${query.toString()}`);
|
||||
}
|
||||
|
||||
// Reports
|
||||
async getSalesReport(params: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
groupBy?: string;
|
||||
includeVoids?: boolean;
|
||||
includeRefunds?: boolean;
|
||||
}) {
|
||||
const query = new URLSearchParams();
|
||||
query.append("startDate", params.startDate);
|
||||
query.append("endDate", params.endDate);
|
||||
if (params.groupBy) query.append("groupBy", params.groupBy);
|
||||
if (params.includeVoids !== undefined) query.append("includeVoids", params.includeVoids.toString());
|
||||
if (params.includeRefunds !== undefined) query.append("includeRefunds", params.includeRefunds.toString());
|
||||
return this.get(`/reports/sales?${query.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_orders",
|
||||
description: "List orders from TouchBistro POS. Filter by status, order type, and date range.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination (default: 1)" },
|
||||
pageSize: { type: "number", description: "Number of results per page (default: 25, max: 100)" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by order status",
|
||||
enum: ["open", "closed", "voided", "refunded"]
|
||||
},
|
||||
orderType: {
|
||||
type: "string",
|
||||
description: "Filter by order type",
|
||||
enum: ["dine_in", "takeout", "delivery", "bar"]
|
||||
},
|
||||
startDate: { type: "string", description: "Filter by order date (start) in YYYY-MM-DD format" },
|
||||
endDate: { type: "string", description: "Filter by order date (end) in YYYY-MM-DD format" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_order",
|
||||
description: "Get detailed information about a specific order by ID, including all items, modifiers, payments, and discounts",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
id: { type: "string", description: "The order ID" },
|
||||
},
|
||||
required: ["id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_menu_items",
|
||||
description: "List menu items from TouchBistro. Get all items available for ordering.",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
categoryId: { type: "string", description: "Filter by menu category ID" },
|
||||
active: { type: "boolean", description: "Filter by active status (true = available for ordering)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_reservations",
|
||||
description: "List reservations from TouchBistro",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
date: { type: "string", description: "Filter by reservation date in YYYY-MM-DD format" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by reservation status",
|
||||
enum: ["pending", "confirmed", "seated", "completed", "cancelled", "no_show"]
|
||||
},
|
||||
partySize: { type: "number", description: "Filter by party size" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_reservation",
|
||||
description: "Create a new reservation in TouchBistro",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
customerName: { type: "string", description: "Customer name (required)" },
|
||||
customerPhone: { type: "string", description: "Customer phone number" },
|
||||
customerEmail: { type: "string", description: "Customer email address" },
|
||||
partySize: { type: "number", description: "Number of guests (required)" },
|
||||
date: { type: "string", description: "Reservation date in YYYY-MM-DD format (required)" },
|
||||
time: { type: "string", description: "Reservation time in HH:MM format (required)" },
|
||||
tableId: { type: "string", description: "Specific table ID to reserve" },
|
||||
notes: { type: "string", description: "Special requests or notes" },
|
||||
source: {
|
||||
type: "string",
|
||||
description: "Reservation source",
|
||||
enum: ["phone", "walk_in", "online", "third_party"]
|
||||
},
|
||||
},
|
||||
required: ["customerName", "partySize", "date", "time"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_staff",
|
||||
description: "List staff members from TouchBistro",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number for pagination" },
|
||||
pageSize: { type: "number", description: "Number of results per page (max: 100)" },
|
||||
role: {
|
||||
type: "string",
|
||||
description: "Filter by staff role",
|
||||
enum: ["server", "bartender", "host", "manager", "kitchen", "cashier"]
|
||||
},
|
||||
active: { type: "boolean", description: "Filter by active employment status" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_sales_report",
|
||||
description: "Get sales report data from TouchBistro for analysis and reporting",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
startDate: { type: "string", description: "Report start date in YYYY-MM-DD format (required)" },
|
||||
endDate: { type: "string", description: "Report end date in YYYY-MM-DD format (required)" },
|
||||
groupBy: {
|
||||
type: "string",
|
||||
description: "How to group the report data",
|
||||
enum: ["day", "week", "month", "category", "item", "server"]
|
||||
},
|
||||
includeVoids: { type: "boolean", description: "Include voided orders in the report" },
|
||||
includeRefunds: { type: "boolean", description: "Include refunded orders in the report" },
|
||||
},
|
||||
required: ["startDate", "endDate"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: TouchBistroClient, name: string, args: any) {
|
||||
switch (name) {
|
||||
case "list_orders":
|
||||
return await client.listOrders(args);
|
||||
|
||||
case "get_order":
|
||||
return await client.getOrder(args.id);
|
||||
|
||||
case "list_menu_items":
|
||||
return await client.listMenuItems(args);
|
||||
|
||||
case "list_reservations":
|
||||
return await client.listReservations(args);
|
||||
|
||||
case "create_reservation":
|
||||
return await client.createReservation(args);
|
||||
|
||||
case "list_staff":
|
||||
return await client.listStaff(args);
|
||||
|
||||
case "get_sales_report":
|
||||
return await client.getSalesReport(args);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const apiKey = process.env.TOUCHBISTRO_API_KEY;
|
||||
const venueId = process.env.TOUCHBISTRO_VENUE_ID;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error("Error: TOUCHBISTRO_API_KEY environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!venueId) {
|
||||
console.error("Error: TOUCHBISTRO_VENUE_ID environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new TouchBistroClient(apiKey, venueId);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
244
servers/touchbistro/src/tools/customers-tools.ts
Normal file
244
servers/touchbistro/src/tools/customers-tools.ts
Normal file
@ -0,0 +1,244 @@
|
||||
/**
|
||||
* TouchBistro Customers Tools
|
||||
*/
|
||||
|
||||
import type { TouchBistroAPIClient } from '../api-client.js';
|
||||
import type { Customer, LoyaltyTransaction } from '../types.js';
|
||||
|
||||
export function registerCustomersTools(client: TouchBistroAPIClient) {
|
||||
return {
|
||||
list_customers: {
|
||||
description: 'List all customers',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search by name, email, or phone',
|
||||
},
|
||||
hasLoyalty: {
|
||||
type: 'boolean',
|
||||
description: 'Filter customers with loyalty points',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: Record<string, unknown>) => {
|
||||
const response = await client.getPaginated<Customer>('/customers', args);
|
||||
return {
|
||||
customers: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
get_customer: {
|
||||
description: 'Get a specific customer by ID',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: { customerId: string }) => {
|
||||
return await client.get<Customer>(`/customers/${args.customerId}`);
|
||||
},
|
||||
},
|
||||
|
||||
create_customer: {
|
||||
description: 'Create a new customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
firstName: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
description: 'Phone number',
|
||||
},
|
||||
},
|
||||
required: ['firstName', 'lastName'],
|
||||
},
|
||||
handler: async (args: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}) => {
|
||||
return await client.post<Customer>('/customers', args);
|
||||
},
|
||||
},
|
||||
|
||||
update_customer: {
|
||||
description: 'Update a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
firstName: {
|
||||
type: 'string',
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
},
|
||||
phone: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: {
|
||||
customerId: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}) => {
|
||||
const { customerId, ...updates } = args;
|
||||
return await client.patch<Customer>(`/customers/${customerId}`, updates);
|
||||
},
|
||||
},
|
||||
|
||||
list_customer_loyalty: {
|
||||
description: 'List loyalty transactions for a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['earn', 'redeem'],
|
||||
description: 'Filter by transaction type',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
required: ['customerId'],
|
||||
},
|
||||
handler: async (args: {
|
||||
customerId: string;
|
||||
type?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const { customerId, ...params } = args;
|
||||
const response = await client.getPaginated<LoyaltyTransaction>(
|
||||
`/customers/${customerId}/loyalty`,
|
||||
params
|
||||
);
|
||||
return {
|
||||
transactions: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
add_loyalty_points: {
|
||||
description: 'Add loyalty points to a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
points: {
|
||||
type: 'number',
|
||||
description: 'Points to add',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Associated order ID',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of transaction',
|
||||
},
|
||||
},
|
||||
required: ['customerId', 'points'],
|
||||
},
|
||||
handler: async (args: {
|
||||
customerId: string;
|
||||
points: number;
|
||||
orderId?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const { customerId, ...data } = args;
|
||||
return await client.post<LoyaltyTransaction>(
|
||||
`/customers/${customerId}/loyalty/earn`,
|
||||
data
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
redeem_loyalty_points: {
|
||||
description: 'Redeem loyalty points for a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customerId: {
|
||||
type: 'string',
|
||||
description: 'The customer ID',
|
||||
},
|
||||
points: {
|
||||
type: 'number',
|
||||
description: 'Points to redeem',
|
||||
},
|
||||
orderId: {
|
||||
type: 'string',
|
||||
description: 'Associated order ID',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Description of redemption',
|
||||
},
|
||||
},
|
||||
required: ['customerId', 'points'],
|
||||
},
|
||||
handler: async (args: {
|
||||
customerId: string;
|
||||
points: number;
|
||||
orderId?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const { customerId, ...data } = args;
|
||||
return await client.post<LoyaltyTransaction>(
|
||||
`/customers/${customerId}/loyalty/redeem`,
|
||||
data
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
173
servers/touchbistro/src/tools/inventory-tools.ts
Normal file
173
servers/touchbistro/src/tools/inventory-tools.ts
Normal file
@ -0,0 +1,173 @@
|
||||
/**
|
||||
* TouchBistro Inventory Tools
|
||||
*/
|
||||
|
||||
import type { TouchBistroAPIClient } from '../api-client.js';
|
||||
import type { InventoryItem, InventoryCategory } from '../types.js';
|
||||
|
||||
export function registerInventoryTools(client: TouchBistroAPIClient) {
|
||||
return {
|
||||
list_inventory_items: {
|
||||
description: 'List all inventory items',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
categoryId: {
|
||||
type: 'string',
|
||||
description: 'Filter by category',
|
||||
},
|
||||
lowStock: {
|
||||
type: 'boolean',
|
||||
description: 'Show only items below minimum quantity',
|
||||
},
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search by name or SKU',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: Record<string, unknown>) => {
|
||||
const response = await client.getPaginated<InventoryItem>(
|
||||
'/inventory/items',
|
||||
args
|
||||
);
|
||||
return {
|
||||
items: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
get_inventory_item: {
|
||||
description: 'Get a specific inventory item',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The inventory item ID',
|
||||
},
|
||||
},
|
||||
required: ['itemId'],
|
||||
},
|
||||
handler: async (args: { itemId: string }) => {
|
||||
return await client.get<InventoryItem>(
|
||||
`/inventory/items/${args.itemId}`
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
update_inventory_count: {
|
||||
description: 'Update inventory count for an item',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The inventory item ID',
|
||||
},
|
||||
quantity: {
|
||||
type: 'number',
|
||||
description: 'New quantity',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Reason for adjustment (restock, waste, theft, etc.)',
|
||||
},
|
||||
},
|
||||
required: ['itemId', 'quantity'],
|
||||
},
|
||||
handler: async (args: {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
reason?: string;
|
||||
}) => {
|
||||
const { itemId, ...data } = args;
|
||||
return await client.post<InventoryItem>(
|
||||
`/inventory/items/${itemId}/adjust`,
|
||||
data
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
add_inventory_stock: {
|
||||
description: 'Add stock to an inventory item (restock)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The inventory item ID',
|
||||
},
|
||||
quantity: {
|
||||
type: 'number',
|
||||
description: 'Quantity to add',
|
||||
},
|
||||
cost: {
|
||||
type: 'number',
|
||||
description: 'Cost per unit',
|
||||
},
|
||||
supplier: {
|
||||
type: 'string',
|
||||
description: 'Supplier name',
|
||||
},
|
||||
},
|
||||
required: ['itemId', 'quantity'],
|
||||
},
|
||||
handler: async (args: {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
cost?: number;
|
||||
supplier?: string;
|
||||
}) => {
|
||||
const { itemId, ...data } = args;
|
||||
return await client.post<InventoryItem>(
|
||||
`/inventory/items/${itemId}/restock`,
|
||||
data
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
list_inventory_categories: {
|
||||
description: 'List all inventory categories',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
handler: async () => {
|
||||
return await client.get<{ categories: InventoryCategory[] }>(
|
||||
'/inventory/categories'
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
get_low_stock_items: {
|
||||
description: 'Get items that are low in stock',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
categoryId: {
|
||||
type: 'string',
|
||||
description: 'Filter by category',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: { categoryId?: string }) => {
|
||||
const response = await client.getPaginated<InventoryItem>(
|
||||
'/inventory/low-stock',
|
||||
args
|
||||
);
|
||||
return {
|
||||
items: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
224
servers/touchbistro/src/tools/menus-tools.ts
Normal file
224
servers/touchbistro/src/tools/menus-tools.ts
Normal file
@ -0,0 +1,224 @@
|
||||
/**
|
||||
* TouchBistro Menus Tools
|
||||
*/
|
||||
|
||||
import type { TouchBistroAPIClient } from '../api-client.js';
|
||||
import type { Menu, MenuCategory, MenuItem } from '../types.js';
|
||||
|
||||
export function registerMenusTools(client: TouchBistroAPIClient) {
|
||||
return {
|
||||
list_menus: {
|
||||
description: 'List all menus',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
active: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by active status',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: Record<string, unknown>) => {
|
||||
const response = await client.getPaginated<Menu>('/menus', args);
|
||||
return {
|
||||
menus: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
get_menu: {
|
||||
description: 'Get a specific menu by ID',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
menuId: {
|
||||
type: 'string',
|
||||
description: 'The menu ID',
|
||||
},
|
||||
includeCategories: {
|
||||
type: 'boolean',
|
||||
description: 'Include categories and items',
|
||||
},
|
||||
},
|
||||
required: ['menuId'],
|
||||
},
|
||||
handler: async (args: { menuId: string; includeCategories?: boolean }) => {
|
||||
const params = args.includeCategories ? { expand: 'categories,items' } : {};
|
||||
return await client.get<Menu>(`/menus/${args.menuId}`, params);
|
||||
},
|
||||
},
|
||||
|
||||
list_menu_categories: {
|
||||
description: 'List all categories in a menu',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
menuId: {
|
||||
type: 'string',
|
||||
description: 'The menu ID',
|
||||
},
|
||||
},
|
||||
required: ['menuId'],
|
||||
},
|
||||
handler: async (args: { menuId: string }) => {
|
||||
const response = await client.get<{ categories: MenuCategory[] }>(
|
||||
`/menus/${args.menuId}/categories`
|
||||
);
|
||||
return response;
|
||||
},
|
||||
},
|
||||
|
||||
list_menu_items: {
|
||||
description: 'List all menu items, optionally filtered by category',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
menuId: {
|
||||
type: 'string',
|
||||
description: 'Menu ID to filter by',
|
||||
},
|
||||
categoryId: {
|
||||
type: 'string',
|
||||
description: 'Category ID to filter by',
|
||||
},
|
||||
available: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by availability',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Results per page',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (args: Record<string, unknown>) => {
|
||||
const response = await client.getPaginated<MenuItem>('/menu-items', args);
|
||||
return {
|
||||
items: response.data,
|
||||
pagination: response.pagination,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
get_menu_item: {
|
||||
description: 'Get a specific menu item',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The menu item ID',
|
||||
},
|
||||
},
|
||||
required: ['itemId'],
|
||||
},
|
||||
handler: async (args: { itemId: string }) => {
|
||||
return await client.get<MenuItem>(`/menu-items/${args.itemId}`);
|
||||
},
|
||||
},
|
||||
|
||||
update_menu_item: {
|
||||
description: 'Update a menu item',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The menu item ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Item description',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'Item price',
|
||||
},
|
||||
available: {
|
||||
type: 'boolean',
|
||||
description: 'Item availability',
|
||||
},
|
||||
categoryId: {
|
||||
type: 'string',
|
||||
description: 'Category ID',
|
||||
},
|
||||
},
|
||||
required: ['itemId'],
|
||||
},
|
||||
handler: async (args: {
|
||||
itemId: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
available?: boolean;
|
||||
categoryId?: string;
|
||||
}) => {
|
||||
const { itemId, ...updates } = args;
|
||||
return await client.patch<MenuItem>(`/menu-items/${itemId}`, updates);
|
||||
},
|
||||
},
|
||||
|
||||
update_menu_item_price: {
|
||||
description: 'Update the price of a menu item',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The menu item ID',
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
description: 'New price',
|
||||
},
|
||||
},
|
||||
required: ['itemId', 'price'],
|
||||
},
|
||||
handler: async (args: { itemId: string; price: number }) => {
|
||||
return await client.patch<MenuItem>(`/menu-items/${args.itemId}`, {
|
||||
price: args.price,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
set_menu_item_availability: {
|
||||
description: 'Set menu item availability (86 an item)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: {
|
||||
type: 'string',
|
||||
description: 'The menu item ID',
|
||||
},
|
||||
available: {
|
||||
type: 'boolean',
|
||||
description: 'Set availability',
|
||||
},
|
||||
},
|
||||
required: ['itemId', 'available'],
|
||||
},
|
||||
handler: async (args: { itemId: string; available: boolean }) => {
|
||||
return await client.patch<MenuItem>(`/menu-items/${args.itemId}`, {
|
||||
available: args.available,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
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