diff --git a/servers/fieldedge/README.md b/servers/fieldedge/README.md
index cfd04eb..542a53b 100644
--- a/servers/fieldedge/README.md
+++ b/servers/fieldedge/README.md
@@ -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
diff --git a/servers/fieldedge/package.json b/servers/fieldedge/package.json
index bc121c4..f7bd6bf 100644
--- a/servers/fieldedge/package.json
+++ b/servers/fieldedge/package.json
@@ -9,7 +9,6 @@
},
"scripts": {
"build": "tsc",
- "prepare": "npm run build",
"start": "node dist/main.js"
},
"keywords": [
diff --git a/servers/fieldedge/src/apps/index.ts b/servers/fieldedge/src/apps/index.ts
new file mode 100644
index 0000000..cf4e286
--- /dev/null
+++ b/servers/fieldedge/src/apps/index.ts
@@ -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: `
+
+
+
+
+
+
+
+
+
+
📋 Jobs Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `.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...',
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/index.ts b/servers/fieldedge/src/index.ts
deleted file mode 100644
index 885285f..0000000
--- a/servers/fieldedge/src/index.ts
+++ /dev/null
@@ -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);
diff --git a/servers/fieldedge/src/main.ts b/servers/fieldedge/src/main.ts
new file mode 100644
index 0000000..4ac465b
--- /dev/null
+++ b/servers/fieldedge/src/main.ts
@@ -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();
diff --git a/servers/fieldedge/src/server.ts b/servers/fieldedge/src/server.ts
new file mode 100644
index 0000000..48480e4
--- /dev/null
+++ b/servers/fieldedge/src/server.ts
@@ -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 {
+ const transport = new StdioServerTransport();
+ await this.server.connect(transport);
+ console.error('FieldEdge MCP Server running on stdio');
+ }
+
+ getToolCount(): number {
+ return this.tools.length;
+ }
+}
diff --git a/servers/fieldedge/src/tools/agreements-tools.ts b/servers/fieldedge/src/tools/agreements-tools.ts
new file mode 100644
index 0000000..52b8e7a
--- /dev/null
+++ b/servers/fieldedge/src/tools/agreements-tools.ts
@@ -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(
+ '/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(
+ `/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('/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(
+ `/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(
+ `/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(
+ `/agreements/${agreementId}/renew`,
+ renewData
+ );
+ return {
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify(agreement, null, 2),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/dispatch-tools.ts b/servers/fieldedge/src/tools/dispatch-tools.ts
new file mode 100644
index 0000000..4ae0550
--- /dev/null
+++ b/servers/fieldedge/src/tools/dispatch-tools.ts
@@ -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('/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(
+ `/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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/equipment-tools.ts b/servers/fieldedge/src/tools/equipment-tools.ts
new file mode 100644
index 0000000..1017050
--- /dev/null
+++ b/servers/fieldedge/src/tools/equipment-tools.ts
@@ -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', 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/${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', 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/${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(
+ `/equipment/${equipmentId}/service-history`,
+ queryParams
+ );
+ return {
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify(result, null, 2),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/equipment.ts b/servers/fieldedge/src/tools/equipment.ts
new file mode 100644
index 0000000..54270b3
--- /dev/null
+++ b/servers/fieldedge/src/tools/equipment.ts
@@ -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)}`,
+ }],
+ };
+ },
+ },
+];
diff --git a/servers/fieldedge/src/tools/estimates-tools.ts b/servers/fieldedge/src/tools/estimates-tools.ts
new file mode 100644
index 0000000..830f4d6
--- /dev/null
+++ b/servers/fieldedge/src/tools/estimates-tools.ts
@@ -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('/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(
+ `/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('/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(
+ `/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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/inventory-tools.ts b/servers/fieldedge/src/tools/inventory-tools.ts
new file mode 100644
index 0000000..2095043
--- /dev/null
+++ b/servers/fieldedge/src/tools/inventory-tools.ts
@@ -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(
+ '/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(
+ `/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(
+ `/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(
+ '/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(
+ `/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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/invoices-tools.ts b/servers/fieldedge/src/tools/invoices-tools.ts
new file mode 100644
index 0000000..3369387
--- /dev/null
+++ b/servers/fieldedge/src/tools/invoices-tools.ts
@@ -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('/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(`/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('/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(
+ `/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(
+ `/invoices/${invoiceId}/payments`,
+ paymentData
+ );
+ return {
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify(payment, null, 2),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/payments.ts b/servers/fieldedge/src/tools/payments.ts
new file mode 100644
index 0000000..366f0ff
--- /dev/null
+++ b/servers/fieldedge/src/tools/payments.ts
@@ -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)}`,
+ }],
+ };
+ },
+ },
+];
diff --git a/servers/fieldedge/src/tools/reporting-tools.ts b/servers/fieldedge/src/tools/reporting-tools.ts
new file mode 100644
index 0000000..469676f
--- /dev/null
+++ b/servers/fieldedge/src/tools/reporting-tools.ts
@@ -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('/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('/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;
+ }>('/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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/reporting.ts b/servers/fieldedge/src/tools/reporting.ts
new file mode 100644
index 0000000..a90af1f
--- /dev/null
+++ b/servers/fieldedge/src/tools/reporting.ts
@@ -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)}`,
+ }],
+ };
+ },
+ },
+];
diff --git a/servers/fieldedge/src/tools/technicians-tools.ts b/servers/fieldedge/src/tools/technicians-tools.ts
new file mode 100644
index 0000000..95006d0
--- /dev/null
+++ b/servers/fieldedge/src/tools/technicians-tools.ts
@@ -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('/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(
+ `/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('/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(
+ `/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(
+ `/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(
+ `/technicians/${technicianId}/time-entries`,
+ queryParams
+ );
+ return {
+ content: [
+ {
+ type: 'text',
+ text: JSON.stringify(result, null, 2),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/fieldedge/src/tools/technicians.ts b/servers/fieldedge/src/tools/technicians.ts
new file mode 100644
index 0000000..a002120
--- /dev/null
+++ b/servers/fieldedge/src/tools/technicians.ts
@@ -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)}`,
+ }],
+ };
+ },
+ },
+];
diff --git a/servers/fieldedge/tsconfig.json b/servers/fieldedge/tsconfig.json
index f4624bd..f44baa9 100644
--- a/servers/fieldedge/tsconfig.json
+++ b/servers/fieldedge/tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2022",
- "module": "Node16",
- "moduleResolution": "Node16",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
diff --git a/servers/lightspeed/README.md b/servers/lightspeed/README.md
new file mode 100644
index 0000000..bb4c3b2
--- /dev/null
+++ b/servers/lightspeed/README.md
@@ -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)
diff --git a/servers/lightspeed/package.json b/servers/lightspeed/package.json
index 90856dc..36db690 100644
--- a/servers/lightspeed/package.json
+++ b/servers/lightspeed/package.json
@@ -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"
+ ]
}
diff --git a/servers/lightspeed/src/apps/index.ts b/servers/lightspeed/src/apps/index.ts
new file mode 100644
index 0000000..09d9a48
--- /dev/null
+++ b/servers/lightspeed/src/apps/index.ts
@@ -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\`
+`;
+ },
+ },
+};
diff --git a/servers/lightspeed/src/clients/lightspeed.ts b/servers/lightspeed/src/clients/lightspeed.ts
new file mode 100644
index 0000000..7b03923
--- /dev/null
+++ b/servers/lightspeed/src/clients/lightspeed.ts
@@ -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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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): Promise> {
+ 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): Promise> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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> {
+ 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 };
+ }
+ }
+}
diff --git a/servers/lightspeed/src/main.ts b/servers/lightspeed/src/main.ts
new file mode 100644
index 0000000..ecb6b08
--- /dev/null
+++ b/servers/lightspeed/src/main.ts
@@ -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);
+});
diff --git a/servers/lightspeed/src/server.ts b/servers/lightspeed/src/server.ts
new file mode 100644
index 0000000..d6d2f50
--- /dev/null
+++ b/servers/lightspeed/src/server.ts
@@ -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 = {};
+
+ 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 {
+ const transport = new StdioServerTransport();
+ await this.server.connect(transport);
+ console.error('Lightspeed MCP Server running on stdio');
+ }
+}
diff --git a/servers/lightspeed/src/tools/categories-tools.ts b/servers/lightspeed/src/tools/categories-tools.ts
new file mode 100644
index 0000000..fca9c43
--- /dev/null
+++ b/servers/lightspeed/src/tools/categories-tools.ts
@@ -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', 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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/categories.ts b/servers/lightspeed/src/tools/categories.ts
new file mode 100644
index 0000000..379e295
--- /dev/null
+++ b/servers/lightspeed/src/tools/categories.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/customers.ts b/servers/lightspeed/src/tools/customers.ts
new file mode 100644
index 0000000..31e5243
--- /dev/null
+++ b/servers/lightspeed/src/tools/customers.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/discounts-tools.ts b/servers/lightspeed/src/tools/discounts-tools.ts
new file mode 100644
index 0000000..c3ee0d5
--- /dev/null
+++ b/servers/lightspeed/src/tools/discounts-tools.ts
@@ -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', 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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/discounts.ts b/servers/lightspeed/src/tools/discounts.ts
new file mode 100644
index 0000000..3b92d87
--- /dev/null
+++ b/servers/lightspeed/src/tools/discounts.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/employees-tools.ts b/servers/lightspeed/src/tools/employees-tools.ts
new file mode 100644
index 0000000..8f72b50
--- /dev/null
+++ b/servers/lightspeed/src/tools/employees-tools.ts
@@ -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', 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', 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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/employees.ts b/servers/lightspeed/src/tools/employees.ts
new file mode 100644
index 0000000..1aec139
--- /dev/null
+++ b/servers/lightspeed/src/tools/employees.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/inventory-tools.ts b/servers/lightspeed/src/tools/inventory-tools.ts
new file mode 100644
index 0000000..23d460a
--- /dev/null
+++ b/servers/lightspeed/src/tools/inventory-tools.ts
@@ -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(
+ `/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('/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',
+ 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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/inventory.ts b/servers/lightspeed/src/tools/inventory.ts
new file mode 100644
index 0000000..a521441
--- /dev/null
+++ b/servers/lightspeed/src/tools/inventory.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/loyalty.ts b/servers/lightspeed/src/tools/loyalty.ts
new file mode 100644
index 0000000..83d96dc
--- /dev/null
+++ b/servers/lightspeed/src/tools/loyalty.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/orders.ts b/servers/lightspeed/src/tools/orders.ts
new file mode 100644
index 0000000..67726f7
--- /dev/null
+++ b/servers/lightspeed/src/tools/orders.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/products.ts b/servers/lightspeed/src/tools/products.ts
new file mode 100644
index 0000000..ee9dd76
--- /dev/null
+++ b/servers/lightspeed/src/tools/products.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/registers-tools.ts b/servers/lightspeed/src/tools/registers-tools.ts
new file mode 100644
index 0000000..509faa7
--- /dev/null
+++ b/servers/lightspeed/src/tools/registers-tools.ts
@@ -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', 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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/reporting-tools.ts b/servers/lightspeed/src/tools/reporting-tools.ts
new file mode 100644
index 0000000..2079b96
--- /dev/null
+++ b/servers/lightspeed/src/tools/reporting-tools.ts
@@ -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(
+ `/Shop/${args.shopId}/ItemShop`,
+ 'ItemShop',
+ 1000
+ );
+ const items = await client.getAll('/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('/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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/tools/reporting.ts b/servers/lightspeed/src/tools/reporting.ts
new file mode 100644
index 0000000..f655f93
--- /dev/null
+++ b/servers/lightspeed/src/tools/reporting.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/sales.ts b/servers/lightspeed/src/tools/sales.ts
new file mode 100644
index 0000000..ddc4388
--- /dev/null
+++ b/servers/lightspeed/src/tools/sales.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/shops.ts b/servers/lightspeed/src/tools/shops.ts
new file mode 100644
index 0000000..bd3ba03
--- /dev/null
+++ b/servers/lightspeed/src/tools/shops.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/suppliers.ts b/servers/lightspeed/src/tools/suppliers.ts
new file mode 100644
index 0000000..eb5c961
--- /dev/null
+++ b/servers/lightspeed/src/tools/suppliers.ts
@@ -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) }] };
+ }
+ }
+ ];
+}
diff --git a/servers/lightspeed/src/tools/taxes-tools.ts b/servers/lightspeed/src/tools/taxes-tools.ts
new file mode 100644
index 0000000..4e06c35
--- /dev/null
+++ b/servers/lightspeed/src/tools/taxes-tools.ts
@@ -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('/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,
+ };
+ }
+ },
+ },
+ };
+}
diff --git a/servers/lightspeed/src/types/index.ts b/servers/lightspeed/src/types/index.ts
new file mode 100644
index 0000000..c7be680
--- /dev/null
+++ b/servers/lightspeed/src/types/index.ts
@@ -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 = {
+ success: true;
+ data: T;
+} | {
+ success: false;
+ error: string;
+ details?: any;
+};
diff --git a/servers/lightspeed/tsconfig.json b/servers/lightspeed/tsconfig.json
index 156b6d5..b534769 100644
--- a/servers/lightspeed/tsconfig.json
+++ b/servers/lightspeed/tsconfig.json
@@ -16,5 +16,5 @@
"sourceMap": true
},
"include": ["src/**/*"],
- "exclude": ["node_modules", "dist"]
+ "exclude": ["node_modules", "dist", "src/ui"]
}
diff --git a/servers/servicetitan/src/ui/react-app/call-tracking.tsx b/servers/servicetitan/src/ui/react-app/call-tracking.tsx
new file mode 100644
index 0000000..582ca22
--- /dev/null
+++ b/servers/servicetitan/src/ui/react-app/call-tracking.tsx
@@ -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([
+ { 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 (
+
+
+ {/* Header */}
+
+
📞 Call Tracking
+
Monitor and analyze customer calls
+
+
+ {/* Stats */}
+
+
+
Total Calls
+
{stats.totalCalls}
+
Today
+
+
+
+
Jobs Booked
+
{stats.booked}
+
{conversionRate}% conversion
+
+
+
+
Estimated Value
+
${stats.totalValue.toLocaleString()}
+
+
+
+
Avg Duration
+
{stats.avgDuration.toFixed(1)} min
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+ {/* Calls Table */}
+
+
+
+
+ | Time |
+ Caller |
+ Phone |
+ Duration |
+ Source |
+ Outcome |
+ Est. Value |
+
+
+
+ {filteredCalls.map((call) => (
+
+ | {call.timestamp} |
+ {call.caller} |
+ {call.phone} |
+ {call.duration} |
+
+
+ {call.source}
+
+ |
+
+
+ {call.outcome}
+
+ |
+
+ {call.estimatedValue ? `$${call.estimatedValue.toLocaleString()}` : '-'}
+ |
+
+ ))}
+
+
+
+
+
+ Showing {filteredCalls.length} of {calls.length} calls
+
+
+
+ );
+}
diff --git a/servers/servicetitan/src/ui/react-app/lead-source-analytics.tsx b/servers/servicetitan/src/ui/react-app/lead-source-analytics.tsx
new file mode 100644
index 0000000..f768dcf
--- /dev/null
+++ b/servers/servicetitan/src/ui/react-app/lead-source-analytics.tsx
@@ -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([
+ { 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 (
+
+
+ {/* Header */}
+
+
📈 Lead Source Analytics
+
Track marketing performance by lead source
+
+
+ {/* Summary Stats */}
+
+
+
Total Leads
+
{totalLeads}
+
This month
+
+
+
+
Conversions
+
{totalConversions}
+
{overallConversion}% rate
+
+
+
+
Total Revenue
+
${totalRevenue.toLocaleString()}
+
↑ 18%
+
+
+
+
Overall ROI
+
+ {typeof overallROI === 'number' ? `${overallROI}%` : overallROI}
+
+
Cost: ${totalCost.toLocaleString()}
+
+
+
+ {/* Sort Controls */}
+
+
+
Sort by:
+
+ {(['leads', 'conversions', 'revenue', 'roi'] as const).map((option) => (
+
+ ))}
+
+
+
+
+ {/* Lead Sources Grid */}
+
+ {sortedSources.map((source, idx) => (
+
+
+
{source.name}
+ = 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
+
+
+
+
+
+
Leads
+
{source.leads}
+
+
+
Conversions
+
{source.conversions}
+
+
+
Revenue
+
${source.revenue.toLocaleString()}
+
+
+
Cost
+
+ {source.cost > 0 ? `$${source.cost.toLocaleString()}` : 'Free'}
+
+
+
+
+
+
+ ROI
+
+ {source.roi === Infinity ? '∞' : `${source.roi}%`}
+
+
+
+
+ ))}
+
+
+ {/* Detailed Table */}
+
+
+
Performance Breakdown
+
+
+
+
+ | Source |
+ Leads |
+ Conversions |
+ Conv. Rate |
+ Revenue |
+ Cost |
+ ROI |
+
+
+
+ {sortedSources.map((source, idx) => (
+
+ | {source.name} |
+ {source.leads} |
+ {source.conversions} |
+ {source.conversionRate.toFixed(1)}% |
+ ${source.revenue.toLocaleString()} |
+
+ {source.cost > 0 ? `$${source.cost.toLocaleString()}` : '-'}
+ |
+
+ {source.roi === Infinity ? '∞' : `${source.roi}%`}
+ |
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/servers/servicetitan/src/ui/react-app/performance-metrics.tsx b/servers/servicetitan/src/ui/react-app/performance-metrics.tsx
new file mode 100644
index 0000000..94a153d
--- /dev/null
+++ b/servers/servicetitan/src/ui/react-app/performance-metrics.tsx
@@ -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([
+ { 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 (
+
+
+ {/* Header */}
+
+
+
+
📊 Performance Metrics
+
Monitor key business performance indicators
+
+
+ {(['today', 'week', 'month'] as const).map((range) => (
+
+ ))}
+
+
+
+
+ {/* Summary Stats */}
+
+
+
Metrics On Target
+
+ {metrics.filter(m => {
+ if (m.unit === 'min' || m.name.includes('Callback')) {
+ return m.value <= m.target;
+ }
+ return m.value >= m.target;
+ }).length}/{metrics.length}
+
+
+
+
+
Improving Metrics
+
+ {metrics.filter(m => {
+ if (m.unit === 'min' || m.name.includes('Callback')) {
+ return m.trend === 'down';
+ }
+ return m.trend === 'up';
+ }).length}
+
+
+
+
+
Needs Attention
+
+ {metrics.filter(m => {
+ if (m.unit === 'min' || m.name.includes('Callback')) {
+ return m.value > m.target;
+ }
+ return m.value < m.target;
+ }).length}
+
+
+
+
+ {/* Metrics Grid */}
+
+ {metrics.map((metric, idx) => (
+
+
+
{metric.name}
+
+ {getTrendIcon(metric.trend)} {Math.abs(metric.change)}{metric.unit === '%' ? '%' : ''}
+
+
+
+
+
+
+ {metric.unit === '$' && '$'}
+ {metric.value}
+ {metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
+
+
+ Target: {metric.unit === '$' && '$'}{metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
+
+
+
+
+
+
+
= metric.target ? 'bg-green-500' : 'bg-amber-500')
+ }`}
+ style={{
+ width: `${Math.min((metric.value / metric.target) * 100, 100)}%`
+ }}
+ />
+
+
+ 0
+ {metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
+
+
+
+ ))}
+
+
+ {/* Performance Table */}
+
+
+
Detailed Breakdown
+
+
+
+
+ | Metric |
+ Current |
+ Target |
+ Trend |
+ Status |
+
+
+
+ {metrics.map((metric, idx) => {
+ const onTarget = (metric.unit === 'min' || metric.name.includes('Callback'))
+ ? metric.value <= metric.target
+ : metric.value >= metric.target;
+
+ return (
+
+ | {metric.name} |
+
+ {metric.unit === '$' && '$'}{metric.value}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
+ |
+
+ {metric.unit === '$' && '$'}{metric.target}{metric.unit !== '$' && metric.unit !== 'jobs' && metric.unit}
+ |
+
+ {getTrendIcon(metric.trend)} {Math.abs(metric.change)}{metric.unit === '%' ? '%' : ''}
+ |
+
+
+ {onTarget ? 'On Target' : 'Below Target'}
+
+ |
+
+ );
+ })}
+
+
+
+
+
+ );
+}
diff --git a/servers/servicetitan/src/ui/react-app/schedule-calendar.tsx b/servers/servicetitan/src/ui/react-app/schedule-calendar.tsx
new file mode 100644
index 0000000..4ebd7f9
--- /dev/null
+++ b/servers/servicetitan/src/ui/react-app/schedule-calendar.tsx
@@ -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
([
+ { 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 (
+
+
+ {/* Header */}
+
+
+
+
📅 Schedule Calendar
+
View and manage appointments
+
+
+
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"
+ />
+
+
+
+
+
+
+
+
+
+ {/* Stats */}
+
+
+
Today's Appointments
+
{appointments.length}
+
+
+
+
Confirmed
+
+ {appointments.filter(a => a.status === 'confirmed').length}
+
+
+
+
+
Completed
+
+ {appointments.filter(a => a.status === 'completed').length}
+
+
+
+
+
Technicians Active
+
{technicians.length}
+
+
+
+ {/* Calendar View */}
+
+
+
Schedule for {selectedDate}
+
+
+
+
+ {/* Header */}
+
+
TIME
+ {technicians.map((tech, idx) => (
+
+ ))}
+
+
+ {/* Time Grid */}
+
+ {timeSlots.map((time, idx) => (
+
+
{time}
+ {technicians.map((tech, techIdx) => (
+
+ {/* 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 (
+
+
{apt.jobNumber}
+
{apt.customer}
+
{apt.type}
+
{apt.startTime} - {apt.endTime}
+
+ );
+ })}
+
+ ))}
+
+ ))}
+
+
+
+
+
+ {/* Legend */}
+
+
+
+ );
+}
diff --git a/servers/squarespace/package.json b/servers/squarespace/package.json
index 61a90cb..9beb881 100644
--- a/servers/squarespace/package.json
+++ b/servers/squarespace/package.json
@@ -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"
}
}
diff --git a/servers/squarespace/src/apps/blog-manager.ts b/servers/squarespace/src/apps/blog-manager.ts
new file mode 100644
index 0000000..eacc7ed
--- /dev/null
+++ b/servers/squarespace/src/apps/blog-manager.ts
@@ -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 `
+
+
+
+
+
+ Blog Manager - Squarespace
+
+
+
+
+
✍️ Blog Manager
+
+
+
+
+
+
+
+
+ ${posts.map((post: any) => `
+
+ ${post.featuredImage?.url ? `
+

+ ` : '
'}
+
+
${post.title}
+ ${post.excerpt ? `
${post.excerpt}
` : ''}
+
+ By ${post.author.displayName} • ${post.publishedOn ? new Date(post.publishedOn).toLocaleDateString() : 'Not published'}
+
+ ${post.tags?.length ? `
+
+ ${post.tags.map((tag: string) => `${tag}`).join('')}
+
+ ` : ''}
+
+ ${post.isPublished ? 'Published' : 'Draft'}
+
+
+
+
+
+
+
+
+ `).join('')}
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/customer-grid.ts b/servers/squarespace/src/apps/customer-grid.ts
new file mode 100644
index 0000000..73a8e5f
--- /dev/null
+++ b/servers/squarespace/src/apps/customer-grid.ts
@@ -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 `
+
+
+
+
+
+ Customers - Squarespace
+
+
+
+
+
👥 Customers
+
+
+
+
+
+
+ | Customer |
+ Email |
+ Orders |
+ Total Spent |
+ Member Since |
+ Actions |
+
+
+
+ ${customers.map((customer: any) => `
+
+ |
+ ${customer.firstName || ''} ${customer.lastName || ''}
+ |
+ ${customer.email} |
+ ${customer.orderCount || 0} |
+ ${customer.totalSpent ? `${customer.totalSpent.currency} ${customer.totalSpent.value}` : '$0.00'} |
+ ${new Date(customer.createdOn).toLocaleDateString()} |
+ |
+
+ `).join('')}
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/form-submissions.ts b/servers/squarespace/src/apps/form-submissions.ts
new file mode 100644
index 0000000..148c5f1
--- /dev/null
+++ b/servers/squarespace/src/apps/form-submissions.ts
@@ -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 `
+
+
+
+
+
+ Form Submissions - Squarespace
+
+
+
+
+
📝 Form Submissions
+
+
+ ${submissions.length === 0 ? `
+
+
No form submissions yet
+
+ ` : submissions.map((submission: any) => `
+
+
+
+ ${submission.fields.map((field: any) => `
+
+
${field.name}:
+
${field.value}
+
+ `).join('')}
+
+
+ `).join('')}
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/inventory-tracker.ts b/servers/squarespace/src/apps/inventory-tracker.ts
new file mode 100644
index 0000000..9922363
--- /dev/null
+++ b/servers/squarespace/src/apps/inventory-tracker.ts
@@ -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 `
+
+
+
+
+
+ Inventory Tracker - Squarespace
+
+
+
+
+
📊 Inventory Tracker
+
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/order-dashboard.ts b/servers/squarespace/src/apps/order-dashboard.ts
new file mode 100644
index 0000000..ffa8218
--- /dev/null
+++ b/servers/squarespace/src/apps/order-dashboard.ts
@@ -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 `
+
+
+
+
+
+ Order Dashboard - Squarespace
+
+
+
+
+
📦 Order Dashboard
+
+
+
+
Total Orders
+
${stats.totalOrders || 0}
+
+${stats.ordersChange || 0}% vs last period
+
+
+
Total Revenue
+
$${stats.totalRevenue || '0.00'}
+
+${stats.revenueChange || 0}% vs last period
+
+
+
Avg Order Value
+
$${stats.avgOrderValue || '0.00'}
+
+
+
Pending Orders
+
${stats.pendingOrders || 0}
+
+
+
+
+
+
+
+
+ | Order # |
+ Customer |
+ Date |
+ Total |
+ Status |
+ Actions |
+
+
+
+ ${orders.map((order: any) => `
+
+ | #${order.orderNumber} |
+ ${order.customerEmail} |
+ ${new Date(order.createdOn).toLocaleDateString()} |
+ ${order.grandTotal.currency} ${order.grandTotal.value} |
+ ${order.fulfillmentStatus} |
+ |
+
+ `).join('')}
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/order-detail.ts b/servers/squarespace/src/apps/order-detail.ts
new file mode 100644
index 0000000..9f4e20c
--- /dev/null
+++ b/servers/squarespace/src/apps/order-detail.ts
@@ -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 `
+
+
+
+
+
+ Order #${order.orderNumber} - Squarespace
+
+
+
+
+
+
+
+
+
+
Order Items
+ ${order.lineItems.map((item: any) => `
+
+ ${item.imageUrl ? `

` : '
'}
+
+
${item.productName}
+
Quantity: ${item.quantity}${item.sku ? ` • SKU: ${item.sku}` : ''}
+ ${item.customizations ? `
${item.customizations.map((c: any) => `${c.label}: ${c.value}`).join(', ')}
` : ''}
+
+
${item.unitPricePaid.currency} ${item.unitPricePaid.value}
+
+ `).join('')}
+
+
+
+
Order Summary
+
+ Subtotal
+ ${order.subtotal.currency} ${order.subtotal.value}
+
+
+ Shipping
+ ${order.shippingTotal.currency} ${order.shippingTotal.value}
+
+
+ Tax
+ ${order.taxTotal.currency} ${order.taxTotal.value}
+
+ ${parseFloat(order.discountTotal.value) > 0 ? `
+
+ Discount
+ -${order.discountTotal.currency} ${order.discountTotal.value}
+
+ ` : ''}
+
+ Total
+ ${order.grandTotal.currency} ${order.grandTotal.value}
+
+
+
+
+
+
+
Customer
+
+ Email
+ ${order.customerEmail}
+
+
+ Order Date
+ ${new Date(order.createdOn).toLocaleString()}
+
+
+
+
+
Shipping Address
+ ${order.shippingAddress ? `
+
${order.shippingAddress.firstName} ${order.shippingAddress.lastName}
+
${order.shippingAddress.address1}
+ ${order.shippingAddress.address2 ? `
${order.shippingAddress.address2}
` : ''}
+
${order.shippingAddress.city}, ${order.shippingAddress.state || ''} ${order.shippingAddress.postalCode}
+
${order.shippingAddress.countryCode}
+ ${order.shippingAddress.phone ? `
Phone: ${order.shippingAddress.phone}
` : ''}
+ ` : '
No shipping address
'}
+
+
+
+
Billing Address
+
${order.billingAddress.firstName} ${order.billingAddress.lastName}
+
${order.billingAddress.address1}
+ ${order.billingAddress.address2 ? `
${order.billingAddress.address2}
` : ''}
+
${order.billingAddress.city}, ${order.billingAddress.state || ''} ${order.billingAddress.postalCode}
+
${order.billingAddress.countryCode}
+ ${order.billingAddress.phone ? `
Phone: ${order.billingAddress.phone}
` : ''}
+
+
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/product-dashboard.ts b/servers/squarespace/src/apps/product-dashboard.ts
new file mode 100644
index 0000000..085f813
--- /dev/null
+++ b/servers/squarespace/src/apps/product-dashboard.ts
@@ -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 `
+
+
+
+
+
+ Product Dashboard - Squarespace
+
+
+
+
+
🛍️ Products
+
+
+
+
Total Products
+
${stats.totalProducts || 0}
+
+
+
Published
+
${stats.publishedProducts || 0}
+
+
+
On Sale
+
${stats.onSale || 0}
+
+
+
Low Stock
+
${stats.lowStock || 0}
+
+
+
+
+
+
+
+
+
+
+ ${products.map((product: any) => {
+ const firstVariant = product.variants?.[0];
+ const price = firstVariant?.pricing?.basePrice;
+ const image = product.images?.[0]?.url || '';
+
+ return `
+
+ ${image ? `

` : '
'}
+
+
${product.name}
+
${price ? `${price.currency} ${price.value}` : 'Price varies'}
+
+ ${product.isVisible ? 'Published' : 'Hidden'}
+ ${firstVariant?.onSale ? 'On Sale' : ''}
+ ${product.variants?.length > 1 ? `${product.variants.length} variants` : ''}
+
+
+
+ `;
+ }).join('')}
+
+
+
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/apps/site-analytics.ts b/servers/squarespace/src/apps/site-analytics.ts
new file mode 100644
index 0000000..9951a74
--- /dev/null
+++ b/servers/squarespace/src/apps/site-analytics.ts
@@ -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 `
+
+
+
+
+
+ Analytics - Squarespace
+
+
+
+
+
📊 Site Analytics
+
${traffic.startDate ? `${traffic.startDate} - ${traffic.endDate}` : 'Last 30 days'}
+
+
+
+
Page Views
+
${(traffic.pageViews || 0).toLocaleString()}
+
+
+
Unique Visitors
+
${(traffic.uniqueVisitors || 0).toLocaleString()}
+
+
+
Bounce Rate
+
${traffic.bounceRate ? `${traffic.bounceRate}%` : 'N/A'}
+
+
+
Avg. Session
+
${traffic.avgSessionDuration ? `${Math.round(traffic.avgSessionDuration / 60)}m` : 'N/A'}
+
+
+
+ ${popular?.pages?.length ? `
+
+
Popular Pages
+
+
+
+ | Page |
+ Views |
+ Popularity |
+
+
+
+ ${popular.pages.map((page: any, index: number) => {
+ const maxViews = popular.pages[0]?.pageViews || 1;
+ const width = (page.pageViews / maxViews) * 100;
+ return `
+
+ |
+ ${page.title}
+ ${page.url}
+ |
+ ${page.pageViews.toLocaleString()} |
+
+
+ |
+
+ `;
+ }).join('')}
+
+
+
+ ` : ''}
+
+ ${commerce.revenue ? `
+
+
Commerce Performance
+
+
+
Revenue
+
${commerce.revenue.currency} ${commerce.revenue.value}
+
+
+
Orders
+
${commerce.orderCount || 0}
+
+
+
Avg Order Value
+
${commerce.averageOrderValue.currency} ${commerce.averageOrderValue.value}
+
+
+
+ ` : ''}
+
+
+
+ `;
+ }
+};
diff --git a/servers/squarespace/src/clients/squarespace.ts b/servers/squarespace/src/clients/squarespace.ts
new file mode 100644
index 0000000..bcbbdec
--- /dev/null
+++ b/servers/squarespace/src/clients/squarespace.ts
@@ -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 {
+ const response = await this.client.get('/sites');
+ return response.data.sites || [];
+ }
+
+ async getSite(siteId: string): Promise {
+ const response = await this.client.get(`/sites/${siteId}`);
+ return response.data;
+ }
+
+ async updateSite(siteId: string, updates: Partial): Promise {
+ const response = await this.client.patch(`/sites/${siteId}`, updates);
+ return response.data;
+ }
+
+ // Page Management
+ async getPages(siteId: string, collectionId?: string): Promise {
+ 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 {
+ const response = await this.client.get(`/sites/${siteId}/pages/${pageId}`);
+ return response.data;
+ }
+
+ async createPage(siteId: string, collectionId: string, page: Partial): Promise {
+ const response = await this.client.post(
+ `/sites/${siteId}/collections/${collectionId}/pages`,
+ page
+ );
+ return response.data;
+ }
+
+ async updatePage(siteId: string, pageId: string, updates: Partial): Promise {
+ const response = await this.client.patch(`/sites/${siteId}/pages/${pageId}`, updates);
+ return response.data;
+ }
+
+ async deletePage(siteId: string, pageId: string): Promise {
+ 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 {
+ const response = await this.client.get(`/commerce/products/${productId}`);
+ return response.data;
+ }
+
+ async createProduct(product: Partial): Promise {
+ const response = await this.client.post('/commerce/products', product);
+ return response.data;
+ }
+
+ async updateProduct(productId: string, updates: Partial): Promise {
+ const response = await this.client.put(`/commerce/products/${productId}`, updates);
+ return response.data;
+ }
+
+ async deleteProduct(productId: string): Promise {
+ 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 {
+ const response = await this.client.get(`/commerce/orders/${orderId}`);
+ return response.data;
+ }
+
+ async updateOrderFulfillment(
+ orderId: string,
+ fulfillmentStatus: string,
+ shipmentDetails?: any
+ ): Promise {
+ const response = await this.client.post(`/commerce/orders/${orderId}/fulfillments`, {
+ fulfillmentStatus,
+ shipmentDetails,
+ });
+ return response.data;
+ }
+
+ // Inventory Management
+ async getInventory(productId: string, variantId?: string): Promise {
+ 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 {
+ 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 {
+ const response = await this.client.get(`/commerce/customers/${customerId}`);
+ return response.data;
+ }
+
+ // Form Submissions
+ async getFormSubmissions(siteId: string, formId?: string): Promise {
+ 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 {
+ 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 {
+ const response = await this.client.get(`/sites/${siteId}/analytics`, { params });
+ return response.data;
+ }
+
+ // SEO Settings
+ async getSEOSettings(siteId: string, pageId?: string): Promise {
+ 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,
+ pageId?: string
+ ): Promise {
+ 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 {
+ const response = await this.client.get(`/sites/${siteId}/blogs/${blogId}/posts/${postId}`);
+ return response.data;
+ }
+
+ async createBlogPost(siteId: string, blogId: string, post: Partial): Promise {
+ 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
+ ): Promise {
+ 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 {
+ await this.client.delete(`/sites/${siteId}/blogs/${blogId}/posts/${postId}`);
+ }
+
+ // Transaction Management
+ async getTransactions(orderId: string): Promise {
+ const response = await this.client.get(`/commerce/orders/${orderId}/transactions`);
+ return response.data.transactions || [];
+ }
+
+ async createRefund(orderId: string, amount: string, currency: string): Promise {
+ const response = await this.client.post(`/commerce/orders/${orderId}/refunds`, {
+ amount: { value: amount, currency },
+ });
+ return response.data;
+ }
+
+ // Webhook Management
+ async getWebhooks(siteId: string): Promise {
+ const response = await this.client.get(`/sites/${siteId}/webhooks`);
+ return response.data.webhooks || [];
+ }
+
+ async createWebhook(
+ siteId: string,
+ endpointUrl: string,
+ topics: string[]
+ ): Promise {
+ const response = await this.client.post(`/sites/${siteId}/webhooks`, {
+ endpointUrl,
+ topics,
+ });
+ return response.data;
+ }
+
+ async deleteWebhook(siteId: string, webhookId: string): Promise {
+ await this.client.delete(`/sites/${siteId}/webhooks/${webhookId}`);
+ }
+
+ // Collections
+ async getCollections(siteId: string): Promise {
+ const response = await this.client.get(`/sites/${siteId}/collections`);
+ return response.data.collections || [];
+ }
+
+ async getCollection(siteId: string, collectionId: string): Promise {
+ const response = await this.client.get(`/sites/${siteId}/collections/${collectionId}`);
+ return response.data;
+ }
+
+ // Domain Management
+ async getDomains(siteId: string): Promise {
+ const response = await this.client.get(`/sites/${siteId}/domains`);
+ return response.data.domains || [];
+ }
+}
diff --git a/servers/squarespace/src/index.ts b/servers/squarespace/src/index.ts
deleted file mode 100644
index baa6007..0000000
--- a/servers/squarespace/src/index.ts
+++ /dev/null
@@ -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);
diff --git a/servers/squarespace/src/tools/analytics-tools.ts b/servers/squarespace/src/tools/analytics-tools.ts
new file mode 100644
index 0000000..678eded
--- /dev/null
+++ b/servers/squarespace/src/tools/analytics-tools.ts
@@ -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 = {
+ startDate: args.startDate,
+ endDate: args.endDate,
+ period: args.period || 'day'
+ };
+
+ const traffic = await client.get(
+ '/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 = {
+ startDate: args.startDate,
+ endDate: args.endDate,
+ period: args.period || 'day'
+ };
+
+ const analytics = await client.get(
+ '/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 = {
+ startDate: args.startDate,
+ endDate: args.endDate,
+ limit: String(args.limit || 10)
+ };
+
+ const popular = await client.get(
+ '/1.0/analytics/popular',
+ queryParams
+ );
+
+ return {
+ content: [{
+ type: 'text',
+ text: JSON.stringify(popular, null, 2)
+ }]
+ };
+ }
+ }
+ ];
+}
diff --git a/servers/squarespace/src/tools/analytics.ts b/servers/squarespace/src/tools/analytics.ts
new file mode 100644
index 0000000..b220093
--- /dev/null
+++ b/servers/squarespace/src/tools/analytics.ts
@@ -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
+ ),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/collections-tools.ts b/servers/squarespace/src/tools/collections-tools.ts
new file mode 100644
index 0000000..bfce9ec
--- /dev/null
+++ b/servers/squarespace/src/tools/collections-tools.ts
@@ -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 = {};
+
+ if (args.type) queryParams.type = args.type;
+
+ const collections = await client.fetchAll(
+ '/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(
+ `/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 = {};
+
+ if (args.tag) queryParams.tag = args.tag;
+
+ const items = await client.fetchAll(
+ `/1.0/collections/${args.collectionId}/items`,
+ queryParams,
+ args.limit || 50
+ );
+
+ return {
+ content: [{
+ type: 'text',
+ text: JSON.stringify({ items, count: items.length }, null, 2)
+ }]
+ };
+ }
+ }
+ ];
+}
diff --git a/servers/squarespace/src/tools/customers.ts b/servers/squarespace/src/tools/customers.ts
new file mode 100644
index 0000000..4342690
--- /dev/null
+++ b/servers/squarespace/src/tools/customers.ts
@@ -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
+ ),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/forms-tools.ts b/servers/squarespace/src/tools/forms-tools.ts
new file mode 100644
index 0000000..3ce2ec2
--- /dev/null
+++ b/servers/squarespace/src/tools/forms-tools.ts
@@ -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 = {};
+
+ 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(
+ '/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(
+ `/1.0/forms/submissions/${args.submissionId}`
+ );
+
+ return {
+ content: [{
+ type: 'text',
+ text: JSON.stringify(submission, null, 2)
+ }]
+ };
+ }
+ }
+ ];
+}
diff --git a/servers/squarespace/src/tools/forms.ts b/servers/squarespace/src/tools/forms.ts
new file mode 100644
index 0000000..eaac4e0
--- /dev/null
+++ b/servers/squarespace/src/tools/forms.ts
@@ -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();
+ 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,
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/inventory.ts b/servers/squarespace/src/tools/inventory.ts
new file mode 100644
index 0000000..0de26c4
--- /dev/null
+++ b/servers/squarespace/src/tools/inventory.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/menu-tools.ts b/servers/squarespace/src/tools/menu-tools.ts
new file mode 100644
index 0000000..21a8a83
--- /dev/null
+++ b/servers/squarespace/src/tools/menu-tools.ts
@@ -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(
+ `/1.0/navigation/menus/${args.menuId}`
+ );
+
+ return {
+ content: [{
+ type: 'text',
+ text: JSON.stringify(menu, null, 2)
+ }]
+ };
+ }
+ }
+ ];
+}
diff --git a/servers/squarespace/src/tools/orders.ts b/servers/squarespace/src/tools/orders.ts
new file mode 100644
index 0000000..45a7504
--- /dev/null
+++ b/servers/squarespace/src/tools/orders.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/pages.ts b/servers/squarespace/src/tools/pages.ts
new file mode 100644
index 0000000..3b3a843
--- /dev/null
+++ b/servers/squarespace/src/tools/pages.ts
@@ -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`,
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/products.ts b/servers/squarespace/src/tools/products.ts
new file mode 100644
index 0000000..784d135
--- /dev/null
+++ b/servers/squarespace/src/tools/products.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/tools/settings-tools.ts b/servers/squarespace/src/tools/settings-tools.ts
new file mode 100644
index 0000000..1ca4102
--- /dev/null
+++ b/servers/squarespace/src/tools/settings-tools.ts
@@ -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('/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)
+ }]
+ };
+ }
+ }
+ ];
+}
diff --git a/servers/squarespace/src/tools/sites.ts b/servers/squarespace/src/tools/sites.ts
new file mode 100644
index 0000000..2599e2e
--- /dev/null
+++ b/servers/squarespace/src/tools/sites.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/squarespace/src/types/index.ts b/servers/squarespace/src/types/index.ts
new file mode 100644
index 0000000..3f992e2
--- /dev/null
+++ b/servers/squarespace/src/types/index.ts
@@ -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;
+};
diff --git a/servers/squarespace/tsconfig.json b/servers/squarespace/tsconfig.json
index de6431e..0995f70 100644
--- a/servers/squarespace/tsconfig.json
+++ b/servers/squarespace/tsconfig.json
@@ -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"]
}
diff --git a/servers/toast/package.json b/servers/toast/package.json
index 637c4e8..3369d31 100644
--- a/servers/toast/package.json
+++ b/servers/toast/package.json
@@ -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"
}
}
diff --git a/servers/toast/src/api-client.ts b/servers/toast/src/api-client.ts
new file mode 100644
index 0000000..d416ddb
--- /dev/null
+++ b/servers/toast/src/api-client.ts
@@ -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(
+ endpoint: string,
+ params?: Record,
+ config?: AxiosRequestConfig
+ ): Promise {
+ const response = await this.client.get(endpoint, {
+ params,
+ ...config,
+ });
+ return response.data;
+ }
+
+ // Generic POST request
+ async post(
+ endpoint: string,
+ data?: any,
+ config?: AxiosRequestConfig
+ ): Promise {
+ const response = await this.client.post(endpoint, data, config);
+ return response.data;
+ }
+
+ // Generic PUT request
+ async put(
+ endpoint: string,
+ data?: any,
+ config?: AxiosRequestConfig
+ ): Promise {
+ const response = await this.client.put(endpoint, data, config);
+ return response.data;
+ }
+
+ // Generic PATCH request
+ async patch(
+ endpoint: string,
+ data?: any,
+ config?: AxiosRequestConfig
+ ): Promise {
+ const response = await this.client.patch(endpoint, data, config);
+ return response.data;
+ }
+
+ // Generic DELETE request
+ async delete(
+ endpoint: string,
+ config?: AxiosRequestConfig
+ ): Promise {
+ const response = await this.client.delete(endpoint, config);
+ return response.data;
+ }
+
+ // Paginated GET request
+ async getPaginated(
+ endpoint: string,
+ pagination?: PaginationParams,
+ additionalParams?: Record
+ ): Promise> {
+ const params = {
+ page: pagination?.page || 1,
+ pageSize: pagination?.pageSize || 100,
+ ...additionalParams,
+ };
+
+ const response = await this.get(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(
+ endpoint: string,
+ pageSize: number = 100,
+ additionalParams?: Record
+ ): Promise {
+ const allData: T[] = [];
+ let page = 1;
+ let hasMore = true;
+
+ while (hasMore) {
+ const response = await this.getPaginated(
+ 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}`),
+ };
+}
diff --git a/servers/toast/src/apps/index.ts b/servers/toast/src/apps/index.ts
new file mode 100644
index 0000000..dbc372b
--- /dev/null
+++ b/servers/toast/src/apps/index.ts
@@ -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' },
+ ],
+ },
+ },
+];
diff --git a/servers/toast/src/index.ts b/servers/toast/src/index.ts
deleted file mode 100644
index 05b587e..0000000
--- a/servers/toast/src/index.ts
+++ /dev/null
@@ -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 {
- // 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) {
- 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 = {
- 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 = {};
- 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 = {
- 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 = {
- 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);
diff --git a/servers/toast/src/tools/cash-tools.ts b/servers/toast/src/tools/cash-tools.ts
new file mode 100644
index 0000000..4d71e91
--- /dev/null
+++ b/servers/toast/src/tools/cash-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/customers-tools.ts b/servers/toast/src/tools/customers-tools.ts
new file mode 100644
index 0000000..68622e9
--- /dev/null
+++ b/servers/toast/src/tools/customers-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/employees-tools.ts b/servers/toast/src/tools/employees-tools.ts
new file mode 100644
index 0000000..1bfba9f
--- /dev/null
+++ b/servers/toast/src/tools/employees-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/inventory-tools.ts b/servers/toast/src/tools/inventory-tools.ts
new file mode 100644
index 0000000..546fd0c
--- /dev/null
+++ b/servers/toast/src/tools/inventory-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/labor-tools.ts b/servers/toast/src/tools/labor-tools.ts
new file mode 100644
index 0000000..2e0ded4
--- /dev/null
+++ b/servers/toast/src/tools/labor-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/menus-tools.ts b/servers/toast/src/tools/menus-tools.ts
new file mode 100644
index 0000000..c528f5d
--- /dev/null
+++ b/servers/toast/src/tools/menus-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/orders-tools.ts b/servers/toast/src/tools/orders-tools.ts
new file mode 100644
index 0000000..14a467d
--- /dev/null
+++ b/servers/toast/src/tools/orders-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/payments-tools.ts b/servers/toast/src/tools/payments-tools.ts
new file mode 100644
index 0000000..d921aca
--- /dev/null
+++ b/servers/toast/src/tools/payments-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/reporting-tools.ts b/servers/toast/src/tools/reporting-tools.ts
new file mode 100644
index 0000000..09b28d0
--- /dev/null
+++ b/servers/toast/src/tools/reporting-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/tools/restaurant-tools.ts b/servers/toast/src/tools/restaurant-tools.ts
new file mode 100644
index 0000000..bfbddf8
--- /dev/null
+++ b/servers/toast/src/tools/restaurant-tools.ts
@@ -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),
+ },
+ ],
+ };
+ },
+ },
+ ];
+}
diff --git a/servers/toast/src/types/index.ts b/servers/toast/src/types/index.ts
new file mode 100644
index 0000000..e641138
--- /dev/null
+++ b/servers/toast/src/types/index.ts
@@ -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 {
+ 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;
+}
diff --git a/servers/toast/tsconfig.json b/servers/toast/tsconfig.json
index de6431e..156b6d5 100644
--- a/servers/toast/tsconfig.json
+++ b/servers/toast/tsconfig.json
@@ -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"]
diff --git a/servers/touchbistro/.gitignore b/servers/touchbistro/.gitignore
new file mode 100644
index 0000000..a2661e4
--- /dev/null
+++ b/servers/touchbistro/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+dist/
+*.log
+.env
+.DS_Store
+*.tsbuildinfo
diff --git a/servers/touchbistro/README.md b/servers/touchbistro/README.md
deleted file mode 100644
index dbb870a..0000000
--- a/servers/touchbistro/README.md
+++ /dev/null
@@ -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.
diff --git a/servers/touchbistro/package.json b/servers/touchbistro/package.json
index 835ca97..8dda115 100644
--- a/servers/touchbistro/package.json
+++ b/servers/touchbistro/package.json
@@ -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"
}
}
diff --git a/servers/touchbistro/src/api-client.ts b/servers/touchbistro/src/api-client.ts
new file mode 100644
index 0000000..7fc1665
--- /dev/null
+++ b/servers/touchbistro/src/api-client.ts
@@ -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(
+ endpoint: string,
+ options: RequestInit = {}
+ ): Promise {
+ 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(endpoint: string, params?: Record): Promise {
+ const queryString = params
+ ? '?' + new URLSearchParams(
+ Object.entries(params)
+ .filter(([_, v]) => v !== undefined && v !== null)
+ .map(([k, v]) => [k, String(v)])
+ ).toString()
+ : '';
+
+ return this.request(`${endpoint}${queryString}`, {
+ method: 'GET',
+ });
+ }
+
+ /**
+ * POST request
+ */
+ async post(endpoint: string, body?: unknown): Promise {
+ return this.request(endpoint, {
+ method: 'POST',
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ }
+
+ /**
+ * PUT request
+ */
+ async put(endpoint: string, body?: unknown): Promise {
+ return this.request(endpoint, {
+ method: 'PUT',
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ }
+
+ /**
+ * PATCH request
+ */
+ async patch(endpoint: string, body?: unknown): Promise {
+ return this.request(endpoint, {
+ method: 'PATCH',
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ }
+
+ /**
+ * DELETE request
+ */
+ async delete(endpoint: string): Promise {
+ return this.request(endpoint, {
+ method: 'DELETE',
+ });
+ }
+
+ /**
+ * Paginated GET request - automatically handles pagination
+ */
+ async getPaginated(
+ endpoint: string,
+ params: PaginationParams & Record = {}
+ ): Promise> {
+ 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(
+ endpoint: string,
+ params: Record = {},
+ maxPages = 10
+ ): Promise {
+ const allData: T[] = [];
+ let page = 1;
+ let hasMore = true;
+
+ while (hasMore && page <= maxPages) {
+ const response = await this.getPaginated(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(),
+ };
+ }
+ }
+}
diff --git a/servers/touchbistro/src/index.ts b/servers/touchbistro/src/index.ts
deleted file mode 100644
index 87154a6..0000000
--- a/servers/touchbistro/src/index.ts
+++ /dev/null
@@ -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);
diff --git a/servers/touchbistro/src/tools/customers-tools.ts b/servers/touchbistro/src/tools/customers-tools.ts
new file mode 100644
index 0000000..3aa4bf7
--- /dev/null
+++ b/servers/touchbistro/src/tools/customers-tools.ts
@@ -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) => {
+ const response = await client.getPaginated('/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(`/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('/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(`/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(
+ `/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(
+ `/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(
+ `/customers/${customerId}/loyalty/redeem`,
+ data
+ );
+ },
+ },
+ };
+}
diff --git a/servers/touchbistro/src/tools/inventory-tools.ts b/servers/touchbistro/src/tools/inventory-tools.ts
new file mode 100644
index 0000000..0bff5a4
--- /dev/null
+++ b/servers/touchbistro/src/tools/inventory-tools.ts
@@ -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) => {
+ const response = await client.getPaginated(
+ '/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(
+ `/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(
+ `/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(
+ `/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(
+ '/inventory/low-stock',
+ args
+ );
+ return {
+ items: response.data,
+ pagination: response.pagination,
+ };
+ },
+ },
+ };
+}
diff --git a/servers/touchbistro/src/tools/menus-tools.ts b/servers/touchbistro/src/tools/menus-tools.ts
new file mode 100644
index 0000000..c57933f
--- /dev/null
+++ b/servers/touchbistro/src/tools/menus-tools.ts
@@ -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) => {
+ const response = await client.getPaginated