- READMEs added: asana, close, freshdesk, google-console, gusto, square - main.ts + server.ts (lazy loading): activecampaign, clickup, klaviyo, mailchimp, pipedrive, trello, touchbistro, closebot, close, google-console - All 13 compile with 0 TSC errors
226 lines
6.0 KiB
TypeScript
226 lines
6.0 KiB
TypeScript
/**
|
|
* TouchBistro MCP Server - Server Class with Lazy Tool Loading
|
|
*/
|
|
|
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
import { z } from 'zod';
|
|
import type { TouchBistroApiClient } from './lib/api-client.js';
|
|
|
|
// Type definition for tool modules
|
|
interface ToolDefinition {
|
|
description: string;
|
|
parameters: z.ZodTypeAny;
|
|
handler: (args: any, client: TouchBistroApiClient) => Promise<any>;
|
|
}
|
|
|
|
type ToolModule = Record<string, ToolDefinition>;
|
|
|
|
export class TouchBistroMCPServer {
|
|
private server: Server;
|
|
private client: TouchBistroApiClient;
|
|
private toolModules: Map<string, () => Promise<ToolModule>>;
|
|
private allToolDefinitions: Record<string, ToolDefinition> = {};
|
|
|
|
constructor(client: TouchBistroApiClient) {
|
|
this.client = client;
|
|
this.toolModules = new Map();
|
|
|
|
this.server = new Server(
|
|
{
|
|
name: 'touchbistro-mcp-server',
|
|
version: '1.0.0',
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
this.setupToolModules();
|
|
this.setupHandlers();
|
|
}
|
|
|
|
/**
|
|
* Register tool modules for lazy loading
|
|
*/
|
|
private setupToolModules(): void {
|
|
this.toolModules.set('orders', async () => {
|
|
const module = await import('./tools/orders.js');
|
|
return module.orderTools;
|
|
});
|
|
|
|
this.toolModules.set('menu', async () => {
|
|
const module = await import('./tools/menu.js');
|
|
return module.menuTools;
|
|
});
|
|
|
|
this.toolModules.set('tables', async () => {
|
|
const module = await import('./tools/tables.js');
|
|
return module.tableTools;
|
|
});
|
|
|
|
this.toolModules.set('staff', async () => {
|
|
const module = await import('./tools/staff.js');
|
|
return module.staffTools;
|
|
});
|
|
|
|
this.toolModules.set('customers', async () => {
|
|
const module = await import('./tools/customers.js');
|
|
return module.customerTools;
|
|
});
|
|
|
|
this.toolModules.set('reservations', async () => {
|
|
const module = await import('./tools/reservations.js');
|
|
return module.reservationTools;
|
|
});
|
|
|
|
this.toolModules.set('inventory', async () => {
|
|
const module = await import('./tools/inventory.js');
|
|
return module.inventoryTools;
|
|
});
|
|
|
|
this.toolModules.set('payments', async () => {
|
|
const module = await import('./tools/payments.js');
|
|
return module.paymentTools;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Load all tools from registered modules
|
|
*/
|
|
private async loadAllTools(): Promise<void> {
|
|
const toolModules = await Promise.all(
|
|
Array.from(this.toolModules.values()).map(loader => loader())
|
|
);
|
|
|
|
for (const toolModule of toolModules) {
|
|
Object.assign(this.allToolDefinitions, toolModule);
|
|
}
|
|
|
|
console.error(`Loaded ${Object.keys(this.allToolDefinitions).length} TouchBistro tools`);
|
|
}
|
|
|
|
/**
|
|
* Convert Zod schema to JSON Schema for MCP
|
|
*/
|
|
private zodToJsonSchema(schema: z.ZodTypeAny): any {
|
|
if (schema instanceof z.ZodObject) {
|
|
const shape = schema.shape;
|
|
const properties: any = {};
|
|
const required: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(shape)) {
|
|
properties[key] = this.zodToJsonSchema(value as z.ZodTypeAny);
|
|
if (!(value instanceof z.ZodOptional)) {
|
|
required.push(key);
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: 'object',
|
|
properties,
|
|
...(required.length > 0 ? { required } : {}),
|
|
};
|
|
}
|
|
|
|
if (schema instanceof z.ZodString) {
|
|
const base: any = { type: 'string' };
|
|
if (schema.description) base.description = schema.description;
|
|
return base;
|
|
}
|
|
|
|
if (schema instanceof z.ZodNumber) {
|
|
const base: any = { type: 'number' };
|
|
if (schema.description) base.description = schema.description;
|
|
return base;
|
|
}
|
|
|
|
if (schema instanceof z.ZodBoolean) {
|
|
const base: any = { type: 'boolean' };
|
|
if (schema.description) base.description = schema.description;
|
|
return base;
|
|
}
|
|
|
|
if (schema instanceof z.ZodArray) {
|
|
return {
|
|
type: 'array',
|
|
items: this.zodToJsonSchema(schema.element),
|
|
...(schema.description ? { description: schema.description } : {}),
|
|
};
|
|
}
|
|
|
|
if (schema instanceof z.ZodEnum) {
|
|
return {
|
|
type: 'string',
|
|
enum: schema.options,
|
|
...(schema.description ? { description: schema.description } : {}),
|
|
};
|
|
}
|
|
|
|
if (schema instanceof z.ZodOptional) {
|
|
return this.zodToJsonSchema(schema.unwrap());
|
|
}
|
|
|
|
return { type: 'string' };
|
|
}
|
|
|
|
/**
|
|
* Setup MCP protocol handlers
|
|
*/
|
|
private setupHandlers(): void {
|
|
// List tools handler
|
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
if (Object.keys(this.allToolDefinitions).length === 0) {
|
|
await this.loadAllTools();
|
|
}
|
|
|
|
const mcpTools = Object.entries(this.allToolDefinitions).map(([name, tool]) => ({
|
|
name,
|
|
description: tool.description,
|
|
inputSchema: this.zodToJsonSchema(tool.parameters),
|
|
}));
|
|
|
|
return {
|
|
tools: mcpTools,
|
|
};
|
|
});
|
|
|
|
// Call tool handler
|
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
if (Object.keys(this.allToolDefinitions).length === 0) {
|
|
await this.loadAllTools();
|
|
}
|
|
|
|
const { name, arguments: args } = request.params;
|
|
const tool = this.allToolDefinitions[name as keyof typeof this.allToolDefinitions];
|
|
|
|
if (!tool) {
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
|
|
try {
|
|
const validatedArgs = tool.parameters.parse(args);
|
|
const result = await tool.handler(validatedArgs, this.client);
|
|
return result;
|
|
} catch (error: any) {
|
|
if (error instanceof z.ZodError) {
|
|
throw new Error(`Invalid parameters: ${error.message}`);
|
|
}
|
|
throw new Error(`Tool execution failed: ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start the server with the given transport
|
|
*/
|
|
async start(transport: any): Promise<void> {
|
|
await this.server.connect(transport);
|
|
}
|
|
}
|