mcpengine/servers/clover/src/tools/reports-tools.ts
Jake Shore 7ee40342c8 Clover: Complete MCP server with 50+ tools and 18 React apps
- API client with Clover REST API v3 integration (OAuth2 + API key auth)
- 50+ comprehensive tools across 10 categories:
  * Orders: list, get, create, update, delete, add/remove line items, discounts, payments, fire order
  * Inventory: items, categories, modifiers, stock management
  * Customers: CRUD, search, addresses, payment cards
  * Employees: CRUD, roles, shifts, clock in/out
  * Payments: list, get, refunds
  * Merchants: settings, devices, tender types
  * Discounts: CRUD operations
  * Taxes: CRUD, tax rates
  * Reports: sales summary, revenue by item/category, employee performance
  * Cash: cash drawer tracking and events

- 18 React MCP apps with full UI:
  * Order management: dashboard, detail, grid
  * Inventory: dashboard, detail, category manager
  * Customer: detail, grid
  * Employee: dashboard, schedule
  * Payment history
  * Analytics: sales dashboard, revenue by item, revenue by category
  * Configuration: discount manager, tax manager, device manager
  * Cash drawer

- Complete TypeScript types for Clover API
- Pagination support with automatic result fetching
- Comprehensive error handling
- Full README with examples and setup guide
2026-02-12 17:42:59 -05:00

257 lines
7.9 KiB
TypeScript

import { CloverClient } from '../clients/clover.js';
import {
CloverOrder,
CloverPayment,
SalesSummary,
RevenueByItem,
RevenueByCategory,
EmployeePerformance,
} from '../types/index.js';
export function createReportsTools(client: CloverClient) {
return {
clover_sales_summary: {
description: 'Get sales summary report for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
// Fetch orders and payments in date range
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
const payments = await client.fetchPaginated<CloverPayment>('/payments', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
});
const totalSales = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + p.amount, 0);
const totalRefunds = payments
.filter((p) => p.refunds && p.refunds.length > 0)
.reduce(
(sum, p) =>
sum + p.refunds!.reduce((rsum, r) => rsum + r.amount, 0),
0
);
const totalTax = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + (p.taxAmount || 0), 0);
const totalTips = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + (p.tipAmount || 0), 0);
const summary: SalesSummary = {
totalSales,
totalOrders: orders.length,
averageOrderValue: orders.length > 0 ? totalSales / orders.length : 0,
totalRefunds,
netSales: totalSales - totalRefunds,
totalTax,
totalTips,
};
return summary;
},
},
clover_revenue_by_item: {
description: 'Get revenue breakdown by item for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
const itemStats = new Map<
string,
{ name: string; quantity: number; revenue: number }
>();
orders.forEach((order) => {
order.lineItems?.forEach((lineItem) => {
const itemId = lineItem.item.id;
const existing = itemStats.get(itemId) || {
name: lineItem.name,
quantity: 0,
revenue: 0,
};
existing.quantity += lineItem.unitQty || 1;
existing.revenue += lineItem.price * (lineItem.unitQty || 1);
itemStats.set(itemId, existing);
});
});
const results: RevenueByItem[] = Array.from(itemStats.entries()).map(
([itemId, stats]) => ({
itemId,
itemName: stats.name,
quantitySold: stats.quantity,
totalRevenue: stats.revenue,
averagePrice: stats.revenue / stats.quantity,
})
);
return { items: results, count: results.length };
},
},
clover_revenue_by_category: {
description: 'Get revenue breakdown by category for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
// Fetch items with categories
const items = await client.fetchPaginated('/items', {
expand: 'categories',
});
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
// Build item-to-category map
const itemCategories = new Map<string, any[]>();
items.forEach((item: any) => {
if (item.categories) {
itemCategories.set(item.id, item.categories);
}
});
const categoryStats = new Map<
string,
{ name: string; itemCount: Set<string>; revenue: number }
>();
orders.forEach((order) => {
order.lineItems?.forEach((lineItem) => {
const categories = itemCategories.get(lineItem.item.id) || [];
categories.forEach((cat: any) => {
const existing = categoryStats.get(cat.id) || {
name: cat.name,
itemCount: new Set<string>(),
revenue: 0,
};
existing.itemCount.add(lineItem.item.id);
existing.revenue += lineItem.price * (lineItem.unitQty || 1);
categoryStats.set(cat.id, existing);
});
});
});
const results: RevenueByCategory[] = Array.from(
categoryStats.entries()
).map(([categoryId, stats]) => ({
categoryId,
categoryName: stats.name,
itemCount: stats.itemCount.size,
totalRevenue: stats.revenue,
}));
return { categories: results, count: results.length };
},
},
clover_employee_performance: {
description: 'Get employee performance report for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const payments = await client.fetchPaginated<CloverPayment>('/payments', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'employee',
});
const employeeStats = new Map<
string,
{ name: string; sales: number; orderCount: number }
>();
payments
.filter((p) => p.result === 'SUCCESS')
.forEach((payment) => {
const empId = payment.employee.id;
const existing = employeeStats.get(empId) || {
name: (payment.employee as any).name || 'Unknown',
sales: 0,
orderCount: 0,
};
existing.sales += payment.amount;
existing.orderCount += 1;
employeeStats.set(empId, existing);
});
const results: EmployeePerformance[] = Array.from(
employeeStats.entries()
).map(([employeeId, stats]) => ({
employeeId,
employeeName: stats.name,
totalSales: stats.sales,
orderCount: stats.orderCount,
averageOrderValue: stats.sales / stats.orderCount,
}));
return { employees: results, count: results.length };
},
},
};
}