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:
Jake Shore 2026-02-12 17:58:15 -05:00
parent c435e6c3a4
commit d3382ec35a
108 changed files with 17560 additions and 1700 deletions

View File

@ -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

View File

@ -9,7 +9,6 @@
},
"scripts": {
"build": "tsc",
"prepare": "npm run build",
"start": "node dist/main.js"
},
"keywords": [

View 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...',
};
},
},
];
}

View File

@ -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);

View 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();

View 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;
}
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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)}`,
}],
};
},
},
];

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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)}`,
}],
};
},
},
];

View 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),
},
],
};
},
},
];
}

View 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)}`,
}],
};
},
},
];

View 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),
},
],
};
},
},
];
}

View 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)}`,
}],
};
},
},
];

View File

@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,

View 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)

View File

@ -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"
]
}

View 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\`
`;
},
},
};

View 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 };
}
}
}

View 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);
});

View 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');
}
}

View 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,
};
}
},
},
};
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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,
};
}
},
},
};
}

View 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) }] };
}
}
];
}

View 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,
};
}
},
},
};
}

View 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) }] };
}
}
];
}

View 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,
};
}
},
},
};
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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,
};
}
},
},
};
}

View 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,
};
}
},
},
};
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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) }] };
}
}
];
}

View 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,
};
}
},
},
};
}

View 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;
};

View File

@ -16,5 +16,5 @@
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "src/ui"]
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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"
}
}

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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>
`;
}
};

View 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 || [];
}
}

View File

@ -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);

View 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)
}]
};
}
}
];
}

View 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
),
},
],
};
},
},
];
}

View 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)
}]
};
}
}
];
}

View 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
),
},
],
};
},
},
];
}

View 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)
}]
};
}
}
];
}

View 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,
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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)
}]
};
}
}
];
}

View 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),
},
],
};
},
},
];
}

View 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`,
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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)
}]
};
}
}
];
}

View 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),
},
],
};
},
},
];
}

View 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;
};

View File

@ -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"]
}

View File

@ -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"
}
}

View 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}`),
};
}

View 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' },
],
},
},
];

View File

@ -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);

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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),
},
],
};
},
},
];
}

View 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;
}

View File

@ -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
View File

@ -0,0 +1,6 @@
node_modules/
dist/
*.log
.env
.DS_Store
*.tsbuildinfo

View File

@ -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.

View File

@ -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"
}
}

View 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(),
};
}
}
}

View File

@ -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);

View 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
);
},
},
};
}

View 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,
};
},
},
};
}

View 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