Jake Shore a78d044005 housecall-pro: Complete MCP server with 112 tools and 15 React apps
- 112 MCP tools across 17 domains (jobs, customers, estimates, invoices, payments, employees, scheduling, dispatch, tags, notifications, reviews, reporting, price book, leads, webhooks, time tracking, settings)
- 15 React apps with proper Vite build setup and dark theme
- Full API client with Bearer auth, pagination, error handling, rate limiting
- Complete TypeScript types for all entities
- TSC passes clean
- GHL-quality standard achieved
2026-02-12 17:48:10 -05:00

198 lines
5.8 KiB
TypeScript

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { CloverClient } from './clients/clover.js';
import { CloverConfig } from './types/index.js';
// Tool imports
import { createOrdersTools } from './tools/orders-tools.js';
import { createInventoryTools } from './tools/inventory-tools.js';
import { createCustomersTools } from './tools/customers-tools.js';
import { createEmployeesTools } from './tools/employees-tools.js';
import { createPaymentsTools } from './tools/payments-tools.js';
import { createMerchantsTools } from './tools/merchants-tools.js';
import { createDiscountsTools } from './tools/discounts-tools.js';
import { createTaxesTools } from './tools/taxes-tools.js';
import { createReportsTools } from './tools/reports-tools.js';
import { createCashTools } from './tools/cash-tools.js';
import { createLineItemsTools } from './tools/line-items-tools.js';
import { createRefundsTools } from './tools/refunds-tools.js';
import { createCategoriesTools } from './tools/categories-tools.js';
import { createModifiersTools } from './tools/modifiers-tools.js';
import { createTipsTools } from './tools/tips-tools.js';
import { createShiftsTools } from './tools/shifts-tools.js';
import { createDevicesTools } from './tools/devices-tools.js';
import { createAppsTools } from './tools/apps-tools.js';
// React app imports
import { readFileSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class CloverServer {
private server: Server;
private client: CloverClient;
private tools: Map<string, any>;
private apps: Map<string, string>;
constructor(config: CloverConfig) {
this.server = new Server(
{
name: 'clover-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.client = new CloverClient(config);
this.tools = new Map();
this.apps = new Map();
this.initializeTools();
this.loadApps();
this.setupHandlers();
}
private initializeTools() {
const toolGroups = [
createOrdersTools(this.client),
createInventoryTools(this.client),
createCustomersTools(this.client),
createEmployeesTools(this.client),
createPaymentsTools(this.client),
createMerchantsTools(this.client),
createDiscountsTools(this.client),
createTaxesTools(this.client),
createReportsTools(this.client),
createCashTools(this.client),
createLineItemsTools(this.client),
createRefundsTools(this.client),
createCategoriesTools(this.client),
createModifiersTools(this.client),
createTipsTools(this.client),
createShiftsTools(this.client),
createDevicesTools(this.client),
createAppsTools(this.client),
];
toolGroups.forEach((group) => {
Object.entries(group).forEach(([name, tool]) => {
this.tools.set(name, tool);
});
});
}
private loadApps() {
const appsDir = join(__dirname, 'ui', 'react-app');
try {
const files = readdirSync(appsDir).filter((f) => f.endsWith('.tsx'));
files.forEach((file) => {
const appName = file.replace('.tsx', '');
const content = readFileSync(join(appsDir, file), 'utf-8');
this.apps.set(appName, content);
});
} catch (error) {
console.warn('Could not load React apps:', error);
}
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = Array.from(this.tools.entries()).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await tool.handler(args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = Array.from(this.apps.keys()).map((appName) => ({
uri: `clover://app/${appName}`,
name: appName.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
mimeType: 'text/tsx',
description: `Clover ${appName.replace(/-/g, ' ')} React app`,
}));
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^clover:\/\/app\/(.+)$/);
if (!match) {
throw new Error(`Invalid resource URI: ${uri}`);
}
const appName = match[1];
const appCode = this.apps.get(appName);
if (!appCode) {
throw new Error(`App not found: ${appName}`);
}
return {
contents: [
{
uri,
mimeType: 'text/tsx',
text: appCode,
},
],
};
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}