diff --git a/servers/brevo/src/api-client.ts b/servers/brevo/src/api-client.ts new file mode 100644 index 0000000..1a60f0d --- /dev/null +++ b/servers/brevo/src/api-client.ts @@ -0,0 +1,789 @@ +/** + * Brevo API Client + * API v3: https://api.brevo.com/v3/ + */ + +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + BrevoConfig, + Contact, + CreateContactParams, + UpdateContactParams, + ContactList, + EmailCampaign, + CreateEmailCampaignParams, + SendSmtpEmail, + SendSmtpEmailResponse, + SendTransacSms, + SendTransacSmsResponse, + SmsCampaign, + CreateSmsCampaignParams, + EmailTemplate, + CreateEmailTemplateParams, + Sender, + CreateSenderParams, + Automation, + Deal, + CreateDealParams, + Company, + CreateCompanyParams, + Task, + CreateTaskParams, + Webhook, + CreateWebhookParams, + AccountInfo, + EmailEventReport, + Folder, + CreateFolderParams, + Process, + CreateDoiContactParams, + PaginationParams, + DateRangeParams, +} from './types/index.js'; + +export class BrevoAPIClient { + private client: AxiosInstance; + + constructor(config: BrevoConfig) { + this.client = axios.create({ + baseURL: config.baseUrl || 'https://api.brevo.com/v3', + headers: { + 'api-key': config.apiKey, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + }); + + // Error interceptor + this.client.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + const errorMessage = error.response?.data + ? JSON.stringify(error.response.data) + : error.message; + throw new Error(`Brevo API Error: ${errorMessage}`); + } + ); + } + + // ============================================================================ + // CONTACTS + // ============================================================================ + + async getContact(identifier: string): Promise { + const { data } = await this.client.get(`/contacts/${encodeURIComponent(identifier)}`); + return data; + } + + async createContact(params: CreateContactParams): Promise<{ id: number }> { + const { data } = await this.client.post('/contacts', params); + return data; + } + + async updateContact(identifier: string, params: UpdateContactParams): Promise { + await this.client.put(`/contacts/${encodeURIComponent(identifier)}`, params); + } + + async deleteContact(identifier: string): Promise { + await this.client.delete(`/contacts/${encodeURIComponent(identifier)}`); + } + + async getContacts(params?: PaginationParams & { modifiedSince?: string; listIds?: number[]; sort?: string }): Promise<{ contacts: Contact[]; count: number }> { + const { data } = await this.client.get('/contacts', { params }); + return data; + } + + async importContacts(params: { + fileBody?: string; + fileUrl?: string; + listIds?: number[]; + notifyUrl?: string; + newList?: { listName: string; folderId?: number }; + emailBlacklist?: boolean; + smsBlacklist?: boolean; + updateExistingContacts?: boolean; + emptyContactsAttributes?: boolean; + }): Promise<{ processId: number }> { + const { data } = await this.client.post('/contacts/import', params); + return data; + } + + async exportContacts(params: { + exportAttributes?: string[]; + filter?: Record; + notifyUrl?: string; + }): Promise<{ exportId: string }> { + const { data } = await this.client.post('/contacts/export', params); + return data; + } + + async addContactToList(listId: number, contactEmails: string[]): Promise<{ contacts: { success: string[]; failure: string[] } }> { + const { data } = await this.client.post(`/contacts/lists/${listId}/contacts/add`, { emails: contactEmails }); + return data; + } + + async removeContactFromList(listId: number, contactEmails: string[]): Promise<{ contacts: { success: string[]; failure: string[] } }> { + const { data } = await this.client.post(`/contacts/lists/${listId}/contacts/remove`, { emails: contactEmails }); + return data; + } + + async getContactStats(identifier: string): Promise { + const { data } = await this.client.get(`/contacts/${encodeURIComponent(identifier)}/campaignStats`); + return data; + } + + async getContactAttributes(): Promise<{ attributes: any[] }> { + const { data } = await this.client.get('/contacts/attributes'); + return data; + } + + async createAttribute(params: { + value?: string; + enumeration?: { value: number; label: string }[]; + type?: 'text' | 'date' | 'float' | 'boolean' | 'id'; + isRecurring?: boolean; + category?: 'normal' | 'transactional' | 'category' | 'calculated' | 'global'; + }): Promise { + await this.client.post('/contacts/attributes/normal', params); + } + + async createDoiContact(params: CreateDoiContactParams): Promise { + await this.client.post('/contacts/doubleOptinConfirmation', params); + } + + // ============================================================================ + // LISTS + // ============================================================================ + + async getLists(params?: PaginationParams & { sort?: string }): Promise<{ lists: ContactList[]; count: number }> { + const { data } = await this.client.get('/contacts/lists', { params }); + return data; + } + + async getList(listId: number): Promise { + const { data } = await this.client.get(`/contacts/lists/${listId}`); + return data; + } + + async createList(params: { name: string; folderId?: number }): Promise<{ id: number }> { + const { data } = await this.client.post('/contacts/lists', params); + return data; + } + + async updateList(listId: number, params: { name?: string; folderId?: number }): Promise { + await this.client.put(`/contacts/lists/${listId}`, params); + } + + async deleteList(listId: number): Promise { + await this.client.delete(`/contacts/lists/${listId}`); + } + + async getContactsFromList(listId: number, params?: PaginationParams & { modifiedSince?: string; sort?: string }): Promise<{ contacts: Contact[]; count: number }> { + const { data } = await this.client.get(`/contacts/lists/${listId}/contacts`, { params }); + return data; + } + + // ============================================================================ + // FOLDERS + // ============================================================================ + + async getFolders(params?: { limit?: number; offset?: number }): Promise<{ folders: Folder[]; count: number }> { + const { data } = await this.client.get('/contacts/folders', { params }); + return data; + } + + async getFolder(folderId: number): Promise { + const { data } = await this.client.get(`/contacts/folders/${folderId}`); + return data; + } + + async createFolder(params: CreateFolderParams): Promise<{ id: number }> { + const { data } = await this.client.post('/contacts/folders', params); + return data; + } + + async updateFolder(folderId: number, params: { name: string }): Promise { + await this.client.put(`/contacts/folders/${folderId}`, params); + } + + async deleteFolder(folderId: number): Promise { + await this.client.delete(`/contacts/folders/${folderId}`); + } + + async getListsInFolder(folderId: number, params?: PaginationParams & { sort?: string }): Promise<{ lists: ContactList[]; count: number }> { + const { data } = await this.client.get(`/contacts/folders/${folderId}/lists`, { params }); + return data; + } + + // ============================================================================ + // EMAIL CAMPAIGNS + // ============================================================================ + + async getEmailCampaigns(params?: { + type?: 'classic' | 'trigger'; + status?: 'draft' | 'sent' | 'queued' | 'suspended' | 'archive'; + limit?: number; + offset?: number; + sort?: string; + excludeHtmlContent?: boolean; + }): Promise<{ campaigns: EmailCampaign[]; count: number }> { + const { data } = await this.client.get('/emailCampaigns', { params }); + return data; + } + + async getEmailCampaign(campaignId: number): Promise { + const { data } = await this.client.get(`/emailCampaigns/${campaignId}`); + return data; + } + + async createEmailCampaign(params: CreateEmailCampaignParams): Promise<{ id: number }> { + const { data } = await this.client.post('/emailCampaigns', params); + return data; + } + + async updateEmailCampaign(campaignId: number, params: Partial): Promise { + await this.client.put(`/emailCampaigns/${campaignId}`, params); + } + + async deleteEmailCampaign(campaignId: number): Promise { + await this.client.delete(`/emailCampaigns/${campaignId}`); + } + + async sendEmailCampaignNow(campaignId: number): Promise { + await this.client.post(`/emailCampaigns/${campaignId}/sendNow`); + } + + async sendTestEmail(campaignId: number, emailTo: string[]): Promise { + await this.client.post(`/emailCampaigns/${campaignId}/sendTest`, { emailTo }); + } + + async updateEmailCampaignStatus(campaignId: number, status: 'draft' | 'suspended' | 'archive'): Promise { + await this.client.put(`/emailCampaigns/${campaignId}/status`, { status }); + } + + async getEmailCampaignReport(campaignId: number, params?: DateRangeParams): Promise { + const { data } = await this.client.get(`/emailCampaigns/${campaignId}/report`, { params }); + return data; + } + + async exportEmailCampaignRecipients(campaignId: number, params?: { + notifyURL?: string; + recipientsType?: 'all' | 'nonClickers' | 'nonOpeners' | 'clickers' | 'openers' | 'softBounces' | 'hardBounces' | 'unsubscribed'; + }): Promise<{ exportId: string }> { + const { data } = await this.client.post(`/emailCampaigns/${campaignId}/exportRecipients`, params); + return data; + } + + // ============================================================================ + // TRANSACTIONAL EMAILS (SMTP) + // ============================================================================ + + async sendTransacEmail(params: SendSmtpEmail): Promise { + const { data } = await this.client.post('/smtp/email', params); + return data; + } + + async getTransacEmailContent(uuid: string): Promise { + const { data } = await this.client.get(`/smtp/emails/${uuid}`); + return data; + } + + async deleteScheduledEmail(identifier: string): Promise { + await this.client.delete(`/smtp/email/${identifier}`); + } + + async getTransacEmailEvents(params?: { + limit?: number; + offset?: number; + email?: string; + templateId?: number; + messageId?: string; + tags?: string; + event?: string; + days?: number; + sort?: string; + } & DateRangeParams): Promise { + const { data } = await this.client.get('/smtp/statistics/events', { params }); + return data; + } + + async getTransacEmailStats(params?: DateRangeParams & { days?: number; tag?: string }): Promise { + const { data } = await this.client.get('/smtp/statistics/aggregatedReport', { params }); + return data; + } + + async getTransacEmailReports(params?: DateRangeParams & { days?: number; tag?: string; sort?: string }): Promise { + const { data } = await this.client.get('/smtp/statistics/reports', { params }); + return data; + } + + async getBlockedDomains(): Promise<{ domains: string[] }> { + const { data } = await this.client.get('/smtp/blockedDomains'); + return data; + } + + async blockDomain(domain: string): Promise { + await this.client.post('/smtp/blockedDomains', { domain }); + } + + async unblockDomain(domain: string): Promise { + await this.client.delete(`/smtp/blockedDomains/${domain}`); + } + + async getBlockedEmails(): Promise<{ emails: any[] }> { + const { data } = await this.client.get('/smtp/blockedContacts'); + return data; + } + + async blockEmail(params: { emails: string[] }): Promise { + await this.client.post('/smtp/blockedContacts', params); + } + + async unblockEmail(email: string): Promise { + await this.client.delete(`/smtp/blockedContacts/${email}`); + } + + async getEmailActivityLog(messageId: string): Promise { + const { data } = await this.client.get(`/smtp/emails/${messageId}/activity`); + return data; + } + + // ============================================================================ + // EMAIL TEMPLATES + // ============================================================================ + + async getEmailTemplates(params?: { templateStatus?: boolean; limit?: number; offset?: number; sort?: string }): Promise<{ templates: EmailTemplate[]; count: number }> { + const { data } = await this.client.get('/smtp/templates', { params }); + return data; + } + + async getEmailTemplate(templateId: number): Promise { + const { data } = await this.client.get(`/smtp/templates/${templateId}`); + return data; + } + + async createEmailTemplate(params: CreateEmailTemplateParams): Promise<{ id: number }> { + const { data } = await this.client.post('/smtp/templates', params); + return data; + } + + async updateEmailTemplate(templateId: number, params: Partial): Promise { + await this.client.put(`/smtp/templates/${templateId}`, params); + } + + async deleteEmailTemplate(templateId: number): Promise { + await this.client.delete(`/smtp/templates/${templateId}`); + } + + async sendTestEmailTemplate(templateId: number, params: { emailTo: string[] }): Promise { + await this.client.post(`/smtp/templates/${templateId}/sendTest`, params); + } + + // ============================================================================ + // SENDERS + // ============================================================================ + + async getSenders(params?: { ip?: string; domain?: string }): Promise<{ senders: Sender[] }> { + const { data } = await this.client.get('/senders', { params }); + return data; + } + + async getSender(senderId: number): Promise { + const { data } = await this.client.get(`/senders/${senderId}`); + return data; + } + + async createSender(params: CreateSenderParams): Promise<{ id: number }> { + const { data } = await this.client.post('/senders', params); + return data; + } + + async updateSender(senderId: number, params: { name?: string; email?: string; ips?: { ip: string; domain: string }[] }): Promise { + await this.client.put(`/senders/${senderId}`, params); + } + + async deleteSender(senderId: number): Promise { + await this.client.delete(`/senders/${senderId}`); + } + + async validateSenderDomain(domain: string): Promise { + const { data } = await this.client.get(`/senders/domains/${domain}/validate`); + return data; + } + + async authenticateSenderDomain(domain: string): Promise { + const { data } = await this.client.put(`/senders/domains/${domain}/authenticate`); + return data; + } + + // ============================================================================ + // SMS + // ============================================================================ + + async sendTransacSms(params: SendTransacSms): Promise { + const { data } = await this.client.post('/transactionalSMS/sms', params); + return data; + } + + async getTransacSmsEvents(params?: { + limit?: number; + offset?: number; + phoneNumber?: string; + event?: string; + tags?: string; + sort?: string; + days?: number; + } & DateRangeParams): Promise { + const { data } = await this.client.get('/transactionalSMS/statistics/events', { params }); + return data; + } + + async getTransacSmsReports(params?: DateRangeParams & { days?: number; tag?: string; sort?: string }): Promise { + const { data } = await this.client.get('/transactionalSMS/statistics/reports', { params }); + return data; + } + + async getTransacSmsStats(params?: DateRangeParams & { days?: number; tag?: string }): Promise { + const { data } = await this.client.get('/transactionalSMS/statistics/aggregatedReport', { params }); + return data; + } + + async getSmsCampaigns(params?: { + status?: 'draft' | 'sent' | 'queued' | 'suspended' | 'inProgress'; + limit?: number; + offset?: number; + sort?: string; + } & DateRangeParams): Promise<{ campaigns: SmsCampaign[]; count: number }> { + const { data } = await this.client.get('/smsCampaigns', { params }); + return data; + } + + async getSmsCampaign(campaignId: number): Promise { + const { data } = await this.client.get(`/smsCampaigns/${campaignId}`); + return data; + } + + async createSmsCampaign(params: CreateSmsCampaignParams): Promise<{ id: number }> { + const { data } = await this.client.post('/smsCampaigns', params); + return data; + } + + async updateSmsCampaign(campaignId: number, params: Partial): Promise { + await this.client.put(`/smsCampaigns/${campaignId}`, params); + } + + async deleteSmsCampaign(campaignId: number): Promise { + await this.client.delete(`/smsCampaigns/${campaignId}`); + } + + async sendSmsCampaignNow(campaignId: number): Promise { + await this.client.post(`/smsCampaigns/${campaignId}/sendNow`); + } + + async sendTestSms(campaignId: number, phoneNumber: string): Promise { + await this.client.post(`/smsCampaigns/${campaignId}/sendTest`, { phoneNumber }); + } + + async updateSmsCampaignStatus(campaignId: number, status: 'draft' | 'suspended' | 'archive'): Promise { + await this.client.put(`/smsCampaigns/${campaignId}/status`, { status }); + } + + async requestSmsSender(senderName: string): Promise { + await this.client.post('/senders/sms', { senderName }); + } + + async getSmsSenders(params?: { limit?: number; offset?: number }): Promise<{ senders: any[] }> { + const { data } = await this.client.get('/senders/sms', { params }); + return data; + } + + // ============================================================================ + // AUTOMATIONS (WORKFLOWS) + // ============================================================================ + + async getAutomations(params?: { limit?: number; offset?: number; sort?: string }): Promise<{ automations: Automation[] }> { + const { data } = await this.client.get('/automation/workflows', { params }); + return data; + } + + async getAutomation(workflowId: number): Promise { + const { data } = await this.client.get(`/automation/workflows/${workflowId}`); + return data; + } + + async deleteAutomation(workflowId: number): Promise { + await this.client.delete(`/automation/workflows/${workflowId}`); + } + + // ============================================================================ + // CRM - DEALS + // ============================================================================ + + async getDeals(params?: { + 'filters[attributes.deal_name]'?: string; + 'filters[attributes.deal_stage]'?: string; + 'filters[linkedContactsIds]'?: number; + 'filters[linkedCompaniesIds]'?: string; + limit?: number; + offset?: number; + sort?: string; + }): Promise<{ items: Deal[] }> { + const { data } = await this.client.get('/crm/deals', { params }); + return data; + } + + async getDeal(dealId: string): Promise { + const { data } = await this.client.get(`/crm/deals/${dealId}`); + return data; + } + + async createDeal(params: CreateDealParams): Promise<{ id: string }> { + const { data } = await this.client.post('/crm/deals', params); + return data; + } + + async updateDeal(dealId: string, params: Partial): Promise { + await this.client.patch(`/crm/deals/${dealId}`, params); + } + + async deleteDeal(dealId: string): Promise { + await this.client.delete(`/crm/deals/${dealId}`); + } + + async linkDealWithContact(dealId: string, contactId: number): Promise { + await this.client.patch(`/crm/deals/link-unlink/${dealId}`, { + linkContactIds: [contactId], + }); + } + + async unlinkDealFromContact(dealId: string, contactId: number): Promise { + await this.client.patch(`/crm/deals/link-unlink/${dealId}`, { + unlinkContactIds: [contactId], + }); + } + + async getDealsPipeline(): Promise { + const { data } = await this.client.get('/crm/pipeline/details'); + return data; + } + + async getDealAttributes(): Promise<{ attributes: any[] }> { + const { data } = await this.client.get('/crm/deals/attributes'); + return data; + } + + // ============================================================================ + // CRM - COMPANIES + // ============================================================================ + + async getCompanies(params?: { + 'filters[attributes.name]'?: string; + 'filters[linkedContactsIds]'?: number; + 'filters[linkedDealsIds]'?: string; + limit?: number; + offset?: number; + sort?: string; + }): Promise<{ items: Company[] }> { + const { data } = await this.client.get('/crm/companies', { params }); + return data; + } + + async getCompany(companyId: string): Promise { + const { data } = await this.client.get(`/crm/companies/${companyId}`); + return data; + } + + async createCompany(params: CreateCompanyParams): Promise<{ id: string }> { + const { data } = await this.client.post('/crm/companies', params); + return data; + } + + async updateCompany(companyId: string, params: Partial): Promise { + await this.client.patch(`/crm/companies/${companyId}`, params); + } + + async deleteCompany(companyId: string): Promise { + await this.client.delete(`/crm/companies/${companyId}`); + } + + async linkCompanyWithContact(companyId: string, contactId: number): Promise { + await this.client.patch(`/crm/companies/link-unlink/${companyId}`, { + linkContactIds: [contactId], + }); + } + + async unlinkCompanyFromContact(companyId: string, contactId: number): Promise { + await this.client.patch(`/crm/companies/link-unlink/${companyId}`, { + unlinkContactIds: [contactId], + }); + } + + async getCompanyAttributes(): Promise<{ attributes: any[] }> { + const { data } = await this.client.get('/crm/companies/attributes'); + return data; + } + + // ============================================================================ + // CRM - TASKS + // ============================================================================ + + async getTasks(params?: { + 'filterType'?: 'contacts' | 'companies' | 'deals'; + 'filterId'?: string; + 'dateFrom'?: string; + 'dateTo'?: string; + limit?: number; + offset?: number; + sort?: string; + }): Promise<{ items: Task[] }> { + const { data } = await this.client.get('/crm/tasks', { params }); + return data; + } + + async getTask(taskId: string): Promise { + const { data } = await this.client.get(`/crm/tasks/${taskId}`); + return data; + } + + async createTask(params: CreateTaskParams): Promise<{ id: string }> { + const { data } = await this.client.post('/crm/tasks', params); + return data; + } + + async updateTask(taskId: string, params: Partial): Promise { + await this.client.patch(`/crm/tasks/${taskId}`, params); + } + + async deleteTask(taskId: string): Promise { + await this.client.delete(`/crm/tasks/${taskId}`); + } + + async getTaskTypes(): Promise<{ taskTypes: any[] }> { + const { data } = await this.client.get('/crm/tasktypes'); + return data; + } + + // ============================================================================ + // CRM - NOTES + // ============================================================================ + + async getNotes(params?: { + entity?: 'companies' | 'deals' | 'contacts'; + entityIds?: string; + dateFrom?: number; + dateTo?: number; + limit?: number; + offset?: number; + sort?: string; + }): Promise<{ items: any[] }> { + const { data } = await this.client.get('/crm/notes', { params }); + return data; + } + + async getNote(noteId: string): Promise { + const { data } = await this.client.get(`/crm/notes/${noteId}`); + return data; + } + + async createNote(params: { + text: string; + contactIds?: number[]; + dealIds?: string[]; + companyIds?: string[]; + }): Promise<{ id: string }> { + const { data } = await this.client.post('/crm/notes', params); + return data; + } + + async updateNote(noteId: string, params: { text: string }): Promise { + await this.client.patch(`/crm/notes/${noteId}`, params); + } + + async deleteNote(noteId: string): Promise { + await this.client.delete(`/crm/notes/${noteId}`); + } + + // ============================================================================ + // WEBHOOKS + // ============================================================================ + + async getWebhooks(params?: { type?: 'marketing' | 'transactional' | 'inbound'; sort?: string }): Promise<{ webhooks: Webhook[] }> { + const { data } = await this.client.get('/webhooks', { params }); + return data; + } + + async getWebhook(webhookId: number): Promise { + const { data } = await this.client.get(`/webhooks/${webhookId}`); + return data; + } + + async createWebhook(params: CreateWebhookParams): Promise<{ id: number }> { + const { data } = await this.client.post('/webhooks', params); + return data; + } + + async updateWebhook(webhookId: number, params: Partial): Promise { + await this.client.put(`/webhooks/${webhookId}`, params); + } + + async deleteWebhook(webhookId: number): Promise { + await this.client.delete(`/webhooks/${webhookId}`); + } + + async exportWebhookEvents(webhookId: number, params?: { + startDate?: string; + endDate?: string; + days?: number; + messageId?: string; + email?: string; + event?: string; + notifyURL?: string; + }): Promise<{ exportId: string }> { + const { data } = await this.client.post(`/webhooks/${webhookId}/export`, params); + return data; + } + + // ============================================================================ + // ACCOUNT + // ============================================================================ + + async getAccount(): Promise { + const { data } = await this.client.get('/account'); + return data; + } + + // ============================================================================ + // PROCESSES + // ============================================================================ + + async getProcess(processId: number): Promise { + const { data } = await this.client.get(`/processes/${processId}`); + return data; + } + + async getProcesses(params?: { limit?: number; offset?: number; sort?: string }): Promise<{ processes: Process[] }> { + const { data } = await this.client.get('/processes', { params }); + return data; + } + + // ============================================================================ + // INBOUND PARSING + // ============================================================================ + + async getInboundEmailEvents(params?: { + sender?: string; + startDate?: string; + endDate?: string; + limit?: number; + offset?: number; + sort?: string; + }): Promise<{ events: any[] }> { + const { data } = await this.client.get('/inbound/events', { params }); + return data; + } + + async getInboundEmailAttachment(downloadToken: string): Promise { + const { data } = await this.client.get(`/inbound/attachments/${downloadToken}`); + return data; + } +} diff --git a/servers/brevo/src/server.ts b/servers/brevo/src/server.ts index 64f3d94..72b3d33 100644 --- a/servers/brevo/src/server.ts +++ b/servers/brevo/src/server.ts @@ -1,3 +1,8 @@ +/** + * Brevo MCP Server + * Provides comprehensive tools for Brevo email marketing, SMS, CRM, and automation + */ + import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { @@ -5,201 +10,1419 @@ import { ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, + Tool, } from '@modelcontextprotocol/sdk/types.js'; -import { BrevoApiClient } from './types/api-client.js'; -import { createContactsTools } from './tools/contacts-tools.js'; -import { createCampaignsTools } from './tools/campaigns-tools.js'; -import { createTransactionalTools } from './tools/transactional-tools.js'; -import { createListsTools } from './tools/lists-tools.js'; -import { createSendersTools } from './tools/senders-tools.js'; -import { createTemplatesTools } from './tools/templates-tools.js'; -import { createAutomationsTools } from './tools/automations-tools.js'; -import { createSmsTools } from './tools/sms-tools.js'; -import { createDealsTools } from './tools/deals-tools.js'; -import { createWebhooksTools } from './tools/webhooks-tools.js'; -import { contactDashboardApp } from './apps/contact-dashboard.js'; -import { contactDetailApp } from './apps/contact-detail.js'; -import { contactGridApp } from './apps/contact-grid.js'; -import { campaignDashboardApp } from './apps/campaign-dashboard.js'; -import { campaignBuilderApp } from './apps/campaign-builder.js'; -import { automationDashboardApp } from './apps/automation-dashboard.js'; -import { dealPipelineApp } from './apps/deal-pipeline.js'; -import { transactionalMonitorApp } from './apps/transactional-monitor.js'; -import { templateGalleryApp } from './apps/template-gallery.js'; -import { smsDashboardApp } from './apps/sms-dashboard.js'; -import { listManagerApp } from './apps/list-manager.js'; -import { reportDashboardApp } from './apps/report-dashboard.js'; -import { webhookManagerApp } from './apps/webhook-manager.js'; -import { importWizardApp } from './apps/import-wizard.js'; +import { BrevoAPIClient } from './api-client.js'; -export class BrevoServer { - private server: Server; - private client: BrevoApiClient; - private tools: any[]; - private apps: any[]; - - constructor(apiKey: string) { - this.server = new Server( - { - name: 'brevo-server', - version: '1.0.0', - }, - { - capabilities: { - tools: {}, - resources: {}, - }, - } - ); - - this.client = new BrevoApiClient({ apiKey }); - - // Initialize all tools - this.tools = [ - ...createContactsTools(this.client), - ...createCampaignsTools(this.client), - ...createTransactionalTools(this.client), - ...createListsTools(this.client), - ...createSendersTools(this.client), - ...createTemplatesTools(this.client), - ...createAutomationsTools(this.client), - ...createSmsTools(this.client), - ...createDealsTools(this.client), - ...createWebhooksTools(this.client), - ]; - - // Initialize all apps - this.apps = [ - contactDashboardApp(), - contactDetailApp(), - contactGridApp(), - campaignDashboardApp(), - campaignBuilderApp(), - automationDashboardApp(), - dealPipelineApp(), - transactionalMonitorApp(), - templateGalleryApp(), - smsDashboardApp(), - listManagerApp(), - reportDashboardApp(), - webhookManagerApp(), - importWizardApp(), - ]; - - this.setupHandlers(); - } - - private setupHandlers() { - // List available tools - this.server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: this.tools.map(tool => ({ - name: tool.name, - description: tool.description, - inputSchema: { - type: 'object', - properties: tool.inputSchema.shape, - required: Object.keys(tool.inputSchema.shape).filter( - key => !tool.inputSchema.shape[key].isOptional() - ), - }, - })), - }; - }); - - // Execute tool - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = this.tools.find(t => t.name === request.params.name); - if (!tool) { - throw new Error(`Tool not found: ${request.params.name}`); - } - - try { - const result = await tool.execute(request.params.arguments || {}); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - } catch (error: any) { - return { - content: [ - { - type: 'text', - text: `Error: ${error.message}`, - }, - ], - isError: true, - }; - } - }); - - // List resources (MCP apps) - this.server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { - resources: this.apps.map(app => ({ - uri: `brevo://app/${app.name}`, - name: app.name, - description: app.description, - mimeType: 'text/html', - })), - }; - }); - - // Read resource (render app UI) - this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const appName = request.params.uri.replace('brevo://app/', ''); - const app = this.apps.find(a => a.name === appName); - - if (!app) { - throw new Error(`App not found: ${appName}`); - } - - const html = ` - - - - - - ${app.ui.title} - - - - ${app.ui.content} - - - `; - - return { - contents: [ - { - uri: request.params.uri, - mimeType: 'text/html', - text: html, - }, - ], - }; - }); - } - - async run() { - const transport = new StdioServerTransport(); - await this.server.connect(transport); - console.error('Brevo MCP Server running on stdio'); - } +const API_KEY = process.env.BREVO_API_KEY; +if (!API_KEY) { + throw new Error('BREVO_API_KEY environment variable is required'); +} + +const client = new BrevoAPIClient({ apiKey: API_KEY }); +const server = new Server( + { + name: 'brevo-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } +); + +// ============================================================================ +// TOOL DEFINITIONS +// ============================================================================ + +const tools: Tool[] = [ + // CONTACTS (8 tools) + { + name: 'brevo_get_contact', + description: 'Get a contact by email or ID', + inputSchema: { + type: 'object', + properties: { + identifier: { type: 'string', description: 'Contact email or ID' }, + }, + required: ['identifier'], + }, + }, + { + name: 'brevo_create_contact', + description: 'Create a new contact', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Contact email address' }, + attributes: { type: 'object', description: 'Contact attributes (firstName, lastName, etc.)' }, + listIds: { type: 'array', items: { type: 'number' }, description: 'List IDs to add contact to' }, + emailBlacklisted: { type: 'boolean', description: 'Whether email is blacklisted' }, + smsBlacklisted: { type: 'boolean', description: 'Whether SMS is blacklisted' }, + updateEnabled: { type: 'boolean', description: 'Update if exists' }, + }, + required: ['email'], + }, + }, + { + name: 'brevo_update_contact', + description: 'Update an existing contact', + inputSchema: { + type: 'object', + properties: { + identifier: { type: 'string', description: 'Contact email or ID' }, + attributes: { type: 'object', description: 'Contact attributes to update' }, + listIds: { type: 'array', items: { type: 'number' }, description: 'List IDs to add contact to' }, + unlinkListIds: { type: 'array', items: { type: 'number' }, description: 'List IDs to remove contact from' }, + emailBlacklisted: { type: 'boolean', description: 'Whether email is blacklisted' }, + smsBlacklisted: { type: 'boolean', description: 'Whether SMS is blacklisted' }, + }, + required: ['identifier'], + }, + }, + { + name: 'brevo_delete_contact', + description: 'Delete a contact', + inputSchema: { + type: 'object', + properties: { + identifier: { type: 'string', description: 'Contact email or ID' }, + }, + required: ['identifier'], + }, + }, + { + name: 'brevo_list_contacts', + description: 'List all contacts with pagination', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Number of contacts to return (max 50)' }, + offset: { type: 'number', description: 'Offset for pagination' }, + modifiedSince: { type: 'string', description: 'Filter by modified date (ISO 8601)' }, + listIds: { type: 'array', items: { type: 'number' }, description: 'Filter by list IDs' }, + sort: { type: 'string', description: 'Sort order (asc/desc)' }, + }, + }, + }, + { + name: 'brevo_import_contacts', + description: 'Import contacts from file or URL', + inputSchema: { + type: 'object', + properties: { + fileUrl: { type: 'string', description: 'URL of CSV file to import' }, + listIds: { type: 'array', items: { type: 'number' }, description: 'List IDs to add contacts to' }, + notifyUrl: { type: 'string', description: 'Webhook URL for import completion' }, + updateExistingContacts: { type: 'boolean', description: 'Update existing contacts' }, + emailBlacklist: { type: 'boolean', description: 'Import blacklisted emails' }, + smsBlacklist: { type: 'boolean', description: 'Import blacklisted SMS' }, + }, + }, + }, + { + name: 'brevo_export_contacts', + description: 'Export contacts to CSV', + inputSchema: { + type: 'object', + properties: { + exportAttributes: { type: 'array', items: { type: 'string' }, description: 'Attributes to export' }, + filter: { type: 'object', description: 'Filter criteria for export' }, + notifyUrl: { type: 'string', description: 'Webhook URL for export completion' }, + }, + }, + }, + { + name: 'brevo_get_contact_stats', + description: 'Get campaign statistics for a contact', + inputSchema: { + type: 'object', + properties: { + identifier: { type: 'string', description: 'Contact email or ID' }, + }, + required: ['identifier'], + }, + }, + + // LISTS (7 tools) + { + name: 'brevo_list_contact_lists', + description: 'Get all contact lists', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Number of lists to return' }, + offset: { type: 'number', description: 'Offset for pagination' }, + sort: { type: 'string', description: 'Sort order' }, + }, + }, + }, + { + name: 'brevo_get_contact_list', + description: 'Get a specific contact list', + inputSchema: { + type: 'object', + properties: { + listId: { type: 'number', description: 'List ID' }, + }, + required: ['listId'], + }, + }, + { + name: 'brevo_create_contact_list', + description: 'Create a new contact list', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'List name' }, + folderId: { type: 'number', description: 'Folder ID to organize list' }, + }, + required: ['name'], + }, + }, + { + name: 'brevo_update_contact_list', + description: 'Update a contact list', + inputSchema: { + type: 'object', + properties: { + listId: { type: 'number', description: 'List ID' }, + name: { type: 'string', description: 'New list name' }, + folderId: { type: 'number', description: 'New folder ID' }, + }, + required: ['listId'], + }, + }, + { + name: 'brevo_delete_contact_list', + description: 'Delete a contact list', + inputSchema: { + type: 'object', + properties: { + listId: { type: 'number', description: 'List ID' }, + }, + required: ['listId'], + }, + }, + { + name: 'brevo_add_contact_to_list', + description: 'Add contacts to a list', + inputSchema: { + type: 'object', + properties: { + listId: { type: 'number', description: 'List ID' }, + emails: { type: 'array', items: { type: 'string' }, description: 'Contact emails to add' }, + }, + required: ['listId', 'emails'], + }, + }, + { + name: 'brevo_remove_contact_from_list', + description: 'Remove contacts from a list', + inputSchema: { + type: 'object', + properties: { + listId: { type: 'number', description: 'List ID' }, + emails: { type: 'array', items: { type: 'string' }, description: 'Contact emails to remove' }, + }, + required: ['listId', 'emails'], + }, + }, + + // FOLDERS (4 tools) + { + name: 'brevo_list_folders', + description: 'Get all contact folders', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Number of folders to return' }, + offset: { type: 'number', description: 'Offset for pagination' }, + }, + }, + }, + { + name: 'brevo_create_folder', + description: 'Create a new folder', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Folder name' }, + }, + required: ['name'], + }, + }, + { + name: 'brevo_update_folder', + description: 'Update a folder', + inputSchema: { + type: 'object', + properties: { + folderId: { type: 'number', description: 'Folder ID' }, + name: { type: 'string', description: 'New folder name' }, + }, + required: ['folderId', 'name'], + }, + }, + { + name: 'brevo_delete_folder', + description: 'Delete a folder', + inputSchema: { + type: 'object', + properties: { + folderId: { type: 'number', description: 'Folder ID' }, + }, + required: ['folderId'], + }, + }, + + // EMAIL CAMPAIGNS (9 tools) + { + name: 'brevo_list_email_campaigns', + description: 'Get all email campaigns', + inputSchema: { + type: 'object', + properties: { + type: { type: 'string', enum: ['classic', 'trigger'], description: 'Campaign type' }, + status: { type: 'string', enum: ['draft', 'sent', 'queued', 'suspended', 'archive'], description: 'Campaign status' }, + limit: { type: 'number', description: 'Number of campaigns to return' }, + offset: { type: 'number', description: 'Offset for pagination' }, + sort: { type: 'string', description: 'Sort order' }, + }, + }, + }, + { + name: 'brevo_get_email_campaign', + description: 'Get a specific email campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_create_email_campaign', + description: 'Create a new email campaign', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Campaign name' }, + subject: { type: 'string', description: 'Email subject' }, + sender: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' } }, description: 'Sender details' }, + htmlContent: { type: 'string', description: 'HTML content of email' }, + htmlUrl: { type: 'string', description: 'URL to HTML content' }, + scheduledAt: { type: 'string', description: 'Schedule date/time (ISO 8601)' }, + recipients: { + type: 'object', + properties: { + listIds: { type: 'array', items: { type: 'number' } }, + exclusionListIds: { type: 'array', items: { type: 'number' } }, + }, + description: 'Recipient lists', + }, + replyTo: { type: 'string', description: 'Reply-to email' }, + tag: { type: 'string', description: 'Campaign tag' }, + }, + required: ['name', 'subject', 'sender'], + }, + }, + { + name: 'brevo_update_email_campaign', + description: 'Update an email campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + subject: { type: 'string', description: 'Email subject' }, + htmlContent: { type: 'string', description: 'HTML content' }, + scheduledAt: { type: 'string', description: 'Schedule date/time' }, + recipients: { type: 'object', description: 'Recipient lists' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_delete_email_campaign', + description: 'Delete an email campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_send_email_campaign', + description: 'Send an email campaign immediately', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_send_test_email', + description: 'Send test email for a campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + emailTo: { type: 'array', items: { type: 'string' }, description: 'Test recipient emails' }, + }, + required: ['campaignId', 'emailTo'], + }, + }, + { + name: 'brevo_update_email_campaign_status', + description: 'Update email campaign status', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + status: { type: 'string', enum: ['draft', 'suspended', 'archive'], description: 'New status' }, + }, + required: ['campaignId', 'status'], + }, + }, + { + name: 'brevo_get_email_campaign_report', + description: 'Get email campaign statistics and report', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + + // TRANSACTIONAL EMAIL (6 tools) + { + name: 'brevo_send_transactional_email', + description: 'Send a transactional email', + inputSchema: { + type: 'object', + properties: { + to: { + type: 'array', + items: { + type: 'object', + properties: { + email: { type: 'string' }, + name: { type: 'string' }, + }, + }, + description: 'Recipients', + }, + sender: { type: 'object', properties: { email: { type: 'string' }, name: { type: 'string' } }, description: 'Sender' }, + subject: { type: 'string', description: 'Email subject' }, + htmlContent: { type: 'string', description: 'HTML content' }, + textContent: { type: 'string', description: 'Plain text content' }, + templateId: { type: 'number', description: 'Template ID to use' }, + params: { type: 'object', description: 'Template parameters' }, + tags: { type: 'array', items: { type: 'string' }, description: 'Email tags' }, + scheduledAt: { type: 'string', description: 'Schedule time (ISO 8601)' }, + }, + required: ['to'], + }, + }, + { + name: 'brevo_get_transactional_email', + description: 'Get transactional email by UUID', + inputSchema: { + type: 'object', + properties: { + uuid: { type: 'string', description: 'Email UUID' }, + }, + required: ['uuid'], + }, + }, + { + name: 'brevo_get_transactional_email_events', + description: 'Get transactional email events', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Filter by email' }, + templateId: { type: 'number', description: 'Filter by template ID' }, + messageId: { type: 'string', description: 'Filter by message ID' }, + event: { type: 'string', description: 'Filter by event type' }, + tags: { type: 'string', description: 'Filter by tags' }, + days: { type: 'number', description: 'Number of days to look back' }, + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + }, + }, + }, + { + name: 'brevo_get_transactional_email_stats', + description: 'Get aggregated transactional email statistics', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, + days: { type: 'number', description: 'Number of days' }, + tag: { type: 'string', description: 'Filter by tag' }, + }, + }, + }, + { + name: 'brevo_block_email_domain', + description: 'Block an email domain', + inputSchema: { + type: 'object', + properties: { + domain: { type: 'string', description: 'Domain to block' }, + }, + required: ['domain'], + }, + }, + { + name: 'brevo_unblock_email_domain', + description: 'Unblock an email domain', + inputSchema: { + type: 'object', + properties: { + domain: { type: 'string', description: 'Domain to unblock' }, + }, + required: ['domain'], + }, + }, + + // EMAIL TEMPLATES (6 tools) + { + name: 'brevo_list_email_templates', + description: 'Get all email templates', + inputSchema: { + type: 'object', + properties: { + templateStatus: { type: 'boolean', description: 'Filter by active status' }, + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + }, + }, + }, + { + name: 'brevo_get_email_template', + description: 'Get a specific email template', + inputSchema: { + type: 'object', + properties: { + templateId: { type: 'number', description: 'Template ID' }, + }, + required: ['templateId'], + }, + }, + { + name: 'brevo_create_email_template', + description: 'Create a new email template', + inputSchema: { + type: 'object', + properties: { + templateName: { type: 'string', description: 'Template name' }, + subject: { type: 'string', description: 'Email subject' }, + sender: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' } }, description: 'Sender' }, + htmlContent: { type: 'string', description: 'HTML content' }, + htmlUrl: { type: 'string', description: 'URL to HTML content' }, + isActive: { type: 'boolean', description: 'Is template active' }, + tag: { type: 'string', description: 'Template tag' }, + }, + required: ['templateName', 'subject', 'sender'], + }, + }, + { + name: 'brevo_update_email_template', + description: 'Update an email template', + inputSchema: { + type: 'object', + properties: { + templateId: { type: 'number', description: 'Template ID' }, + templateName: { type: 'string', description: 'Template name' }, + subject: { type: 'string', description: 'Email subject' }, + htmlContent: { type: 'string', description: 'HTML content' }, + isActive: { type: 'boolean', description: 'Is template active' }, + }, + required: ['templateId'], + }, + }, + { + name: 'brevo_delete_email_template', + description: 'Delete an email template', + inputSchema: { + type: 'object', + properties: { + templateId: { type: 'number', description: 'Template ID' }, + }, + required: ['templateId'], + }, + }, + { + name: 'brevo_send_test_template', + description: 'Send test email using a template', + inputSchema: { + type: 'object', + properties: { + templateId: { type: 'number', description: 'Template ID' }, + emailTo: { type: 'array', items: { type: 'string' }, description: 'Test recipient emails' }, + }, + required: ['templateId', 'emailTo'], + }, + }, + + // SENDERS (4 tools) + { + name: 'brevo_list_senders', + description: 'Get all senders', + inputSchema: { + type: 'object', + properties: { + ip: { type: 'string', description: 'Filter by IP' }, + domain: { type: 'string', description: 'Filter by domain' }, + }, + }, + }, + { + name: 'brevo_create_sender', + description: 'Create a new sender', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Sender name' }, + email: { type: 'string', description: 'Sender email' }, + }, + required: ['name', 'email'], + }, + }, + { + name: 'brevo_update_sender', + description: 'Update a sender', + inputSchema: { + type: 'object', + properties: { + senderId: { type: 'number', description: 'Sender ID' }, + name: { type: 'string', description: 'Sender name' }, + email: { type: 'string', description: 'Sender email' }, + }, + required: ['senderId'], + }, + }, + { + name: 'brevo_delete_sender', + description: 'Delete a sender', + inputSchema: { + type: 'object', + properties: { + senderId: { type: 'number', description: 'Sender ID' }, + }, + required: ['senderId'], + }, + }, + + // SMS (8 tools) + { + name: 'brevo_send_transactional_sms', + description: 'Send a transactional SMS', + inputSchema: { + type: 'object', + properties: { + sender: { type: 'string', description: 'Sender name/number (max 11 chars)' }, + recipient: { type: 'string', description: 'Recipient phone number (E.164 format)' }, + content: { type: 'string', description: 'SMS content' }, + type: { type: 'string', enum: ['transactional', 'marketing'], description: 'SMS type' }, + tag: { type: 'string', description: 'SMS tag' }, + webUrl: { type: 'string', description: 'Webhook URL for delivery status' }, + }, + required: ['sender', 'recipient', 'content'], + }, + }, + { + name: 'brevo_get_transactional_sms_events', + description: 'Get transactional SMS events', + inputSchema: { + type: 'object', + properties: { + phoneNumber: { type: 'string', description: 'Filter by phone number' }, + event: { type: 'string', description: 'Filter by event type' }, + tags: { type: 'string', description: 'Filter by tags' }, + days: { type: 'number', description: 'Number of days' }, + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + }, + }, + }, + { + name: 'brevo_get_transactional_sms_stats', + description: 'Get aggregated SMS statistics', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, + days: { type: 'number', description: 'Number of days' }, + tag: { type: 'string', description: 'Filter by tag' }, + }, + }, + }, + { + name: 'brevo_list_sms_campaigns', + description: 'Get all SMS campaigns', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['draft', 'sent', 'queued', 'suspended', 'inProgress'], description: 'Filter by status' }, + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + }, + }, + }, + { + name: 'brevo_get_sms_campaign', + description: 'Get a specific SMS campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_create_sms_campaign', + description: 'Create a new SMS campaign', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Campaign name' }, + sender: { type: 'string', description: 'Sender name/number' }, + content: { type: 'string', description: 'SMS content' }, + recipients: { + type: 'object', + properties: { + listIds: { type: 'array', items: { type: 'number' } }, + exclusionListIds: { type: 'array', items: { type: 'number' } }, + }, + description: 'Recipient lists', + }, + scheduledAt: { type: 'string', description: 'Schedule time (ISO 8601)' }, + }, + required: ['name', 'sender', 'content'], + }, + }, + { + name: 'brevo_update_sms_campaign', + description: 'Update an SMS campaign', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + content: { type: 'string', description: 'SMS content' }, + scheduledAt: { type: 'string', description: 'Schedule time' }, + }, + required: ['campaignId'], + }, + }, + { + name: 'brevo_send_sms_campaign', + description: 'Send SMS campaign immediately', + inputSchema: { + type: 'object', + properties: { + campaignId: { type: 'number', description: 'Campaign ID' }, + }, + required: ['campaignId'], + }, + }, + + // CRM - DEALS (5 tools) + { + name: 'brevo_list_deals', + description: 'Get all CRM deals', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + sort: { type: 'string', description: 'Sort order' }, + }, + }, + }, + { + name: 'brevo_get_deal', + description: 'Get a specific deal', + inputSchema: { + type: 'object', + properties: { + dealId: { type: 'string', description: 'Deal ID' }, + }, + required: ['dealId'], + }, + }, + { + name: 'brevo_create_deal', + description: 'Create a new deal', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Deal name' }, + attributes: { type: 'object', description: 'Deal attributes (amount, pipeline, stage, etc.)' }, + linkedContactsIds: { type: 'array', items: { type: 'number' }, description: 'Linked contact IDs' }, + linkedCompaniesIds: { type: 'array', items: { type: 'string' }, description: 'Linked company IDs' }, + }, + required: ['name'], + }, + }, + { + name: 'brevo_update_deal', + description: 'Update a deal', + inputSchema: { + type: 'object', + properties: { + dealId: { type: 'string', description: 'Deal ID' }, + name: { type: 'string', description: 'Deal name' }, + attributes: { type: 'object', description: 'Deal attributes' }, + }, + required: ['dealId'], + }, + }, + { + name: 'brevo_delete_deal', + description: 'Delete a deal', + inputSchema: { + type: 'object', + properties: { + dealId: { type: 'string', description: 'Deal ID' }, + }, + required: ['dealId'], + }, + }, + + // CRM - COMPANIES (5 tools) + { + name: 'brevo_list_companies', + description: 'Get all CRM companies', + inputSchema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + sort: { type: 'string', description: 'Sort order' }, + }, + }, + }, + { + name: 'brevo_get_company', + description: 'Get a specific company', + inputSchema: { + type: 'object', + properties: { + companyId: { type: 'string', description: 'Company ID' }, + }, + required: ['companyId'], + }, + }, + { + name: 'brevo_create_company', + description: 'Create a new company', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Company name' }, + attributes: { type: 'object', description: 'Company attributes' }, + linkedContactsIds: { type: 'array', items: { type: 'number' }, description: 'Linked contact IDs' }, + }, + required: ['name'], + }, + }, + { + name: 'brevo_update_company', + description: 'Update a company', + inputSchema: { + type: 'object', + properties: { + companyId: { type: 'string', description: 'Company ID' }, + name: { type: 'string', description: 'Company name' }, + attributes: { type: 'object', description: 'Company attributes' }, + }, + required: ['companyId'], + }, + }, + { + name: 'brevo_delete_company', + description: 'Delete a company', + inputSchema: { + type: 'object', + properties: { + companyId: { type: 'string', description: 'Company ID' }, + }, + required: ['companyId'], + }, + }, + + // CRM - TASKS (5 tools) + { + name: 'brevo_list_tasks', + description: 'Get all CRM tasks', + inputSchema: { + type: 'object', + properties: { + filterType: { type: 'string', enum: ['contacts', 'companies', 'deals'], description: 'Filter by entity type' }, + filterId: { type: 'string', description: 'Entity ID to filter by' }, + limit: { type: 'number', description: 'Results limit' }, + offset: { type: 'number', description: 'Pagination offset' }, + }, + }, + }, + { + name: 'brevo_get_task', + description: 'Get a specific task', + inputSchema: { + type: 'object', + properties: { + taskId: { type: 'string', description: 'Task ID' }, + }, + required: ['taskId'], + }, + }, + { + name: 'brevo_create_task', + description: 'Create a new task', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Task name' }, + taskTypeId: { type: 'string', description: 'Task type ID' }, + date: { type: 'string', description: 'Task date (ISO 8601)' }, + duration: { type: 'number', description: 'Duration in minutes' }, + notes: { type: 'string', description: 'Task notes' }, + done: { type: 'boolean', description: 'Is task done' }, + assignToId: { type: 'string', description: 'User ID to assign to' }, + contactsIds: { type: 'array', items: { type: 'number' }, description: 'Linked contact IDs' }, + dealsIds: { type: 'array', items: { type: 'string' }, description: 'Linked deal IDs' }, + companiesIds: { type: 'array', items: { type: 'string' }, description: 'Linked company IDs' }, + }, + required: ['name', 'taskTypeId', 'date', 'assignToId'], + }, + }, + { + name: 'brevo_update_task', + description: 'Update a task', + inputSchema: { + type: 'object', + properties: { + taskId: { type: 'string', description: 'Task ID' }, + name: { type: 'string', description: 'Task name' }, + date: { type: 'string', description: 'Task date' }, + done: { type: 'boolean', description: 'Is task done' }, + notes: { type: 'string', description: 'Task notes' }, + }, + required: ['taskId'], + }, + }, + { + name: 'brevo_delete_task', + description: 'Delete a task', + inputSchema: { + type: 'object', + properties: { + taskId: { type: 'string', description: 'Task ID' }, + }, + required: ['taskId'], + }, + }, + + // WEBHOOKS (4 tools) + { + name: 'brevo_list_webhooks', + description: 'Get all webhooks', + inputSchema: { + type: 'object', + properties: { + type: { type: 'string', enum: ['marketing', 'transactional', 'inbound'], description: 'Filter by webhook type' }, + }, + }, + }, + { + name: 'brevo_create_webhook', + description: 'Create a new webhook', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string', description: 'Webhook URL' }, + description: { type: 'string', description: 'Webhook description' }, + events: { + type: 'array', + items: { type: 'string' }, + description: 'Events to subscribe to (e.g., request, delivered, hardBounce, opened, click)', + }, + type: { type: 'string', enum: ['transactional', 'marketing', 'inbound'], description: 'Webhook type' }, + }, + required: ['url', 'events'], + }, + }, + { + name: 'brevo_update_webhook', + description: 'Update a webhook', + inputSchema: { + type: 'object', + properties: { + webhookId: { type: 'number', description: 'Webhook ID' }, + url: { type: 'string', description: 'Webhook URL' }, + description: { type: 'string', description: 'Webhook description' }, + events: { type: 'array', items: { type: 'string' }, description: 'Events to subscribe to' }, + }, + required: ['webhookId'], + }, + }, + { + name: 'brevo_delete_webhook', + description: 'Delete a webhook', + inputSchema: { + type: 'object', + properties: { + webhookId: { type: 'number', description: 'Webhook ID' }, + }, + required: ['webhookId'], + }, + }, + + // ACCOUNT & MISC (3 tools) + { + name: 'brevo_get_account', + description: 'Get account information including plan and credits', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'brevo_get_process', + description: 'Get status of an import/export process', + inputSchema: { + type: 'object', + properties: { + processId: { type: 'number', description: 'Process ID' }, + }, + required: ['processId'], + }, + }, + { + name: 'brevo_create_doi_contact', + description: 'Create double opt-in (DOI) contact subscription', + inputSchema: { + type: 'object', + properties: { + email: { type: 'string', description: 'Contact email' }, + attributes: { type: 'object', description: 'Contact attributes' }, + includeListIds: { type: 'array', items: { type: 'number' }, description: 'Lists to subscribe to' }, + templateId: { type: 'number', description: 'DOI template ID' }, + redirectionUrl: { type: 'string', description: 'Redirect URL after confirmation' }, + }, + required: ['email', 'includeListIds', 'templateId', 'redirectionUrl'], + }, + }, +]; + +// ============================================================================ +// TOOL HANDLERS +// ============================================================================ + +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools, +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + // CONTACTS + if (name === 'brevo_get_contact') { + const result = await client.getContact(args.identifier as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_contact') { + const result = await client.createContact(args as any); + return { content: [{ type: 'text', text: `Contact created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_contact') { + const { identifier, ...params } = args; + await client.updateContact(identifier as string, params as any); + return { content: [{ type: 'text', text: 'Contact updated successfully' }] }; + } + if (name === 'brevo_delete_contact') { + await client.deleteContact(args.identifier as string); + return { content: [{ type: 'text', text: 'Contact deleted successfully' }] }; + } + if (name === 'brevo_list_contacts') { + const result = await client.getContacts(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_import_contacts') { + const result = await client.importContacts(args as any); + return { content: [{ type: 'text', text: `Import started with process ID: ${result.processId}` }] }; + } + if (name === 'brevo_export_contacts') { + const result = await client.exportContacts(args as any); + return { content: [{ type: 'text', text: `Export started with ID: ${result.exportId}` }] }; + } + if (name === 'brevo_get_contact_stats') { + const result = await client.getContactStats(args.identifier as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + + // LISTS + if (name === 'brevo_list_contact_lists') { + const result = await client.getLists(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_contact_list') { + const result = await client.getList(args.listId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_contact_list') { + const result = await client.createList(args as any); + return { content: [{ type: 'text', text: `List created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_contact_list') { + const { listId, ...params } = args; + await client.updateList(listId as number, params as any); + return { content: [{ type: 'text', text: 'List updated successfully' }] }; + } + if (name === 'brevo_delete_contact_list') { + await client.deleteList(args.listId as number); + return { content: [{ type: 'text', text: 'List deleted successfully' }] }; + } + if (name === 'brevo_add_contact_to_list') { + const result = await client.addContactToList(args.listId as number, args.emails as string[]); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_remove_contact_from_list') { + const result = await client.removeContactFromList(args.listId as number, args.emails as string[]); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + + // FOLDERS + if (name === 'brevo_list_folders') { + const result = await client.getFolders(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_folder') { + const result = await client.createFolder(args as any); + return { content: [{ type: 'text', text: `Folder created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_folder') { + const { folderId, name } = args; + await client.updateFolder(folderId as number, { name: name as string }); + return { content: [{ type: 'text', text: 'Folder updated successfully' }] }; + } + if (name === 'brevo_delete_folder') { + await client.deleteFolder(args.folderId as number); + return { content: [{ type: 'text', text: 'Folder deleted successfully' }] }; + } + + // EMAIL CAMPAIGNS + if (name === 'brevo_list_email_campaigns') { + const result = await client.getEmailCampaigns(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_email_campaign') { + const result = await client.getEmailCampaign(args.campaignId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_email_campaign') { + const result = await client.createEmailCampaign(args as any); + return { content: [{ type: 'text', text: `Campaign created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_email_campaign') { + const { campaignId, ...params } = args; + await client.updateEmailCampaign(campaignId as number, params as any); + return { content: [{ type: 'text', text: 'Campaign updated successfully' }] }; + } + if (name === 'brevo_delete_email_campaign') { + await client.deleteEmailCampaign(args.campaignId as number); + return { content: [{ type: 'text', text: 'Campaign deleted successfully' }] }; + } + if (name === 'brevo_send_email_campaign') { + await client.sendEmailCampaignNow(args.campaignId as number); + return { content: [{ type: 'text', text: 'Campaign sent successfully' }] }; + } + if (name === 'brevo_send_test_email') { + await client.sendTestEmail(args.campaignId as number, args.emailTo as string[]); + return { content: [{ type: 'text', text: 'Test email sent successfully' }] }; + } + if (name === 'brevo_update_email_campaign_status') { + await client.updateEmailCampaignStatus(args.campaignId as number, args.status as any); + return { content: [{ type: 'text', text: 'Campaign status updated successfully' }] }; + } + if (name === 'brevo_get_email_campaign_report') { + const result = await client.getEmailCampaignReport(args.campaignId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + + // TRANSACTIONAL EMAIL + if (name === 'brevo_send_transactional_email') { + const result = await client.sendTransacEmail(args as any); + return { content: [{ type: 'text', text: `Email sent with message ID: ${result.messageId}` }] }; + } + if (name === 'brevo_get_transactional_email') { + const result = await client.getTransacEmailContent(args.uuid as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_transactional_email_events') { + const result = await client.getTransacEmailEvents(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_transactional_email_stats') { + const result = await client.getTransacEmailStats(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_block_email_domain') { + await client.blockDomain(args.domain as string); + return { content: [{ type: 'text', text: 'Domain blocked successfully' }] }; + } + if (name === 'brevo_unblock_email_domain') { + await client.unblockDomain(args.domain as string); + return { content: [{ type: 'text', text: 'Domain unblocked successfully' }] }; + } + + // EMAIL TEMPLATES + if (name === 'brevo_list_email_templates') { + const result = await client.getEmailTemplates(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_email_template') { + const result = await client.getEmailTemplate(args.templateId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_email_template') { + const result = await client.createEmailTemplate(args as any); + return { content: [{ type: 'text', text: `Template created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_email_template') { + const { templateId, ...params } = args; + await client.updateEmailTemplate(templateId as number, params as any); + return { content: [{ type: 'text', text: 'Template updated successfully' }] }; + } + if (name === 'brevo_delete_email_template') { + await client.deleteEmailTemplate(args.templateId as number); + return { content: [{ type: 'text', text: 'Template deleted successfully' }] }; + } + if (name === 'brevo_send_test_template') { + await client.sendTestEmailTemplate(args.templateId as number, { emailTo: args.emailTo as string[] }); + return { content: [{ type: 'text', text: 'Test template email sent successfully' }] }; + } + + // SENDERS + if (name === 'brevo_list_senders') { + const result = await client.getSenders(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_sender') { + const result = await client.createSender(args as any); + return { content: [{ type: 'text', text: `Sender created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_sender') { + const { senderId, ...params } = args; + await client.updateSender(senderId as number, params as any); + return { content: [{ type: 'text', text: 'Sender updated successfully' }] }; + } + if (name === 'brevo_delete_sender') { + await client.deleteSender(args.senderId as number); + return { content: [{ type: 'text', text: 'Sender deleted successfully' }] }; + } + + // SMS + if (name === 'brevo_send_transactional_sms') { + const result = await client.sendTransacSms(args as any); + return { content: [{ type: 'text', text: `SMS sent. Message ID: ${result.messageId}, Used credits: ${result.usedCredits}` }] }; + } + if (name === 'brevo_get_transactional_sms_events') { + const result = await client.getTransacSmsEvents(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_transactional_sms_stats') { + const result = await client.getTransacSmsStats(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_list_sms_campaigns') { + const result = await client.getSmsCampaigns(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_sms_campaign') { + const result = await client.getSmsCampaign(args.campaignId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_sms_campaign') { + const result = await client.createSmsCampaign(args as any); + return { content: [{ type: 'text', text: `SMS campaign created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_sms_campaign') { + const { campaignId, ...params } = args; + await client.updateSmsCampaign(campaignId as number, params as any); + return { content: [{ type: 'text', text: 'SMS campaign updated successfully' }] }; + } + if (name === 'brevo_send_sms_campaign') { + await client.sendSmsCampaignNow(args.campaignId as number); + return { content: [{ type: 'text', text: 'SMS campaign sent successfully' }] }; + } + + // CRM - DEALS + if (name === 'brevo_list_deals') { + const result = await client.getDeals(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_deal') { + const result = await client.getDeal(args.dealId as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_deal') { + const result = await client.createDeal(args as any); + return { content: [{ type: 'text', text: `Deal created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_deal') { + const { dealId, ...params } = args; + await client.updateDeal(dealId as string, params as any); + return { content: [{ type: 'text', text: 'Deal updated successfully' }] }; + } + if (name === 'brevo_delete_deal') { + await client.deleteDeal(args.dealId as string); + return { content: [{ type: 'text', text: 'Deal deleted successfully' }] }; + } + + // CRM - COMPANIES + if (name === 'brevo_list_companies') { + const result = await client.getCompanies(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_company') { + const result = await client.getCompany(args.companyId as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_company') { + const result = await client.createCompany(args as any); + return { content: [{ type: 'text', text: `Company created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_company') { + const { companyId, ...params } = args; + await client.updateCompany(companyId as string, params as any); + return { content: [{ type: 'text', text: 'Company updated successfully' }] }; + } + if (name === 'brevo_delete_company') { + await client.deleteCompany(args.companyId as string); + return { content: [{ type: 'text', text: 'Company deleted successfully' }] }; + } + + // CRM - TASKS + if (name === 'brevo_list_tasks') { + const result = await client.getTasks(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_task') { + const result = await client.getTask(args.taskId as string); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_task') { + const result = await client.createTask(args as any); + return { content: [{ type: 'text', text: `Task created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_task') { + const { taskId, ...params } = args; + await client.updateTask(taskId as string, params as any); + return { content: [{ type: 'text', text: 'Task updated successfully' }] }; + } + if (name === 'brevo_delete_task') { + await client.deleteTask(args.taskId as string); + return { content: [{ type: 'text', text: 'Task deleted successfully' }] }; + } + + // WEBHOOKS + if (name === 'brevo_list_webhooks') { + const result = await client.getWebhooks(args as any); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_webhook') { + const result = await client.createWebhook(args as any); + return { content: [{ type: 'text', text: `Webhook created with ID: ${result.id}` }] }; + } + if (name === 'brevo_update_webhook') { + const { webhookId, ...params } = args; + await client.updateWebhook(webhookId as number, params as any); + return { content: [{ type: 'text', text: 'Webhook updated successfully' }] }; + } + if (name === 'brevo_delete_webhook') { + await client.deleteWebhook(args.webhookId as number); + return { content: [{ type: 'text', text: 'Webhook deleted successfully' }] }; + } + + // ACCOUNT & MISC + if (name === 'brevo_get_account') { + const result = await client.getAccount(); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_get_process') { + const result = await client.getProcess(args.processId as number); + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; + } + if (name === 'brevo_create_doi_contact') { + await client.createDoiContact(args as any); + return { content: [{ type: 'text', text: 'DOI contact created successfully. Confirmation email sent.' }] }; + } + + throw new Error(`Unknown tool: ${name}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + return { + content: [{ type: 'text', text: `Error: ${errorMessage}` }], + isError: true, + }; + } +}); + +// ============================================================================ +// RESOURCES +// ============================================================================ + +server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + uri: 'brevo://account', + name: 'Account Information', + description: 'Current Brevo account details, plan, and credits', + mimeType: 'application/json', + }, + ], +})); + +server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + + if (uri === 'brevo://account') { + const account = await client.getAccount(); + return { + contents: [ + { + uri, + mimeType: 'application/json', + text: JSON.stringify(account, null, 2), + }, + ], + }; + } + + throw new Error(`Unknown resource: ${uri}`); +}); + +export async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Brevo MCP Server running on stdio'); } diff --git a/servers/close/src/api/client.ts b/servers/close/src/api/client.ts new file mode 100644 index 0000000..c46f39d --- /dev/null +++ b/servers/close/src/api/client.ts @@ -0,0 +1,779 @@ +/** + * Close CRM API Client + * Full implementation for Close API v1 with Basic Auth + */ + +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + CloseConfig, + Lead, + Contact, + Opportunity, + Activity, + Task, + Pipeline, + SmartView, + User, + CustomField, + Sequence, + SequenceSubscription, + PaginatedResponse, + SearchParams, + LeadSearchParams, + OpportunitySearchParams, +} from '../types.js'; + +export class CloseAPIClient { + private client: AxiosInstance; + private apiKey: string; + + constructor(config: CloseConfig) { + this.apiKey = config.apiKey; + this.client = axios.create({ + baseURL: config.baseUrl || 'https://api.close.com/api/v1', + headers: { + 'Content-Type': 'application/json', + }, + auth: { + username: config.apiKey, + password: '', // Close uses API key as username with empty password + }, + }); + } + + private handleError(error: any): never { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response) { + throw new Error( + `Close API Error: ${axiosError.response.status} - ${ + JSON.stringify(axiosError.response.data) || axiosError.message + }` + ); + } + throw new Error(`Close API Request Failed: ${axiosError.message}`); + } + throw error; + } + + // ============================================================================ + // LEADS + // ============================================================================ + + async getLeads(params?: LeadSearchParams): Promise> { + try { + const response = await this.client.get('/lead/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getLead(id: string): Promise { + try { + const response = await this.client.get(`/lead/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createLead(data: Partial): Promise { + try { + const response = await this.client.post('/lead/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateLead(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/lead/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteLead(id: string): Promise { + try { + await this.client.delete(`/lead/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + async searchLeads(query: string, params?: SearchParams): Promise> { + try { + const response = await this.client.get('/lead/', { + params: { query, ...params }, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async mergeLeads(source: string, destination: string): Promise { + try { + const response = await this.client.post('/lead/merge/', { + source, + destination, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // CONTACTS + // ============================================================================ + + async getContacts(params?: SearchParams): Promise> { + try { + const response = await this.client.get('/contact/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getContact(id: string): Promise { + try { + const response = await this.client.get(`/contact/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createContact(data: Partial): Promise { + try { + const response = await this.client.post('/contact/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateContact(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/contact/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteContact(id: string): Promise { + try { + await this.client.delete(`/contact/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // OPPORTUNITIES + // ============================================================================ + + async getOpportunities(params?: OpportunitySearchParams): Promise> { + try { + const response = await this.client.get('/opportunity/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getOpportunity(id: string): Promise { + try { + const response = await this.client.get(`/opportunity/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createOpportunity(data: Partial): Promise { + try { + const response = await this.client.post('/opportunity/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateOpportunity(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/opportunity/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteOpportunity(id: string): Promise { + try { + await this.client.delete(`/opportunity/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // ACTIVITIES + // ============================================================================ + + async getActivities(params?: SearchParams & { lead_id?: string }): Promise> { + try { + const response = await this.client.get('/activity/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getActivity(id: string): Promise { + try { + const response = await this.client.get(`/activity/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // Email Activities + async createEmail(data: any): Promise { + try { + const response = await this.client.post('/activity/email/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async sendEmail(data: any): Promise { + try { + const response = await this.client.post('/activity/email/', { + ...data, + status: 'sent', + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // Call Activities + async createCall(data: any): Promise { + try { + const response = await this.client.post('/activity/call/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateCall(id: string, data: any): Promise { + try { + const response = await this.client.put(`/activity/call/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // Note Activities + async createNote(data: { lead_id?: string; contact_id?: string; note: string }): Promise { + try { + const response = await this.client.post('/activity/note/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // SMS Activities + async sendSMS(data: any): Promise { + try { + const response = await this.client.post('/activity/sms/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // Meeting Activities + async createMeeting(data: any): Promise { + try { + const response = await this.client.post('/activity/meeting/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // TASKS + // ============================================================================ + + async getTasks(params?: SearchParams & { lead_id?: string; is_complete?: boolean }): Promise> { + try { + const response = await this.client.get('/task/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getTask(id: string): Promise { + try { + const response = await this.client.get(`/task/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createTask(data: Partial): Promise { + try { + const response = await this.client.post('/task/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateTask(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/task/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteTask(id: string): Promise { + try { + await this.client.delete(`/task/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + async completeTask(id: string): Promise { + try { + const response = await this.client.put(`/task/${id}/`, { + is_complete: true, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // PIPELINES + // ============================================================================ + + async getPipelines(): Promise<{ data: Pipeline[] }> { + try { + const response = await this.client.get('/status/opportunity/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getPipeline(id: string): Promise { + try { + const response = await this.client.get(`/status/opportunity/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getLeadStatuses(): Promise { + try { + const response = await this.client.get('/status/lead/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // SMART VIEWS + // ============================================================================ + + async getSmartViews(params?: { type?: string }): Promise> { + try { + const response = await this.client.get('/saved_search/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getSmartView(id: string): Promise { + try { + const response = await this.client.get(`/saved_search/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createSmartView(data: Partial): Promise { + try { + const response = await this.client.post('/saved_search/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateSmartView(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/saved_search/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteSmartView(id: string): Promise { + try { + await this.client.delete(`/saved_search/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // USERS + // ============================================================================ + + async getUsers(): Promise<{ data: User[] }> { + try { + const response = await this.client.get('/user/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getUser(id: string): Promise { + try { + const response = await this.client.get(`/user/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getCurrentUser(): Promise { + try { + const response = await this.client.get('/me/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // CUSTOM FIELDS + // ============================================================================ + + async getCustomFields(type?: 'lead' | 'contact' | 'opportunity'): Promise<{ data: CustomField[] }> { + try { + const response = await this.client.get('/custom_fields/', { + params: type ? { type } : undefined, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getCustomField(id: string): Promise { + try { + const response = await this.client.get(`/custom_fields/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createCustomField(data: Partial): Promise { + try { + const response = await this.client.post('/custom_fields/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async updateCustomField(id: string, data: Partial): Promise { + try { + const response = await this.client.put(`/custom_fields/${id}/`, data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteCustomField(id: string): Promise { + try { + await this.client.delete(`/custom_fields/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // SEQUENCES + // ============================================================================ + + async getSequences(): Promise<{ data: Sequence[] }> { + try { + const response = await this.client.get('/sequence/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getSequence(id: string): Promise { + try { + const response = await this.client.get(`/sequence/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async subscribeToSequence(data: { + sequence_id: string; + contact_id: string; + sender_account_id?: string; + }): Promise { + try { + const response = await this.client.post('/sequence_subscription/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getSequenceSubscriptions(params?: { + sequence_id?: string; + contact_id?: string; + lead_id?: string; + }): Promise> { + try { + const response = await this.client.get('/sequence_subscription/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async pauseSequenceSubscription(id: string): Promise { + try { + const response = await this.client.post(`/sequence_subscription/${id}/pause/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async resumeSequenceSubscription(id: string): Promise { + try { + const response = await this.client.post(`/sequence_subscription/${id}/resume/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async unsubscribeFromSequence(id: string): Promise { + try { + await this.client.delete(`/sequence_subscription/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // BULK OPERATIONS + // ============================================================================ + + async bulkDeleteLeads(leadIds: string[]): Promise { + try { + const response = await this.client.post('/lead/bulk_delete/', { + lead_ids: leadIds, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async bulkUpdateLeads(leadIds: string[], data: Partial): Promise { + try { + const response = await this.client.post('/lead/bulk_update/', { + lead_ids: leadIds, + ...data, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async bulkEmailLeads(leadIds: string[], emailData: any): Promise { + try { + const response = await this.client.post('/lead/bulk_email/', { + lead_ids: leadIds, + ...emailData, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async bulkDeleteContacts(contactIds: string[]): Promise { + try { + const response = await this.client.post('/contact/bulk_delete/', { + contact_ids: contactIds, + }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // REPORTING & ANALYTICS + // ============================================================================ + + async getLeadsReport(params?: any): Promise { + try { + const response = await this.client.get('/report/lead/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getOpportunitiesReport(params?: any): Promise { + try { + const response = await this.client.get('/report/opportunity/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getActivitiesReport(params?: any): Promise { + try { + const response = await this.client.get('/report/activity/', { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getUserActivityReport(userId: string, params?: any): Promise { + try { + const response = await this.client.get(`/report/user/${userId}/`, { params }); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // ORGANIZATION + // ============================================================================ + + async getOrganization(): Promise { + try { + const response = await this.client.get('/organization/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // WEBHOOKS + // ============================================================================ + + async getWebhooks(): Promise { + try { + const response = await this.client.get('/webhook/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async createWebhook(data: { url: string; events: string[] }): Promise { + try { + const response = await this.client.post('/webhook/', data); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async deleteWebhook(id: string): Promise { + try { + await this.client.delete(`/webhook/${id}/`); + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // EMAIL TEMPLATES + // ============================================================================ + + async getEmailTemplates(): Promise { + try { + const response = await this.client.get('/email_template/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + async getEmailTemplate(id: string): Promise { + try { + const response = await this.client.get(`/email_template/${id}/`); + return response.data; + } catch (error) { + return this.handleError(error); + } + } + + // ============================================================================ + // CONNECTED ACCOUNTS + // ============================================================================ + + async getConnectedAccounts(): Promise { + try { + const response = await this.client.get('/connected_account/'); + return response.data; + } catch (error) { + return this.handleError(error); + } + } +} diff --git a/servers/close/src/apps/ContactsManager.tsx b/servers/close/src/apps/ContactsManager.tsx new file mode 100644 index 0000000..e5ffe61 --- /dev/null +++ b/servers/close/src/apps/ContactsManager.tsx @@ -0,0 +1,133 @@ +import React, { useState, useEffect } from 'react'; +import { Contact, Mail, Phone, Plus, Search } from 'lucide-react'; + +export const ContactsManager: React.FC = () => { + const [contacts, setContacts] = useState([]); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const loadContacts = async () => { + setLoading(true); + try { + const result = await window.mcp?.callTool('close_list_contacts', { _limit: 100 }); + if (result?.content?.[0]?.text) { + const data = JSON.parse(result.content[0].text); + setContacts(data.data || []); + } + } catch (error) { + console.error('Failed to load contacts:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadContacts(); + }, []); + + const filteredContacts = contacts.filter(c => + c.name?.toLowerCase().includes(searchQuery.toLowerCase()) || + c.emails?.some((e: any) => e.email.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + return ( +
+
+
+
+ +

Contacts

+
+ +
+ +
+
+ + setSearchQuery(e.target.value)} + style={{ + width: '100%', + background: '#2a2a2a', + border: '1px solid #3a3a3a', + color: '#e0e0e0', + padding: '12px 12px 12px 44px', + borderRadius: '6px', + }} + /> +
+
+ + {loading ? ( +
Loading...
+ ) : ( +
+ {filteredContacts.map((contact) => ( +
window.mcp?.showPrompt(`Show contact ${contact.id}`)} + style={{ + background: '#2a2a2a', + border: '1px solid #3a3a3a', + borderRadius: '8px', + padding: '16px', + cursor: 'pointer', + }} + onMouseEnter={(e) => (e.currentTarget.style.borderColor = '#8b5cf6')} + onMouseLeave={(e) => (e.currentTarget.style.borderColor = '#3a3a3a')} + > +
+ {contact.name || 'Unnamed Contact'} +
+ {contact.title && ( +
+ {contact.title} +
+ )} + +
+ {contact.emails?.[0] && ( +
+ + {contact.emails[0].email} +
+ )} + {contact.phones?.[0] && ( +
+ + {contact.phones[0].phone} +
+ )} +
+
+ ))} +
+ )} +
+
+ ); +}; + +export default ContactsManager; diff --git a/servers/close/src/apps/LeadsDashboard.tsx b/servers/close/src/apps/LeadsDashboard.tsx new file mode 100644 index 0000000..cfe5a09 --- /dev/null +++ b/servers/close/src/apps/LeadsDashboard.tsx @@ -0,0 +1,154 @@ +import React, { useState, useEffect } from 'react'; +import { Users, Search, Filter, Plus } from 'lucide-react'; + +export const LeadsDashboard: React.FC = () => { + const [leads, setLeads] = useState([]); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const loadLeads = async () => { + setLoading(true); + try { + const result = await window.mcp?.callTool('close_list_leads', { + query: searchQuery, + _limit: 50, + }); + if (result?.content?.[0]?.text) { + const data = JSON.parse(result.content[0].text); + setLeads(data.data || []); + } + } catch (error) { + console.error('Failed to load leads:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadLeads(); + }, []); + + return ( +
+
+ {/* Header */} +
+
+ +

Leads Dashboard

+
+ +
+ + {/* Search Bar */} +
+
+ + setSearchQuery(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && loadLeads()} + style={{ + width: '100%', + background: '#2a2a2a', + border: '1px solid #3a3a3a', + color: '#e0e0e0', + padding: '12px 12px 12px 44px', + borderRadius: '6px', + fontSize: '14px', + }} + /> +
+
+ + {/* Stats */} +
+
+
Total Leads
+
{leads.length}
+
+
+
Active
+
+ {leads.filter(l => l.status_label?.toLowerCase().includes('active')).length} +
+
+
+ + {/* Leads Table */} + {loading ? ( +
Loading...
+ ) : ( +
+ + + + + + + + + + + {leads.map((lead) => ( + window.mcp?.showPrompt(`Show details for lead ${lead.id}`)} + style={{ borderBottom: '1px solid #3a3a3a', cursor: 'pointer' }} + onMouseEnter={(e) => (e.currentTarget.style.background = '#333')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + + + + + + ))} + +
NameStatusContactsCreated
+
{lead.display_name}
+ {lead.url &&
{lead.url}
} +
+ + {lead.status_label} + + {lead.contacts?.length || 0} + {new Date(lead.date_created).toLocaleDateString()} +
+
+ )} +
+
+ ); +}; + +export default LeadsDashboard; diff --git a/servers/close/src/apps/OpportunitiesPipeline.tsx b/servers/close/src/apps/OpportunitiesPipeline.tsx new file mode 100644 index 0000000..e94d887 --- /dev/null +++ b/servers/close/src/apps/OpportunitiesPipeline.tsx @@ -0,0 +1,147 @@ +import React, { useState, useEffect } from 'react'; +import { TrendingUp, DollarSign, Target } from 'lucide-react'; + +export const OpportunitiesPipeline: React.FC = () => { + const [opportunities, setOpportunities] = useState([]); + const [pipelines, setPipelines] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const [oppsResult, pipelinesResult] = await Promise.all([ + window.mcp?.callTool('close_list_opportunities', { _limit: 100 }), + window.mcp?.callTool('close_get_opportunity_pipelines', {}), + ]); + + if (oppsResult?.content?.[0]?.text) { + const data = JSON.parse(oppsResult.content[0].text); + setOpportunities(data.data || []); + } + if (pipelinesResult?.content?.[0]?.text) { + const data = JSON.parse(pipelinesResult.content[0].text); + setPipelines(data.data || []); + } + } catch (error) { + console.error('Failed to load data:', error); + } finally { + setLoading(false); + } + }; + + const totalValue = opportunities.reduce((sum, opp) => sum + (opp.value || 0), 0) / 100; + const activeOpps = opportunities.filter(o => o.status_type === 'active').length; + const wonOpps = opportunities.filter(o => o.status_type === 'won').length; + + return ( +
+
+
+ +

Opportunities Pipeline

+
+ + {/* Stats */} +
+
+
+ + Total Pipeline Value +
+
+ ${totalValue.toLocaleString()} +
+
+
+
+ + Active Opportunities +
+
+ {activeOpps} +
+
+
+
+ + Won This Month +
+
+ {wonOpps} +
+
+
+ + {/* Pipeline Board */} + {loading ? ( +
Loading pipeline...
+ ) : ( +
+ {['active', 'won', 'lost'].map((statusType) => { + const statusOpps = opportunities.filter(o => o.status_type === statusType); + const statusValue = statusOpps.reduce((sum, opp) => sum + (opp.value || 0), 0) / 100; + + return ( +
+
+
+ {statusType} +
+
+ {statusOpps.length} deals · ${statusValue.toLocaleString()} +
+
+ +
+ {statusOpps.map((opp) => ( +
window.mcp?.showPrompt(`Show opportunity ${opp.id}`)} + style={{ + background: '#333', + border: '1px solid #3a3a3a', + borderRadius: '6px', + padding: '12px', + cursor: 'pointer', + }} + onMouseEnter={(e) => (e.currentTarget.style.borderColor = '#3b82f6')} + onMouseLeave={(e) => (e.currentTarget.style.borderColor = '#3a3a3a')} + > +
+ {opp.lead_name || `Opportunity ${opp.id.slice(0, 8)}`} +
+
+ ${((opp.value || 0) / 100).toLocaleString()} +
+ {opp.confidence !== undefined && ( +
+ {opp.confidence}% confidence +
+ )} +
+ ))} +
+
+ ); + })} +
+ )} +
+
+ ); +}; + +export default OpportunitiesPipeline; diff --git a/servers/close/src/tools/index.ts b/servers/close/src/tools/index.ts new file mode 100644 index 0000000..9efa331 --- /dev/null +++ b/servers/close/src/tools/index.ts @@ -0,0 +1,976 @@ +/** + * Close CRM MCP Tools - 60+ Tools + * Complete tool definitions for MCP server + */ + +import { Tool } from '@modelcontextprotocol/sdk/types.js'; + +export const CLOSE_TOOLS: Tool[] = [ + // ============================================================================ + // LEAD TOOLS (10 tools) + // ============================================================================ + { + name: 'close_list_leads', + description: 'List all leads with optional filtering and pagination', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + status_id: { type: 'string', description: 'Filter by status ID' }, + user_id: { type: 'string', description: 'Filter by assigned user ID' }, + _skip: { type: 'number', description: 'Number of records to skip (pagination)' }, + _limit: { type: 'number', description: 'Max number of records to return (default: 100)' }, + }, + }, + }, + { + name: 'close_get_lead', + description: 'Get a specific lead by ID with all details', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Lead ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_lead', + description: 'Create a new lead', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Company/lead name' }, + description: { type: 'string', description: 'Lead description' }, + url: { type: 'string', description: 'Company website URL' }, + status_id: { type: 'string', description: 'Lead status ID' }, + contacts: { + type: 'array', + description: 'Array of contacts to create with this lead', + items: { type: 'object' }, + }, + custom: { type: 'object', description: 'Custom field values (key-value pairs)' }, + }, + required: ['name'], + }, + }, + { + name: 'close_update_lead', + description: 'Update an existing lead', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Lead ID' }, + name: { type: 'string', description: 'Company/lead name' }, + description: { type: 'string', description: 'Lead description' }, + url: { type: 'string', description: 'Company website URL' }, + status_id: { type: 'string', description: 'Lead status ID' }, + custom: { type: 'object', description: 'Custom field values' }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_lead', + description: 'Delete a lead permanently', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Lead ID to delete' }, + }, + required: ['id'], + }, + }, + { + name: 'close_search_leads', + description: 'Search leads with advanced query', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query (supports field:value syntax)' }, + _limit: { type: 'number', description: 'Max results' }, + }, + required: ['query'], + }, + }, + { + name: 'close_merge_leads', + description: 'Merge two leads (moves all data from source to destination)', + inputSchema: { + type: 'object', + properties: { + source: { type: 'string', description: 'Source lead ID (will be deleted)' }, + destination: { type: 'string', description: 'Destination lead ID (will receive all data)' }, + }, + required: ['source', 'destination'], + }, + }, + { + name: 'close_get_lead_statuses', + description: 'Get all available lead statuses', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_bulk_update_leads', + description: 'Update multiple leads at once', + inputSchema: { + type: 'object', + properties: { + lead_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Array of lead IDs to update', + }, + status_id: { type: 'string', description: 'New status ID' }, + custom: { type: 'object', description: 'Custom field updates' }, + }, + required: ['lead_ids'], + }, + }, + { + name: 'close_bulk_delete_leads', + description: 'Delete multiple leads at once', + inputSchema: { + type: 'object', + properties: { + lead_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Array of lead IDs to delete', + }, + }, + required: ['lead_ids'], + }, + }, + + // ============================================================================ + // CONTACT TOOLS (7 tools) + // ============================================================================ + { + name: 'close_list_contacts', + description: 'List all contacts with pagination', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Filter by lead ID' }, + _skip: { type: 'number', description: 'Pagination skip' }, + _limit: { type: 'number', description: 'Pagination limit' }, + }, + }, + }, + { + name: 'close_get_contact', + description: 'Get a specific contact by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Contact ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_contact', + description: 'Create a new contact', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID this contact belongs to' }, + name: { type: 'string', description: 'Contact full name' }, + title: { type: 'string', description: 'Job title' }, + emails: { + type: 'array', + description: 'Array of email objects [{email: "...", type: "office"}]', + items: { type: 'object' }, + }, + phones: { + type: 'array', + description: 'Array of phone objects [{phone: "...", type: "mobile"}]', + items: { type: 'object' }, + }, + }, + required: ['lead_id'], + }, + }, + { + name: 'close_update_contact', + description: 'Update an existing contact', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Contact ID' }, + name: { type: 'string', description: 'Contact name' }, + title: { type: 'string', description: 'Job title' }, + emails: { type: 'array', items: { type: 'object' } }, + phones: { type: 'array', items: { type: 'object' } }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_contact', + description: 'Delete a contact', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Contact ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_bulk_delete_contacts', + description: 'Delete multiple contacts at once', + inputSchema: { + type: 'object', + properties: { + contact_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Array of contact IDs to delete', + }, + }, + required: ['contact_ids'], + }, + }, + { + name: 'close_search_contacts', + description: 'Search contacts by name, email, or phone', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + _limit: { type: 'number', description: 'Max results' }, + }, + required: ['query'], + }, + }, + + // ============================================================================ + // OPPORTUNITY TOOLS (8 tools) + // ============================================================================ + { + name: 'close_list_opportunities', + description: 'List all opportunities with filtering', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Filter by lead ID' }, + status_id: { type: 'string', description: 'Filter by status ID' }, + status_type: { + type: 'string', + enum: ['active', 'won', 'lost'], + description: 'Filter by status type', + }, + user_id: { type: 'string', description: 'Filter by assigned user' }, + _skip: { type: 'number' }, + _limit: { type: 'number' }, + }, + }, + }, + { + name: 'close_get_opportunity', + description: 'Get a specific opportunity by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Opportunity ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_opportunity', + description: 'Create a new opportunity/deal', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + status_id: { type: 'string', description: 'Pipeline status ID' }, + value: { type: 'number', description: 'Deal value in cents' }, + value_period: { + type: 'string', + enum: ['one_time', 'monthly', 'annual'], + description: 'Value period', + }, + confidence: { type: 'number', description: 'Win probability (0-100)' }, + note: { type: 'string', description: 'Opportunity note' }, + contact_id: { type: 'string', description: 'Primary contact ID' }, + }, + required: ['lead_id', 'status_id'], + }, + }, + { + name: 'close_update_opportunity', + description: 'Update an existing opportunity', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Opportunity ID' }, + status_id: { type: 'string', description: 'New status ID' }, + value: { type: 'number', description: 'Deal value' }, + confidence: { type: 'number', description: 'Win probability' }, + note: { type: 'string', description: 'Note' }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_opportunity', + description: 'Delete an opportunity', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Opportunity ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_mark_opportunity_won', + description: 'Mark an opportunity as won', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Opportunity ID' }, + status_id: { type: 'string', description: 'Won status ID' }, + date_won: { type: 'string', description: 'Won date (ISO 8601)' }, + }, + required: ['id', 'status_id'], + }, + }, + { + name: 'close_mark_opportunity_lost', + description: 'Mark an opportunity as lost', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Opportunity ID' }, + status_id: { type: 'string', description: 'Lost status ID' }, + date_lost: { type: 'string', description: 'Lost date (ISO 8601)' }, + }, + required: ['id', 'status_id'], + }, + }, + { + name: 'close_get_opportunity_pipelines', + description: 'Get all opportunity pipelines and their statuses', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // ============================================================================ + // ACTIVITY TOOLS (12 tools) + // ============================================================================ + { + name: 'close_list_activities', + description: 'List activities (emails, calls, notes, etc.)', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Filter by lead ID' }, + contact_id: { type: 'string', description: 'Filter by contact ID' }, + _skip: { type: 'number' }, + _limit: { type: 'number' }, + }, + }, + }, + { + name: 'close_get_activity', + description: 'Get a specific activity by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Activity ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_send_email', + description: 'Send an email to a lead or contact', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + to: { + type: 'array', + items: { type: 'string' }, + description: 'Array of recipient email addresses', + }, + subject: { type: 'string', description: 'Email subject' }, + body_text: { type: 'string', description: 'Plain text body' }, + body_html: { type: 'string', description: 'HTML body' }, + cc: { type: 'array', items: { type: 'string' }, description: 'CC recipients' }, + bcc: { type: 'array', items: { type: 'string' }, description: 'BCC recipients' }, + }, + required: ['to', 'subject'], + }, + }, + { + name: 'close_create_email_draft', + description: 'Create an email draft (not sent)', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string' }, + contact_id: { type: 'string' }, + to: { type: 'array', items: { type: 'string' } }, + subject: { type: 'string' }, + body_text: { type: 'string' }, + body_html: { type: 'string' }, + }, + required: ['to', 'subject'], + }, + }, + { + name: 'close_log_call', + description: 'Log a phone call activity', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + direction: { + type: 'string', + enum: ['inbound', 'outbound'], + description: 'Call direction', + }, + status: { + type: 'string', + enum: ['completed', 'canceled', 'no-answer', 'busy', 'failed'], + description: 'Call outcome', + }, + duration: { type: 'number', description: 'Call duration in seconds' }, + note: { type: 'string', description: 'Call notes' }, + phone: { type: 'string', description: 'Phone number called' }, + }, + required: ['direction', 'status'], + }, + }, + { + name: 'close_update_call', + description: 'Update a call activity (e.g., add notes after call)', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Call activity ID' }, + note: { type: 'string', description: 'Call notes' }, + disposition: { type: 'string', description: 'Call disposition' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_note', + description: 'Add a note to a lead or contact', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + note: { type: 'string', description: 'Note content' }, + }, + required: ['note'], + }, + }, + { + name: 'close_send_sms', + description: 'Send an SMS message', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + text: { type: 'string', description: 'SMS message text' }, + remote_phone: { type: 'string', description: 'Recipient phone number' }, + local_phone: { type: 'string', description: 'Sender phone number' }, + }, + required: ['text', 'remote_phone'], + }, + }, + { + name: 'close_create_meeting', + description: 'Schedule a meeting activity', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + title: { type: 'string', description: 'Meeting title' }, + note: { type: 'string', description: 'Meeting notes/agenda' }, + starts_at: { type: 'string', description: 'Meeting start time (ISO 8601)' }, + ends_at: { type: 'string', description: 'Meeting end time (ISO 8601)' }, + location: { type: 'string', description: 'Meeting location' }, + }, + required: ['starts_at'], + }, + }, + { + name: 'close_bulk_email', + description: 'Send bulk email to multiple leads', + inputSchema: { + type: 'object', + properties: { + lead_ids: { + type: 'array', + items: { type: 'string' }, + description: 'Array of lead IDs', + }, + subject: { type: 'string', description: 'Email subject' }, + body_text: { type: 'string', description: 'Email body (plain text)' }, + body_html: { type: 'string', description: 'Email body (HTML)' }, + }, + required: ['lead_ids', 'subject'], + }, + }, + { + name: 'close_get_email_templates', + description: 'List all email templates', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_get_email_template', + description: 'Get a specific email template', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Template ID' }, + }, + required: ['id'], + }, + }, + + // ============================================================================ + // TASK TOOLS (6 tools) + // ============================================================================ + { + name: 'close_list_tasks', + description: 'List tasks with filtering', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Filter by lead ID' }, + contact_id: { type: 'string', description: 'Filter by contact ID' }, + is_complete: { type: 'boolean', description: 'Filter by completion status' }, + assigned_to: { type: 'string', description: 'Filter by assigned user ID' }, + _skip: { type: 'number' }, + _limit: { type: 'number' }, + }, + }, + }, + { + name: 'close_get_task', + description: 'Get a specific task by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_task', + description: 'Create a new task', + inputSchema: { + type: 'object', + properties: { + lead_id: { type: 'string', description: 'Lead ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + assigned_to: { type: 'string', description: 'User ID to assign to' }, + text: { type: 'string', description: 'Task description' }, + date: { type: 'string', description: 'Due date (ISO 8601)' }, + }, + required: ['text', 'assigned_to'], + }, + }, + { + name: 'close_update_task', + description: 'Update an existing task', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + text: { type: 'string', description: 'Task description' }, + date: { type: 'string', description: 'Due date' }, + assigned_to: { type: 'string', description: 'User ID' }, + is_complete: { type: 'boolean', description: 'Completion status' }, + }, + required: ['id'], + }, + }, + { + name: 'close_complete_task', + description: 'Mark a task as complete', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_task', + description: 'Delete a task', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + }, + required: ['id'], + }, + }, + + // ============================================================================ + // SMART VIEW TOOLS (5 tools) + // ============================================================================ + { + name: 'close_list_smart_views', + description: 'List all saved smart views/searches', + inputSchema: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['lead', 'opportunity', 'contact'], + description: 'Filter by view type', + }, + }, + }, + }, + { + name: 'close_get_smart_view', + description: 'Get a specific smart view by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Smart view ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_smart_view', + description: 'Create a new smart view/saved search', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'View name' }, + type: { + type: 'string', + enum: ['lead', 'opportunity', 'contact'], + description: 'View type', + }, + query: { type: 'object', description: 'Search query object' }, + }, + required: ['name', 'type', 'query'], + }, + }, + { + name: 'close_update_smart_view', + description: 'Update an existing smart view', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Smart view ID' }, + name: { type: 'string', description: 'View name' }, + query: { type: 'object', description: 'Search query' }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_smart_view', + description: 'Delete a smart view', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Smart view ID' }, + }, + required: ['id'], + }, + }, + + // ============================================================================ + // USER TOOLS (3 tools) + // ============================================================================ + { + name: 'close_list_users', + description: 'List all users in the organization', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_get_user', + description: 'Get a specific user by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'User ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_get_current_user', + description: 'Get the currently authenticated user', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + + // ============================================================================ + // CUSTOM FIELD TOOLS (5 tools) + // ============================================================================ + { + name: 'close_list_custom_fields', + description: 'List all custom fields', + inputSchema: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['lead', 'contact', 'opportunity'], + description: 'Filter by object type', + }, + }, + }, + }, + { + name: 'close_get_custom_field', + description: 'Get a specific custom field by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Custom field ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_create_custom_field', + description: 'Create a new custom field', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Field name' }, + type: { + type: 'string', + enum: ['text', 'number', 'date', 'choices', 'user'], + description: 'Field type', + }, + choices: { + type: 'array', + items: { type: 'string' }, + description: 'Available choices (for choices type)', + }, + }, + required: ['name', 'type'], + }, + }, + { + name: 'close_update_custom_field', + description: 'Update a custom field definition', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Custom field ID' }, + name: { type: 'string', description: 'Field name' }, + choices: { type: 'array', items: { type: 'string' } }, + }, + required: ['id'], + }, + }, + { + name: 'close_delete_custom_field', + description: 'Delete a custom field', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Custom field ID' }, + }, + required: ['id'], + }, + }, + + // ============================================================================ + // SEQUENCE TOOLS (6 tools) + // ============================================================================ + { + name: 'close_list_sequences', + description: 'List all email sequences', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_get_sequence', + description: 'Get a specific sequence by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Sequence ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_subscribe_to_sequence', + description: 'Subscribe a contact to an email sequence', + inputSchema: { + type: 'object', + properties: { + sequence_id: { type: 'string', description: 'Sequence ID' }, + contact_id: { type: 'string', description: 'Contact ID' }, + sender_account_id: { type: 'string', description: 'Email account ID to send from' }, + }, + required: ['sequence_id', 'contact_id'], + }, + }, + { + name: 'close_list_sequence_subscriptions', + description: 'List sequence subscriptions with filtering', + inputSchema: { + type: 'object', + properties: { + sequence_id: { type: 'string', description: 'Filter by sequence ID' }, + contact_id: { type: 'string', description: 'Filter by contact ID' }, + lead_id: { type: 'string', description: 'Filter by lead ID' }, + }, + }, + }, + { + name: 'close_pause_sequence_subscription', + description: 'Pause a sequence subscription', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Subscription ID' }, + }, + required: ['id'], + }, + }, + { + name: 'close_unsubscribe_from_sequence', + description: 'Unsubscribe/remove a contact from a sequence', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Subscription ID' }, + }, + required: ['id'], + }, + }, + + // ============================================================================ + // REPORTING TOOLS (4 tools) + // ============================================================================ + { + name: 'close_get_leads_report', + description: 'Get leads report/analytics', + inputSchema: { + type: 'object', + properties: { + date_start: { type: 'string', description: 'Start date (ISO 8601)' }, + date_end: { type: 'string', description: 'End date (ISO 8601)' }, + group_by: { type: 'string', description: 'Grouping field' }, + }, + }, + }, + { + name: 'close_get_opportunities_report', + description: 'Get opportunities/pipeline report', + inputSchema: { + type: 'object', + properties: { + date_start: { type: 'string', description: 'Start date' }, + date_end: { type: 'string', description: 'End date' }, + group_by: { type: 'string', description: 'Grouping field (e.g., status_id, user_id)' }, + }, + }, + }, + { + name: 'close_get_activities_report', + description: 'Get activities report (calls, emails, etc.)', + inputSchema: { + type: 'object', + properties: { + date_start: { type: 'string', description: 'Start date' }, + date_end: { type: 'string', description: 'End date' }, + activity_type: { type: 'string', description: 'Filter by activity type' }, + }, + }, + }, + { + name: 'close_get_user_activity_report', + description: 'Get activity report for a specific user', + inputSchema: { + type: 'object', + properties: { + user_id: { type: 'string', description: 'User ID' }, + date_start: { type: 'string', description: 'Start date' }, + date_end: { type: 'string', description: 'End date' }, + }, + required: ['user_id'], + }, + }, + + // ============================================================================ + // ORGANIZATION & MISC TOOLS (4 tools) + // ============================================================================ + { + name: 'close_get_organization', + description: 'Get organization details and settings', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_list_webhooks', + description: 'List all configured webhooks', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'close_create_webhook', + description: 'Create a new webhook subscription', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string', description: 'Webhook endpoint URL' }, + events: { + type: 'array', + items: { type: 'string' }, + description: 'Array of event types to subscribe to', + }, + }, + required: ['url', 'events'], + }, + }, + { + name: 'close_get_connected_accounts', + description: 'List all connected email/calendar accounts', + inputSchema: { + type: 'object', + properties: {}, + }, + }, +]; diff --git a/servers/close/src/types.ts b/servers/close/src/types.ts new file mode 100644 index 0000000..e592010 --- /dev/null +++ b/servers/close/src/types.ts @@ -0,0 +1,302 @@ +/** + * Close CRM MCP Server - TypeScript Type Definitions + * Complete type system for Close API v1 + */ + +// Configuration +export interface CloseConfig { + apiKey: string; + baseUrl?: string; +} + +// Core Resources +export interface Lead { + id: string; + name: string; + display_name: string; + status_id: string; + status_label: string; + description?: string; + url?: string; + created_by: string; + updated_by: string; + date_created: string; + date_updated: string; + organization_id: string; + contacts?: Contact[]; + opportunities?: Opportunity[]; + tasks?: Task[]; + custom?: Record; + addresses?: Address[]; +} + +export interface Contact { + id: string; + lead_id: string; + name: string; + title?: string; + emails: EmailAddress[]; + phones: PhoneNumber[]; + urls?: UrlField[]; + date_created: string; + date_updated: string; + created_by: string; + updated_by: string; + organization_id: string; +} + +export interface EmailAddress { + email: string; + type: string; +} + +export interface PhoneNumber { + phone: string; + type: string; + country?: string; +} + +export interface UrlField { + url: string; + type?: string; +} + +export interface Address { + address_1?: string; + address_2?: string; + city?: string; + state?: string; + zipcode?: string; + country?: string; + label?: string; +} + +export interface Opportunity { + id: string; + lead_id: string; + status_id: string; + status_label: string; + status_type: 'active' | 'won' | 'lost'; + confidence?: number; + value?: number; + value_period?: 'one_time' | 'monthly' | 'annual'; + value_currency?: string; + date_created: string; + date_updated: string; + created_by: string; + updated_by: string; + note?: string; + organization_id: string; + contact_id?: string; + user_id?: string; + date_won?: string; + date_lost?: string; +} + +export interface Activity { + id: string; + type: string; + lead_id?: string; + contact_id?: string; + opportunity_id?: string; + user_id: string; + created_by: string; + date_created: string; + date_updated: string; + organization_id: string; +} + +export interface Task { + id: string; + _type: 'lead' | 'contact'; + lead_id?: string; + contact_id?: string; + assigned_to: string; + text: string; + date: string; + is_complete: boolean; + created_by: string; + date_created: string; + date_updated: string; + organization_id: string; + view?: string; +} + +export interface Pipeline { + id: string; + name: string; + organization_id: string; + statuses: PipelineStatus[]; +} + +export interface PipelineStatus { + id: string; + label: string; + type: 'active' | 'won' | 'lost'; +} + +export interface SmartView { + id: string; + name: string; + type: 'lead' | 'opportunity' | 'contact'; + query: any; + organization_id: string; + created_by: string; + date_created: string; + date_updated: string; +} + +export interface User { + id: string; + first_name: string; + last_name: string; + email: string; + image?: string; + date_created: string; + date_updated: string; +} + +export interface CustomField { + id: string; + name: string; + type: 'text' | 'number' | 'date' | 'choices' | 'user'; + choices?: string[]; + accepts_multiple_values?: boolean; + editable_with_roles?: string[]; + created_by: string; + date_created: string; + date_updated: string; + organization_id: string; +} + +export interface Sequence { + id: string; + name: string; + status: 'active' | 'paused' | 'archived'; + created_by: string; + updated_by: string; + date_created: string; + date_updated: string; + organization_id: string; +} + +export interface SequenceSubscription { + id: string; + sequence_id: string; + contact_id: string; + lead_id: string; + sender_account_id: string; + sender_name: string; + sender_email: string; + status: 'active' | 'paused' | 'finished'; + date_created: string; + date_updated: string; +} + +// Activity Types +export interface EmailActivity extends Activity { + type: 'Email'; + direction: 'incoming' | 'outgoing'; + status: 'draft' | 'scheduled' | 'sent' | 'delivered' | 'bounced'; + subject?: string; + body_text?: string; + body_html?: string; + sender?: string; + to?: string[]; + cc?: string[]; + bcc?: string[]; + template_id?: string; + thread_id?: string; + opens?: any[]; + clicks?: any[]; +} + +export interface CallActivity extends Activity { + type: 'Call'; + direction: 'inbound' | 'outbound'; + status: 'completed' | 'canceled' | 'no-answer' | 'busy' | 'failed'; + duration?: number; + note?: string; + phone?: string; + disposition?: string; + recording_url?: string; + voicemail_url?: string; +} + +export interface NoteActivity extends Activity { + type: 'Note'; + note: string; +} + +export interface SMSActivity extends Activity { + type: 'SMS'; + direction: 'incoming' | 'outgoing'; + status: 'sent' | 'delivered' | 'failed'; + text: string; + local_phone?: string; + remote_phone?: string; +} + +export interface MeetingActivity extends Activity { + type: 'Meeting'; + title?: string; + note?: string; + starts_at?: string; + ends_at?: string; + location?: string; + attendees?: string[]; +} + +// Bulk Operations +export interface BulkAction { + id: string; + type: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + created_by: string; + date_created: string; + date_updated: string; + organization_id: string; +} + +// Reporting +export interface Report { + data: any[]; + aggregate?: Record; + cursor?: string; +} + +// API Response Types +export interface PaginatedResponse { + data: T[]; + has_more: boolean; + cursor?: string; +} + +export interface ApiResponse { + data?: T; + error?: string; +} + +// Search/Filter +export interface SearchParams { + query?: string; + _skip?: number; + _limit?: number; + _fields?: string[]; + _order_by?: string; +} + +export interface LeadSearchParams extends SearchParams { + status_id?: string; + user_id?: string; +} + +export interface OpportunitySearchParams extends SearchParams { + status_id?: string; + status_type?: 'active' | 'won' | 'lost'; + user_id?: string; + lead_id?: string; +} + +// Export types +export type ActivityType = EmailActivity | CallActivity | NoteActivity | SMSActivity | MeetingActivity; diff --git a/servers/fieldedge/src/clients/fieldedge.ts b/servers/fieldedge/src/clients/fieldedge.ts index f060c26..cf70a2e 100644 --- a/servers/fieldedge/src/clients/fieldedge.ts +++ b/servers/fieldedge/src/clients/fieldedge.ts @@ -1,290 +1,392 @@ /** * FieldEdge API Client - * Handles authentication, rate limiting, pagination, and error handling + * Handles authentication, requests, error mapping, and rate limiting */ -import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; import type { FieldEdgeConfig, - ApiResponse, + Customer, + Job, + Invoice, + Estimate, + Equipment, + Technician, + InventoryItem, + Payment, + Appointment, + WorkOrder, + Location, + StockAdjustment, PaginatedResponse, - QueryParams, + ApiResponse, + CustomerSearchParams, + JobSearchParams, + InvoiceSearchParams, } from '../types/index.js'; -export class FieldEdgeAPIError extends Error { - constructor( - message: string, - public statusCode?: number, - public response?: any - ) { - super(message); - this.name = 'FieldEdgeAPIError'; - } -} - export class FieldEdgeClient { - private client: AxiosInstance; - private config: FieldEdgeConfig; - private rateLimitRemaining: number = 1000; - private rateLimitReset: number = 0; + private apiKey: string; + private baseUrl: string; + private headers: Record; constructor(config: FieldEdgeConfig) { - this.config = { - apiUrl: 'https://api.fieldedge.com/v1', - timeout: 30000, - ...config, - }; - - this.client = axios.create({ - baseURL: this.config.apiUrl, - timeout: this.config.timeout, - headers: { - 'Authorization': `Bearer ${this.config.apiKey}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - ...(this.config.companyId && { 'X-Company-Id': this.config.companyId }), - }, - }); - - // Request interceptor for rate limiting - this.client.interceptors.request.use( - async (config) => { - // Check rate limit - if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) { - const waitTime = this.rateLimitReset - Date.now(); - console.warn(`Rate limit exceeded. Waiting ${waitTime}ms...`); - await new Promise(resolve => setTimeout(resolve, waitTime)); - } - return config; - }, - (error) => Promise.reject(error) - ); - - // Response interceptor for error handling and rate limit tracking - this.client.interceptors.response.use( - (response) => { - // Update rate limit info from headers - const remaining = response.headers['x-ratelimit-remaining']; - const reset = response.headers['x-ratelimit-reset']; - - if (remaining) this.rateLimitRemaining = parseInt(remaining); - if (reset) this.rateLimitReset = parseInt(reset) * 1000; - - return response; - }, - (error: AxiosError) => { - if (error.response) { - const { status, data } = error.response; - - // Handle specific error codes - switch (status) { - case 401: - throw new FieldEdgeAPIError('Authentication failed. Invalid API key.', 401, data); - case 403: - throw new FieldEdgeAPIError('Access forbidden. Check permissions.', 403, data); - case 404: - throw new FieldEdgeAPIError('Resource not found.', 404, data); - case 429: - throw new FieldEdgeAPIError('Rate limit exceeded.', 429, data); - case 500: - case 502: - case 503: - throw new FieldEdgeAPIError('FieldEdge server error. Please try again later.', status, data); - default: - const message = (data as any)?.message || (data as any)?.error || 'API request failed'; - throw new FieldEdgeAPIError(message, status, data); - } - } else if (error.request) { - throw new FieldEdgeAPIError('No response from FieldEdge API. Check network connection.'); - } else { - throw new FieldEdgeAPIError(`Request error: ${error.message}`); - } - } - ); - } - - /** - * Generic GET request with pagination support - */ - async get(endpoint: string, params?: QueryParams): Promise { - const response = await this.client.get(endpoint, { params }); - return response.data; - } - - /** - * Generic GET request for paginated data - */ - async getPaginated( - endpoint: string, - params?: QueryParams - ): Promise> { - const queryParams = { - page: params?.page || 1, - pageSize: params?.pageSize || 50, - ...params, - }; - - const response = await this.client.get(endpoint, { params: queryParams }); + this.apiKey = config.apiKey; + this.baseUrl = config.baseUrl || 'https://api.fieldedge.com/v1'; - return { - items: response.data.items || response.data.data || response.data, - total: response.data.total || response.data.items?.length || 0, - page: response.data.page || queryParams.page, - pageSize: response.data.pageSize || queryParams.pageSize, - totalPages: response.data.totalPages || Math.ceil((response.data.total || 0) / queryParams.pageSize), + if (config.environment === 'sandbox') { + this.baseUrl = 'https://sandbox-api.fieldedge.com/v1'; + } + + this.headers = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', }; } - /** - * Get all pages of paginated data - */ - async getAllPaginated( + private async request( + method: string, endpoint: string, - params?: QueryParams - ): Promise { - const allItems: T[] = []; - let page = 1; - let hasMore = true; - - while (hasMore) { - const response = await this.getPaginated(endpoint, { - ...params, - page, - pageSize: params?.pageSize || 100, + data?: any, + params?: Record + ): Promise { + const url = new URL(`${this.baseUrl}${endpoint}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)); + } }); - - allItems.push(...response.items); - - hasMore = page < response.totalPages; - page++; } - return allItems; - } - - /** - * 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): Promise { - const response = await this.client.put(endpoint, data); - return response.data; - } - - /** - * Generic PATCH request - */ - async patch(endpoint: string, data?: any): Promise { - const response = await this.client.patch(endpoint, data); - return response.data; - } - - /** - * Generic DELETE request - */ - async delete(endpoint: string): Promise { - const response = await this.client.delete(endpoint); - return response.data; - } - - /** - * Upload file - */ - async uploadFile(endpoint: string, file: Buffer, filename: string, mimeType?: string): Promise { - const formData = new FormData(); - const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength) as ArrayBuffer; - const blob = new Blob([arrayBuffer], { type: mimeType || 'application/octet-stream' }); - formData.append('file', blob, filename); - - const response = await this.client.post(endpoint, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - return response.data; - } - - /** - * Download file - */ - async downloadFile(endpoint: string): Promise { - const response = await this.client.get(endpoint, { - responseType: 'arraybuffer', - }); - - return Buffer.from(response.data); - } - - /** - * Test API connectivity - */ - async testConnection(): Promise { - try { - await this.client.get('/health'); - return true; - } catch (error) { - return false; - } - } - - /** - * Get API usage/rate limit info - */ - getRateLimitInfo(): { remaining: number; resetAt: number } { - return { - remaining: this.rateLimitRemaining, - resetAt: this.rateLimitReset, + const options: RequestInit = { + method, + headers: this.headers, }; + + if (data) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(url.toString(), options); + + if (!response.ok) { + const errorText = await response.text(); + let errorData; + try { + errorData = JSON.parse(errorText); + } catch { + errorData = { message: errorText }; + } + + throw new Error( + `FieldEdge API Error (${response.status}): ${errorData.message || errorText}` + ); + } + + const responseText = await response.text(); + if (!responseText) return {} as T; + + return JSON.parse(responseText) as T; + } catch (error) { + if (error instanceof Error) { + throw new Error(`FieldEdge API request failed: ${error.message}`); + } + throw error; + } } - /** - * Update API configuration - */ - updateConfig(config: Partial): void { - this.config = { ...this.config, ...config }; - - if (config.apiKey) { - this.client.defaults.headers['Authorization'] = `Bearer ${config.apiKey}`; - } - - if (config.companyId) { - this.client.defaults.headers['X-Company-Id'] = config.companyId; - } - - if (config.apiUrl) { - this.client.defaults.baseURL = config.apiUrl; - } - - if (config.timeout) { - this.client.defaults.timeout = config.timeout; - } + // Customer Methods + async getCustomers(params?: CustomerSearchParams): Promise> { + return this.request>('GET', '/customers', undefined, params); + } + + async getCustomer(customerId: string): Promise { + return this.request('GET', `/customers/${customerId}`); + } + + async createCustomer(data: Partial): Promise { + return this.request('POST', '/customers', data); + } + + async updateCustomer(customerId: string, data: Partial): Promise { + return this.request('PUT', `/customers/${customerId}`, data); + } + + async deleteCustomer(customerId: string): Promise { + return this.request('DELETE', `/customers/${customerId}`); + } + + async searchCustomers(query: string): Promise { + const response = await this.request>( + 'GET', + '/customers/search', + undefined, + { q: query } + ); + return response.data; + } + + // Job/Work Order Methods + async getJobs(params?: JobSearchParams): Promise> { + return this.request>('GET', '/jobs', undefined, params); + } + + async getJob(jobId: string): Promise { + return this.request('GET', `/jobs/${jobId}`); + } + + async createJob(data: Partial): Promise { + return this.request('POST', '/jobs', data); + } + + async updateJob(jobId: string, data: Partial): Promise { + return this.request('PUT', `/jobs/${jobId}`, data); + } + + async deleteJob(jobId: string): Promise { + return this.request('DELETE', `/jobs/${jobId}`); + } + + async getWorkOrders(jobId?: string): Promise { + const endpoint = jobId ? `/jobs/${jobId}/work-orders` : '/work-orders'; + return this.request('GET', endpoint); + } + + async getWorkOrder(workOrderId: string): Promise { + return this.request('GET', `/work-orders/${workOrderId}`); + } + + async createWorkOrder(data: Partial): Promise { + return this.request('POST', '/work-orders', data); + } + + async updateWorkOrder(workOrderId: string, data: Partial): Promise { + return this.request('PUT', `/work-orders/${workOrderId}`, data); + } + + // Scheduling Methods + async getAppointments(params?: { + technicianId?: string; + startDate?: string; + endDate?: string; + }): Promise { + return this.request('GET', '/appointments', undefined, params); + } + + async getAppointment(appointmentId: string): Promise { + return this.request('GET', `/appointments/${appointmentId}`); + } + + async createAppointment(data: Partial): Promise { + return this.request('POST', '/appointments', data); + } + + async updateAppointment(appointmentId: string, data: Partial): Promise { + return this.request('PUT', `/appointments/${appointmentId}`, data); + } + + async deleteAppointment(appointmentId: string): Promise { + return this.request('DELETE', `/appointments/${appointmentId}`); + } + + async getTechnicianSchedule(technicianId: string, startDate: string, endDate: string): Promise { + return this.request( + 'GET', + `/technicians/${technicianId}/schedule`, + undefined, + { startDate, endDate } + ); + } + + // Invoice Methods + async getInvoices(params?: InvoiceSearchParams): Promise> { + return this.request>('GET', '/invoices', undefined, params); + } + + async getInvoice(invoiceId: string): Promise { + return this.request('GET', `/invoices/${invoiceId}`); + } + + async createInvoice(data: Partial): Promise { + return this.request('POST', '/invoices', data); + } + + async updateInvoice(invoiceId: string, data: Partial): Promise { + return this.request('PUT', `/invoices/${invoiceId}`, data); + } + + async deleteInvoice(invoiceId: string): Promise { + return this.request('DELETE', `/invoices/${invoiceId}`); + } + + async sendInvoice(invoiceId: string, email?: string): Promise { + return this.request('POST', `/invoices/${invoiceId}/send`, { email }); + } + + async recordPayment(invoiceId: string, paymentData: Partial): Promise { + return this.request('POST', `/invoices/${invoiceId}/payments`, paymentData); + } + + async getPayments(invoiceId: string): Promise { + return this.request('GET', `/invoices/${invoiceId}/payments`); + } + + // Estimate Methods + async getEstimates(customerId?: string): Promise { + const params = customerId ? { customerId } : undefined; + return this.request('GET', '/estimates', undefined, params); + } + + async getEstimate(estimateId: string): Promise { + return this.request('GET', `/estimates/${estimateId}`); + } + + async createEstimate(data: Partial): Promise { + return this.request('POST', '/estimates', data); + } + + async updateEstimate(estimateId: string, data: Partial): Promise { + return this.request('PUT', `/estimates/${estimateId}`, data); + } + + async deleteEstimate(estimateId: string): Promise { + return this.request('DELETE', `/estimates/${estimateId}`); + } + + async sendEstimate(estimateId: string, email?: string): Promise { + return this.request('POST', `/estimates/${estimateId}/send`, { email }); + } + + async convertEstimateToJob(estimateId: string): Promise { + return this.request('POST', `/estimates/${estimateId}/convert-to-job`); + } + + // Equipment Methods + async getEquipment(customerId?: string): Promise { + const params = customerId ? { customerId } : undefined; + return this.request('GET', '/equipment', undefined, params); + } + + async getEquipmentById(equipmentId: string): Promise { + return this.request('GET', `/equipment/${equipmentId}`); + } + + async createEquipment(data: Partial): Promise { + return this.request('POST', '/equipment', data); + } + + async updateEquipment(equipmentId: string, data: Partial): Promise { + return this.request('PUT', `/equipment/${equipmentId}`, data); + } + + async deleteEquipment(equipmentId: string): Promise { + return this.request('DELETE', `/equipment/${equipmentId}`); + } + + async getEquipmentHistory(equipmentId: string): Promise { + return this.request('GET', `/equipment/${equipmentId}/history`); + } + + // Technician Methods + async getTechnicians(status?: 'active' | 'inactive'): Promise { + const params = status ? { status } : undefined; + return this.request('GET', '/technicians', undefined, params); + } + + async getTechnician(technicianId: string): Promise { + return this.request('GET', `/technicians/${technicianId}`); + } + + async createTechnician(data: Partial): Promise { + return this.request('POST', '/technicians', data); + } + + async updateTechnician(technicianId: string, data: Partial): Promise { + return this.request('PUT', `/technicians/${technicianId}`, data); + } + + async deleteTechnician(technicianId: string): Promise { + return this.request('DELETE', `/technicians/${technicianId}`); + } + + // Inventory Methods + async getInventoryItems(category?: string): Promise { + const params = category ? { category } : undefined; + return this.request('GET', '/inventory', undefined, params); + } + + async getInventoryItem(itemId: string): Promise { + return this.request('GET', `/inventory/${itemId}`); + } + + async createInventoryItem(data: Partial): Promise { + return this.request('POST', '/inventory', data); + } + + async updateInventoryItem(itemId: string, data: Partial): Promise { + return this.request('PUT', `/inventory/${itemId}`, data); + } + + async deleteInventoryItem(itemId: string): Promise { + return this.request('DELETE', `/inventory/${itemId}`); + } + + async adjustStock(itemId: string, adjustment: Partial): Promise { + return this.request('POST', `/inventory/${itemId}/adjust`, adjustment); + } + + async getStockHistory(itemId: string): Promise { + return this.request('GET', `/inventory/${itemId}/history`); + } + + async getLowStockItems(): Promise { + return this.request('GET', '/inventory/low-stock'); + } + + // Location Methods + async getLocations(customerId: string): Promise { + return this.request('GET', `/customers/${customerId}/locations`); + } + + async getLocation(locationId: string): Promise { + return this.request('GET', `/locations/${locationId}`); + } + + async createLocation(customerId: string, data: Partial): Promise { + return this.request('POST', `/customers/${customerId}/locations`, data); + } + + async updateLocation(locationId: string, data: Partial): Promise { + return this.request('PUT', `/locations/${locationId}`, data); + } + + async deleteLocation(locationId: string): Promise { + return this.request('DELETE', `/locations/${locationId}`); + } + + // Reporting Methods + async getRevenueReport(startDate: string, endDate: string): Promise { + return this.request('GET', '/reports/revenue', undefined, { startDate, endDate }); + } + + async getTechnicianPerformance(startDate: string, endDate: string, technicianId?: string): Promise { + const params: any = { startDate, endDate }; + if (technicianId) params.technicianId = technicianId; + return this.request('GET', '/reports/technician-performance', undefined, params); + } + + async getCustomerReport(customerId: string): Promise { + return this.request('GET', `/reports/customers/${customerId}`); + } + + async getJobStatusReport(startDate: string, endDate: string): Promise { + return this.request('GET', '/reports/job-status', undefined, { startDate, endDate }); + } + + async getInventoryValuation(): Promise { + return this.request('GET', '/reports/inventory-valuation'); } } - -// Singleton instance -let clientInstance: FieldEdgeClient | null = null; - -export function getFieldEdgeClient(config?: FieldEdgeConfig): FieldEdgeClient { - if (!clientInstance && config) { - clientInstance = new FieldEdgeClient(config); - } - - if (!clientInstance) { - throw new Error('FieldEdge client not initialized. Provide API key configuration.'); - } - - return clientInstance; -} - -export function initializeFieldEdgeClient(config: FieldEdgeConfig): FieldEdgeClient { - clientInstance = new FieldEdgeClient(config); - return clientInstance; -} diff --git a/servers/fieldedge/src/tools/customers-tools.ts b/servers/fieldedge/src/tools/customers-tools.ts new file mode 100644 index 0000000..d28d898 --- /dev/null +++ b/servers/fieldedge/src/tools/customers-tools.ts @@ -0,0 +1,160 @@ +/** + * FieldEdge Customer Tools + */ + +import { z } from 'zod'; +import type { FieldEdgeClient } from '../clients/fieldedge.js'; + +export function registerCustomerTools(client: FieldEdgeClient) { + return { + fieldedge_list_customers: { + description: 'List all customers with optional filters (status, type, search query)', + parameters: z.object({ + query: z.string().optional().describe('Search query for customer name, email, or phone'), + status: z.enum(['active', 'inactive']).optional().describe('Filter by customer status'), + type: z.enum(['residential', 'commercial']).optional().describe('Filter by customer type'), + tags: z.array(z.string()).optional().describe('Filter by tags'), + page: z.number().optional().describe('Page number for pagination'), + pageSize: z.number().optional().describe('Number of results per page'), + sortBy: z.enum(['name', 'createdDate', 'balance']).optional().describe('Sort field'), + sortOrder: z.enum(['asc', 'desc']).optional().describe('Sort order'), + }), + execute: async (args: any) => { + const customers = await client.getCustomers(args); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(customers, null, 2), + }, + ], + }; + }, + }, + + fieldedge_get_customer: { + description: 'Get detailed information about a specific customer by ID', + parameters: z.object({ + customerId: z.string().describe('The customer ID'), + }), + execute: async (args: { customerId: string }) => { + const customer = await client.getCustomer(args.customerId); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(customer, null, 2), + }, + ], + }; + }, + }, + + fieldedge_create_customer: { + description: 'Create a new customer', + parameters: z.object({ + firstName: z.string().describe('Customer first name'), + lastName: z.string().describe('Customer last name'), + companyName: z.string().optional().describe('Company name for commercial customers'), + email: z.string().optional().describe('Email address'), + phone: z.string().optional().describe('Primary phone number'), + mobilePhone: z.string().optional().describe('Mobile phone number'), + type: z.enum(['residential', 'commercial']).optional().describe('Customer type'), + address: z.object({ + street1: z.string(), + street2: z.string().optional(), + city: z.string(), + state: z.string(), + zip: z.string(), + country: z.string().optional(), + }).optional().describe('Customer address'), + tags: z.array(z.string()).optional().describe('Customer tags'), + notes: z.string().optional().describe('Notes about the customer'), + }), + execute: async (args: any) => { + const customer = await client.createCustomer(args); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(customer, null, 2), + }, + ], + }; + }, + }, + + fieldedge_update_customer: { + description: 'Update an existing customer', + parameters: 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'), + companyName: z.string().optional().describe('Company name'), + email: z.string().optional().describe('Email address'), + phone: z.string().optional().describe('Primary phone number'), + mobilePhone: z.string().optional().describe('Mobile phone number'), + status: z.enum(['active', 'inactive']).optional().describe('Customer status'), + type: z.enum(['residential', 'commercial']).optional().describe('Customer type'), + address: z.object({ + street1: z.string(), + street2: z.string().optional(), + city: z.string(), + state: z.string(), + zip: z.string(), + country: z.string().optional(), + }).optional().describe('Customer address'), + tags: z.array(z.string()).optional().describe('Customer tags'), + notes: z.string().optional().describe('Notes about the customer'), + }), + execute: async (args: any) => { + const { customerId, ...updateData } = args; + const customer = await client.updateCustomer(customerId, updateData); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(customer, null, 2), + }, + ], + }; + }, + }, + + fieldedge_delete_customer: { + description: 'Delete a customer', + parameters: z.object({ + customerId: z.string().describe('The customer ID to delete'), + }), + execute: async (args: { customerId: string }) => { + await client.deleteCustomer(args.customerId); + return { + content: [ + { + type: 'text' as const, + text: `Customer ${args.customerId} deleted successfully`, + }, + ], + }; + }, + }, + + fieldedge_search_customers: { + description: 'Search for customers by name, email, phone, or other criteria', + parameters: z.object({ + query: z.string().describe('Search query'), + }), + execute: async (args: { query: string }) => { + const customers = await client.searchCustomers(args.query); + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(customers, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/fieldedge/src/types/index.ts b/servers/fieldedge/src/types/index.ts index 1d13bf5..934f215 100644 --- a/servers/fieldedge/src/types/index.ts +++ b/servers/fieldedge/src/types/index.ts @@ -1,13 +1,12 @@ /** - * FieldEdge MCP Server Type Definitions - * Comprehensive types for field service management + * FieldEdge API TypeScript Types + * Field service management for HVAC, plumbing, electrical contractors */ export interface FieldEdgeConfig { apiKey: string; - apiUrl?: string; - companyId?: string; - timeout?: number; + baseUrl?: string; + environment?: 'production' | 'sandbox'; } // Customer Types @@ -20,17 +19,14 @@ export interface Customer { phone?: string; mobilePhone?: string; address?: Address; - billingAddress?: Address; - status: 'active' | 'inactive' | 'prospect'; - customerType: 'residential' | 'commercial'; - balance: number; - creditLimit?: number; - taxExempt: boolean; - notes?: string; - tags?: string[]; + balance?: number; + status: 'active' | 'inactive'; + type?: 'residential' | 'commercial'; + createdDate?: string; + modifiedDate?: string; customFields?: Record; - createdAt: string; - updatedAt: string; + tags?: string[]; + notes?: string; } export interface Address { @@ -50,210 +46,78 @@ export interface Job { jobNumber: string; customerId: string; locationId?: string; - jobType: string; - status: JobStatus; + status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold'; priority: 'low' | 'normal' | 'high' | 'emergency'; - description: string; - scheduledStart?: string; - scheduledEnd?: string; - actualStart?: string; - actualEnd?: string; - assignedTechnicians: string[]; - equipmentIds?: string[]; - subtotal: number; - tax: number; - total: number; - invoiceId?: string; + jobType: string; + description?: string; + scheduledDate?: string; + scheduledStartTime?: string; + scheduledEndTime?: string; + actualStartTime?: string; + actualEndTime?: string; + assignedTechnicianIds?: string[]; + estimatedDuration?: number; tags?: string[]; customFields?: Record; - createdAt: string; - updatedAt: string; + createdDate: string; + modifiedDate?: string; + completedDate?: string; } -export type JobStatus = - | 'scheduled' - | 'dispatched' - | 'in-progress' - | 'on-hold' - | 'completed' - | 'cancelled' - | 'invoiced'; - -// Invoice Types -export interface Invoice { +export interface WorkOrder { id: string; - invoiceNumber: string; - customerId: string; - jobId?: string; - status: InvoiceStatus; - issueDate: string; - dueDate: string; - paidDate?: string; - lineItems: LineItem[]; - subtotal: number; - tax: number; - discount: number; - total: number; - amountPaid: number; - balance: number; - paymentTerms?: string; + jobId: string; + workOrderNumber: string; + status: 'open' | 'in_progress' | 'completed' | 'invoiced'; + lineItems?: LineItem[]; + subtotal?: number; + tax?: number; + total?: number; notes?: string; - createdAt: string; - updatedAt: string; + createdDate: string; } -export type InvoiceStatus = 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void'; - export interface LineItem { - id: string; - type: 'service' | 'part' | 'equipment' | 'labor'; + id?: string; + type: 'labor' | 'material' | 'equipment' | 'service'; description: string; quantity: number; unitPrice: number; - discount: number; - tax: number; total: number; + taxable?: boolean; itemId?: string; } -// Estimate Types -export interface Estimate { - id: string; - estimateNumber: string; - customerId: string; - status: EstimateStatus; - issueDate: string; - expiryDate: string; - lineItems: LineItem[]; - subtotal: number; - tax: number; - discount: number; - total: number; - approvedDate?: string; - jobId?: string; - notes?: string; - createdAt: string; - updatedAt: string; -} - -export type EstimateStatus = 'draft' | 'sent' | 'viewed' | 'approved' | 'declined' | 'expired'; - -// Scheduling/Dispatch Types +// Scheduling Types export interface Appointment { id: string; jobId: string; - customerId: string; technicianId: string; + scheduledDate: string; startTime: string; endTime: string; duration: number; - status: AppointmentStatus; - appointmentType: string; - arrivalWindow?: { - start: string; - end: string; - }; - actualArrival?: string; - actualDeparture?: string; + status: 'scheduled' | 'confirmed' | 'en_route' | 'arrived' | 'completed' | 'cancelled'; notes?: string; - createdAt: string; - updatedAt: string; + customerNotified?: boolean; } -export type AppointmentStatus = 'scheduled' | 'confirmed' | 'dispatched' | 'en-route' | 'arrived' | 'completed' | 'cancelled' | 'no-show'; - -export interface DispatchBoard { - date: string; - technicians: TechnicianSchedule[]; - unassignedJobs: Job[]; -} - -export interface TechnicianSchedule { - technicianId: string; - technicianName: string; - appointments: Appointment[]; - availability: TimeSlot[]; - capacity: number; -} - -export interface TimeSlot { - start: string; - end: string; - available: boolean; -} - -// Inventory Types -export interface InventoryItem { - id: string; - sku: string; - name: string; - description?: string; - category: string; - manufacturer?: string; - modelNumber?: string; - unitOfMeasure: string; - costPrice: number; - sellPrice: number; - msrp?: number; - quantityOnHand: number; - quantityAllocated: number; - quantityAvailable: number; - reorderPoint: number; - reorderQuantity: number; - warehouse?: string; - binLocation?: string; - serialized: boolean; - taxable: boolean; - tags?: string[]; - createdAt: string; - updatedAt: string; -} - -export interface InventoryTransaction { - id: string; - itemId: string; - type: 'receipt' | 'issue' | 'adjustment' | 'transfer' | 'return'; - quantity: number; - unitCost?: number; - totalCost?: number; - reference?: string; - jobId?: string; - technicianId?: string; - notes?: string; - createdAt: string; -} - -// Technician Types export interface Technician { id: string; - employeeNumber: string; firstName: string; lastName: string; email: string; - phone: string; - status: 'active' | 'inactive' | 'on-leave'; - role: string; - skills: string[]; - certifications: Certification[]; + phone?: string; + status: 'active' | 'inactive'; + certifications?: string[]; + skills?: string[]; hourlyRate?: number; - overtimeRate?: number; - serviceRadius?: number; - homeAddress?: Address; - vehicleId?: string; - defaultWorkHours?: WorkHours; - createdAt: string; - updatedAt: string; + color?: string; // For calendar display + defaultSchedule?: WeeklySchedule; + avatar?: string; } -export interface Certification { - name: string; - number: string; - issuer: string; - issueDate: string; - expiryDate?: string; -} - -export interface WorkHours { +export interface WeeklySchedule { monday?: DaySchedule; tuesday?: DaySchedule; wednesday?: DaySchedule; @@ -264,339 +128,225 @@ export interface WorkHours { } export interface DaySchedule { - start: string; + start: string; // HH:mm format end: string; - breaks?: TimeSlot[]; + breaks?: TimeRange[]; +} + +export interface TimeRange { + start: string; + end: string; +} + +// Invoice Types +export interface Invoice { + id: string; + invoiceNumber: string; + customerId: string; + jobId?: string; + workOrderId?: string; + status: 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void'; + issueDate: string; + dueDate: string; + lineItems: LineItem[]; + subtotal: number; + taxRate?: number; + taxAmount: number; + discountAmount?: number; + total: number; + amountPaid?: number; + balance?: number; + notes?: string; + terms?: string; + createdDate: string; + paidDate?: string; } -// Payment Types export interface Payment { id: string; invoiceId: string; - customerId: string; amount: number; - paymentMethod: PaymentMethod; paymentDate: string; - reference?: string; - cardLast4?: string; - checkNumber?: string; - status: 'pending' | 'processed' | 'failed' | 'refunded'; + paymentMethod: 'cash' | 'check' | 'credit_card' | 'ach' | 'other'; + referenceNumber?: string; notes?: string; - createdAt: string; - updatedAt: string; } -export type PaymentMethod = 'cash' | 'check' | 'credit-card' | 'debit-card' | 'ach' | 'wire' | 'other'; +// Estimate Types +export interface Estimate { + id: string; + estimateNumber: string; + customerId: string; + locationId?: string; + status: 'draft' | 'sent' | 'viewed' | 'accepted' | 'declined' | 'expired'; + issueDate: string; + expirationDate?: string; + lineItems: LineItem[]; + subtotal: number; + taxRate?: number; + taxAmount: number; + discountAmount?: number; + total: number; + notes?: string; + terms?: string; + acceptedDate?: string; + jobId?: string; // If converted to job + createdDate: string; +} // Equipment Types export interface Equipment { id: string; customerId: string; locationId?: string; - type: string; - manufacturer: string; - model: string; + type: string; // HVAC, Plumbing, Electrical + manufacturer?: string; + model?: string; serialNumber?: string; installDate?: string; - warrantyExpiry?: string; + warrantyExpiration?: string; + notes?: string; + status: 'active' | 'inactive' | 'retired'; + maintenanceSchedule?: MaintenanceSchedule; lastServiceDate?: string; - nextServiceDue?: string; - status: 'active' | 'inactive' | 'decommissioned'; - notes?: string; - customFields?: Record; - createdAt: string; - updatedAt: string; } -export interface ServiceHistory { +export interface MaintenanceSchedule { + frequency: 'monthly' | 'quarterly' | 'semi_annual' | 'annual'; + lastServiceDate?: string; + nextServiceDate?: string; + notes?: string; +} + +// Inventory Types +export interface InventoryItem { id: string; - equipmentId: string; - jobId: string; - serviceDate: string; - technicianId: string; - serviceType: string; - description: string; - partsReplaced?: LineItem[]; - cost: number; + sku: string; + name: string; + description?: string; + category?: string; + type: 'part' | 'material' | 'equipment'; + unitCost: number; + unitPrice: number; + quantityOnHand: number; + reorderPoint?: number; + reorderQuantity?: number; + preferredVendor?: string; + location?: string; + status: 'active' | 'inactive' | 'discontinued'; +} + +export interface StockAdjustment { + id: string; + itemId: string; + adjustmentType: 'addition' | 'subtraction' | 'transfer' | 'count'; + quantity: number; + reason?: string; + technicianId?: string; + jobId?: string; + date: string; notes?: string; } -// Location Types +// Reporting Types +export interface RevenueReport { + startDate: string; + endDate: string; + totalRevenue: number; + invoicedAmount: number; + paidAmount: number; + outstandingAmount: number; + byJobType?: Record; + byTechnician?: Record; + byCustomer?: Record; +} + +export interface TechnicianPerformance { + technicianId: string; + technicianName: string; + period: string; + jobsCompleted: number; + revenueGenerated: number; + hoursWorked: number; + avgJobDuration: number; + customerRating?: number; + efficiency?: number; +} + +export interface CustomerReport { + customerId: string; + totalJobs: number; + totalRevenue: number; + averageInvoice: number; + lastServiceDate?: string; + lifetimeValue: number; +} + +// Location Types (for multi-location customers) export interface Location { id: string; customerId: string; name: string; address: Address; - type: 'primary' | 'secondary' | 'billing' | 'service'; contactName?: string; contactPhone?: string; - accessNotes?: string; - gateCode?: string; - equipmentIds?: string[]; - status: 'active' | 'inactive'; - createdAt: string; - updatedAt: string; -} - -// Price Book Types -export interface PriceBook { - id: string; - name: string; - description?: string; - effectiveDate: string; - expiryDate?: string; - isDefault: boolean; - status: 'active' | 'inactive'; - items: PriceBookItem[]; - createdAt: string; - updatedAt: string; -} - -export interface PriceBookItem { - id: string; - itemId: string; - itemType: 'service' | 'part' | 'equipment' | 'labor'; - name: string; - sku?: string; - description?: string; - unitPrice: number; - costPrice?: number; - margin?: number; - taxable: boolean; - category?: string; -} - -// Service Agreement Types -export interface ServiceAgreement { - id: string; - customerId: string; - agreementNumber: string; - name: string; - type: 'maintenance' | 'warranty' | 'service-plan'; - status: 'active' | 'expired' | 'cancelled'; - startDate: string; - endDate: string; - renewalDate?: string; - autoRenew: boolean; - billingCycle: 'monthly' | 'quarterly' | 'annual'; - amount: number; - services: ServiceItem[]; - equipmentIds?: string[]; notes?: string; - createdAt: string; - updatedAt: string; -} - -export interface ServiceItem { - id: string; - name: string; - description: string; - frequency: 'weekly' | 'monthly' | 'quarterly' | 'semi-annual' | 'annual'; - nextDue?: string; - lastCompleted?: string; -} - -// Task Types -export interface Task { - id: string; - jobId?: string; - customerId?: string; - technicianId?: string; - title: string; - description: string; - type: 'call' | 'email' | 'follow-up' | 'inspection' | 'other'; - priority: 'low' | 'normal' | 'high' | 'urgent'; - status: 'pending' | 'in-progress' | 'completed' | 'cancelled'; - dueDate?: string; - completedDate?: string; - assignedTo?: string; - notes?: string; - createdAt: string; - updatedAt: string; -} - -// Call Tracking Types -export interface Call { - id: string; - customerId?: string; - phone: string; - direction: 'inbound' | 'outbound'; - callType: 'inquiry' | 'booking' | 'follow-up' | 'support' | 'sales'; - status: 'answered' | 'missed' | 'voicemail' | 'busy'; - duration?: number; - recordingUrl?: string; - notes?: string; - outcome?: string; - jobId?: string; - technicianId?: string; - timestamp: string; -} - -// Reporting Types -export interface Report { - id: string; - name: string; - type: ReportType; - parameters?: Record; - generatedAt: string; - data: any; -} - -export type ReportType = - | 'revenue' - | 'technician-productivity' - | 'job-completion' - | 'customer-satisfaction' - | 'inventory-valuation' - | 'aging-receivables' - | 'sales-by-category' - | 'equipment-maintenance' - | 'custom'; - -export interface RevenueReport { - period: string; - totalRevenue: number; - invoicedAmount: number; - collectedAmount: number; - outstandingAmount: number; - byCategory: Record; - byTechnician: Record; - trend: RevenueDataPoint[]; -} - -export interface RevenueDataPoint { - date: string; - revenue: number; - jobs: number; -} - -export interface TechnicianProductivityReport { - period: string; - technicians: TechnicianMetrics[]; -} - -export interface TechnicianMetrics { - technicianId: string; - technicianName: string; - jobsCompleted: number; - hoursWorked: number; - revenue: number; - averageJobTime: number; - utilizationRate: number; + isPrimary?: boolean; } // API Response Types -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; - message?: string; -} - export interface PaginatedResponse { - items: T[]; - total: number; - page: number; + data: T[]; + totalCount: number; pageSize: number; + currentPage: number; totalPages: number; } -export interface QueryParams { +export interface ApiResponse { + success: boolean; + data?: T; + error?: ApiError; + message?: string; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +// Search/Filter Types +export interface CustomerSearchParams { + query?: string; + status?: 'active' | 'inactive'; + type?: 'residential' | 'commercial'; + tags?: string[]; page?: number; pageSize?: number; - sortBy?: string; + sortBy?: 'name' | 'createdDate' | 'balance'; sortOrder?: 'asc' | 'desc'; - filter?: Record; - search?: string; - [key: string]: any; // Allow additional query parameters } -// Vehicle Types -export interface Vehicle { - id: string; - name: string; - type: 'truck' | 'van' | 'car' | 'trailer'; - make: string; - model: string; - year: number; - vin?: string; - licensePlate: string; - status: 'active' | 'maintenance' | 'inactive'; - assignedTechnicianId?: string; - mileage?: number; - lastServiceDate?: string; - nextServiceDue?: string; - inventoryItems?: string[]; - gpsEnabled: boolean; - createdAt: string; - updatedAt: string; -} - -// Time Tracking Types -export interface TimeEntry { - id: string; - technicianId: string; - jobId?: string; - type: 'regular' | 'overtime' | 'travel' | 'break'; - startTime: string; - endTime?: string; - duration?: number; - billable: boolean; - approved: boolean; - notes?: string; - createdAt: string; - updatedAt: string; -} - -// Forms/Checklist Types -export interface Form { - id: string; - name: string; - description?: string; - type: 'inspection' | 'safety' | 'maintenance' | 'quote' | 'custom'; - status: 'active' | 'inactive'; - fields: FormField[]; - createdAt: string; - updatedAt: string; -} - -export interface FormField { - id: string; - label: string; - type: 'text' | 'number' | 'checkbox' | 'select' | 'radio' | 'date' | 'signature' | 'photo'; - required: boolean; - options?: string[]; - defaultValue?: any; - validationRules?: Record; -} - -export interface FormSubmission { - id: string; - formId: string; - jobId?: string; - technicianId: string; +export interface JobSearchParams { customerId?: string; - responses: Record; - attachments?: string[]; - submittedAt: string; + status?: Job['status']; + priority?: Job['priority']; + technicianId?: string; + startDate?: string; + endDate?: string; + jobType?: string; + page?: number; + pageSize?: number; } -// Marketing Campaign Types -export interface Campaign { - id: string; - name: string; - type: 'email' | 'sms' | 'direct-mail' | 'social'; - status: 'draft' | 'scheduled' | 'active' | 'completed' | 'cancelled'; - startDate: string; +export interface InvoiceSearchParams { + customerId?: string; + status?: Invoice['status']; + startDate?: string; endDate?: string; - targetAudience: string[]; - message: string; - sentCount: number; - deliveredCount: number; - openedCount: number; - clickedCount: number; - convertedCount: number; - roi?: number; - createdAt: string; - updatedAt: string; + minAmount?: number; + maxAmount?: number; + page?: number; + pageSize?: number; } diff --git a/servers/freshdesk/src/api-client.ts b/servers/freshdesk/src/api-client.ts new file mode 100644 index 0000000..4ced042 --- /dev/null +++ b/servers/freshdesk/src/api-client.ts @@ -0,0 +1,503 @@ +// FreshDesk API v2 Client with Basic Auth + +import type { FreshDeskConfig } from './types/index.js'; + +export class FreshDeskClient { + private domain: string; + private authHeader: string; + private baseUrl: string; + + constructor(config: FreshDeskConfig) { + this.domain = config.domain; + // Basic auth: base64(apiKey:X) + this.authHeader = 'Basic ' + Buffer.from(`${config.apiKey}:X`).toString('base64'); + this.baseUrl = `https://${this.domain}.freshdesk.com/api/v2`; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const url = `${this.baseUrl}${endpoint}`; + + const headers = new Headers(options.headers); + headers.set('Authorization', this.authHeader); + headers.set('Content-Type', 'application/json'); + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `FreshDesk API error (${response.status}): ${errorText}` + ); + } + + // Handle 204 No Content + if (response.status === 204) { + return {} as T; + } + + const data = await response.json(); + return data as T; + } + + // GET request + async get(endpoint: string, params?: Record): Promise { + let url = endpoint; + if (params) { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if (Array.isArray(value)) { + value.forEach((v) => searchParams.append(key, String(v))); + } else { + searchParams.append(key, String(value)); + } + } + }); + const queryString = searchParams.toString(); + if (queryString) { + url += `?${queryString}`; + } + } + return this.request(url, { method: 'GET' }); + } + + // POST request + async post(endpoint: string, body?: any): Promise { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(body), + }); + } + + // PUT request + async put(endpoint: string, body?: any): Promise { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(body), + }); + } + + // DELETE request + async delete(endpoint: string): Promise { + return this.request(endpoint, { method: 'DELETE' }); + } + + // TICKETS + async getTickets(params?: any) { + return this.get('/tickets', params); + } + + async getTicket(id: number, include?: string[]) { + const params = include ? { include: include.join(',') } : undefined; + return this.get(`/tickets/${id}`, params); + } + + async createTicket(data: any) { + return this.post('/tickets', data); + } + + async updateTicket(id: number, data: any) { + return this.put(`/tickets/${id}`, data); + } + + async deleteTicket(id: number) { + return this.delete(`/tickets/${id}`); + } + + async restoreTicket(id: number) { + return this.put(`/tickets/${id}/restore`, {}); + } + + async getTicketConversations(id: number) { + return this.get(`/tickets/${id}/conversations`); + } + + async addTicketReply(id: number, data: any) { + return this.post(`/tickets/${id}/reply`, data); + } + + async addTicketNote(id: number, data: any) { + return this.post(`/tickets/${id}/notes`, data); + } + + async getTicketTimeEntries(id: number) { + return this.get(`/tickets/${id}/time_entries`); + } + + async getTicketSatisfactionRatings(id: number) { + return this.get(`/tickets/${id}/satisfaction_ratings`); + } + + async searchTickets(query: string) { + return this.get(`/search/tickets?query="${encodeURIComponent(query)}"`); + } + + async filterTickets(query: string, params?: any) { + return this.get(`/tickets/filter?query=${encodeURIComponent(query)}`, params); + } + + // CONTACTS + async getContacts(params?: any) { + return this.get('/contacts', params); + } + + async getContact(id: number) { + return this.get(`/contacts/${id}`); + } + + async createContact(data: any) { + return this.post('/contacts', data); + } + + async updateContact(id: number, data: any) { + return this.put(`/contacts/${id}`, data); + } + + async deleteContact(id: number) { + return this.delete(`/contacts/${id}`); + } + + async makeContactAgent(id: number) { + return this.put(`/contacts/${id}/make_agent`, {}); + } + + async searchContacts(params: any) { + return this.get('/contacts/autocomplete', params); + } + + async filterContacts(query: string) { + return this.get(`/contacts?query=${encodeURIComponent(query)}`); + } + + // COMPANIES + async getCompanies(params?: any) { + return this.get('/companies', params); + } + + async getCompany(id: number) { + return this.get(`/companies/${id}`); + } + + async createCompany(data: any) { + return this.post('/companies', data); + } + + async updateCompany(id: number, data: any) { + return this.put(`/companies/${id}`, data); + } + + async deleteCompany(id: number) { + return this.delete(`/companies/${id}`); + } + + async searchCompanies(name: string) { + return this.get('/companies/autocomplete', { name }); + } + + async filterCompanies(query: string) { + return this.get(`/companies/filter?query=${encodeURIComponent(query)}`); + } + + // AGENTS + async getAgents(params?: any) { + return this.get('/agents', params); + } + + async getAgent(id: number) { + return this.get(`/agents/${id}`); + } + + async updateAgent(id: number, data: any) { + return this.put(`/agents/${id}`, data); + } + + async deleteAgent(id: number) { + return this.delete(`/agents/${id}`); + } + + async getCurrentAgent() { + return this.get('/agents/me'); + } + + // GROUPS + async getGroups(params?: any) { + return this.get('/groups', params); + } + + async getGroup(id: number) { + return this.get(`/groups/${id}`); + } + + async createGroup(data: any) { + return this.post('/groups', data); + } + + async updateGroup(id: number, data: any) { + return this.put(`/groups/${id}`, data); + } + + async deleteGroup(id: number) { + return this.delete(`/groups/${id}`); + } + + // PRODUCTS + async getProducts(params?: any) { + return this.get('/products', params); + } + + async getProduct(id: number) { + return this.get(`/products/${id}`); + } + + async createProduct(data: any) { + return this.post('/products', data); + } + + async updateProduct(id: number, data: any) { + return this.put(`/products/${id}`, data); + } + + async deleteProduct(id: number) { + return this.delete(`/products/${id}`); + } + + // ROLES + async getRoles() { + return this.get('/roles'); + } + + async getRole(id: number) { + return this.get(`/roles/${id}`); + } + + // CANNED RESPONSES + async getCannedResponses(params?: any) { + return this.get('/canned_responses', params); + } + + async getCannedResponse(id: number) { + return this.get(`/canned_responses/${id}`); + } + + async createCannedResponse(data: any) { + return this.post('/canned_responses', data); + } + + async updateCannedResponse(id: number, data: any) { + return this.put(`/canned_responses/${id}`, data); + } + + async deleteCannedResponse(id: number) { + return this.delete(`/canned_responses/${id}`); + } + + async getCannedResponseFolders() { + return this.get('/canned_response_folders'); + } + + // SOLUTIONS (KB) + async getSolutionCategories(params?: any) { + return this.get('/solutions/categories', params); + } + + async getSolutionCategory(id: number) { + return this.get(`/solutions/categories/${id}`); + } + + async createSolutionCategory(data: any) { + return this.post('/solutions/categories', data); + } + + async updateSolutionCategory(id: number, data: any) { + return this.put(`/solutions/categories/${id}`, data); + } + + async deleteSolutionCategory(id: number) { + return this.delete(`/solutions/categories/${id}`); + } + + async getSolutionFolders(category_id: number) { + return this.get(`/solutions/categories/${category_id}/folders`); + } + + async getSolutionFolder(category_id: number, folder_id: number) { + return this.get(`/solutions/categories/${category_id}/folders/${folder_id}`); + } + + async createSolutionFolder(category_id: number, data: any) { + return this.post(`/solutions/categories/${category_id}/folders`, data); + } + + async updateSolutionFolder(category_id: number, folder_id: number, data: any) { + return this.put(`/solutions/categories/${category_id}/folders/${folder_id}`, data); + } + + async deleteSolutionFolder(category_id: number, folder_id: number) { + return this.delete(`/solutions/categories/${category_id}/folders/${folder_id}`); + } + + async getSolutionArticles(folder_id: number, params?: any) { + return this.get(`/solutions/folders/${folder_id}/articles`, params); + } + + async getSolutionArticle(id: number) { + return this.get(`/solutions/articles/${id}`); + } + + async createSolutionArticle(folder_id: number, data: any) { + return this.post(`/solutions/folders/${folder_id}/articles`, data); + } + + async updateSolutionArticle(id: number, data: any) { + return this.put(`/solutions/articles/${id}`, data); + } + + async deleteSolutionArticle(id: number) { + return this.delete(`/solutions/articles/${id}`); + } + + async searchSolutions(term: string) { + return this.get(`/solutions/articles?term=${encodeURIComponent(term)}`); + } + + // FORUMS + async getForumCategories() { + return this.get('/discussions/categories'); + } + + async getForums(category_id?: number) { + if (category_id) { + return this.get(`/discussions/categories/${category_id}/forums`); + } + return this.get('/discussions/forums'); + } + + async getForum(id: number) { + return this.get(`/discussions/forums/${id}`); + } + + async createForum(data: any) { + return this.post('/discussions/forums', data); + } + + async updateForum(id: number, data: any) { + return this.put(`/discussions/forums/${id}`, data); + } + + async getForumTopics(forum_id: number, params?: any) { + return this.get(`/discussions/forums/${forum_id}/topics`, params); + } + + async getForumTopic(id: number) { + return this.get(`/discussions/topics/${id}`); + } + + async createForumTopic(forum_id: number, data: any) { + return this.post(`/discussions/forums/${forum_id}/topics`, data); + } + + async updateForumTopic(id: number, data: any) { + return this.put(`/discussions/topics/${id}`, data); + } + + async deleteForumTopic(id: number) { + return this.delete(`/discussions/topics/${id}`); + } + + async getForumPosts(topic_id: number) { + return this.get(`/discussions/topics/${topic_id}/posts`); + } + + async createForumPost(topic_id: number, data: any) { + return this.post(`/discussions/topics/${topic_id}/posts`, data); + } + + // SURVEYS + async getSurveys() { + return this.get('/surveys'); + } + + async getSurveyResults(survey_id: number, params?: any) { + return this.get(`/surveys/${survey_id}/survey_results`, params); + } + + async getSatisfactionRatings(params?: any) { + return this.get('/surveys/satisfaction_ratings', params); + } + + // BUSINESS HOURS + async getBusinessHours() { + return this.get('/business_hours'); + } + + async getBusinessHour(id: number) { + return this.get(`/business_hours/${id}`); + } + + // SLA POLICIES + async getSLAPolicies() { + return this.get('/sla_policies'); + } + + async getSLAPolicy(id: number) { + return this.get(`/sla_policies/${id}`); + } + + // EMAIL CONFIGS + async getEmailConfigs() { + return this.get('/email_configs'); + } + + async getEmailConfig(id: number) { + return this.get(`/email_configs/${id}`); + } + + // TICKET FIELDS + async getTicketFields() { + return this.get('/ticket_fields'); + } + + async getTicketField(id: number) { + return this.get(`/ticket_fields/${id}`); + } + + // SETTINGS + async getSettings() { + return this.get('/settings/helpdesk'); + } + + // TIME ENTRIES + async getAllTimeEntries(params?: any) { + return this.get('/time_entries', params); + } + + async getTimeEntry(id: number) { + return this.get(`/time_entries/${id}`); + } + + async createTimeEntry(data: any) { + return this.post('/time_entries', data); + } + + async updateTimeEntry(id: number, data: any) { + return this.put(`/time_entries/${id}`, data); + } + + async deleteTimeEntry(id: number) { + return this.delete(`/time_entries/${id}`); + } + + async startTimer(id: number) { + return this.put(`/time_entries/${id}/start_timer`, {}); + } + + async stopTimer(id: number) { + return this.put(`/time_entries/${id}/stop_timer`, {}); + } +} diff --git a/servers/freshdesk/src/tools/index.ts b/servers/freshdesk/src/tools/index.ts new file mode 100644 index 0000000..843f854 --- /dev/null +++ b/servers/freshdesk/src/tools/index.ts @@ -0,0 +1,1355 @@ +// FreshDesk MCP Tools - 60+ comprehensive tools + +import { z } from 'zod'; +import type { FreshDeskClient } from '../api-client.js'; + +export function registerTools(client: FreshDeskClient) { + return [ + // ==================== TICKET TOOLS ==================== + { + name: 'freshdesk_list_tickets', + description: 'List all tickets with optional filters and pagination', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page (max 100)'), + updated_since: z.string().optional().describe('Filter by updated date (ISO 8601)'), + include: z.array(z.string()).optional().describe('Include related resources: requester, company, stats'), + order_by: z.string().optional().describe('Field to order by'), + order_type: z.enum(['asc', 'desc']).optional().describe('Sort order'), + }), + execute: async (args: any) => { + const tickets = await client.getTickets(args); + return { tickets }; + }, + }, + + { + name: 'freshdesk_get_ticket', + description: 'Get a specific ticket by ID', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + include: z.array(z.string()).optional().describe('Include: requester, company, stats, conversations'), + }), + execute: async (args: any) => { + const ticket = await client.getTicket(args.id, args.include); + return { ticket }; + }, + }, + + { + name: 'freshdesk_create_ticket', + description: 'Create a new support ticket', + inputSchema: z.object({ + subject: z.string().describe('Ticket subject'), + description: z.string().describe('Ticket description (HTML allowed)'), + email: z.string().email().optional().describe('Requester email'), + phone: z.string().optional().describe('Requester phone'), + requester_id: z.number().optional().describe('Existing contact ID'), + status: z.number().optional().describe('Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed'), + priority: z.number().optional().describe('Priority: 1=Low, 2=Medium, 3=High, 4=Urgent'), + source: z.number().optional().describe('Source: 1=Email, 2=Portal, 3=Phone, 7=Chat'), + type: z.string().optional().describe('Ticket type'), + group_id: z.number().optional().describe('Group ID'), + product_id: z.number().optional().describe('Product ID'), + company_id: z.number().optional().describe('Company ID'), + tags: z.array(z.string()).optional().describe('Tags'), + cc_emails: z.array(z.string()).optional().describe('CC email addresses'), + custom_fields: z.record(z.any()).optional().describe('Custom field values'), + }), + execute: async (args: any) => { + const ticket = await client.createTicket(args); + return { ticket, message: 'Ticket created successfully' }; + }, + }, + + { + name: 'freshdesk_update_ticket', + description: 'Update an existing ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + subject: z.string().optional().describe('New subject'), + description: z.string().optional().describe('New description'), + status: z.number().optional().describe('Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed'), + priority: z.number().optional().describe('Priority: 1=Low, 2=Medium, 3=High, 4=Urgent'), + type: z.string().optional().describe('Ticket type'), + group_id: z.number().optional().describe('Assign to group'), + responder_id: z.number().optional().describe('Assign to agent'), + tags: z.array(z.string()).optional().describe('Tags'), + custom_fields: z.record(z.any()).optional().describe('Custom fields'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const ticket = await client.updateTicket(id, data); + return { ticket, message: 'Ticket updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_ticket', + description: 'Delete (trash) a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + }), + execute: async (args: any) => { + await client.deleteTicket(args.id); + return { message: 'Ticket deleted successfully' }; + }, + }, + + { + name: 'freshdesk_restore_ticket', + description: 'Restore a deleted ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + }), + execute: async (args: any) => { + await client.restoreTicket(args.id); + return { message: 'Ticket restored successfully' }; + }, + }, + + { + name: 'freshdesk_search_tickets', + description: 'Search tickets using query string', + inputSchema: z.object({ + query: z.string().describe('Search query (e.g., "priority:3 AND status:2")'), + }), + execute: async (args: any) => { + const results = await client.searchTickets(args.query); + return { results }; + }, + }, + + { + name: 'freshdesk_filter_tickets', + description: 'Filter tickets with advanced query', + inputSchema: z.object({ + query: z.string().describe('Filter query string'), + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const { query, ...params } = args; + const results = await client.filterTickets(query, params); + return { results }; + }, + }, + + { + name: 'freshdesk_get_ticket_conversations', + description: 'Get all conversations (replies/notes) for a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + }), + execute: async (args: any) => { + const conversations = await client.getTicketConversations(args.id); + return { conversations }; + }, + }, + + { + name: 'freshdesk_add_ticket_reply', + description: 'Add a public reply to a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + body: z.string().describe('Reply content (HTML allowed)'), + from_email: z.string().optional().describe('From email address'), + user_id: z.number().optional().describe('Agent user ID'), + cc_emails: z.array(z.string()).optional().describe('CC addresses'), + bcc_emails: z.array(z.string()).optional().describe('BCC addresses'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const reply = await client.addTicketReply(id, data); + return { reply, message: 'Reply added successfully' }; + }, + }, + + { + name: 'freshdesk_add_ticket_note', + description: 'Add a private note to a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + body: z.string().describe('Note content (HTML allowed)'), + user_id: z.number().optional().describe('Agent user ID'), + notify_emails: z.array(z.string()).optional().describe('Agents to notify'), + private: z.boolean().optional().describe('Private note (default true)'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const note = await client.addTicketNote(id, data); + return { note, message: 'Note added successfully' }; + }, + }, + + { + name: 'freshdesk_get_ticket_time_entries', + description: 'Get time entries logged on a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + }), + execute: async (args: any) => { + const timeEntries = await client.getTicketTimeEntries(args.id); + return { timeEntries }; + }, + }, + + { + name: 'freshdesk_get_ticket_satisfaction', + description: 'Get satisfaction ratings for a ticket', + inputSchema: z.object({ + id: z.number().describe('Ticket ID'), + }), + execute: async (args: any) => { + const ratings = await client.getTicketSatisfactionRatings(args.id); + return { ratings }; + }, + }, + + // ==================== CONTACT TOOLS ==================== + { + name: 'freshdesk_list_contacts', + description: 'List all contacts with optional filters', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + email: z.string().optional().describe('Filter by email'), + mobile: z.string().optional().describe('Filter by mobile'), + phone: z.string().optional().describe('Filter by phone'), + company_id: z.number().optional().describe('Filter by company'), + state: z.enum(['verified', 'unverified', 'all', 'deleted', 'blocked']).optional(), + }), + execute: async (args: any) => { + const contacts = await client.getContacts(args); + return { contacts }; + }, + }, + + { + name: 'freshdesk_get_contact', + description: 'Get a specific contact by ID', + inputSchema: z.object({ + id: z.number().describe('Contact ID'), + }), + execute: async (args: any) => { + const contact = await client.getContact(args.id); + return { contact }; + }, + }, + + { + name: 'freshdesk_create_contact', + description: 'Create a new contact', + inputSchema: z.object({ + name: z.string().describe('Contact name'), + email: z.string().email().optional().describe('Email address'), + phone: z.string().optional().describe('Phone number'), + mobile: z.string().optional().describe('Mobile number'), + twitter_id: z.string().optional().describe('Twitter handle'), + unique_external_id: z.string().optional().describe('External ID'), + description: z.string().optional().describe('Description/notes'), + job_title: z.string().optional().describe('Job title'), + language: z.string().optional().describe('Language code (e.g., en)'), + time_zone: z.string().optional().describe('Time zone'), + company_id: z.number().optional().describe('Company ID'), + address: z.string().optional().describe('Address'), + tags: z.array(z.string()).optional().describe('Tags'), + custom_fields: z.record(z.any()).optional().describe('Custom fields'), + }), + execute: async (args: any) => { + const contact = await client.createContact(args); + return { contact, message: 'Contact created successfully' }; + }, + }, + + { + name: 'freshdesk_update_contact', + description: 'Update an existing contact', + inputSchema: z.object({ + id: z.number().describe('Contact ID'), + name: z.string().optional().describe('Contact name'), + email: z.string().email().optional().describe('Email address'), + phone: z.string().optional().describe('Phone number'), + mobile: z.string().optional().describe('Mobile number'), + job_title: z.string().optional().describe('Job title'), + company_id: z.number().optional().describe('Company ID'), + address: z.string().optional().describe('Address'), + tags: z.array(z.string()).optional().describe('Tags'), + custom_fields: z.record(z.any()).optional().describe('Custom fields'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const contact = await client.updateContact(id, data); + return { contact, message: 'Contact updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_contact', + description: 'Delete a contact permanently', + inputSchema: z.object({ + id: z.number().describe('Contact ID'), + }), + execute: async (args: any) => { + await client.deleteContact(args.id); + return { message: 'Contact deleted successfully' }; + }, + }, + + { + name: 'freshdesk_make_agent', + description: 'Convert a contact to an agent', + inputSchema: z.object({ + id: z.number().describe('Contact ID'), + }), + execute: async (args: any) => { + const agent = await client.makeContactAgent(args.id); + return { agent, message: 'Contact converted to agent successfully' }; + }, + }, + + { + name: 'freshdesk_search_contacts', + description: 'Search contacts by name or email', + inputSchema: z.object({ + query: z.string().describe('Search query (name or email)'), + }), + execute: async (args: any) => { + const contacts = await client.searchContacts({ query: args.query }); + return { contacts }; + }, + }, + + { + name: 'freshdesk_filter_contacts', + description: 'Filter contacts with query string', + inputSchema: z.object({ + query: z.string().describe('Filter query'), + }), + execute: async (args: any) => { + const contacts = await client.filterContacts(args.query); + return { contacts }; + }, + }, + + // ==================== COMPANY TOOLS ==================== + { + name: 'freshdesk_list_companies', + description: 'List all companies with pagination', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const companies = await client.getCompanies(args); + return { companies }; + }, + }, + + { + name: 'freshdesk_get_company', + description: 'Get a specific company by ID', + inputSchema: z.object({ + id: z.number().describe('Company ID'), + }), + execute: async (args: any) => { + const company = await client.getCompany(args.id); + return { company }; + }, + }, + + { + name: 'freshdesk_create_company', + description: 'Create a new company', + inputSchema: z.object({ + name: z.string().describe('Company name'), + description: z.string().optional().describe('Description'), + domains: z.array(z.string()).optional().describe('Email domains'), + note: z.string().optional().describe('Internal note'), + custom_fields: z.record(z.any()).optional().describe('Custom fields'), + health_score: z.string().optional().describe('Health score'), + account_tier: z.string().optional().describe('Account tier'), + renewal_date: z.string().optional().describe('Renewal date (ISO 8601)'), + industry: z.string().optional().describe('Industry'), + }), + execute: async (args: any) => { + const company = await client.createCompany(args); + return { company, message: 'Company created successfully' }; + }, + }, + + { + name: 'freshdesk_update_company', + description: 'Update an existing company', + inputSchema: z.object({ + id: z.number().describe('Company ID'), + name: z.string().optional().describe('Company name'), + description: z.string().optional().describe('Description'), + domains: z.array(z.string()).optional().describe('Email domains'), + note: z.string().optional().describe('Internal note'), + custom_fields: z.record(z.any()).optional().describe('Custom fields'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const company = await client.updateCompany(id, data); + return { company, message: 'Company updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_company', + description: 'Delete a company', + inputSchema: z.object({ + id: z.number().describe('Company ID'), + }), + execute: async (args: any) => { + await client.deleteCompany(args.id); + return { message: 'Company deleted successfully' }; + }, + }, + + { + name: 'freshdesk_search_companies', + description: 'Search companies by name', + inputSchema: z.object({ + name: z.string().describe('Company name to search'), + }), + execute: async (args: any) => { + const companies = await client.searchCompanies(args.name); + return { companies }; + }, + }, + + { + name: 'freshdesk_filter_companies', + description: 'Filter companies with query', + inputSchema: z.object({ + query: z.string().describe('Filter query'), + }), + execute: async (args: any) => { + const companies = await client.filterCompanies(args.query); + return { companies }; + }, + }, + + // ==================== AGENT TOOLS ==================== + { + name: 'freshdesk_list_agents', + description: 'List all agents', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const agents = await client.getAgents(args); + return { agents }; + }, + }, + + { + name: 'freshdesk_get_agent', + description: 'Get a specific agent by ID', + inputSchema: z.object({ + id: z.number().describe('Agent ID'), + }), + execute: async (args: any) => { + const agent = await client.getAgent(args.id); + return { agent }; + }, + }, + + { + name: 'freshdesk_get_current_agent', + description: 'Get currently authenticated agent details', + inputSchema: z.object({}), + execute: async () => { + const agent = await client.getCurrentAgent(); + return { agent }; + }, + }, + + { + name: 'freshdesk_update_agent', + description: 'Update an agent', + inputSchema: z.object({ + id: z.number().describe('Agent ID'), + occasional: z.boolean().optional().describe('Occasional agent'), + signature: z.string().optional().describe('Email signature'), + ticket_scope: z.number().optional().describe('Ticket scope'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const agent = await client.updateAgent(id, data); + return { agent, message: 'Agent updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_agent', + description: 'Delete (deactivate) an agent', + inputSchema: z.object({ + id: z.number().describe('Agent ID'), + }), + execute: async (args: any) => { + await client.deleteAgent(args.id); + return { message: 'Agent deleted successfully' }; + }, + }, + + // ==================== GROUP TOOLS ==================== + { + name: 'freshdesk_list_groups', + description: 'List all agent groups', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const groups = await client.getGroups(args); + return { groups }; + }, + }, + + { + name: 'freshdesk_get_group', + description: 'Get a specific group by ID', + inputSchema: z.object({ + id: z.number().describe('Group ID'), + }), + execute: async (args: any) => { + const group = await client.getGroup(args.id); + return { group }; + }, + }, + + { + name: 'freshdesk_create_group', + description: 'Create a new agent group', + inputSchema: z.object({ + name: z.string().describe('Group name'), + description: z.string().optional().describe('Group description'), + escalate_to: z.number().optional().describe('Escalate to group ID'), + unassigned_for: z.string().optional().describe('Time before escalation'), + business_calendar_id: z.number().optional().describe('Business hours ID'), + agent_ids: z.array(z.number()).optional().describe('Agent IDs'), + }), + execute: async (args: any) => { + const group = await client.createGroup(args); + return { group, message: 'Group created successfully' }; + }, + }, + + { + name: 'freshdesk_update_group', + description: 'Update an existing group', + inputSchema: z.object({ + id: z.number().describe('Group ID'), + name: z.string().optional().describe('Group name'), + description: z.string().optional().describe('Group description'), + escalate_to: z.number().optional().describe('Escalate to group ID'), + agent_ids: z.array(z.number()).optional().describe('Agent IDs'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const group = await client.updateGroup(id, data); + return { group, message: 'Group updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_group', + description: 'Delete a group', + inputSchema: z.object({ + id: z.number().describe('Group ID'), + }), + execute: async (args: any) => { + await client.deleteGroup(args.id); + return { message: 'Group deleted successfully' }; + }, + }, + + // ==================== PRODUCT TOOLS ==================== + { + name: 'freshdesk_list_products', + description: 'List all products', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const products = await client.getProducts(args); + return { products }; + }, + }, + + { + name: 'freshdesk_get_product', + description: 'Get a specific product by ID', + inputSchema: z.object({ + id: z.number().describe('Product ID'), + }), + execute: async (args: any) => { + const product = await client.getProduct(args.id); + return { product }; + }, + }, + + { + name: 'freshdesk_create_product', + description: 'Create a new product', + inputSchema: z.object({ + name: z.string().describe('Product name'), + description: z.string().optional().describe('Product description'), + }), + execute: async (args: any) => { + const product = await client.createProduct(args); + return { product, message: 'Product created successfully' }; + }, + }, + + { + name: 'freshdesk_update_product', + description: 'Update an existing product', + inputSchema: z.object({ + id: z.number().describe('Product ID'), + name: z.string().optional().describe('Product name'), + description: z.string().optional().describe('Product description'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const product = await client.updateProduct(id, data); + return { product, message: 'Product updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_product', + description: 'Delete a product', + inputSchema: z.object({ + id: z.number().describe('Product ID'), + }), + execute: async (args: any) => { + await client.deleteProduct(args.id); + return { message: 'Product deleted successfully' }; + }, + }, + + // ==================== ROLE TOOLS ==================== + { + name: 'freshdesk_list_roles', + description: 'List all agent roles', + inputSchema: z.object({}), + execute: async () => { + const roles = await client.getRoles(); + return { roles }; + }, + }, + + { + name: 'freshdesk_get_role', + description: 'Get a specific role by ID', + inputSchema: z.object({ + id: z.number().describe('Role ID'), + }), + execute: async (args: any) => { + const role = await client.getRole(args.id); + return { role }; + }, + }, + + // ==================== CANNED RESPONSE TOOLS ==================== + { + name: 'freshdesk_list_canned_responses', + description: 'List all canned responses', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const responses = await client.getCannedResponses(args); + return { responses }; + }, + }, + + { + name: 'freshdesk_get_canned_response', + description: 'Get a specific canned response by ID', + inputSchema: z.object({ + id: z.number().describe('Canned response ID'), + }), + execute: async (args: any) => { + const response = await client.getCannedResponse(args.id); + return { response }; + }, + }, + + { + name: 'freshdesk_create_canned_response', + description: 'Create a new canned response', + inputSchema: z.object({ + title: z.string().describe('Response title'), + content: z.string().describe('Response content (HTML allowed)'), + folder_id: z.number().optional().describe('Folder ID'), + }), + execute: async (args: any) => { + const response = await client.createCannedResponse(args); + return { response, message: 'Canned response created successfully' }; + }, + }, + + { + name: 'freshdesk_update_canned_response', + description: 'Update an existing canned response', + inputSchema: z.object({ + id: z.number().describe('Canned response ID'), + title: z.string().optional().describe('Response title'), + content: z.string().optional().describe('Response content'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const response = await client.updateCannedResponse(id, data); + return { response, message: 'Canned response updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_canned_response', + description: 'Delete a canned response', + inputSchema: z.object({ + id: z.number().describe('Canned response ID'), + }), + execute: async (args: any) => { + await client.deleteCannedResponse(args.id); + return { message: 'Canned response deleted successfully' }; + }, + }, + + { + name: 'freshdesk_list_canned_response_folders', + description: 'List all canned response folders', + inputSchema: z.object({}), + execute: async () => { + const folders = await client.getCannedResponseFolders(); + return { folders }; + }, + }, + + // ==================== SOLUTION/KB TOOLS ==================== + { + name: 'freshdesk_list_solution_categories', + description: 'List all knowledge base categories', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const categories = await client.getSolutionCategories(args); + return { categories }; + }, + }, + + { + name: 'freshdesk_get_solution_category', + description: 'Get a specific KB category by ID', + inputSchema: z.object({ + id: z.number().describe('Category ID'), + }), + execute: async (args: any) => { + const category = await client.getSolutionCategory(args.id); + return { category }; + }, + }, + + { + name: 'freshdesk_create_solution_category', + description: 'Create a new KB category', + inputSchema: z.object({ + name: z.string().describe('Category name'), + description: z.string().optional().describe('Category description'), + }), + execute: async (args: any) => { + const category = await client.createSolutionCategory(args); + return { category, message: 'Category created successfully' }; + }, + }, + + { + name: 'freshdesk_update_solution_category', + description: 'Update a KB category', + inputSchema: z.object({ + id: z.number().describe('Category ID'), + name: z.string().optional().describe('Category name'), + description: z.string().optional().describe('Description'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const category = await client.updateSolutionCategory(id, data); + return { category, message: 'Category updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_solution_category', + description: 'Delete a KB category', + inputSchema: z.object({ + id: z.number().describe('Category ID'), + }), + execute: async (args: any) => { + await client.deleteSolutionCategory(args.id); + return { message: 'Category deleted successfully' }; + }, + }, + + { + name: 'freshdesk_list_solution_folders', + description: 'List all folders in a KB category', + inputSchema: z.object({ + category_id: z.number().describe('Category ID'), + }), + execute: async (args: any) => { + const folders = await client.getSolutionFolders(args.category_id); + return { folders }; + }, + }, + + { + name: 'freshdesk_get_solution_folder', + description: 'Get a specific KB folder', + inputSchema: z.object({ + category_id: z.number().describe('Category ID'), + folder_id: z.number().describe('Folder ID'), + }), + execute: async (args: any) => { + const folder = await client.getSolutionFolder(args.category_id, args.folder_id); + return { folder }; + }, + }, + + { + name: 'freshdesk_create_solution_folder', + description: 'Create a new KB folder', + inputSchema: z.object({ + category_id: z.number().describe('Category ID'), + name: z.string().describe('Folder name'), + description: z.string().optional().describe('Folder description'), + visibility: z.number().optional().describe('Visibility level'), + }), + execute: async (args: any) => { + const { category_id, ...data } = args; + const folder = await client.createSolutionFolder(category_id, data); + return { folder, message: 'Folder created successfully' }; + }, + }, + + { + name: 'freshdesk_update_solution_folder', + description: 'Update a KB folder', + inputSchema: z.object({ + category_id: z.number().describe('Category ID'), + folder_id: z.number().describe('Folder ID'), + name: z.string().optional().describe('Folder name'), + description: z.string().optional().describe('Description'), + }), + execute: async (args: any) => { + const { category_id, folder_id, ...data } = args; + const folder = await client.updateSolutionFolder(category_id, folder_id, data); + return { folder, message: 'Folder updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_solution_folder', + description: 'Delete a KB folder', + inputSchema: z.object({ + category_id: z.number().describe('Category ID'), + folder_id: z.number().describe('Folder ID'), + }), + execute: async (args: any) => { + await client.deleteSolutionFolder(args.category_id, args.folder_id); + return { message: 'Folder deleted successfully' }; + }, + }, + + { + name: 'freshdesk_list_solution_articles', + description: 'List all articles in a KB folder', + inputSchema: z.object({ + folder_id: z.number().describe('Folder ID'), + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const { folder_id, ...params } = args; + const articles = await client.getSolutionArticles(folder_id, params); + return { articles }; + }, + }, + + { + name: 'freshdesk_get_solution_article', + description: 'Get a specific KB article by ID', + inputSchema: z.object({ + id: z.number().describe('Article ID'), + }), + execute: async (args: any) => { + const article = await client.getSolutionArticle(args.id); + return { article }; + }, + }, + + { + name: 'freshdesk_create_solution_article', + description: 'Create a new KB article', + inputSchema: z.object({ + folder_id: z.number().describe('Folder ID'), + title: z.string().describe('Article title'), + description: z.string().describe('Article content (HTML allowed)'), + status: z.number().optional().describe('Status: 1=Draft, 2=Published'), + tags: z.array(z.string()).optional().describe('Tags'), + }), + execute: async (args: any) => { + const { folder_id, ...data } = args; + const article = await client.createSolutionArticle(folder_id, data); + return { article, message: 'Article created successfully' }; + }, + }, + + { + name: 'freshdesk_update_solution_article', + description: 'Update a KB article', + inputSchema: z.object({ + id: z.number().describe('Article ID'), + title: z.string().optional().describe('Article title'), + description: z.string().optional().describe('Article content'), + status: z.number().optional().describe('Status'), + tags: z.array(z.string()).optional().describe('Tags'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const article = await client.updateSolutionArticle(id, data); + return { article, message: 'Article updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_solution_article', + description: 'Delete a KB article', + inputSchema: z.object({ + id: z.number().describe('Article ID'), + }), + execute: async (args: any) => { + await client.deleteSolutionArticle(args.id); + return { message: 'Article deleted successfully' }; + }, + }, + + { + name: 'freshdesk_search_solutions', + description: 'Search knowledge base articles', + inputSchema: z.object({ + term: z.string().describe('Search term'), + }), + execute: async (args: any) => { + const results = await client.searchSolutions(args.term); + return { results }; + }, + }, + + // ==================== FORUM TOOLS ==================== + { + name: 'freshdesk_list_forum_categories', + description: 'List all forum categories', + inputSchema: z.object({}), + execute: async () => { + const categories = await client.getForumCategories(); + return { categories }; + }, + }, + + { + name: 'freshdesk_list_forums', + description: 'List all forums (optionally filter by category)', + inputSchema: z.object({ + category_id: z.number().optional().describe('Filter by category ID'), + }), + execute: async (args: any) => { + const forums = await client.getForums(args.category_id); + return { forums }; + }, + }, + + { + name: 'freshdesk_get_forum', + description: 'Get a specific forum by ID', + inputSchema: z.object({ + id: z.number().describe('Forum ID'), + }), + execute: async (args: any) => { + const forum = await client.getForum(args.id); + return { forum }; + }, + }, + + { + name: 'freshdesk_create_forum', + description: 'Create a new forum', + inputSchema: z.object({ + name: z.string().describe('Forum name'), + description: z.string().optional().describe('Forum description'), + forum_category_id: z.number().describe('Category ID'), + forum_visibility: z.number().optional().describe('Visibility level'), + }), + execute: async (args: any) => { + const forum = await client.createForum(args); + return { forum, message: 'Forum created successfully' }; + }, + }, + + { + name: 'freshdesk_update_forum', + description: 'Update an existing forum', + inputSchema: z.object({ + id: z.number().describe('Forum ID'), + name: z.string().optional().describe('Forum name'), + description: z.string().optional().describe('Description'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const forum = await client.updateForum(id, data); + return { forum, message: 'Forum updated successfully' }; + }, + }, + + { + name: 'freshdesk_list_forum_topics', + description: 'List all topics in a forum', + inputSchema: z.object({ + forum_id: z.number().describe('Forum ID'), + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const { forum_id, ...params } = args; + const topics = await client.getForumTopics(forum_id, params); + return { topics }; + }, + }, + + { + name: 'freshdesk_get_forum_topic', + description: 'Get a specific forum topic by ID', + inputSchema: z.object({ + id: z.number().describe('Topic ID'), + }), + execute: async (args: any) => { + const topic = await client.getForumTopic(args.id); + return { topic }; + }, + }, + + { + name: 'freshdesk_create_forum_topic', + description: 'Create a new forum topic', + inputSchema: z.object({ + forum_id: z.number().describe('Forum ID'), + title: z.string().describe('Topic title'), + message: z.string().describe('Topic message (HTML allowed)'), + sticky: z.boolean().optional().describe('Sticky topic'), + locked: z.boolean().optional().describe('Locked topic'), + }), + execute: async (args: any) => { + const { forum_id, ...data } = args; + const topic = await client.createForumTopic(forum_id, data); + return { topic, message: 'Topic created successfully' }; + }, + }, + + { + name: 'freshdesk_update_forum_topic', + description: 'Update a forum topic', + inputSchema: z.object({ + id: z.number().describe('Topic ID'), + title: z.string().optional().describe('Topic title'), + message: z.string().optional().describe('Message'), + sticky: z.boolean().optional().describe('Sticky'), + locked: z.boolean().optional().describe('Locked'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const topic = await client.updateForumTopic(id, data); + return { topic, message: 'Topic updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_forum_topic', + description: 'Delete a forum topic', + inputSchema: z.object({ + id: z.number().describe('Topic ID'), + }), + execute: async (args: any) => { + await client.deleteForumTopic(args.id); + return { message: 'Topic deleted successfully' }; + }, + }, + + { + name: 'freshdesk_list_forum_posts', + description: 'List all posts in a forum topic', + inputSchema: z.object({ + topic_id: z.number().describe('Topic ID'), + }), + execute: async (args: any) => { + const posts = await client.getForumPosts(args.topic_id); + return { posts }; + }, + }, + + { + name: 'freshdesk_create_forum_post', + description: 'Create a reply/post in a forum topic', + inputSchema: z.object({ + topic_id: z.number().describe('Topic ID'), + body: z.string().describe('Post content (HTML allowed)'), + }), + execute: async (args: any) => { + const { topic_id, ...data } = args; + const post = await client.createForumPost(topic_id, data); + return { post, message: 'Post created successfully' }; + }, + }, + + // ==================== SURVEY TOOLS ==================== + { + name: 'freshdesk_list_surveys', + description: 'List all customer satisfaction surveys', + inputSchema: z.object({}), + execute: async () => { + const surveys = await client.getSurveys(); + return { surveys }; + }, + }, + + { + name: 'freshdesk_get_survey_results', + description: 'Get results for a specific survey', + inputSchema: z.object({ + survey_id: z.number().describe('Survey ID'), + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const { survey_id, ...params } = args; + const results = await client.getSurveyResults(survey_id, params); + return { results }; + }, + }, + + { + name: 'freshdesk_get_satisfaction_ratings', + description: 'Get all satisfaction ratings across surveys', + inputSchema: z.object({ + created_since: z.string().optional().describe('Filter by date (ISO 8601)'), + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const ratings = await client.getSatisfactionRatings(args); + return { ratings }; + }, + }, + + // ==================== TIME ENTRY TOOLS ==================== + { + name: 'freshdesk_list_time_entries', + description: 'List all time entries', + inputSchema: z.object({ + page: z.number().optional().describe('Page number'), + per_page: z.number().optional().describe('Results per page'), + }), + execute: async (args: any) => { + const timeEntries = await client.getAllTimeEntries(args); + return { timeEntries }; + }, + }, + + { + name: 'freshdesk_get_time_entry', + description: 'Get a specific time entry by ID', + inputSchema: z.object({ + id: z.number().describe('Time entry ID'), + }), + execute: async (args: any) => { + const timeEntry = await client.getTimeEntry(args.id); + return { timeEntry }; + }, + }, + + { + name: 'freshdesk_create_time_entry', + description: 'Create a new time entry', + inputSchema: z.object({ + ticket_id: z.number().describe('Ticket ID'), + time_spent: z.string().describe('Time spent (e.g., "01:30" for 1h 30m)'), + billable: z.boolean().optional().describe('Is billable'), + note: z.string().optional().describe('Note/description'), + agent_id: z.number().optional().describe('Agent ID'), + }), + execute: async (args: any) => { + const timeEntry = await client.createTimeEntry(args); + return { timeEntry, message: 'Time entry created successfully' }; + }, + }, + + { + name: 'freshdesk_update_time_entry', + description: 'Update an existing time entry', + inputSchema: z.object({ + id: z.number().describe('Time entry ID'), + time_spent: z.string().optional().describe('Time spent'), + billable: z.boolean().optional().describe('Is billable'), + note: z.string().optional().describe('Note'), + }), + execute: async (args: any) => { + const { id, ...data } = args; + const timeEntry = await client.updateTimeEntry(id, data); + return { timeEntry, message: 'Time entry updated successfully' }; + }, + }, + + { + name: 'freshdesk_delete_time_entry', + description: 'Delete a time entry', + inputSchema: z.object({ + id: z.number().describe('Time entry ID'), + }), + execute: async (args: any) => { + await client.deleteTimeEntry(args.id); + return { message: 'Time entry deleted successfully' }; + }, + }, + + { + name: 'freshdesk_start_timer', + description: 'Start a time tracking timer', + inputSchema: z.object({ + id: z.number().describe('Time entry ID'), + }), + execute: async (args: any) => { + const timeEntry = await client.startTimer(args.id); + return { timeEntry, message: 'Timer started successfully' }; + }, + }, + + { + name: 'freshdesk_stop_timer', + description: 'Stop a running time tracking timer', + inputSchema: z.object({ + id: z.number().describe('Time entry ID'), + }), + execute: async (args: any) => { + const timeEntry = await client.stopTimer(args.id); + return { timeEntry, message: 'Timer stopped successfully' }; + }, + }, + + // ==================== CONFIGURATION TOOLS ==================== + { + name: 'freshdesk_get_business_hours', + description: 'Get all business hours configurations', + inputSchema: z.object({}), + execute: async () => { + const businessHours = await client.getBusinessHours(); + return { businessHours }; + }, + }, + + { + name: 'freshdesk_get_business_hour', + description: 'Get a specific business hours configuration', + inputSchema: z.object({ + id: z.number().describe('Business hours ID'), + }), + execute: async (args: any) => { + const businessHour = await client.getBusinessHour(args.id); + return { businessHour }; + }, + }, + + { + name: 'freshdesk_get_sla_policies', + description: 'Get all SLA policies', + inputSchema: z.object({}), + execute: async () => { + const policies = await client.getSLAPolicies(); + return { policies }; + }, + }, + + { + name: 'freshdesk_get_sla_policy', + description: 'Get a specific SLA policy', + inputSchema: z.object({ + id: z.number().describe('SLA policy ID'), + }), + execute: async (args: any) => { + const policy = await client.getSLAPolicy(args.id); + return { policy }; + }, + }, + + { + name: 'freshdesk_get_email_configs', + description: 'Get all email configurations', + inputSchema: z.object({}), + execute: async () => { + const configs = await client.getEmailConfigs(); + return { configs }; + }, + }, + + { + name: 'freshdesk_get_email_config', + description: 'Get a specific email configuration', + inputSchema: z.object({ + id: z.number().describe('Email config ID'), + }), + execute: async (args: any) => { + const config = await client.getEmailConfig(args.id); + return { config }; + }, + }, + + { + name: 'freshdesk_get_ticket_fields', + description: 'Get all ticket field configurations', + inputSchema: z.object({}), + execute: async () => { + const fields = await client.getTicketFields(); + return { fields }; + }, + }, + + { + name: 'freshdesk_get_ticket_field', + description: 'Get a specific ticket field configuration', + inputSchema: z.object({ + id: z.number().describe('Ticket field ID'), + }), + execute: async (args: any) => { + const field = await client.getTicketField(args.id); + return { field }; + }, + }, + + { + name: 'freshdesk_get_settings', + description: 'Get helpdesk settings and configuration', + inputSchema: z.object({}), + execute: async () => { + const settings = await client.getSettings(); + return { settings }; + }, + }, + ]; +} diff --git a/servers/helpscout/.env.example b/servers/helpscout/.env.example index d56030a..370f1df 100644 --- a/servers/helpscout/.env.example +++ b/servers/helpscout/.env.example @@ -1,5 +1,3 @@ -# HelpScout API Credentials -# Get these from: https://secure.helpscout.net/apps/ - HELPSCOUT_APP_ID=your_app_id_here HELPSCOUT_APP_SECRET=your_app_secret_here +HELPSCOUT_ACCESS_TOKEN=your_oauth2_access_token_here diff --git a/servers/helpscout/package.json b/servers/helpscout/package.json index a79a006..9e53a2b 100644 --- a/servers/helpscout/package.json +++ b/servers/helpscout/package.json @@ -1,34 +1,38 @@ { - "name": "@mcpengine/helpscout-server", + "name": "@mcpengine/helpscout-mcp-server", "version": "1.0.0", - "description": "HelpScout MCP Server - Complete Mailbox API v2 integration", + "description": "Complete HelpScout Mailbox API v2 MCP Server with OAuth2 authentication", "type": "module", "main": "dist/main.js", - "bin": { - "helpscout-mcp": "./dist/main.js" - }, "scripts": { - "build": "tsc", + "build": "tsc && npm run copy-apps", + "copy-apps": "mkdir -p dist/apps && cp -r src/apps/*.tsx dist/apps/ 2>/dev/null || true", "dev": "tsc --watch", "start": "node dist/main.js", "prepare": "npm run build" }, "keywords": [ - "mcp", "helpscout", - "customer-support", - "mailbox", - "conversations" + "mcp", + "support", + "customer-service", + "helpdesk", + "oauth2" ], - "author": "MCPEngine", + "author": "MCP Engine", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", "axios": "^1.7.9", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "react": "^18.3.1" }, "devDependencies": { - "@types/node": "^22.10.2", + "@types/node": "^22.10.5", + "@types/react": "^18.3.18", "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/helpscout/src/types/index.ts b/servers/helpscout/src/types/index.ts index 58e7d11..b6e8177 100644 --- a/servers/helpscout/src/types/index.ts +++ b/servers/helpscout/src/types/index.ts @@ -1,252 +1,127 @@ -// HelpScout API Types +// HelpScout Mailbox API v2 Types export interface HelpScoutConfig { - appId: string; - appSecret: string; - accessToken?: string; - tokenExpiry?: number; + appId?: string; + appSecret?: string; + accessToken: string; } -export interface AccessTokenResponse { - access_token: string; - expires_in: number; - token_type: string; -} - -export interface PaginatedResponse { - _embedded?: { [key: string]: T[] }; +export interface APIError { + message: string; + logref?: string; _links?: { + about?: { href: string }; + }; +} + +export interface PagedResponse { + _embedded: T; + _links?: { + self?: { href: string }; first?: { href: string }; last?: { href: string }; next?: { href: string }; prev?: { href: string }; - self?: { href: string }; + page?: { href: string }; }; page?: { - number: number; size: number; totalElements: number; totalPages: number; + number: number; }; } +// Conversations export interface Conversation { id: number; number: number; threads: number; type: 'email' | 'chat' | 'phone'; folderId: number; - status: 'active' | 'pending' | 'closed' | 'spam'; - state: 'draft' | 'published' | 'deleted'; + status: 'active' | 'closed' | 'open' | 'pending' | 'spam'; + state: 'deleted' | 'draft' | 'published'; subject: string; preview: string; mailboxId: number; - assignee?: User; - createdBy: User | Customer; + assignee?: Person; + createdBy: Person; createdAt: string; + closedBy?: number; + closedByUser?: Person; closedAt?: string; - closedBy?: User; - updatedAt?: string; - userUpdatedAt?: string; - customerWaitingSince?: CustomerWaitingSince; - source?: ConversationSource; + userUpdatedAt: string; + customerWaitingSince?: { + time: string; + friendly: string; + }; + source: { + type: string; + via: 'user' | 'customer'; + }; tags?: Tag[]; cc?: string[]; bcc?: string[]; primaryCustomer: Customer; + snooze?: { + snoozedBy: number; + snoozedUntil: string; + unsnoozeOnCustomerReply: boolean; + }; + nextEvent?: { + time: string; + eventType: 'snooze' | 'scheduled'; + userId: number; + cancelOnCustomerReply: boolean; + }; customFields?: CustomField[]; + _embedded?: { + threads?: Thread[]; + }; + _links?: Record; } export interface Thread { id: number; - assignedTo?: User; - status: 'active' | 'nochange' | 'pending' | 'closed'; - createdAt: string; - createdBy: User | Customer; - source?: ThreadSource; - type: 'note' | 'message' | 'reply' | 'customer' | 'lineitem' | 'forwardparent' | 'forwardchild' | 'phone'; - state: 'draft' | 'published' | 'deleted' | 'hidden'; + type: 'note' | 'message' | 'customer' | 'lineitem' | 'chat' | 'phone'; + status: 'active' | 'nochange' | 'pending'; + state: 'published' | 'draft' | 'deleted' | 'hidden'; + action?: { + type: string; + text: string; + }; + body: string; + source: { + type: string; + via: string; + }; customer?: Customer; - fromMailbox?: Mailbox; - body?: string; + createdBy: Person; + assignedTo?: Person; + savedReplyId?: number; to?: string[]; cc?: string[]; bcc?: string[]; - attachments?: Attachment[]; - savedReplyId?: number; + createdAt: string; openedAt?: string; + _embedded?: { + attachments?: Attachment[]; + }; } -export interface Customer { - id: number; - firstName: string; - lastName: string; - email: string; - phone?: string; - photoUrl?: string; - photoType?: string; - gender?: string; - age?: string; - organization?: string; - jobTitle?: string; - location?: string; - background?: string; - address?: CustomerAddress; - socialProfiles?: SocialProfile[]; - chats?: Chat[]; - websites?: Website[]; - emails?: CustomerEmail[]; - phones?: CustomerPhone[]; - createdAt: string; - updatedAt: string; -} - -export interface CustomerAddress { - id: number; - city: string; - state: string; - postalCode: string; - country: string; - lines: string[]; - createdAt: string; -} - -export interface CustomerEmail { - id: number; - value: string; - type: string; - location: string; -} - -export interface CustomerPhone { - id: number; - value: string; - type: string; - location: string; -} - -export interface SocialProfile { - type: string; - value: string; -} - -export interface Chat { - id: number; - type: string; - value: string; -} - -export interface Website { - id: number; - value: string; -} - -export interface Mailbox { - id: number; - name: string; - slug: string; - email: string; - createdAt: string; - updatedAt: string; -} - -export interface Folder { - id: number; - name: string; - type: 'mine' | 'drafts' | 'assigned' | 'closed' | 'spam' | 'open'; - userId: number; - totalCount: number; - activeCount: number; - updatedAt: string; -} - -export interface User { - id: number; - firstName: string; - lastName: string; - email: string; - role: string; - timezone: string; - photoUrl?: string; - createdAt: string; - updatedAt: string; - type?: 'user' | 'team'; -} - -export interface Tag { - id?: number; - name?: string; - color?: string; - createdAt?: string; - updatedAt?: string; - ticketCount?: number; -} - -export interface Workflow { - id: number; - mailboxId: number; - type: 'manual' | 'automatic'; - status: 'active' | 'inactive' | 'invalid'; - order: number; - name: string; - createdAt: string; - modifiedAt: string; -} - -export interface WorkflowStats { - totalRuns: number; - successfulRuns: number; - failedRuns: number; -} - -export interface SavedReply { - id: number; - name: string; +export interface CreateThread { + type: 'note' | 'message' | 'customer' | 'reply' | 'forwardCustomer' | 'forwardParent'; text: string; - mailboxId?: number; - userId?: number; - createdAt: string; - updatedAt: string; -} - -export interface Team { - id: number; - name: string; - createdAt: string; - updatedAt: string; - memberCount?: number; -} - -export interface Webhook { - id: string; - url: string; - secret: string; - events: string[]; - state: 'enabled' | 'disabled'; - createdAt: string; -} - -export interface CustomField { - id: number; - name: string; - value: any; - type: 'TEXT' | 'NUMBER' | 'DATE' | 'DROPDOWN'; -} - -export interface ConversationSource { - type: string; - via: string; -} - -export interface ThreadSource { - type: string; - via: string; -} - -export interface CustomerWaitingSince { - time: string; - friendly: string; - latestReplyFrom: 'customer' | 'user'; + user?: number; + customer?: number; + imported?: boolean; + createdAt?: string; + status?: 'active' | 'nochange' | 'pending'; + to?: string[]; + cc?: string[]; + bcc?: string[]; + attachments?: CreateAttachment[]; + draft?: boolean; } export interface Attachment { @@ -257,64 +132,369 @@ export interface Attachment { width?: number; height?: number; url: string; + _links?: { + data?: { href: string }; + }; } +export interface CreateAttachment { + fileName: string; + mimeType: string; + data: string; // base64 +} + +// Customers +export interface Customer { + id: number; + type?: 'customer'; + first?: string; + last?: string; + email: string; + phone?: string; + photoUrl?: string; + photoType?: 'twitter' | 'facebook' | 'gravatar' | 'google' | 'unknown'; + gender?: 'male' | 'female' | 'unknown'; + age?: string; + organization?: string; + jobTitle?: string; + location?: string; + createdAt?: string; + updatedAt?: string; + background?: string; + address?: Address; + socialProfiles?: SocialProfile[]; + emails?: CustomerEmail[]; + phones?: CustomerPhone[]; + chats?: CustomerChat[]; + websites?: CustomerWebsite[]; + _embedded?: { + entries?: CustomerEntry[]; + }; + _links?: Record; +} + +export interface Address { + id?: number; + lines: string[]; + city: string; + state: string; + postalCode: string; + country: string; + createdAt?: string; + updatedAt?: string; +} + +export interface SocialProfile { + id?: number; + type: 'twitter' | 'facebook' | 'linkedin' | 'aboutme' | 'google' | 'googleplus' | 'tungleme' | 'quora' | 'foursquare' | 'youtube' | 'flickr' | 'other'; + value: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomerEmail { + id?: number; + type: 'home' | 'work' | 'other'; + value: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomerPhone { + id?: number; + type: 'home' | 'work' | 'mobile' | 'fax' | 'pager' | 'other'; + value: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomerChat { + id?: number; + type: 'aim' | 'gtalk' | 'icq' | 'xmpp' | 'msn' | 'skype' | 'yahoo' | 'qq' | 'other'; + value: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomerWebsite { + id?: number; + value: string; + createdAt?: string; + updatedAt?: string; +} + +export interface CustomerEntry { + id: number; + type: 'email' | 'phone' | 'chat' | 'website'; + value: string; +} + +export interface CreateCustomer { + firstName: string; + lastName: string; + email?: string; + phone?: string; + photoUrl?: string; + photoType?: 'twitter' | 'facebook' | 'gravatar' | 'google' | 'unknown'; + gender?: 'male' | 'female' | 'unknown'; + age?: string; + organization?: string; + jobTitle?: string; + location?: string; + background?: string; + address?: Omit; + socialProfiles?: Omit[]; + emails?: Omit[]; + phones?: Omit[]; + chats?: Omit[]; + websites?: Omit[]; +} + +// Mailboxes +export interface Mailbox { + id: number; + name: string; + slug: string; + email: string; + createdAt: string; + updatedAt: string; + _links?: Record; +} + +export interface MailboxFields { + id: number; + name: string; + type: 'SINGLE_LINE' | 'MULTI_LINE' | 'DATE' | 'NUMBER' | 'DROPDOWN'; + order: number; + required: boolean; + options?: string[]; + _links?: Record; +} + +export interface Folder { + id: number; + name: string; + type: 'mytickets' | 'unassigned' | 'drafts' | 'assigned' | 'closed' | 'spam' | 'deleted' | 'mine'; + userId?: number; + totalCount: number; + activeCount: number; + updatedAt: string; + _links?: Record; +} + +// Users +export interface User { + id: number; + type: 'user' | 'team'; + first?: string; + last?: string; + email: string; + role: 'owner' | 'admin' | 'user'; + timezone: string; + photoUrl?: string; + createdAt: string; + updatedAt: string; + _links?: Record; +} + +export interface Person { + id: number; + type: 'user' | 'customer' | 'team'; + first?: string; + last?: string; + email?: string; + photoUrl?: string; +} + +// Teams +export interface Team { + id: number; + name: string; + createdAt: string; + updatedAt: string; + _links?: Record; +} + +export interface TeamMember { + id: number; + first: string; + last: string; + email: string; + role: 'owner' | 'admin' | 'user'; + photoUrl?: string; + _links?: Record; +} + +// Tags +export interface Tag { + id: number; + tag: string; + color: string; + createdAt?: string; + updatedAt?: string; + _links?: Record; +} + +// Workflows +export interface Workflow { + id: number; + mailboxId: number; + type: 'manual' | 'automatic'; + status: 'active' | 'inactive' | 'invalid'; + order: number; + name: string; + createdAt: string; + modifiedAt: string; + _links?: Record; +} + +// Saved Replies +export interface SavedReply { + id: number; + text: string; + name: string; + _links?: Record; +} + +// Webhooks +export interface Webhook { + id: number; + url: string; + state: 'enabled' | 'disabled'; + events: string[]; + notification: boolean; + payloadVersion: 'V1' | 'V2'; + label?: string; + secret?: string; + _links?: Record; +} + +export interface CreateWebhook { + url: string; + events: string[]; + secret?: string; +} + +// Reports export interface Report { - filterTags?: any; - current?: any; - previous?: any; - deltas?: any; + filterTags?: string[]; } -export interface CompanyReport extends Report { - current?: { - conversationsCreated: number; - conversationsClosed: number; - customersHelped: number; - happinessScore: number; - repliesSent: number; - resolutionTime: number; - firstResponseTime: number; +export interface ConversationReport { + current: ReportMetrics; + previous?: ReportMetrics; + deltas?: { + [key: string]: { + value: number; + percent: number; + }; }; } -export interface ConversationReport extends Report { - current?: { - newConversations: number; - totalConversations: number; - conversationsBusiest: any; - conversationsVolumeByDay: any[]; +export interface ReportMetrics { + startDate: string; + endDate: string; + conversations: number; + conversationsCreated: number; + newConversations?: number; + customers?: number; + resolved?: number; + replies?: number; + repliesSent?: number; + resolvedOnFirstReply?: number; + responseTime?: TimeMetrics; + resolutionTime?: TimeMetrics; + firstResponseTime?: TimeMetrics; +} + +export interface TimeMetrics { + friendly: string; + seconds: number; +} + +export interface UserReport { + user: Person; + current: UserMetrics; + previous?: UserMetrics; + deltas?: Record; +} + +export interface UserMetrics { + startDate: string; + endDate: string; + totalConversations: number; + conversationsCreated: number; + conversationsResolved: number; + repliesSent: number; + resolvedOnFirstReply?: number; + responseTime?: TimeMetrics; + resolutionTime?: TimeMetrics; + percentResolved?: number; + happiness?: { + score: number; }; } -export interface HappinessReport extends Report { - current?: { - happinessScore: number; - ratingsCount: number; - greatCount: number; - okayCount: number; - notGoodCount: number; - }; +export interface HappinessReport { + current: HappinessMetrics; + previous?: HappinessMetrics; } -export interface ProductivityReport extends Report { - current?: { - repliesSent: number; - resolutionTime: number; - firstResponseTime: number; - responseTime: number; - resolvedConversations: number; - }; +export interface HappinessMetrics { + startDate: string; + endDate: string; + happinessScore: number; + ratingsCount: number; } -export interface UserReport extends Report { - current?: { - user: User; - repliesSent: number; - conversationsCreated: number; - conversationsClosed: number; - happinessScore: number; - resolutionTime: number; - firstResponseTime: number; - }; +// Custom Fields +export interface CustomField { + id: number; + name: string; + value: string | number; + text?: string; +} + +// Notes +export interface Note { + text: string; +} + +// Search +export interface SearchConversation { + id: number; + number: number; + mailboxid: number; + subject: string; + status: string; + threadCount: number; + preview: string; + customerId: number; + customerEmail: string; + customerName: string; + updatedAt: string; + url?: string; +} + +export interface SearchCustomer { + id: number; + firstName: string; + lastName: string; + email: string; + phone?: string; + photoUrl?: string; + url?: string; +} + +// Ratings +export interface Rating { + id: number; + customerId: number; + userId?: number; + threadId: number; + rating: 'great' | 'okay' | 'bad'; + comments?: string; + createdAt: string; + modifiedAt?: string; + _links?: Record; } diff --git a/servers/helpscout/tsconfig.json b/servers/helpscout/tsconfig.json index 156b6d5..16e6fcd 100644 --- a/servers/helpscout/tsconfig.json +++ b/servers/helpscout/tsconfig.json @@ -13,7 +13,8 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "jsx": "react" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/touchbistro/README.md b/servers/touchbistro/README.md new file mode 100644 index 0000000..1de69cf --- /dev/null +++ b/servers/touchbistro/README.md @@ -0,0 +1,392 @@ +# TouchBistro MCP Server + +A comprehensive Model Context Protocol (MCP) server for TouchBistro restaurant management platform, providing **190 tools** across 12 domains and **15 React MCP Apps** for visual management. + +## Features + +### 🍽️ Core Restaurant Management + +- **Orders Management** (15 tools) + - Create, update, track orders with full CRUD operations + - Send to kitchen, manage modifications, void/cancel orders + - Table orders, open orders, customer order history + +- **Menu Management** (21 tools) + - Menu items, categories, modifiers with full CRUD + - Bulk updates, availability control + - Modifier groups and custom modifiers + +- **Customer Management** (12 tools) + - Customer profiles with preferences and allergens + - VIP tracking, tagging, address management + - Customer statistics and analytics + +- **Employee Management** (19 tools) + - Employee profiles and permissions + - Shift scheduling and time clock + - Performance tracking and labor management + +- **Table Management** (17 tools) + - Floor plans and table layouts + - Table status tracking (available, occupied, reserved) + - Table merging/splitting, server assignments + +### 💰 Financial Operations + +- **Payment Processing** (13 tools) + - Multiple payment methods (cash, card, mobile, gift cards) + - Authorization, capture, void, refund operations + - Split payments and tip management + +- **Reservations** (15 tools) + - Create, confirm, seat, and track reservations + - Availability checking and reminders + - No-show tracking + +- **Loyalty Programs** (10 tools) + - Points earning and redemption + - Birthday bonuses and rewards + - Member analytics and tier management + +- **Gift Cards** (13 tools) + - Purchase, reload, redeem gift cards + - Balance checking and email delivery + - Transaction history and sales tracking + +### 📊 Inventory & Analytics + +- **Inventory Management** (17 tools) + - Stock tracking with par levels + - Purchase orders and receiving + - Low stock alerts and reorder management + +- **Reports & Analytics** (24 tools) + - Sales reports (daily, hourly, by category) + - Employee performance and labor costs + - Customer acquisition and retention + - Menu performance and profitability + - Real-time dashboard + +- **Discounts & Promotions** (14 tools) + - Create and manage discounts + - Happy hour and daily specials + - Promotion scheduling and tracking + +## Installation + +```bash +npm install touchbistro-mcp-server +``` + +## Configuration + +Set the following environment variables: + +```bash +TOUCHBISTRO_API_KEY=your_api_key +TOUCHBISTRO_RESTAURANT_ID=your_restaurant_id +TOUCHBISTRO_BASE_URL=https://api.touchbistro.com/v1 # Optional +``` + +## Usage + +### As an MCP Server + +Add to your Claude Desktop or other MCP client configuration: + +```json +{ + "mcpServers": { + "touchbistro": { + "command": "touchbistro-mcp-server", + "env": { + "TOUCHBISTRO_API_KEY": "your_api_key", + "TOUCHBISTRO_RESTAURANT_ID": "your_restaurant_id" + } + } + } +} +``` + +### Direct Usage + +```bash +touchbistro-mcp-server +``` + +## React MCP Apps (15 Apps) + +This server includes 15 standalone React applications for visual management: + +1. **Orders Dashboard** - Real-time order tracking and management +2. **Menu Manager** - Visual menu builder with drag-and-drop +3. **Reservations Calendar** - Interactive reservation scheduling +4. **Table Layout** - Visual floor plan editor +5. **Customer Directory** - Customer profiles and history +6. **Employee Scheduler** - Shift scheduling and time tracking +7. **Payments Dashboard** - Payment processing and reconciliation +8. **Loyalty Manager** - Loyalty program administration +9. **Gift Card Manager** - Gift card sales and tracking +10. **Inventory Tracker** - Stock levels and purchase orders +11. **Sales Reports** - Interactive sales analytics +12. **Analytics Dashboard** - Business intelligence and KPIs +13. **Discounts Manager** - Promotion creation and tracking +14. **Settings Panel** - Restaurant configuration +15. **Real-time Dashboard** - Live business overview + +Each app is: +- Dark theme optimized +- Fully responsive +- Built with React 18 + TypeScript +- Vite-powered for fast development +- Standalone deployable + +### Building Apps + +```bash +# Build all apps +npm run build + +# Build specific app +npm run build:orders-app +npm run build:menu-app +# ... etc +``` + +## API Structure + +### Orders + +```typescript +// Create an order +{ + "name": "create_order", + "arguments": { + "tableId": "table-123", + "employeeId": "emp-456", + "orderType": "dine_in", + "items": [ + { + "menuItemId": "item-789", + "quantity": 2, + "modifiers": [{ "modifierId": "mod-123" }] + } + ] + } +} +``` + +### Menu Management + +```typescript +// Create menu item +{ + "name": "create_menu_item", + "arguments": { + "name": "Margherita Pizza", + "categoryId": "cat-pizza", + "price": 14.99, + "description": "Classic Italian pizza", + "allergens": ["gluten", "dairy"] + } +} +``` + +### Reservations + +```typescript +// Create reservation +{ + "name": "create_reservation", + "arguments": { + "customerName": "John Doe", + "customerPhone": "555-0123", + "partySize": 4, + "date": "2024-12-25", + "time": "19:00" + } +} +``` + +### Reports + +```typescript +// Get sales report +{ + "name": "get_sales_report", + "arguments": { + "startDate": "2024-01-01", + "endDate": "2024-01-31" + } +} +``` + +## Tool Categories + +### Orders (15 tools) +- list_orders, get_order, create_order, update_order +- add_order_items, remove_order_item, update_order_item +- send_to_kitchen, complete_order, cancel_order, void_order +- get_table_orders, get_open_orders, search_orders, get_customer_orders + +### Menu (21 tools) +- **Items**: list_menu_items, get_menu_item, create_menu_item, update_menu_item, delete_menu_item, bulk_update_menu_items, set_item_availability +- **Categories**: list_menu_categories, get_menu_category, create_menu_category, update_menu_category, delete_menu_category, reorder_menu_categories +- **Modifiers**: list_modifier_groups, get_modifier_group, create_modifier_group, update_modifier_group, delete_modifier_group, add_modifier, update_modifier, delete_modifier + +### Customers (12 tools) +- list_customers, get_customer, create_customer, update_customer, delete_customer +- search_customers, add_customer_address, update_customer_preferences +- get_customer_stats, get_vip_customers, tag_customer, untag_customer + +### Employees (19 tools) +- **Staff**: list_employees, get_employee, create_employee, update_employee, delete_employee, deactivate_employee, update_employee_permissions +- **Shifts**: list_shifts, get_shift, create_shift, start_shift, end_shift, get_current_shifts +- **Time Clock**: clock_in, clock_out, start_break, end_break, get_time_clock_entries, get_employee_hours + +### Tables (17 tools) +- **Tables**: list_tables, get_table, create_table, update_table, delete_table, set_table_status, assign_server, get_available_tables, get_occupied_tables, merge_tables, split_tables +- **Floors**: list_floors, get_floor, create_floor, update_floor, delete_floor, get_floor_layout + +### Payments (13 tools) +- list_payments, get_payment, process_payment, authorize_payment, capture_payment +- void_payment, refund_payment, get_payment_refunds, list_refunds +- split_payment, add_tip, get_payment_summary, get_payment_methods_summary + +### Reservations (15 tools) +- list_reservations, get_reservation, create_reservation, update_reservation, delete_reservation +- confirm_reservation, cancel_reservation, seat_reservation, mark_no_show, complete_reservation +- get_reservations_by_date, get_upcoming_reservations, search_reservations, send_reminder, get_available_slots + +### Loyalty (10 tools) +- get_loyalty_program, update_loyalty_program, get_customer_points +- add_loyalty_points, redeem_loyalty_points, get_loyalty_transactions +- get_loyalty_members, get_top_loyalty_members, award_birthday_points, get_loyalty_analytics + +### Gift Cards (13 tools) +- list_gift_cards, get_gift_card, get_gift_card_by_number, create_gift_card +- purchase_gift_card, reload_gift_card, redeem_gift_card, void_gift_card +- activate_gift_card, check_balance, get_gift_card_transactions, send_gift_card_email, get_gift_card_sales + +### Inventory (17 tools) +- **Items**: list_inventory_items, get_inventory_item, create_inventory_item, update_inventory_item, delete_inventory_item, adjust_stock, get_stock_adjustments, get_low_stock_items, get_items_to_reorder, get_inventory_valuation +- **Purchase Orders**: list_purchase_orders, get_purchase_order, create_purchase_order, update_purchase_order, send_purchase_order, receive_purchase_order, cancel_purchase_order + +### Reports (24 tools) +- **Sales**: get_sales_report, get_daily_sales, get_sales_by_hour, get_sales_by_day, get_sales_by_category, get_sales_by_payment_method +- **Menu**: get_top_selling_items, get_menu_performance, get_menu_item_analytics +- **Staff**: get_employee_performance, get_employee_sales, get_labor_cost_report +- **Customers**: get_customer_analytics, get_customer_acquisition, get_customer_retention +- **Other**: get_tax_report, get_discount_usage, get_tip_report, get_order_type_distribution, get_table_turnover, get_average_order_value, export_report_csv, get_realtime_dashboard, get_profit_loss_report + +### Discounts (14 tools) +- **Discounts**: list_discounts, get_discount, create_discount, update_discount, delete_discount, validate_discount_code, apply_discount, remove_discount +- **Promotions**: list_promotions, get_promotion, create_promotion, update_promotion, delete_promotion, get_active_promotions + +## Resources + +- `touchbistro://config` - View current API configuration +- `touchbistro://docs` - Complete documentation + +## Architecture + +``` +touchbistro/ +├── src/ +│ ├── clients/ +│ │ └── touchbistro.ts # API client with auth & error handling +│ ├── types/ +│ │ └── index.ts # TypeScript type definitions +│ ├── tools/ +│ │ ├── orders.ts # Order management tools +│ │ ├── menus.ts # Menu management tools +│ │ ├── customers.ts # Customer management tools +│ │ ├── employees.ts # Employee management tools +│ │ ├── tables.ts # Table management tools +│ │ ├── payments.ts # Payment processing tools +│ │ ├── reservations.ts # Reservation tools +│ │ ├── loyalty.ts # Loyalty program tools +│ │ ├── giftcards.ts # Gift card tools +│ │ ├── inventory.ts # Inventory management tools +│ │ ├── reports.ts # Reporting tools +│ │ └── discounts.ts # Discount & promotion tools +│ ├── ui/ # 15 React MCP Apps +│ │ ├── orders-app/ +│ │ ├── menu-app/ +│ │ ├── reservations-app/ +│ │ ├── tables-app/ +│ │ ├── customers-app/ +│ │ ├── employees-app/ +│ │ ├── payments-app/ +│ │ ├── loyalty-app/ +│ │ ├── giftcards-app/ +│ │ ├── inventory-app/ +│ │ ├── reports-app/ +│ │ ├── analytics-app/ +│ │ ├── discounts-app/ +│ │ ├── settings-app/ +│ │ └── dashboard-app/ +│ ├── server.ts # MCP server implementation +│ └── main.ts # Entry point +├── package.json +├── tsconfig.json +└── README.md +``` + +## Development + +```bash +# Install dependencies +npm install + +# Build TypeScript +npm run build + +# Watch mode for development +npm run dev + +# Run the server +npm start +``` + +## Testing + +```bash +# Test connection +echo '{"method":"tools/list"}' | touchbistro-mcp-server +``` + +## License + +MIT + +## Support + +For issues and questions, please visit the [GitHub repository](https://github.com/BusyBee3333/mcpengine). + +## TouchBistro API + +This server implements the TouchBistro REST API v1. For API documentation, contact TouchBistro or visit their developer portal. + +### API Authentication + +TouchBistro uses API key authentication: +- Header: `Authorization: Bearer {api_key}` +- Header: `X-Restaurant-ID: {restaurant_id}` + +### Rate Limiting + +Please be aware of TouchBistro's API rate limits. This server implements automatic retry with exponential backoff for rate-limited requests. + +## Contributing + +Contributions welcome! Please submit pull requests to the main repository. + +## Changelog + +### v1.0.0 (2024) +- Initial release +- 190 tools across 12 domains +- 15 React MCP Apps +- Full CRUD operations for all resources +- Comprehensive reporting and analytics diff --git a/servers/touchbistro/src/lib/api-client.ts b/servers/touchbistro/src/lib/api-client.ts new file mode 100644 index 0000000..621d40d --- /dev/null +++ b/servers/touchbistro/src/lib/api-client.ts @@ -0,0 +1,623 @@ +/** + * TouchBistro API Client + * Production-grade client with auth, rate limiting, retry logic + */ + +import type { + TouchBistroConfig, + ApiResponse, + ListParams, + Order, + OrderItem, + MenuItem, + MenuCategory, + Menu, + ModifierGroup, + Table, + Section, + FloorPlan, + Staff, + TimeSheet, + Customer, + LoyaltyTransaction, + Reservation, + InventoryItem, + StockAdjustment, + Supplier, + PurchaseOrder, + Payment, + SalesReport, + InventoryReport, + StaffReport, +} from './types.js'; + +export class TouchBistroApiClient { + private config: TouchBistroConfig; + private baseUrl: string; + private accessToken: string | null = null; + private tokenExpiry: number = 0; + private rateLimitRemaining: number = 1000; + private rateLimitReset: number = 0; + + constructor(config: TouchBistroConfig) { + this.config = config; + this.baseUrl = config.baseUrl || 'https://cloud.touchbistro.com/api/v1'; + if (config.sandbox) { + this.baseUrl = 'https://sandbox.touchbistro.com/api/v1'; + } + } + + // ============================================================================ + // Authentication & Request Management + // ============================================================================ + + private async ensureAuthenticated(): Promise { + if (this.accessToken && Date.now() < this.tokenExpiry) { + return; + } + + const response = await fetch(`${this.baseUrl}/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + client_id: this.config.clientId, + client_secret: this.config.clientSecret, + grant_type: 'client_credentials', + }), + }); + + if (!response.ok) { + throw new Error(`Authentication failed: ${response.statusText}`); + } + + const data = await response.json(); + this.accessToken = data.access_token; + this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000; // Refresh 1 min early + } + + private async checkRateLimit(): Promise { + if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) { + const waitTime = this.rateLimitReset - Date.now(); + await new Promise((resolve) => setTimeout(resolve, waitTime)); + } + } + + private async request( + method: string, + endpoint: string, + body?: any, + retries = 3 + ): Promise> { + await this.ensureAuthenticated(); + await this.checkRateLimit(); + + const url = `${this.baseUrl}${endpoint}`; + const headers: Record = { + 'Authorization': `Bearer ${this.accessToken}`, + 'X-Restaurant-ID': this.config.restaurantId, + 'Content-Type': 'application/json', + }; + + try { + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + // Update rate limit tracking + this.rateLimitRemaining = parseInt( + response.headers.get('X-RateLimit-Remaining') || '1000' + ); + this.rateLimitReset = parseInt( + response.headers.get('X-RateLimit-Reset') || '0' + ); + + if (!response.ok) { + if (response.status === 429 && retries > 0) { + // Rate limited, wait and retry + await new Promise((resolve) => setTimeout(resolve, 1000)); + return this.request(method, endpoint, body, retries - 1); + } + + if (response.status >= 500 && retries > 0) { + // Server error, retry with exponential backoff + await new Promise((resolve) => setTimeout(resolve, (4 - retries) * 1000)); + return this.request(method, endpoint, body, retries - 1); + } + + const error = await response.json().catch(() => ({ + code: 'UNKNOWN_ERROR', + message: response.statusText, + })); + + return { + success: false, + error: { + code: error.code || `HTTP_${response.status}`, + message: error.message || response.statusText, + details: error.details, + }, + }; + } + + const data = await response.json(); + return { + success: true, + data, + metadata: { + timestamp: new Date().toISOString(), + }, + }; + } catch (error: any) { + if (retries > 0) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return this.request(method, endpoint, body, retries - 1); + } + + return { + success: false, + error: { + code: 'NETWORK_ERROR', + message: error.message, + }, + }; + } + } + + // ============================================================================ + // Orders API + // ============================================================================ + + async getOrders(params?: ListParams): Promise> { + const queryParams = new URLSearchParams(); + if (params?.page) queryParams.set('page', params.page.toString()); + if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString()); + if (params?.sortBy) queryParams.set('sortBy', params.sortBy); + if (params?.sortOrder) queryParams.set('sortOrder', params.sortOrder); + if (params?.filter) { + Object.entries(params.filter).forEach(([key, value]) => { + queryParams.set(key, String(value)); + }); + } + + return this.request('GET', `/orders?${queryParams}`); + } + + async getOrder(orderId: string): Promise> { + return this.request('GET', `/orders/${orderId}`); + } + + async createOrder(order: Partial): Promise> { + return this.request('POST', '/orders', order); + } + + async updateOrder(orderId: string, updates: Partial): Promise> { + return this.request('PATCH', `/orders/${orderId}`, updates); + } + + async addOrderItem(orderId: string, item: Partial): Promise> { + return this.request('POST', `/orders/${orderId}/items`, item); + } + + async updateOrderItem( + orderId: string, + itemId: string, + updates: Partial + ): Promise> { + return this.request('PATCH', `/orders/${orderId}/items/${itemId}`, updates); + } + + async removeOrderItem(orderId: string, itemId: string): Promise> { + return this.request('DELETE', `/orders/${orderId}/items/${itemId}`); + } + + async sendOrderToKitchen(orderId: string): Promise> { + return this.request('POST', `/orders/${orderId}/send-to-kitchen`); + } + + async completeOrder(orderId: string): Promise> { + return this.request('POST', `/orders/${orderId}/complete`); + } + + async voidOrder(orderId: string, reason: string): Promise> { + return this.request('POST', `/orders/${orderId}/void`, { reason }); + } + + async splitOrder( + orderId: string, + splits: { items: string[]; amount?: number }[] + ): Promise> { + return this.request('POST', `/orders/${orderId}/split`, { splits }); + } + + // ============================================================================ + // Menu Management API + // ============================================================================ + + async getMenuItems(params?: ListParams): Promise> { + const queryParams = new URLSearchParams(); + if (params?.filter) { + Object.entries(params.filter).forEach(([key, value]) => { + queryParams.set(key, String(value)); + }); + } + return this.request('GET', `/menu/items?${queryParams}`); + } + + async getMenuItem(itemId: string): Promise> { + return this.request('GET', `/menu/items/${itemId}`); + } + + async createMenuItem(item: Partial): Promise> { + return this.request('POST', '/menu/items', item); + } + + async updateMenuItem(itemId: string, updates: Partial): Promise> { + return this.request('PATCH', `/menu/items/${itemId}`, updates); + } + + async deleteMenuItem(itemId: string): Promise> { + return this.request('DELETE', `/menu/items/${itemId}`); + } + + async getMenuCategories(): Promise> { + return this.request('GET', '/menu/categories'); + } + + async createMenuCategory(category: Partial): Promise> { + return this.request('POST', '/menu/categories', category); + } + + async updateMenuCategory( + categoryId: string, + updates: Partial + ): Promise> { + return this.request('PATCH', `/menu/categories/${categoryId}`, updates); + } + + async deleteMenuCategory(categoryId: string): Promise> { + return this.request('DELETE', `/menu/categories/${categoryId}`); + } + + async getMenus(): Promise> { + return this.request('GET', '/menu/menus'); + } + + async createMenu(menu: Partial): Promise> { + return this.request('POST', '/menu/menus', menu); + } + + async updateMenu(menuId: string, updates: Partial): Promise> { + return this.request('PATCH', `/menu/menus/${menuId}`, updates); + } + + async getModifierGroups(): Promise> { + return this.request('GET', '/menu/modifier-groups'); + } + + async createModifierGroup(group: Partial): Promise> { + return this.request('POST', '/menu/modifier-groups', group); + } + + // ============================================================================ + // Table & Floor Management API + // ============================================================================ + + async getTables(params?: ListParams): Promise> { + return this.request('GET', '/tables'); + } + + async getTable(tableId: string): Promise> { + return this.request('GET', `/tables/${tableId}`); + } + + async updateTableStatus( + tableId: string, + status: string, + guestCount?: number + ): Promise> { + return this.request
('PATCH', `/tables/${tableId}/status`, { status, guestCount }); + } + + async assignServer(tableId: string, serverId: string): Promise> { + return this.request
('PATCH', `/tables/${tableId}/assign`, { serverId }); + } + + async getSections(): Promise> { + return this.request('GET', '/sections'); + } + + async createSection(section: Partial
): Promise> { + return this.request
('POST', '/sections', section); + } + + async getFloorPlans(): Promise> { + return this.request('GET', '/floor-plans'); + } + + async getActiveFloorPlan(): Promise> { + return this.request('GET', '/floor-plans/active'); + } + + async createTable(table: Partial
): Promise> { + return this.request
('POST', '/tables', table); + } + + async updateTable(tableId: string, updates: Partial
): Promise> { + return this.request
('PATCH', `/tables/${tableId}`, updates); + } + + // ============================================================================ + // Staff Management API + // ============================================================================ + + async getStaff(params?: ListParams): Promise> { + return this.request('GET', '/staff'); + } + + async getStaffMember(staffId: string): Promise> { + return this.request('GET', `/staff/${staffId}`); + } + + async createStaffMember(staff: Partial): Promise> { + return this.request('POST', '/staff', staff); + } + + async updateStaffMember(staffId: string, updates: Partial): Promise> { + return this.request('PATCH', `/staff/${staffId}`, updates); + } + + async deactivateStaffMember(staffId: string): Promise> { + return this.request('POST', `/staff/${staffId}/deactivate`); + } + + async clockIn(staffId: string): Promise> { + return this.request('POST', `/staff/${staffId}/clock-in`); + } + + async clockOut(staffId: string): Promise> { + return this.request('POST', `/staff/${staffId}/clock-out`); + } + + async getTimeSheets(params?: { + staffId?: string; + dateFrom?: string; + dateTo?: string; + }): Promise> { + const queryParams = new URLSearchParams(); + if (params?.staffId) queryParams.set('staffId', params.staffId); + if (params?.dateFrom) queryParams.set('dateFrom', params.dateFrom); + if (params?.dateTo) queryParams.set('dateTo', params.dateTo); + return this.request('GET', `/timesheets?${queryParams}`); + } + + // ============================================================================ + // Customer Management API + // ============================================================================ + + async getCustomers(params?: ListParams): Promise> { + const queryParams = new URLSearchParams(); + if (params?.filter?.search) { + queryParams.set('search', params.filter.search); + } + return this.request('GET', `/customers?${queryParams}`); + } + + async getCustomer(customerId: string): Promise> { + return this.request('GET', `/customers/${customerId}`); + } + + async createCustomer(customer: Partial): Promise> { + return this.request('POST', '/customers', customer); + } + + async updateCustomer( + customerId: string, + updates: Partial + ): Promise> { + return this.request('PATCH', `/customers/${customerId}`, updates); + } + + async searchCustomers(query: string): Promise> { + return this.request('GET', `/customers/search?q=${encodeURIComponent(query)}`); + } + + async getLoyaltyTransactions(customerId: string): Promise> { + return this.request('GET', `/customers/${customerId}/loyalty`); + } + + async addLoyaltyPoints( + customerId: string, + points: number, + description: string + ): Promise> { + return this.request('POST', `/customers/${customerId}/loyalty`, { + points, + description, + type: 'earned', + }); + } + + async redeemLoyaltyPoints( + customerId: string, + points: number, + description: string + ): Promise> { + return this.request('POST', `/customers/${customerId}/loyalty`, { + points: -points, + description, + type: 'redeemed', + }); + } + + // ============================================================================ + // Reservations API + // ============================================================================ + + async getReservations(params?: { + dateFrom?: string; + dateTo?: string; + status?: string; + }): Promise> { + const queryParams = new URLSearchParams(); + if (params?.dateFrom) queryParams.set('dateFrom', params.dateFrom); + if (params?.dateTo) queryParams.set('dateTo', params.dateTo); + if (params?.status) queryParams.set('status', params.status); + return this.request('GET', `/reservations?${queryParams}`); + } + + async getReservation(reservationId: string): Promise> { + return this.request('GET', `/reservations/${reservationId}`); + } + + async createReservation(reservation: Partial): Promise> { + return this.request('POST', '/reservations', reservation); + } + + async updateReservation( + reservationId: string, + updates: Partial + ): Promise> { + return this.request('PATCH', `/reservations/${reservationId}`, updates); + } + + async confirmReservation(reservationId: string): Promise> { + return this.request('POST', `/reservations/${reservationId}/confirm`); + } + + async seatReservation(reservationId: string, tableId: string): Promise> { + return this.request('POST', `/reservations/${reservationId}/seat`, { tableId }); + } + + async cancelReservation(reservationId: string, reason?: string): Promise> { + return this.request('POST', `/reservations/${reservationId}/cancel`, { reason }); + } + + async markNoShow(reservationId: string): Promise> { + return this.request('POST', `/reservations/${reservationId}/no-show`); + } + + // ============================================================================ + // Inventory Management API + // ============================================================================ + + async getInventoryItems(params?: ListParams): Promise> { + return this.request('GET', '/inventory/items'); + } + + async getInventoryItem(itemId: string): Promise> { + return this.request('GET', `/inventory/items/${itemId}`); + } + + async createInventoryItem(item: Partial): Promise> { + return this.request('POST', '/inventory/items', item); + } + + async updateInventoryItem( + itemId: string, + updates: Partial + ): Promise> { + return this.request('PATCH', `/inventory/items/${itemId}`, updates); + } + + async adjustStock( + itemId: string, + quantity: number, + type: 'addition' | 'subtraction' | 'correction', + reason: string + ): Promise> { + return this.request('POST', `/inventory/items/${itemId}/adjust`, { + quantity, + type, + reason, + }); + } + + async getLowStockItems(): Promise> { + return this.request('GET', '/inventory/items/low-stock'); + } + + async getSuppliers(): Promise> { + return this.request('GET', '/inventory/suppliers'); + } + + async createSupplier(supplier: Partial): Promise> { + return this.request('POST', '/inventory/suppliers', supplier); + } + + async getPurchaseOrders(params?: { status?: string }): Promise> { + const queryParams = new URLSearchParams(); + if (params?.status) queryParams.set('status', params.status); + return this.request('GET', `/inventory/purchase-orders?${queryParams}`); + } + + async createPurchaseOrder(order: Partial): Promise> { + return this.request('POST', '/inventory/purchase-orders', order); + } + + async receivePurchaseOrder(orderId: string): Promise> { + return this.request('POST', `/inventory/purchase-orders/${orderId}/receive`); + } + + // ============================================================================ + // Payment Processing API + // ============================================================================ + + async processPayment( + orderId: string, + payment: Partial + ): Promise> { + return this.request('POST', `/orders/${orderId}/payments`, payment); + } + + async refundPayment(paymentId: string, amount?: number): Promise> { + return this.request('POST', `/payments/${paymentId}/refund`, { amount }); + } + + async voidPayment(paymentId: string): Promise> { + return this.request('POST', `/payments/${paymentId}/void`); + } + + async getPayments(orderId: string): Promise> { + return this.request('GET', `/orders/${orderId}/payments`); + } + + // ============================================================================ + // Reporting & Analytics API + // ============================================================================ + + async getSalesReport(dateFrom: string, dateTo: string): Promise> { + return this.request( + 'GET', + `/reports/sales?dateFrom=${dateFrom}&dateTo=${dateTo}` + ); + } + + async getInventoryReport(): Promise> { + return this.request('GET', '/reports/inventory'); + } + + async getStaffReport(dateFrom: string, dateTo: string): Promise> { + return this.request( + 'GET', + `/reports/staff?dateFrom=${dateFrom}&dateTo=${dateTo}` + ); + } + + async getTopSellingItems(limit: number = 10): Promise> { + return this.request('GET', `/reports/top-items?limit=${limit}`); + } + + async getRevenueByHour(date: string): Promise> { + return this.request('GET', `/reports/revenue-by-hour?date=${date}`); + } + + async getCustomerAnalytics(): Promise> { + return this.request('GET', '/reports/customer-analytics'); + } +} + +export default TouchBistroApiClient; diff --git a/servers/touchbistro/src/lib/types.ts b/servers/touchbistro/src/lib/types.ts new file mode 100644 index 0000000..5a66d56 --- /dev/null +++ b/servers/touchbistro/src/lib/types.ts @@ -0,0 +1,686 @@ +/** + * TouchBistro MCP Server - Complete Type Definitions + * Full production-grade types for restaurant POS operations + */ + +// ============================================================================ +// Core Domain Types +// ============================================================================ + +export interface TouchBistroConfig { + apiKey: string; + clientId: string; + clientSecret: string; + restaurantId: string; + baseUrl?: string; + sandbox?: boolean; +} + +// ============================================================================ +// Menu Management +// ============================================================================ + +export interface MenuItem { + id: string; + name: string; + description?: string; + price: number; + cost?: number; + categoryId: string; + sku?: string; + barcode?: string; + taxable: boolean; + available: boolean; + prepTime?: number; // minutes + modifierGroupIds?: string[]; + allergens?: string[]; + nutritionInfo?: NutritionInfo; + image?: string; + sortOrder: number; + createdAt: string; + updatedAt: string; +} + +export interface MenuCategory { + id: string; + name: string; + description?: string; + parentCategoryId?: string; + sortOrder: number; + available: boolean; + menuIds: string[]; + itemCount: number; + createdAt: string; + updatedAt: string; +} + +export interface Menu { + id: string; + name: string; + description?: string; + startTime?: string; // HH:mm + endTime?: string; // HH:mm + daysOfWeek?: number[]; // 0-6 + available: boolean; + categoryIds: string[]; + createdAt: string; + updatedAt: string; +} + +export interface ModifierGroup { + id: string; + name: string; + minSelections: number; + maxSelections: number; + required: boolean; + modifiers: Modifier[]; + sortOrder: number; +} + +export interface Modifier { + id: string; + name: string; + price: number; + available: boolean; + sortOrder: number; +} + +export interface NutritionInfo { + calories?: number; + protein?: number; + carbohydrates?: number; + fat?: number; + fiber?: number; + sodium?: number; +} + +// ============================================================================ +// Order Management +// ============================================================================ + +export interface Order { + id: string; + orderNumber: string; + status: OrderStatus; + type: OrderType; + tableId?: string; + customerId?: string; + items: OrderItem[]; + subtotal: number; + tax: number; + tip: number; + discount: number; + total: number; + payments: Payment[]; + serverId: string; + guestCount: number; + notes?: string; + specialInstructions?: string; + sentToKitchen: boolean; + kitchenSentAt?: string; + createdAt: string; + updatedAt: string; + completedAt?: string; + voidedAt?: string; + voidReason?: string; +} + +export enum OrderStatus { + Draft = 'draft', + Pending = 'pending', + InProgress = 'in_progress', + Ready = 'ready', + Completed = 'completed', + Voided = 'voided', + Cancelled = 'cancelled' +} + +export enum OrderType { + DineIn = 'dine_in', + Takeout = 'takeout', + Delivery = 'delivery', + Curbside = 'curbside', + DriveThru = 'drive_thru' +} + +export interface OrderItem { + id: string; + menuItemId: string; + name: string; + quantity: number; + price: number; + modifiers: OrderModifier[]; + specialInstructions?: string; + sentToKitchen: boolean; + preparedBy?: string; + preparedAt?: string; + seat?: number; + courseNumber?: number; +} + +export interface OrderModifier { + id: string; + name: string; + price: number; +} + +// ============================================================================ +// Payment Processing +// ============================================================================ + +export interface Payment { + id: string; + orderId: string; + type: PaymentType; + method: PaymentMethod; + amount: number; + tip: number; + processorFee?: number; + status: PaymentStatus; + cardLast4?: string; + cardBrand?: string; + transactionId?: string; + authCode?: string; + processedAt?: string; + refundedAt?: string; + refundAmount?: number; + createdAt: string; +} + +export enum PaymentType { + Full = 'full', + Partial = 'partial', + Split = 'split', + Refund = 'refund' +} + +export enum PaymentMethod { + Cash = 'cash', + CreditCard = 'credit_card', + DebitCard = 'debit_card', + GiftCard = 'gift_card', + MobilePayment = 'mobile_payment', + Check = 'check', + HouseAccount = 'house_account' +} + +export enum PaymentStatus { + Pending = 'pending', + Authorized = 'authorized', + Captured = 'captured', + Declined = 'declined', + Refunded = 'refunded', + Voided = 'voided' +} + +// ============================================================================ +// Table & Floor Management +// ============================================================================ + +export interface Table { + id: string; + number: string; + name?: string; + sectionId: string; + capacity: number; + shape: TableShape; + status: TableStatus; + currentOrderId?: string; + serverId?: string; + guestCount?: number; + seatedAt?: string; + positionX: number; + positionY: number; + rotation?: number; +} + +export enum TableShape { + Square = 'square', + Rectangle = 'rectangle', + Circle = 'circle', + Oval = 'oval' +} + +export enum TableStatus { + Available = 'available', + Occupied = 'occupied', + Reserved = 'reserved', + Cleaning = 'cleaning', + OutOfService = 'out_of_service' +} + +export interface Section { + id: string; + name: string; + floorPlanId: string; + color?: string; + assignedServerIds: string[]; + tableCount: number; +} + +export interface FloorPlan { + id: string; + name: string; + active: boolean; + sections: Section[]; + tableCount: number; + createdAt: string; + updatedAt: string; +} + +// ============================================================================ +// Staff Management +// ============================================================================ + +export interface Staff { + id: string; + employeeNumber: string; + firstName: string; + lastName: string; + email?: string; + phone?: string; + role: StaffRole; + pinCode: string; + active: boolean; + hireDate: string; + terminationDate?: string; + wage?: number; + wageType?: WageType; + permissions: StaffPermission[]; + assignedSections?: string[]; + createdAt: string; + updatedAt: string; +} + +export enum StaffRole { + Owner = 'owner', + Manager = 'manager', + Server = 'server', + Bartender = 'bartender', + Host = 'host', + Chef = 'chef', + Cook = 'cook', + Busser = 'busser', + Cashier = 'cashier' +} + +export enum WageType { + Hourly = 'hourly', + Salary = 'salary', + TippedHourly = 'tipped_hourly' +} + +export interface StaffPermission { + resource: string; + actions: ('create' | 'read' | 'update' | 'delete' | 'void')[]; +} + +export interface TimeSheet { + id: string; + staffId: string; + clockIn: string; + clockOut?: string; + breakMinutes: number; + totalHours?: number; + wage: number; + tips: number; + date: string; + notes?: string; +} + +// ============================================================================ +// Customer Management +// ============================================================================ + +export interface Customer { + id: string; + firstName: string; + lastName: string; + email?: string; + phone?: string; + birthday?: string; + notes?: string; + loyaltyPoints: number; + loyaltyTier?: string; + totalSpent: number; + visitCount: number; + averageSpend: number; + lastVisit?: string; + preferredServer?: string; + dietaryRestrictions?: string[]; + allergies?: string[]; + tags?: string[]; + createdAt: string; + updatedAt: string; +} + +export interface LoyaltyTransaction { + id: string; + customerId: string; + orderId?: string; + points: number; + type: 'earned' | 'redeemed' | 'adjusted'; + description: string; + createdAt: string; +} + +// ============================================================================ +// Reservations +// ============================================================================ + +export interface Reservation { + id: string; + customerId?: string; + customerName: string; + customerPhone?: string; + customerEmail?: string; + partySize: number; + date: string; + time: string; + duration: number; // minutes + status: ReservationStatus; + tableId?: string; + sectionId?: string; + notes?: string; + specialRequests?: string; + confirmationSent: boolean; + reminderSent: boolean; + seatedAt?: string; + completedAt?: string; + noShowAt?: string; + cancelledAt?: string; + createdAt: string; + updatedAt: string; +} + +export enum ReservationStatus { + Pending = 'pending', + Confirmed = 'confirmed', + Seated = 'seated', + Completed = 'completed', + NoShow = 'no_show', + Cancelled = 'cancelled' +} + +// ============================================================================ +// Inventory Management +// ============================================================================ + +export interface InventoryItem { + id: string; + name: string; + category: string; + unit: string; + currentStock: number; + parLevel: number; + reorderPoint: number; + reorderQuantity: number; + unitCost: number; + supplierId?: string; + sku?: string; + barcode?: string; + location?: string; + expirationDate?: string; + lastRestocked?: string; + createdAt: string; + updatedAt: string; +} + +export interface StockAdjustment { + id: string; + inventoryItemId: string; + quantity: number; + type: 'addition' | 'subtraction' | 'correction'; + reason: string; + performedBy: string; + cost?: number; + createdAt: string; +} + +export interface Supplier { + id: string; + name: string; + contactName?: string; + email?: string; + phone?: string; + address?: string; + paymentTerms?: string; + notes?: string; + active: boolean; + createdAt: string; + updatedAt: string; +} + +export interface PurchaseOrder { + id: string; + supplierId: string; + orderNumber: string; + status: 'draft' | 'sent' | 'received' | 'cancelled'; + items: PurchaseOrderItem[]; + subtotal: number; + tax: number; + total: number; + expectedDate?: string; + receivedDate?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export interface PurchaseOrderItem { + inventoryItemId: string; + quantity: number; + unitCost: number; + total: number; +} + +// ============================================================================ +// Reporting & Analytics +// ============================================================================ + +export interface SalesReport { + startDate: string; + endDate: string; + totalSales: number; + totalOrders: number; + averageOrderValue: number; + totalTax: number; + totalTips: number; + totalDiscounts: number; + grossSales: number; + netSales: number; + salesByHour: HourlySales[]; + salesByCategory: CategorySales[]; + salesByMenuItem: MenuItemSales[]; + salesByPaymentMethod: PaymentMethodSales[]; + salesByServer: ServerSales[]; + orderTypeMix: OrderTypeMix[]; +} + +export interface HourlySales { + hour: number; + sales: number; + orders: number; + averageValue: number; +} + +export interface CategorySales { + categoryId: string; + categoryName: string; + quantity: number; + revenue: number; + percentage: number; +} + +export interface MenuItemSales { + itemId: string; + itemName: string; + quantity: number; + revenue: number; + cost?: number; + profit?: number; + margin?: number; +} + +export interface PaymentMethodSales { + method: PaymentMethod; + count: number; + total: number; + percentage: number; +} + +export interface ServerSales { + serverId: string; + serverName: string; + orders: number; + sales: number; + tips: number; + averageCheck: number; +} + +export interface OrderTypeMix { + type: OrderType; + count: number; + revenue: number; + percentage: number; +} + +export interface InventoryReport { + totalValue: number; + lowStockItems: number; + outOfStockItems: number; + expiringItems: number; + itemsByCategory: { + category: string; + count: number; + value: number; + }[]; +} + +export interface StaffReport { + totalHours: number; + totalWages: number; + totalTips: number; + laborCostPercentage: number; + staffPerformance: StaffPerformance[]; +} + +export interface StaffPerformance { + staffId: string; + staffName: string; + hours: number; + sales: number; + ordersServed: number; + salesPerHour: number; + averageCheck: number; +} + +// ============================================================================ +// API Response Types +// ============================================================================ + +export interface ApiResponse { + success: boolean; + data?: T; + error?: ApiError; + metadata?: ResponseMetadata; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; +} + +export interface ResponseMetadata { + page?: number; + pageSize?: number; + totalCount?: number; + totalPages?: number; + timestamp: string; +} + +export interface ListParams { + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + filter?: Record; +} + +// ============================================================================ +// MCP App State Types +// ============================================================================ + +export interface AppState { + loading: boolean; + error: string | null; + lastUpdated: string | null; +} + +export interface OrdersAppState extends AppState { + orders: Order[]; + selectedOrder: Order | null; + filters: OrderFilters; +} + +export interface OrderFilters { + status?: OrderStatus[]; + type?: OrderType[]; + serverId?: string; + dateFrom?: string; + dateTo?: string; +} + +export interface MenuAppState extends AppState { + items: MenuItem[]; + categories: MenuCategory[]; + menus: Menu[]; + selectedItem: MenuItem | null; + editMode: boolean; +} + +export interface TablesAppState extends AppState { + tables: Table[]; + sections: Section[]; + selectedTable: Table | null; + floorPlan: FloorPlan | null; +} + +export interface StaffAppState extends AppState { + staff: Staff[]; + selectedStaff: Staff | null; + timesheets: TimeSheet[]; +} + +export interface CustomersAppState extends AppState { + customers: Customer[]; + selectedCustomer: Customer | null; + searchQuery: string; +} + +export interface ReservationsAppState extends AppState { + reservations: Reservation[]; + selectedReservation: Reservation | null; + filters: ReservationFilters; +} + +export interface ReservationFilters { + status?: ReservationStatus[]; + dateFrom?: string; + dateTo?: string; +} + +export interface InventoryAppState extends AppState { + items: InventoryItem[]; + lowStockItems: InventoryItem[]; + suppliers: Supplier[]; + selectedItem: InventoryItem | null; +} + +export interface ReportsAppState extends AppState { + salesReport: SalesReport | null; + inventoryReport: InventoryReport | null; + staffReport: StaffReport | null; + dateRange: { start: string; end: string }; +} diff --git a/servers/touchbistro/src/tools/customers.ts b/servers/touchbistro/src/tools/customers.ts new file mode 100644 index 0000000..97e9aab --- /dev/null +++ b/servers/touchbistro/src/tools/customers.ts @@ -0,0 +1,223 @@ +/** + * Customer Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const customerTools = { + touchbistro_list_customers: { + description: 'List all customers with loyalty points, visit history, and preferences.', + parameters: z.object({ + search: z.string().optional().describe('Search by name, email, or phone'), + page: z.number().optional().describe('Page number'), + pageSize: z.number().optional().describe('Items per page'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const filter: any = {}; + if (params.search) filter.search = params.search; + + const response = await client.getCustomers({ + page: params.page, + pageSize: params.pageSize, + filter, + }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch customers'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_customer: { + description: 'Get detailed customer information including loyalty, visit history, and preferences.', + parameters: z.object({ + customerId: z.string().describe('Customer ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getCustomer(params.customerId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch customer'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_customer: { + description: 'Create a new customer profile with contact information and preferences.', + parameters: 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'), + birthday: z.string().optional().describe('Birthday (ISO date format)'), + notes: z.string().optional().describe('Customer notes'), + dietaryRestrictions: z.array(z.string()).optional().describe('Dietary restrictions'), + allergies: z.array(z.string()).optional().describe('Allergies'), + tags: z.array(z.string()).optional().describe('Customer tags/labels'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createCustomer(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create customer'); + } + + return { + content: [ + { + type: 'text', + text: `Customer created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_customer: { + description: 'Update customer information, preferences, or notes.', + parameters: z.object({ + customerId: z.string().describe('Customer ID'), + 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'), + birthday: z.string().optional().describe('Birthday (ISO date format)'), + notes: z.string().optional().describe('Customer notes'), + dietaryRestrictions: z.array(z.string()).optional().describe('Dietary restrictions'), + allergies: z.array(z.string()).optional().describe('Allergies'), + tags: z.array(z.string()).optional().describe('Customer tags/labels'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { customerId, ...updates } = params; + const response = await client.updateCustomer(customerId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update customer'); + } + + return { + content: [ + { + type: 'text', + text: `Customer updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_search_customers: { + description: 'Search customers by name, email, phone, or tags.', + parameters: z.object({ + query: z.string().describe('Search query'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.searchCustomers(params.query); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to search customers'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_loyalty_transactions: { + description: 'Get customer loyalty point transaction history.', + parameters: z.object({ + customerId: z.string().describe('Customer ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getLoyaltyTransactions(params.customerId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch loyalty transactions'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_add_loyalty_points: { + description: 'Add loyalty points to a customer account.', + parameters: z.object({ + customerId: z.string().describe('Customer ID'), + points: z.number().describe('Points to add'), + description: z.string().describe('Description/reason for points'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.addLoyaltyPoints(params.customerId, params.points, params.description); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to add loyalty points'); + } + + return { + content: [ + { + type: 'text', + text: `${params.points} loyalty points added!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_redeem_loyalty_points: { + description: 'Redeem loyalty points from a customer account.', + parameters: z.object({ + customerId: z.string().describe('Customer ID'), + points: z.number().describe('Points to redeem'), + description: z.string().describe('Description of redemption'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.redeemLoyaltyPoints(params.customerId, params.points, params.description); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to redeem loyalty points'); + } + + return { + content: [ + { + type: 'text', + text: `${params.points} loyalty points redeemed!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/inventory.ts b/servers/touchbistro/src/tools/inventory.ts new file mode 100644 index 0000000..eb21a29 --- /dev/null +++ b/servers/touchbistro/src/tools/inventory.ts @@ -0,0 +1,290 @@ +/** + * Inventory Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const inventoryTools = { + touchbistro_list_inventory_items: { + description: 'List all inventory items with stock levels, costs, and suppliers.', + parameters: z.object({ + category: z.string().optional().describe('Filter by category'), + lowStock: z.boolean().optional().describe('Show only low stock items'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + if (params.lowStock) { + const response = await client.getLowStockItems(); + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch low stock items'); + } + return { + content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }], + }; + } + + const filter: any = {}; + if (params.category) filter.category = params.category; + + const response = await client.getInventoryItems({ filter }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch inventory items'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_inventory_item: { + description: 'Get detailed information about a specific inventory item.', + parameters: z.object({ + itemId: z.string().describe('Inventory item ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getInventoryItem(params.itemId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch inventory item'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_inventory_item: { + description: 'Create a new inventory item with stock tracking.', + parameters: z.object({ + name: z.string().describe('Item name'), + category: z.string().describe('Item category'), + unit: z.string().describe('Unit of measurement (e.g., lb, oz, each)'), + currentStock: z.number().describe('Current stock quantity'), + parLevel: z.number().describe('Par level (target stock)'), + reorderPoint: z.number().describe('Reorder point (minimum before restocking)'), + reorderQuantity: z.number().describe('Default reorder quantity'), + unitCost: z.number().describe('Cost per unit'), + supplierId: z.string().optional().describe('Supplier ID'), + sku: z.string().optional().describe('SKU'), + barcode: z.string().optional().describe('Barcode'), + location: z.string().optional().describe('Storage location'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createInventoryItem(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create inventory item'); + } + + return { + content: [ + { + type: 'text', + text: `Inventory item created!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_inventory_item: { + description: 'Update inventory item details.', + parameters: z.object({ + itemId: z.string().describe('Inventory item ID'), + name: z.string().optional().describe('Item name'), + category: z.string().optional().describe('Item category'), + parLevel: z.number().optional().describe('Par level'), + reorderPoint: z.number().optional().describe('Reorder point'), + reorderQuantity: z.number().optional().describe('Reorder quantity'), + unitCost: z.number().optional().describe('Cost per unit'), + supplierId: z.string().optional().describe('Supplier ID'), + location: z.string().optional().describe('Storage location'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { itemId, ...updates } = params; + const response = await client.updateInventoryItem(itemId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update inventory item'); + } + + return { + content: [ + { + type: 'text', + text: `Inventory item updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_adjust_stock: { + description: 'Adjust inventory stock levels (add, subtract, or correct).', + parameters: z.object({ + itemId: z.string().describe('Inventory item ID'), + quantity: z.number().describe('Quantity to adjust (positive or negative)'), + type: z.enum(['addition', 'subtraction', 'correction']).describe('Adjustment type'), + reason: z.string().describe('Reason for adjustment'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.adjustStock(params.itemId, params.quantity, params.type, params.reason); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to adjust stock'); + } + + return { + content: [ + { + type: 'text', + text: `Stock adjusted!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_list_suppliers: { + description: 'List all suppliers with contact information.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getSuppliers(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch suppliers'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_supplier: { + description: 'Create a new supplier.', + parameters: z.object({ + name: z.string().describe('Supplier name'), + contactName: z.string().optional().describe('Contact person name'), + email: z.string().optional().describe('Email address'), + phone: z.string().optional().describe('Phone number'), + address: z.string().optional().describe('Address'), + paymentTerms: z.string().optional().describe('Payment terms'), + notes: z.string().optional().describe('Notes'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createSupplier(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create supplier'); + } + + return { + content: [ + { + type: 'text', + text: `Supplier created!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_list_purchase_orders: { + description: 'List purchase orders with filters for status.', + parameters: z.object({ + status: z.enum(['draft', 'sent', 'received', 'cancelled']).optional().describe('Filter by status'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getPurchaseOrders(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch purchase orders'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_purchase_order: { + description: 'Create a new purchase order for a supplier.', + parameters: z.object({ + supplierId: z.string().describe('Supplier ID'), + items: z + .array( + z.object({ + inventoryItemId: z.string(), + quantity: z.number(), + unitCost: z.number(), + }) + ) + .describe('Items to order'), + expectedDate: z.string().optional().describe('Expected delivery date (ISO format)'), + notes: z.string().optional().describe('Order notes'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createPurchaseOrder(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create purchase order'); + } + + return { + content: [ + { + type: 'text', + text: `Purchase order created!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_receive_purchase_order: { + description: 'Mark a purchase order as received and update inventory.', + parameters: z.object({ + orderId: z.string().describe('Purchase order ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.receivePurchaseOrder(params.orderId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to receive purchase order'); + } + + return { + content: [ + { + type: 'text', + text: `Purchase order received!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/menu.ts b/servers/touchbistro/src/tools/menu.ts new file mode 100644 index 0000000..a3a6ca9 --- /dev/null +++ b/servers/touchbistro/src/tools/menu.ts @@ -0,0 +1,386 @@ +/** + * Menu Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const menuTools = { + touchbistro_list_menu_items: { + description: + 'List all menu items with optional filters (category, availability, search). Returns prices, descriptions, and modifiers.', + parameters: z.object({ + categoryId: z.string().optional().describe('Filter by category ID'), + available: z.boolean().optional().describe('Filter by availability'), + search: z.string().optional().describe('Search by name or description'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const filter: any = {}; + if (params.categoryId) filter.categoryId = params.categoryId; + if (params.available !== undefined) filter.available = params.available; + if (params.search) filter.search = params.search; + + const response = await client.getMenuItems({ filter }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch menu items'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_menu_item: { + description: 'Get detailed information about a specific menu item including pricing, modifiers, and nutrition.', + parameters: z.object({ + itemId: z.string().describe('Menu item ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getMenuItem(params.itemId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch menu item'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_menu_item: { + description: 'Create a new menu item with pricing, category, and modifiers.', + parameters: z.object({ + name: z.string().describe('Item name'), + description: z.string().optional().describe('Item description'), + price: z.number().describe('Item price'), + cost: z.number().optional().describe('Item cost'), + categoryId: z.string().describe('Category ID'), + sku: z.string().optional().describe('SKU'), + barcode: z.string().optional().describe('Barcode'), + taxable: z.boolean().default(true).describe('Is taxable'), + available: z.boolean().default(true).describe('Is available'), + prepTime: z.number().optional().describe('Preparation time in minutes'), + modifierGroupIds: z.array(z.string()).optional().describe('Modifier group IDs'), + allergens: z.array(z.string()).optional().describe('Allergen information'), + image: z.string().optional().describe('Image URL'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createMenuItem(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create menu item'); + } + + return { + content: [ + { + type: 'text', + text: `Menu item created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_menu_item: { + description: 'Update menu item details including price, availability, and description.', + parameters: z.object({ + itemId: z.string().describe('Menu item ID'), + name: z.string().optional().describe('Updated name'), + description: z.string().optional().describe('Updated description'), + price: z.number().optional().describe('Updated price'), + cost: z.number().optional().describe('Updated cost'), + available: z.boolean().optional().describe('Availability'), + prepTime: z.number().optional().describe('Preparation time in minutes'), + image: z.string().optional().describe('Image URL'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { itemId, ...updates } = params; + const response = await client.updateMenuItem(itemId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update menu item'); + } + + return { + content: [ + { + type: 'text', + text: `Menu item updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_delete_menu_item: { + description: 'Delete a menu item permanently.', + parameters: z.object({ + itemId: z.string().describe('Menu item ID to delete'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.deleteMenuItem(params.itemId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to delete menu item'); + } + + return { + content: [ + { + type: 'text', + text: 'Menu item deleted successfully!', + }, + ], + }; + }, + }, + + touchbistro_list_menu_categories: { + description: 'List all menu categories with item counts and hierarchy.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getMenuCategories(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch menu categories'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_menu_category: { + description: 'Create a new menu category with name and display order.', + parameters: z.object({ + name: z.string().describe('Category name'), + description: z.string().optional().describe('Category description'), + parentCategoryId: z.string().optional().describe('Parent category ID (for sub-categories)'), + sortOrder: z.number().optional().describe('Display sort order'), + available: z.boolean().default(true).describe('Is available'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createMenuCategory(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create menu category'); + } + + return { + content: [ + { + type: 'text', + text: `Category created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_menu_category: { + description: 'Update menu category details.', + parameters: z.object({ + categoryId: z.string().describe('Category ID'), + name: z.string().optional().describe('Updated name'), + description: z.string().optional().describe('Updated description'), + sortOrder: z.number().optional().describe('Display sort order'), + available: z.boolean().optional().describe('Availability'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { categoryId, ...updates } = params; + const response = await client.updateMenuCategory(categoryId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update menu category'); + } + + return { + content: [ + { + type: 'text', + text: `Category updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_delete_menu_category: { + description: 'Delete a menu category (items in category must be reassigned first).', + parameters: z.object({ + categoryId: z.string().describe('Category ID to delete'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.deleteMenuCategory(params.categoryId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to delete menu category'); + } + + return { + content: [ + { + type: 'text', + text: 'Menu category deleted successfully!', + }, + ], + }; + }, + }, + + touchbistro_list_menus: { + description: 'List all menus (breakfast, lunch, dinner, etc.) with active times and categories.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getMenus(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch menus'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_menu: { + description: 'Create a new menu with time-based availability (breakfast, lunch, happy hour, etc.).', + parameters: z.object({ + name: z.string().describe('Menu name'), + description: z.string().optional().describe('Menu description'), + startTime: z.string().optional().describe('Start time (HH:mm format)'), + endTime: z.string().optional().describe('End time (HH:mm format)'), + daysOfWeek: z.array(z.number()).optional().describe('Active days (0=Sunday, 6=Saturday)'), + available: z.boolean().default(true).describe('Is available'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createMenu(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create menu'); + } + + return { + content: [ + { + type: 'text', + text: `Menu created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_menu: { + description: 'Update menu details and availability schedule.', + parameters: z.object({ + menuId: z.string().describe('Menu ID'), + name: z.string().optional().describe('Updated name'), + description: z.string().optional().describe('Updated description'), + startTime: z.string().optional().describe('Start time (HH:mm format)'), + endTime: z.string().optional().describe('End time (HH:mm format)'), + daysOfWeek: z.array(z.number()).optional().describe('Active days (0=Sunday, 6=Saturday)'), + available: z.boolean().optional().describe('Availability'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { menuId, ...updates } = params; + const response = await client.updateMenu(menuId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update menu'); + } + + return { + content: [ + { + type: 'text', + text: `Menu updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_list_modifier_groups: { + description: 'List all modifier groups (toppings, sides, cooking temps, etc.).', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getModifierGroups(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch modifier groups'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_modifier_group: { + description: 'Create a new modifier group with modifiers.', + parameters: z.object({ + name: z.string().describe('Modifier group name'), + minSelections: z.number().default(0).describe('Minimum selections required'), + maxSelections: z.number().default(1).describe('Maximum selections allowed'), + required: z.boolean().default(false).describe('Is required'), + modifiers: z + .array( + z.object({ + name: z.string(), + price: z.number(), + available: z.boolean().default(true), + sortOrder: z.number().optional(), + }) + ) + .describe('Modifiers in this group'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createModifierGroup(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create modifier group'); + } + + return { + content: [ + { + type: 'text', + text: `Modifier group created!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/orders.ts b/servers/touchbistro/src/tools/orders.ts new file mode 100644 index 0000000..af07d6c --- /dev/null +++ b/servers/touchbistro/src/tools/orders.ts @@ -0,0 +1,338 @@ +/** + * Order Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const orderTools = { + touchbistro_list_orders: { + description: + 'List all orders with optional filters (status, type, date range, server). Returns active, completed, and historical orders.', + parameters: z.object({ + status: z + .enum(['draft', 'pending', 'in_progress', 'ready', 'completed', 'voided', 'cancelled']) + .optional() + .describe('Filter by order status'), + type: z + .enum(['dine_in', 'takeout', 'delivery', 'curbside', 'drive_thru']) + .optional() + .describe('Filter by order type'), + serverId: z.string().optional().describe('Filter by server ID'), + dateFrom: z.string().optional().describe('Start date (ISO format)'), + dateTo: z.string().optional().describe('End date (ISO format)'), + page: z.number().optional().describe('Page number'), + pageSize: z.number().optional().describe('Items per page'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const filter: any = {}; + if (params.status) filter.status = params.status; + if (params.type) filter.type = params.type; + if (params.serverId) filter.serverId = params.serverId; + if (params.dateFrom) filter.dateFrom = params.dateFrom; + if (params.dateTo) filter.dateTo = params.dateTo; + + const response = await client.getOrders({ + page: params.page, + pageSize: params.pageSize, + filter, + }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch orders'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_order: { + description: 'Get detailed information about a specific order including items, payments, and status.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getOrder(params.orderId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch order'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_order: { + description: 'Create a new order with initial items, table assignment, and server.', + parameters: z.object({ + type: z.enum(['dine_in', 'takeout', 'delivery', 'curbside', 'drive_thru']).describe('Order type'), + tableId: z.string().optional().describe('Table ID (for dine-in orders)'), + customerId: z.string().optional().describe('Customer ID'), + serverId: z.string().describe('Server/staff ID'), + guestCount: z.number().optional().describe('Number of guests'), + items: z + .array( + z.object({ + menuItemId: z.string(), + quantity: z.number(), + modifiers: z.array(z.object({ id: z.string(), name: z.string(), price: z.number() })).optional(), + specialInstructions: z.string().optional(), + seat: z.number().optional(), + }) + ) + .optional() + .describe('Initial order items'), + notes: z.string().optional().describe('Order notes'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createOrder(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create order'); + } + + return { + content: [ + { + type: 'text', + text: `Order created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_order: { + description: 'Update order details like guest count, notes, or special instructions.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + guestCount: z.number().optional().describe('Updated guest count'), + notes: z.string().optional().describe('Updated notes'), + specialInstructions: z.string().optional().describe('Special instructions'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { orderId, ...updates } = params; + const response = await client.updateOrder(orderId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update order'); + } + + return { + content: [ + { + type: 'text', + text: `Order updated successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_add_order_item: { + description: 'Add an item to an existing order with modifiers and special instructions.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + menuItemId: z.string().describe('Menu item ID'), + quantity: z.number().describe('Quantity'), + modifiers: z + .array(z.object({ id: z.string(), name: z.string(), price: z.number() })) + .optional() + .describe('Item modifiers'), + specialInstructions: z.string().optional().describe('Special cooking instructions'), + seat: z.number().optional().describe('Seat number'), + courseNumber: z.number().optional().describe('Course number'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { orderId, ...item } = params; + const response = await client.addOrderItem(orderId, item); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to add item to order'); + } + + return { + content: [ + { + type: 'text', + text: `Item added to order!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_order_item: { + description: 'Update an existing order item (quantity, modifiers, instructions).', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + itemId: z.string().describe('Order item ID'), + quantity: z.number().optional().describe('Updated quantity'), + modifiers: z + .array(z.object({ id: z.string(), name: z.string(), price: z.number() })) + .optional() + .describe('Updated modifiers'), + specialInstructions: z.string().optional().describe('Updated special instructions'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { orderId, itemId, ...updates } = params; + const response = await client.updateOrderItem(orderId, itemId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update order item'); + } + + return { + content: [ + { + type: 'text', + text: `Order item updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_remove_order_item: { + description: 'Remove an item from an order.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + itemId: z.string().describe('Order item ID to remove'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.removeOrderItem(params.orderId, params.itemId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to remove order item'); + } + + return { + content: [ + { + type: 'text', + text: `Order item removed successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_send_to_kitchen: { + description: 'Send an order to the kitchen for preparation. Marks items as fired.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.sendOrderToKitchen(params.orderId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to send order to kitchen'); + } + + return { + content: [ + { + type: 'text', + text: `Order sent to kitchen!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_complete_order: { + description: 'Mark an order as completed (all items served, payment received).', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.completeOrder(params.orderId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to complete order'); + } + + return { + content: [ + { + type: 'text', + text: `Order completed successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_void_order: { + description: 'Void an entire order with a reason (requires manager approval).', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + reason: z.string().describe('Reason for voiding the order'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.voidOrder(params.orderId, params.reason); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to void order'); + } + + return { + content: [ + { + type: 'text', + text: `Order voided!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_split_order: { + description: 'Split an order by items or amount across multiple checks.', + parameters: z.object({ + orderId: z.string().describe('Order ID to split'), + splits: z + .array( + z.object({ + items: z.array(z.string()).describe('Order item IDs for this split'), + amount: z.number().optional().describe('Fixed amount (alternative to items)'), + }) + ) + .describe('Split configurations'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.splitOrder(params.orderId, params.splits); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to split order'); + } + + return { + content: [ + { + type: 'text', + text: `Order split into ${params.splits.length} checks!\n\n${JSON.stringify( + response.data, + null, + 2 + )}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/payments.ts b/servers/touchbistro/src/tools/payments.ts new file mode 100644 index 0000000..3a46249 --- /dev/null +++ b/servers/touchbistro/src/tools/payments.ts @@ -0,0 +1,110 @@ +/** + * Payment Processing Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const paymentTools = { + touchbistro_process_payment: { + description: 'Process a payment for an order (full, partial, or split).', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + type: z.enum(['full', 'partial', 'split']).describe('Payment type'), + method: z + .enum(['cash', 'credit_card', 'debit_card', 'gift_card', 'mobile_payment', 'check', 'house_account']) + .describe('Payment method'), + amount: z.number().describe('Payment amount'), + tip: z.number().optional().describe('Tip amount'), + cardLast4: z.string().optional().describe('Last 4 digits of card'), + cardBrand: z.string().optional().describe('Card brand (Visa, Mastercard, etc.)'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { orderId, ...payment } = params; + const response = await client.processPayment(orderId, payment); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to process payment'); + } + + return { + content: [ + { + type: 'text', + text: `Payment processed successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_get_payments: { + description: 'Get all payments for an order.', + parameters: z.object({ + orderId: z.string().describe('Order ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getPayments(params.orderId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch payments'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_refund_payment: { + description: 'Refund a payment (full or partial).', + parameters: z.object({ + paymentId: z.string().describe('Payment ID'), + amount: z.number().optional().describe('Refund amount (full refund if not specified)'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.refundPayment(params.paymentId, params.amount); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to refund payment'); + } + + return { + content: [ + { + type: 'text', + text: `Payment refunded!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_void_payment: { + description: 'Void a payment transaction.', + parameters: z.object({ + paymentId: z.string().describe('Payment ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.voidPayment(params.paymentId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to void payment'); + } + + return { + content: [ + { + type: 'text', + text: `Payment voided!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/reservations.ts b/servers/touchbistro/src/tools/reservations.ts new file mode 100644 index 0000000..bdee08d --- /dev/null +++ b/servers/touchbistro/src/tools/reservations.ts @@ -0,0 +1,218 @@ +/** + * Reservation Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const reservationTools = { + touchbistro_list_reservations: { + description: 'List reservations with filters for date range and status.', + parameters: z.object({ + dateFrom: z.string().optional().describe('Start date (ISO format)'), + dateTo: z.string().optional().describe('End date (ISO format)'), + status: z + .enum(['pending', 'confirmed', 'seated', 'completed', 'no_show', 'cancelled']) + .optional() + .describe('Filter by status'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getReservations(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch reservations'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_reservation: { + description: 'Get detailed information about a specific reservation.', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getReservation(params.reservationId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch reservation'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_reservation: { + description: 'Create a new reservation for a customer.', + parameters: z.object({ + customerId: z.string().optional().describe('Customer ID (if existing customer)'), + customerName: z.string().describe('Customer name'), + customerPhone: z.string().optional().describe('Customer phone number'), + customerEmail: z.string().optional().describe('Customer email'), + partySize: z.number().describe('Number of guests'), + date: z.string().describe('Reservation date (ISO format)'), + time: z.string().describe('Reservation time (HH:mm format)'), + duration: z.number().optional().describe('Estimated duration in minutes'), + tableId: z.string().optional().describe('Specific table ID'), + sectionId: z.string().optional().describe('Preferred section ID'), + notes: z.string().optional().describe('Reservation notes'), + specialRequests: z.string().optional().describe('Special requests'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createReservation(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create reservation'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_reservation: { + description: 'Update reservation details (time, party size, table, etc.).', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + partySize: z.number().optional().describe('Updated party size'), + date: z.string().optional().describe('Updated date (ISO format)'), + time: z.string().optional().describe('Updated time (HH:mm format)'), + duration: z.number().optional().describe('Updated duration in minutes'), + tableId: z.string().optional().describe('Updated table ID'), + notes: z.string().optional().describe('Updated notes'), + specialRequests: z.string().optional().describe('Updated special requests'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { reservationId, ...updates } = params; + const response = await client.updateReservation(reservationId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update reservation'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_confirm_reservation: { + description: 'Confirm a pending reservation.', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.confirmReservation(params.reservationId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to confirm reservation'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation confirmed!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_seat_reservation: { + description: 'Seat a reservation at a table.', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + tableId: z.string().describe('Table ID to seat at'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.seatReservation(params.reservationId, params.tableId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to seat reservation'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation seated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_cancel_reservation: { + description: 'Cancel a reservation.', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + reason: z.string().optional().describe('Cancellation reason'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.cancelReservation(params.reservationId, params.reason); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to cancel reservation'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation cancelled!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_mark_no_show: { + description: 'Mark a reservation as no-show.', + parameters: z.object({ + reservationId: z.string().describe('Reservation ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.markNoShow(params.reservationId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to mark as no-show'); + } + + return { + content: [ + { + type: 'text', + text: `Reservation marked as no-show!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/staff.ts b/servers/touchbistro/src/tools/staff.ts new file mode 100644 index 0000000..536dca6 --- /dev/null +++ b/servers/touchbistro/src/tools/staff.ts @@ -0,0 +1,226 @@ +/** + * Staff Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const staffTools = { + touchbistro_list_staff: { + description: 'List all staff members with roles, active status, and assigned sections.', + parameters: z.object({ + role: z + .enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier']) + .optional() + .describe('Filter by role'), + active: z.boolean().optional().describe('Filter by active status'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const filter: any = {}; + if (params.role) filter.role = params.role; + if (params.active !== undefined) filter.active = params.active; + + const response = await client.getStaff({ filter }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch staff'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_staff_member: { + description: 'Get detailed information about a specific staff member.', + parameters: z.object({ + staffId: z.string().describe('Staff member ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getStaffMember(params.staffId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch staff member'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_staff_member: { + description: 'Create a new staff member with role, permissions, and wage information.', + parameters: z.object({ + employeeNumber: z.string().describe('Employee number'), + 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'), + role: z + .enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier']) + .describe('Staff role'), + pinCode: z.string().describe('POS PIN code'), + hireDate: z.string().describe('Hire date (ISO format)'), + wage: z.number().optional().describe('Wage amount'), + wageType: z.enum(['hourly', 'salary', 'tipped_hourly']).optional().describe('Wage type'), + assignedSections: z.array(z.string()).optional().describe('Assigned section IDs'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createStaffMember(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create staff member'); + } + + return { + content: [ + { + type: 'text', + text: `Staff member created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_staff_member: { + description: 'Update staff member details, role, or wage information.', + parameters: z.object({ + staffId: z.string().describe('Staff member ID'), + 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'), + role: z + .enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier']) + .optional() + .describe('Staff role'), + wage: z.number().optional().describe('Wage amount'), + wageType: z.enum(['hourly', 'salary', 'tipped_hourly']).optional().describe('Wage type'), + assignedSections: z.array(z.string()).optional().describe('Assigned section IDs'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { staffId, ...updates } = params; + const response = await client.updateStaffMember(staffId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update staff member'); + } + + return { + content: [ + { + type: 'text', + text: `Staff member updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_deactivate_staff_member: { + description: 'Deactivate a staff member (termination/resignation).', + parameters: z.object({ + staffId: z.string().describe('Staff member ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.deactivateStaffMember(params.staffId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to deactivate staff member'); + } + + return { + content: [ + { + type: 'text', + text: `Staff member deactivated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_clock_in: { + description: 'Clock in a staff member to start their shift.', + parameters: z.object({ + staffId: z.string().describe('Staff member ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.clockIn(params.staffId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to clock in'); + } + + return { + content: [ + { + type: 'text', + text: `Staff member clocked in!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_clock_out: { + description: 'Clock out a staff member to end their shift.', + parameters: z.object({ + staffId: z.string().describe('Staff member ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.clockOut(params.staffId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to clock out'); + } + + return { + content: [ + { + type: 'text', + text: `Staff member clocked out!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_get_timesheets: { + description: 'Get staff timesheets with hours, wages, and tips for a date range.', + parameters: z.object({ + staffId: z.string().optional().describe('Filter by staff member ID'), + dateFrom: z.string().optional().describe('Start date (ISO format)'), + dateTo: z.string().optional().describe('End date (ISO format)'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getTimeSheets(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch timesheets'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/tools/tables.ts b/servers/touchbistro/src/tools/tables.ts new file mode 100644 index 0000000..77c6c86 --- /dev/null +++ b/servers/touchbistro/src/tools/tables.ts @@ -0,0 +1,262 @@ +/** + * Table & Floor Management Tools + */ + +import { z } from 'zod'; +import type { TouchBistroApiClient } from '../lib/api-client.js'; + +export const tableTools = { + touchbistro_list_tables: { + description: 'List all tables with current status, assigned servers, and guest counts.', + parameters: z.object({ + status: z + .enum(['available', 'occupied', 'reserved', 'cleaning', 'out_of_service']) + .optional() + .describe('Filter by table status'), + sectionId: z.string().optional().describe('Filter by section ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const filter: any = {}; + if (params.status) filter.status = params.status; + if (params.sectionId) filter.sectionId = params.sectionId; + + const response = await client.getTables({ filter }); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch tables'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_table: { + description: 'Get detailed information about a specific table.', + parameters: z.object({ + tableId: z.string().describe('Table ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getTable(params.tableId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch table'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_table: { + description: 'Create a new table in a section with position and capacity.', + parameters: z.object({ + number: z.string().describe('Table number'), + name: z.string().optional().describe('Table name'), + sectionId: z.string().describe('Section ID'), + capacity: z.number().describe('Maximum guest capacity'), + shape: z.enum(['square', 'rectangle', 'circle', 'oval']).describe('Table shape'), + positionX: z.number().describe('X position on floor plan'), + positionY: z.number().describe('Y position on floor plan'), + rotation: z.number().optional().describe('Rotation in degrees'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createTable(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create table'); + } + + return { + content: [ + { + type: 'text', + text: `Table created successfully!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_table: { + description: 'Update table details like capacity, position, or name.', + parameters: z.object({ + tableId: z.string().describe('Table ID'), + number: z.string().optional().describe('Table number'), + name: z.string().optional().describe('Table name'), + capacity: z.number().optional().describe('Maximum guest capacity'), + positionX: z.number().optional().describe('X position on floor plan'), + positionY: z.number().optional().describe('Y position on floor plan'), + rotation: z.number().optional().describe('Rotation in degrees'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const { tableId, ...updates } = params; + const response = await client.updateTable(tableId, updates); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update table'); + } + + return { + content: [ + { + type: 'text', + text: `Table updated!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_update_table_status: { + description: 'Update table status (seat guests, mark as cleaning, reserve, etc.).', + parameters: z.object({ + tableId: z.string().describe('Table ID'), + status: z + .enum(['available', 'occupied', 'reserved', 'cleaning', 'out_of_service']) + .describe('New table status'), + guestCount: z.number().optional().describe('Number of guests (for occupied status)'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.updateTableStatus(params.tableId, params.status, params.guestCount); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to update table status'); + } + + return { + content: [ + { + type: 'text', + text: `Table status updated to ${params.status}!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_assign_server_to_table: { + description: 'Assign a server to a table.', + parameters: z.object({ + tableId: z.string().describe('Table ID'), + serverId: z.string().describe('Server/staff ID'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.assignServer(params.tableId, params.serverId); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to assign server'); + } + + return { + content: [ + { + type: 'text', + text: `Server assigned to table!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_list_sections: { + description: 'List all sections/stations with assigned servers and table counts.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getSections(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch sections'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_create_section: { + description: 'Create a new section/station on a floor plan.', + parameters: z.object({ + name: z.string().describe('Section name'), + floorPlanId: z.string().describe('Floor plan ID'), + color: z.string().optional().describe('Color code for display'), + assignedServerIds: z.array(z.string()).optional().describe('Assigned server IDs'), + }), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.createSection(params); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to create section'); + } + + return { + content: [ + { + type: 'text', + text: `Section created!\n\n${JSON.stringify(response.data, null, 2)}`, + }, + ], + }; + }, + }, + + touchbistro_list_floor_plans: { + description: 'List all floor plans with sections and table layouts.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getFloorPlans(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch floor plans'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, + + touchbistro_get_active_floor_plan: { + description: 'Get the currently active floor plan with all tables and sections.', + parameters: z.object({}), + handler: async (params: any, client: TouchBistroApiClient) => { + const response = await client.getActiveFloorPlan(); + + if (!response.success) { + throw new Error(response.error?.message || 'Failed to fetch active floor plan'); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify(response.data, null, 2), + }, + ], + }; + }, + }, +}; diff --git a/servers/touchbistro/src/ui/orders-app/App.css b/servers/touchbistro/src/ui/orders-app/App.css new file mode 100644 index 0000000..d41c7e4 --- /dev/null +++ b/servers/touchbistro/src/ui/orders-app/App.css @@ -0,0 +1,165 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.filter-bar { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.filter-btn { + padding: 0.75rem 1.5rem; + background: #1e293b; + border: 2px solid #334155; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s; +} + +.filter-btn:hover { + background: #334155; + border-color: #475569; +} + +.filter-btn.active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.orders-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 1.5rem; +} + +.order-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.2s; +} + +.order-card:hover { + border-color: #3b82f6; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1); +} + +.order-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.order-number { + font-size: 1.25rem; + font-weight: 600; + color: #f8fafc; +} + +.order-status { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; +} + +.order-details { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding: 1rem; + background: #0f172a; + border-radius: 0.5rem; +} + +.order-total { + font-size: 1.75rem; + font-weight: 700; + color: #10b981; +} + +.order-time { + color: #94a3b8; + font-size: 0.875rem; +} + +.order-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + flex: 1; + padding: 0.75rem; + background: #334155; + border: none; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + transition: all 0.2s; +} + +.action-btn:hover { + background: #475569; +} + +.action-btn.primary { + background: #3b82f6; + color: #fff; +} + +.action-btn.primary:hover { + background: #2563eb; +} + +.loading { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1.1rem; +} diff --git a/servers/touchbistro/src/ui/orders-app/App.tsx b/servers/touchbistro/src/ui/orders-app/App.tsx new file mode 100644 index 0000000..d1cd88e --- /dev/null +++ b/servers/touchbistro/src/ui/orders-app/App.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect } from 'react'; +import './App.css'; + +interface Order { + id: string; + orderNumber: string; + status: string; + total: number; + items: any[]; + createdAt: string; +} + +export default function OrdersApp() { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + // Simulated data for demonstration + const mockOrders: Order[] = [ + { + id: '1', + orderNumber: 'ORD-001', + status: 'preparing', + total: 45.99, + items: [], + createdAt: new Date().toISOString(), + }, + { + id: '2', + orderNumber: 'ORD-002', + status: 'ready', + total: 32.50, + items: [], + createdAt: new Date().toISOString(), + }, + ]; + setOrders(mockOrders); + setLoading(false); + }, []); + + const getStatusColor = (status: string) => { + const colors: Record = { + pending: '#fbbf24', + preparing: '#3b82f6', + ready: '#10b981', + served: '#8b5cf6', + completed: '#6b7280', + cancelled: '#ef4444', + }; + return colors[status] || '#6b7280'; + }; + + return ( +
+
+

🍽️ Orders Dashboard

+

Real-time order tracking and management

+
+ +
+ {['all', 'pending', 'preparing', 'ready', 'served'].map((status) => ( + + ))} +
+ +
+ {loading ? ( +
Loading orders...
+ ) : ( + orders.map((order) => ( +
+
+ {order.orderNumber} + + {order.status} + +
+
+
${order.total.toFixed(2)}
+
+ {new Date(order.createdAt).toLocaleTimeString()} +
+
+
+ + +
+
+ )) + )} +
+
+ ); +} diff --git a/servers/touchbistro/src/ui/orders-app/index.html b/servers/touchbistro/src/ui/orders-app/index.html new file mode 100644 index 0000000..a2118fe --- /dev/null +++ b/servers/touchbistro/src/ui/orders-app/index.html @@ -0,0 +1,12 @@ + + + + + + TouchBistro Orders Dashboard + + +
+ + + diff --git a/servers/touchbistro/src/ui/orders-app/main.tsx b/servers/touchbistro/src/ui/orders-app/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/orders-app/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/orders-app/vite.config.ts b/servers/touchbistro/src/ui/orders-app/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/touchbistro/src/ui/orders-app/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/touchbistro/src/ui/react-app/customer-directory/App.tsx b/servers/touchbistro/src/ui/react-app/customer-directory/App.tsx new file mode 100644 index 0000000..231f1c1 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/customer-directory/App.tsx @@ -0,0 +1,156 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Customer { + id: string; + name: string; + email: string; + phone: string; + visitCount: number; + totalSpent: number; + avgSpend: number; + lastVisit: string; + loyaltyPoints: number; + tags: string[]; +} + +export default function CustomerDirectory() { + const [customers, setCustomers] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState<'name' | 'visits' | 'spent'>('name'); + + useEffect(() => { + // Mock data + setCustomers([ + { id: '1', name: 'Sarah Johnson', email: 'sarah.j@email.com', phone: '555-0101', visitCount: 23, totalSpent: 1847.50, avgSpend: 80.33, lastVisit: '2024-02-14', loyaltyPoints: 2340, tags: ['VIP', 'Regular'] }, + { id: '2', name: 'Mike Chen', email: 'mike.chen@email.com', phone: '555-0102', visitCount: 45, totalSpent: 3245.75, avgSpend: 72.13, lastVisit: '2024-02-15', loyaltyPoints: 4890, tags: ['VIP', 'Regular', 'Anniversary'] }, + { id: '3', name: 'Emma Davis', email: 'emma.d@email.com', phone: '555-0103', visitCount: 8, totalSpent: 567.20, avgSpend: 70.90, lastVisit: '2024-02-12', loyaltyPoints: 850, tags: [] }, + { id: '4', name: 'John Smith', email: 'john.smith@email.com', phone: '555-0104', visitCount: 12, totalSpent: 982.40, avgSpend: 81.87, lastVisit: '2024-02-10', loyaltyPoints: 1470, tags: ['Regular'] }, + { id: '5', name: 'Lisa Park', email: 'lisa.park@email.com', phone: '555-0105', visitCount: 34, totalSpent: 2456.90, avgSpend: 72.26, lastVisit: '2024-02-15', loyaltyPoints: 3685, tags: ['VIP'] }, + { id: '6', name: 'David Wilson', email: 'david.w@email.com', phone: '555-0106', visitCount: 5, totalSpent: 342.50, avgSpend: 68.50, lastVisit: '2024-02-08', loyaltyPoints: 515, tags: [] }, + { id: '7', name: 'Amy Chen', email: 'amy.chen@email.com', phone: '555-0107', visitCount: 18, totalSpent: 1456.80, avgSpend: 80.93, lastVisit: '2024-02-13', loyaltyPoints: 2185, tags: ['Regular'] }, + { id: '8', name: 'Robert Taylor', email: 'robert.t@email.com', phone: '555-0108', visitCount: 29, totalSpent: 2123.60, avgSpend: 73.23, lastVisit: '2024-02-14', loyaltyPoints: 3185, tags: ['VIP', 'Regular'] }, + ]); + }, []); + + const filteredCustomers = customers + .filter((customer) => + customer.name.toLowerCase().includes(searchQuery.toLowerCase()) || + customer.email.toLowerCase().includes(searchQuery.toLowerCase()) || + customer.phone.includes(searchQuery) + ) + .sort((a, b) => { + if (sortBy === 'name') return a.name.localeCompare(b.name); + if (sortBy === 'visits') return b.visitCount - a.visitCount; + if (sortBy === 'spent') return b.totalSpent - a.totalSpent; + return 0; + }); + + const totalCustomers = customers.length; + const totalRevenue = customers.reduce((sum, c) => sum + c.totalSpent, 0); + const avgVisits = customers.reduce((sum, c) => sum + c.visitCount, 0) / customers.length; + + return ( +
+
+

👥 Customer Directory

+

Searchable customer database

+
+ +
+
+ Total Customers + {totalCustomers} +
+
+ Total Revenue + ${totalRevenue.toFixed(2)} +
+
+ Avg Visits + {avgVisits.toFixed(1)} +
+
+ +
+ setSearchQuery(e.target.value)} + /> + + +
+ +
+ {filteredCustomers.map((customer) => ( +
+
+

{customer.name}

+ {customer.tags.length > 0 && ( +
+ {customer.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ +
+
+ 📧 + {customer.email} +
+
+ 📱 + {customer.phone} +
+
+ +
+
+ {customer.visitCount} + Visits +
+
+ ${customer.totalSpent.toFixed(0)} + Total Spent +
+
+ ${customer.avgSpend.toFixed(0)} + Avg Spend +
+
+ {customer.loyaltyPoints} + Points +
+
+ +
+ + Last visit: {new Date(customer.lastVisit).toLocaleDateString()} + + +
+
+ ))} +
+ + {filteredCustomers.length === 0 && ( +
No customers found
+ )} +
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/customer-directory/index.html b/servers/touchbistro/src/ui/react-app/customer-directory/index.html new file mode 100644 index 0000000..ca10850 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/customer-directory/index.html @@ -0,0 +1,12 @@ + + + + + + Customer Directory - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/customer-directory/main.tsx b/servers/touchbistro/src/ui/react-app/customer-directory/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/customer-directory/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/customer-directory/styles.css b/servers/touchbistro/src/ui/react-app/customer-directory/styles.css new file mode 100644 index 0000000..ff10455 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/customer-directory/styles.css @@ -0,0 +1,274 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.stats-bar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.25rem; + text-align: center; +} + +.stat-label { + display: block; + color: #94a3b8; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 0.5rem; +} + +.stat-value { + display: block; + font-size: 2rem; + font-weight: 700; + color: #f8fafc; +} + +.stat-value.revenue { + color: #10b981; +} + +.controls { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.search-input { + flex: 1; + min-width: 300px; + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.sort-select { + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; + cursor: pointer; +} + +.sort-select:focus { + outline: none; + border-color: #3b82f6; +} + +.btn-primary { + padding: 0.875rem 1.5rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-primary:hover { + background: #2563eb; +} + +.customer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 1.5rem; +} + +.customer-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.2s; +} + +.customer-card:hover { + border-color: #3b82f6; +} + +.customer-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #334155; +} + +.customer-header h3 { + color: #f8fafc; + font-size: 1.25rem; +} + +.tags { + display: flex; + gap: 0.5rem; +} + +.tag { + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; +} + +.tag.vip { + background: #fbbf24; + color: #78350f; +} + +.tag.regular { + background: #3b82f6; + color: #fff; +} + +.tag.anniversary { + background: #ec4899; + color: #fff; +} + +.customer-contact { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.contact-item { + display: flex; + align-items: center; + gap: 0.75rem; + color: #94a3b8; + font-size: 0.9375rem; +} + +.icon { + font-size: 1.125rem; +} + +.customer-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + margin-bottom: 1rem; +} + +.stat-item { + background: #0f172a; + padding: 0.875rem; + border-radius: 0.5rem; + text-align: center; +} + +.stat-item .stat-value { + display: block; + color: #10b981; + font-size: 1.25rem; + font-weight: 700; + margin-bottom: 0.25rem; +} + +.stat-item .stat-label { + display: block; + color: #64748b; + font-size: 0.75rem; + text-transform: uppercase; +} + +.customer-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 1rem; + border-top: 1px solid #334155; +} + +.last-visit { + color: #64748b; + font-size: 0.875rem; +} + +.view-btn { + padding: 0.5rem 1rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s; +} + +.view-btn:hover { + background: #2563eb; +} + +.no-results { + text-align: center; + padding: 3rem; + color: #64748b; + font-size: 1.125rem; + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; +} diff --git a/servers/touchbistro/src/ui/react-app/menu-item-detail/App.tsx b/servers/touchbistro/src/ui/react-app/menu-item-detail/App.tsx new file mode 100644 index 0000000..fdeb8d0 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-item-detail/App.tsx @@ -0,0 +1,170 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Modifier { + id: string; + name: string; + price: number; +} + +interface ModifierGroup { + id: string; + name: string; + required: boolean; + modifiers: Modifier[]; +} + +interface MenuItem { + id: string; + name: string; + description: string; + price: number; + cost: number; + category: string; + available: boolean; + prepTime: number; + allergens: string[]; + modifierGroups: ModifierGroup[]; +} + +export default function MenuItemDetail() { + const [item, setItem] = useState(null); + + useEffect(() => { + // Mock data + setItem({ + id: '1', + name: 'Classic Burger', + description: 'Half-pound beef patty with fresh lettuce, tomato, red onion, pickles, and our signature sauce on a toasted brioche bun', + price: 14.99, + cost: 4.25, + category: 'Burgers & Sandwiches', + available: true, + prepTime: 12, + allergens: ['Gluten', 'Dairy', 'Soy'], + modifierGroups: [ + { + id: '1', + name: 'Cheese Options', + required: false, + modifiers: [ + { id: '1', name: 'Cheddar', price: 1.50 }, + { id: '2', name: 'Swiss', price: 1.50 }, + { id: '3', name: 'Blue Cheese', price: 2.00 }, + { id: '4', name: 'Pepper Jack', price: 1.50 }, + ], + }, + { + id: '2', + name: 'Toppings', + required: false, + modifiers: [ + { id: '5', name: 'Bacon', price: 2.50 }, + { id: '6', name: 'Avocado', price: 2.00 }, + { id: '7', name: 'Fried Egg', price: 2.00 }, + { id: '8', name: 'Mushrooms', price: 1.50 }, + ], + }, + { + id: '3', + name: 'Doneness', + required: true, + modifiers: [ + { id: '9', name: 'Rare', price: 0 }, + { id: '10', name: 'Medium Rare', price: 0 }, + { id: '11', name: 'Medium', price: 0 }, + { id: '12', name: 'Medium Well', price: 0 }, + { id: '13', name: 'Well Done', price: 0 }, + ], + }, + ], + }); + }, []); + + if (!item) { + return
Loading item...
; + } + + const margin = ((item.price - item.cost) / item.price) * 100; + + return ( +
+
+
+

🍔 {item.name}

+

{item.category}

+
+
+ + {item.available ? '✓ Available' : '✗ Unavailable'} + + +
+
+ +
+
+
+

Description

+

{item.description}

+
+ +
+

Pricing & Cost

+
+
+ Menu Price + ${item.price.toFixed(2)} +
+
+ Food Cost + ${item.cost.toFixed(2)} +
+
+ Margin + {margin.toFixed(1)}% +
+
+ Prep Time + {item.prepTime} min +
+
+
+ +
+

Allergens

+
+ {item.allergens.map((allergen) => ( + + ⚠️ {allergen} + + ))} +
+
+
+ +
+

Modifier Groups

+ {item.modifierGroups.map((group) => ( +
+
+

{group.name}

+ {group.required && Required} +
+
+ {group.modifiers.map((mod) => ( +
+ {mod.name} + + {mod.price === 0 ? 'Free' : `+$${mod.price.toFixed(2)}`} + +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/menu-item-detail/index.html b/servers/touchbistro/src/ui/react-app/menu-item-detail/index.html new file mode 100644 index 0000000..eeb902f --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-item-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Menu Item Detail - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/menu-item-detail/main.tsx b/servers/touchbistro/src/ui/react-app/menu-item-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-item-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/menu-item-detail/styles.css b/servers/touchbistro/src/ui/react-app/menu-item-detail/styles.css new file mode 100644 index 0000000..b3d4cab --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-item-detail/styles.css @@ -0,0 +1,247 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.25rem; +} + +.category { + color: #94a3b8; + font-size: 1.1rem; +} + +.header-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.availability { + padding: 0.625rem 1.25rem; + border-radius: 0.5rem; + font-weight: 600; + font-size: 0.875rem; +} + +.availability.available { + background: #10b981; + color: #fff; +} + +.availability.unavailable { + background: #ef4444; + color: #fff; +} + +.btn-primary { + padding: 0.875rem 1.5rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary:hover { + background: #2563eb; +} + +.content-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 2rem; +} + +@media (max-width: 1024px) { + .content-grid { + grid-template-columns: 1fr; + } +} + +.main-info { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.info-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.info-card h2 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1rem; +} + +.description { + color: #cbd5e1; + font-size: 1.125rem; + line-height: 1.7; +} + +.pricing-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.pricing-item { + background: #0f172a; + padding: 1rem; + border-radius: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.pricing-label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + text-transform: uppercase; +} + +.pricing-value { + color: #f8fafc; + font-size: 1.5rem; + font-weight: 700; +} + +.pricing-value.price { + color: #10b981; +} + +.pricing-value.cost { + color: #ef4444; +} + +.pricing-value.margin { + color: #3b82f6; +} + +.allergens { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.allergen-badge { + padding: 0.625rem 1rem; + background: #7f1d1d; + color: #fca5a5; + border-radius: 0.5rem; + font-weight: 600; + font-size: 0.875rem; +} + +.modifiers-panel { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + height: fit-content; +} + +.modifiers-panel h2 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1.5rem; +} + +.modifier-group { + margin-bottom: 1.5rem; +} + +.modifier-group:last-child { + margin-bottom: 0; +} + +.group-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.group-header h3 { + color: #f8fafc; + font-size: 1.125rem; +} + +.required-badge { + padding: 0.25rem 0.625rem; + background: #fbbf24; + color: #78350f; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.modifiers-list { + background: #0f172a; + border-radius: 0.5rem; + overflow: hidden; +} + +.modifier-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.875rem 1rem; + border-bottom: 1px solid #334155; +} + +.modifier-row:last-child { + border-bottom: none; +} + +.modifier-name { + color: #cbd5e1; + font-size: 0.9375rem; +} + +.modifier-price { + color: #10b981; + font-weight: 600; + font-size: 0.875rem; +} + +.loading { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1.1rem; +} diff --git a/servers/touchbistro/src/ui/react-app/menu-manager/App.tsx b/servers/touchbistro/src/ui/react-app/menu-manager/App.tsx new file mode 100644 index 0000000..7aff6b3 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-manager/App.tsx @@ -0,0 +1,140 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface MenuItem { + id: string; + name: string; + description: string; + price: number; + category: string; + available: boolean; + modifiers: number; +} + +interface Category { + id: string; + name: string; + itemCount: number; +} + +export default function MenuManager() { + const [categories, setCategories] = useState([]); + const [items, setItems] = useState([]); + const [selectedCategory, setSelectedCategory] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + // Mock data + setCategories([ + { id: 'appetizers', name: 'Appetizers', itemCount: 8 }, + { id: 'entrees', name: 'Entrees', itemCount: 12 }, + { id: 'burgers', name: 'Burgers & Sandwiches', itemCount: 9 }, + { id: 'salads', name: 'Salads', itemCount: 6 }, + { id: 'desserts', name: 'Desserts', itemCount: 7 }, + { id: 'beverages', name: 'Beverages', itemCount: 15 }, + ]); + + setItems([ + { id: '1', name: 'Classic Burger', description: 'Beef patty, lettuce, tomato, onion, pickles', price: 14.99, category: 'burgers', available: true, modifiers: 8 }, + { id: '2', name: 'Caesar Salad', description: 'Romaine, parmesan, croutons, Caesar dressing', price: 9.99, category: 'salads', available: true, modifiers: 4 }, + { id: '3', name: 'Chicken Wings', description: '8 pieces with choice of sauce', price: 12.99, category: 'appetizers', available: true, modifiers: 6 }, + { id: '4', name: 'Grilled Salmon', description: 'Atlantic salmon with vegetables and rice', price: 24.99, category: 'entrees', available: true, modifiers: 3 }, + { id: '5', name: 'Chocolate Cake', description: 'Rich chocolate layer cake with ganache', price: 7.99, category: 'desserts', available: true, modifiers: 2 }, + { id: '6', name: 'Craft Beer', description: 'Rotating selection of local craft beers', price: 6.99, category: 'beverages', available: true, modifiers: 0 }, + { id: '7', name: 'Margherita Pizza', description: 'Fresh mozzarella, basil, tomato sauce', price: 16.99, category: 'entrees', available: false, modifiers: 5 }, + { id: '8', name: 'French Fries', description: 'Crispy golden fries with sea salt', price: 4.99, category: 'appetizers', available: true, modifiers: 4 }, + ]); + }, []); + + const filteredItems = items.filter((item) => { + const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory; + const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase()); + return matchesCategory && matchesSearch; + }); + + return ( +
+
+

📖 Menu Manager

+

Browse and edit menu items

+
+ +
+ setSearchQuery(e.target.value)} + /> + +
+ +
+
+

Categories

+
+ + {categories.map((cat) => ( + + ))} +
+ +
+ +
+
+

+ {selectedCategory === 'all' + ? 'All Items' + : categories.find((c) => c.id === selectedCategory)?.name} +

+ + {filteredItems.length} {filteredItems.length === 1 ? 'item' : 'items'} + +
+ +
+ {filteredItems.map((item) => ( +
+
+

{item.name}

+ + {item.available ? '✓ Available' : '✗ Unavailable'} + +
+

{item.description}

+
+ ${item.price.toFixed(2)} + {item.modifiers} modifiers +
+
+ + + +
+
+ ))} +
+ + {filteredItems.length === 0 && ( +
No items found
+ )} +
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/menu-manager/index.html b/servers/touchbistro/src/ui/react-app/menu-manager/index.html new file mode 100644 index 0000000..b5cd883 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Menu Manager - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/menu-manager/main.tsx b/servers/touchbistro/src/ui/react-app/menu-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-manager/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/menu-manager/styles.css b/servers/touchbistro/src/ui/react-app/menu-manager/styles.css new file mode 100644 index 0000000..67e9623 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/menu-manager/styles.css @@ -0,0 +1,298 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.toolbar { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.search-input { + flex: 1; + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.btn-primary, +.btn-secondary { + padding: 0.875rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.btn-primary { + background: #3b82f6; + color: #fff; +} + +.btn-primary:hover { + background: #2563eb; +} + +.btn-secondary { + background: #334155; + color: #cbd5e1; + width: 100%; + margin-top: 1rem; +} + +.btn-secondary:hover { + background: #475569; +} + +.content-grid { + display: grid; + grid-template-columns: 300px 1fr; + gap: 2rem; +} + +@media (max-width: 1024px) { + .content-grid { + grid-template-columns: 1fr; + } +} + +.categories-panel { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + height: fit-content; +} + +.categories-panel h2 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1rem; +} + +.category-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.category-btn { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.875rem 1rem; + background: #0f172a; + border: 2px solid #334155; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + text-align: left; + transition: all 0.2s; +} + +.category-btn:hover { + border-color: #475569; + background: #1e293b; +} + +.category-btn.active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.category-btn .count { + background: #334155; + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 600; +} + +.category-btn.active .count { + background: #2563eb; +} + +.items-panel { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.panel-header h2 { + color: #f8fafc; + font-size: 1.5rem; +} + +.results-count { + color: #94a3b8; + font-size: 0.875rem; +} + +.items-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.item-card { + background: #0f172a; + border: 1px solid #334155; + border-radius: 0.5rem; + padding: 1.25rem; + transition: all 0.2s; +} + +.item-card:hover { + border-color: #3b82f6; +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.75rem; +} + +.item-header h3 { + color: #f8fafc; + font-size: 1.125rem; + flex: 1; +} + +.availability { + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + +.availability.available { + background: #10b981; + color: #fff; +} + +.availability.unavailable { + background: #ef4444; + color: #fff; +} + +.item-description { + color: #94a3b8; + font-size: 0.875rem; + margin-bottom: 1rem; + line-height: 1.5; +} + +.item-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-top: 1rem; + border-top: 1px solid #334155; +} + +.price { + color: #10b981; + font-size: 1.25rem; + font-weight: 700; +} + +.modifiers { + color: #64748b; + font-size: 0.875rem; +} + +.item-actions { + display: flex; + gap: 0.5rem; +} + +.action-btn { + flex: 1; + padding: 0.625rem; + background: #334155; + border: none; + color: #cbd5e1; + border-radius: 0.375rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + transition: all 0.2s; +} + +.action-btn:hover { + background: #475569; +} + +.action-btn.delete { + background: #7f1d1d; + color: #fca5a5; +} + +.action-btn.delete:hover { + background: #991b1b; +} + +.no-results { + text-align: center; + padding: 3rem; + color: #64748b; + font-size: 1.125rem; +} diff --git a/servers/touchbistro/src/ui/react-app/order-dashboard/App.tsx b/servers/touchbistro/src/ui/react-app/order-dashboard/App.tsx new file mode 100644 index 0000000..b95c079 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-dashboard/App.tsx @@ -0,0 +1,152 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface OrderStats { + total: number; + pending: number; + inProgress: number; + completed: number; + revenue: number; + averageValue: number; +} + +interface RecentOrder { + id: string; + orderNumber: string; + status: string; + total: number; + time: string; +} + +export default function OrderDashboard() { + const [stats, setStats] = useState({ + total: 0, + pending: 0, + inProgress: 0, + completed: 0, + revenue: 0, + averageValue: 0, + }); + const [recentOrders, setRecentOrders] = useState([]); + const [hourlyVolume, setHourlyVolume] = useState<{ hour: string; count: number }[]>([]); + + useEffect(() => { + // Mock data + setStats({ + total: 142, + pending: 8, + inProgress: 12, + completed: 122, + revenue: 8547.32, + averageValue: 60.19, + }); + + setRecentOrders([ + { id: '1', orderNumber: 'ORD-142', status: 'in_progress', total: 68.50, time: '2 min ago' }, + { id: '2', orderNumber: 'ORD-141', status: 'pending', total: 45.00, time: '5 min ago' }, + { id: '3', orderNumber: 'ORD-140', status: 'completed', total: 92.75, time: '8 min ago' }, + { id: '4', orderNumber: 'ORD-139', status: 'in_progress', total: 54.25, time: '12 min ago' }, + { id: '5', orderNumber: 'ORD-138', status: 'completed', total: 78.90, time: '15 min ago' }, + ]); + + setHourlyVolume([ + { hour: '11am', count: 15 }, + { hour: '12pm', count: 28 }, + { hour: '1pm', count: 32 }, + { hour: '2pm', count: 18 }, + { hour: '3pm', count: 12 }, + { hour: '4pm', count: 8 }, + { hour: '5pm', count: 22 }, + { hour: '6pm', count: 35 }, + ]); + }, []); + + const getStatusColor = (status: string) => { + const colors: Record = { + pending: '#fbbf24', + in_progress: '#3b82f6', + completed: '#10b981', + }; + return colors[status] || '#6b7280'; + }; + + const maxVolume = Math.max(...hourlyVolume.map(h => h.count)); + + return ( +
+
+

📊 Order Dashboard

+

Live order statistics and activity

+
+ +
+
+
Total Orders Today
+
{stats.total}
+
+
+
Pending
+
{stats.pending}
+
+
+
In Progress
+
{stats.inProgress}
+
+
+
Completed
+
{stats.completed}
+
+
+
Total Revenue
+
${stats.revenue.toFixed(2)}
+
+
+
Average Order Value
+
${stats.averageValue.toFixed(2)}
+
+
+ +
+
+

Hourly Order Volume

+
+ {hourlyVolume.map((item) => ( +
+
+ {item.count} +
+
{item.hour}
+
+ ))} +
+
+ +
+

Recent Orders

+
+ {recentOrders.map((order) => ( +
+
+ {order.orderNumber} + {order.time} +
+
+ + {order.status.replace('_', ' ')} + + ${order.total.toFixed(2)} +
+
+ ))} +
+
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/order-dashboard/index.html b/servers/touchbistro/src/ui/react-app/order-dashboard/index.html new file mode 100644 index 0000000..7730d3a --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Order Dashboard - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/order-dashboard/main.tsx b/servers/touchbistro/src/ui/react-app/order-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/order-dashboard/styles.css b/servers/touchbistro/src/ui/react-app/order-dashboard/styles.css new file mode 100644 index 0000000..545d2bd --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-dashboard/styles.css @@ -0,0 +1,197 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.stat-label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #f8fafc; +} + +.stat-value.pending { + color: #fbbf24; +} + +.stat-value.in-progress { + color: #3b82f6; +} + +.stat-value.completed { + color: #10b981; +} + +.stat-value.revenue { + color: #10b981; +} + +.dashboard-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +@media (max-width: 1024px) { + .dashboard-content { + grid-template-columns: 1fr; + } +} + +.section { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.section h2 { + color: #f8fafc; + font-size: 1.5rem; + margin-bottom: 1.5rem; +} + +.chart { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 0.5rem; + height: 200px; + padding: 1rem; + background: #0f172a; + border-radius: 0.5rem; +} + +.chart-bar { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + height: 100%; +} + +.bar-fill { + width: 100%; + background: linear-gradient(to top, #3b82f6, #60a5fa); + border-radius: 0.25rem; + display: flex; + align-items: flex-start; + justify-content: center; + padding-top: 0.5rem; + min-height: 30px; +} + +.bar-label { + color: #fff; + font-size: 0.875rem; + font-weight: 600; +} + +.bar-hour { + color: #94a3b8; + font-size: 0.75rem; + margin-top: 0.5rem; +} + +.recent-orders { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.order-row { + background: #0f172a; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.order-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.order-number { + color: #f8fafc; + font-weight: 600; + font-size: 1rem; +} + +.order-time { + color: #64748b; + font-size: 0.875rem; +} + +.order-details { + display: flex; + align-items: center; + gap: 1rem; +} + +.order-status { + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; +} + +.order-total { + color: #10b981; + font-weight: 700; + font-size: 1.125rem; +} diff --git a/servers/touchbistro/src/ui/react-app/order-detail/App.tsx b/servers/touchbistro/src/ui/react-app/order-detail/App.tsx new file mode 100644 index 0000000..f42981d --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-detail/App.tsx @@ -0,0 +1,211 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface OrderItem { + id: string; + name: string; + quantity: number; + price: number; + modifiers: { name: string; price: number }[]; + instructions?: string; +} + +interface Order { + id: string; + orderNumber: string; + status: string; + type: string; + customer: string; + server: string; + table?: string; + items: OrderItem[]; + subtotal: number; + tax: number; + tip: number; + total: number; + paymentMethod: string; + createdAt: string; + completedAt?: string; +} + +export default function OrderDetail() { + const [order, setOrder] = useState(null); + + useEffect(() => { + // Mock data + setOrder({ + id: '1', + orderNumber: 'ORD-142', + status: 'in_progress', + type: 'dine_in', + customer: 'Sarah Johnson', + server: 'Mike Chen', + table: 'Table 12', + items: [ + { + id: '1', + name: 'Classic Burger', + quantity: 2, + price: 14.99, + modifiers: [ + { name: 'Extra Cheese', price: 1.50 }, + { name: 'No Onions', price: 0 }, + ], + instructions: 'Medium rare', + }, + { + id: '2', + name: 'Caesar Salad', + quantity: 1, + price: 9.99, + modifiers: [ + { name: 'Add Chicken', price: 4.00 }, + ], + }, + { + id: '3', + name: 'French Fries', + quantity: 2, + price: 4.99, + modifiers: [], + }, + { + id: '4', + name: 'Soft Drink', + quantity: 2, + price: 2.99, + modifiers: [], + }, + ], + subtotal: 60.43, + tax: 5.44, + tip: 12.00, + total: 77.87, + paymentMethod: 'Credit Card', + createdAt: new Date(Date.now() - 15 * 60000).toISOString(), + }); + }, []); + + if (!order) { + return
Loading order...
; + } + + const getStatusColor = (status: string) => { + const colors: Record = { + draft: '#6b7280', + pending: '#fbbf24', + in_progress: '#3b82f6', + ready: '#10b981', + completed: '#10b981', + voided: '#ef4444', + }; + return colors[status] || '#6b7280'; + }; + + return ( +
+
+
+

🧾 Order Detail

+

{order.orderNumber}

+
+ + {order.status.replace('_', ' ').toUpperCase()} + +
+ +
+
+

Order Information

+
+
+ Order Type + {order.type.replace('_', ' ')} +
+
+ Customer + {order.customer} +
+
+ Server + {order.server} +
+
+ Table + {order.table || 'N/A'} +
+
+ Created + + {new Date(order.createdAt).toLocaleString()} + +
+
+ Payment + {order.paymentMethod} +
+
+
+ +
+

Order Items

+
+ {order.items.map((item) => ( +
+
+ + {item.quantity}× {item.name} + + + ${(item.price * item.quantity).toFixed(2)} + +
+ {item.modifiers.length > 0 && ( +
+ {item.modifiers.map((mod, idx) => ( +
+ • {mod.name} + {mod.price > 0 && ` (+$${mod.price.toFixed(2)})`} +
+ ))} +
+ )} + {item.instructions && ( +
📝 {item.instructions}
+ )} +
+ ))} +
+ +
+
+ Subtotal + ${order.subtotal.toFixed(2)} +
+
+ Tax + ${order.tax.toFixed(2)} +
+
+ Tip + ${order.tip.toFixed(2)} +
+
+ Total + ${order.total.toFixed(2)} +
+
+ +
+ + + +
+
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/order-detail/index.html b/servers/touchbistro/src/ui/react-app/order-detail/index.html new file mode 100644 index 0000000..671b4e1 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Order Detail - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/order-detail/main.tsx b/servers/touchbistro/src/ui/react-app/order-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/order-detail/styles.css b/servers/touchbistro/src/ui/react-app/order-detail/styles.css new file mode 100644 index 0000000..305e2ec --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-detail/styles.css @@ -0,0 +1,221 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.25rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.status-badge { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + color: #fff; + font-size: 0.875rem; +} + +.order-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 2rem; +} + +@media (max-width: 1024px) { + .order-grid { + grid-template-columns: 1fr; + } +} + +.info-section, +.items-section { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +h2 { + color: #f8fafc; + font-size: 1.5rem; + margin-bottom: 1.5rem; +} + +.info-grid { + display: grid; + gap: 1.25rem; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + background: #0f172a; + border-radius: 0.5rem; +} + +.info-label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + text-transform: uppercase; +} + +.info-value { + color: #f8fafc; + font-size: 1.125rem; + font-weight: 600; + text-transform: capitalize; +} + +.items-list { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.item-card { + background: #0f172a; + border-radius: 0.5rem; + padding: 1rem; +} + +.item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.item-name { + color: #f8fafc; + font-weight: 600; + font-size: 1.125rem; +} + +.item-price { + color: #10b981; + font-weight: 700; + font-size: 1.125rem; +} + +.modifiers { + margin-top: 0.75rem; + padding-left: 1rem; +} + +.modifier { + color: #94a3b8; + font-size: 0.875rem; + margin-bottom: 0.25rem; +} + +.instructions { + margin-top: 0.75rem; + padding: 0.75rem; + background: #1e293b; + border-left: 3px solid #fbbf24; + border-radius: 0.25rem; + color: #fbbf24; + font-size: 0.875rem; +} + +.totals { + background: #0f172a; + border-radius: 0.5rem; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.total-row { + display: flex; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid #334155; + color: #cbd5e1; + font-size: 1rem; +} + +.total-row:last-child { + border-bottom: none; +} + +.total-row.total { + font-size: 1.5rem; + font-weight: 700; + color: #10b981; + padding-top: 1rem; + margin-top: 0.5rem; + border-top: 2px solid #334155; +} + +.actions { + display: flex; + gap: 1rem; +} + +.btn { + flex: 1; + padding: 1rem; + border: none; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary { + background: #334155; + color: #cbd5e1; +} + +.btn-secondary:hover { + background: #475569; +} + +.btn-primary { + background: #3b82f6; + color: #fff; +} + +.btn-primary:hover { + background: #2563eb; +} + +.loading { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1.1rem; +} diff --git a/servers/touchbistro/src/ui/react-app/order-grid/App.tsx b/servers/touchbistro/src/ui/react-app/order-grid/App.tsx new file mode 100644 index 0000000..4385845 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-grid/App.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Order { + id: string; + orderNumber: string; + status: string; + type: string; + customer: string; + server: string; + total: number; + createdAt: string; +} + +export default function OrderGrid() { + const [orders, setOrders] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [typeFilter, setTypeFilter] = useState('all'); + + useEffect(() => { + // Mock data + const mockOrders: Order[] = [ + { id: '1', orderNumber: 'ORD-142', status: 'in_progress', type: 'dine_in', customer: 'Sarah Johnson', server: 'Mike Chen', total: 77.87, createdAt: '2024-01-15T14:30:00Z' }, + { id: '2', orderNumber: 'ORD-141', status: 'completed', type: 'takeout', customer: 'John Smith', server: 'Lisa Park', total: 45.50, createdAt: '2024-01-15T14:15:00Z' }, + { id: '3', orderNumber: 'ORD-140', status: 'pending', type: 'delivery', customer: 'Emma Davis', server: 'Tom Wilson', total: 92.30, createdAt: '2024-01-15T14:00:00Z' }, + { id: '4', orderNumber: 'ORD-139', status: 'completed', type: 'dine_in', customer: 'Michael Brown', server: 'Mike Chen', total: 156.75, createdAt: '2024-01-15T13:45:00Z' }, + { id: '5', orderNumber: 'ORD-138', status: 'voided', type: 'takeout', customer: 'Jessica Lee', server: 'Lisa Park', total: 34.20, createdAt: '2024-01-15T13:30:00Z' }, + { id: '6', orderNumber: 'ORD-137', status: 'completed', type: 'dine_in', customer: 'David Wilson', server: 'Tom Wilson', total: 68.90, createdAt: '2024-01-15T13:15:00Z' }, + { id: '7', orderNumber: 'ORD-136', status: 'completed', type: 'curbside', customer: 'Amy Chen', server: 'Mike Chen', total: 52.40, createdAt: '2024-01-15T13:00:00Z' }, + { id: '8', orderNumber: 'ORD-135', status: 'completed', type: 'dine_in', customer: 'Robert Taylor', server: 'Lisa Park', total: 89.25, createdAt: '2024-01-15T12:45:00Z' }, + ]; + setOrders(mockOrders); + }, []); + + const filteredOrders = orders.filter((order) => { + const matchesSearch = + order.orderNumber.toLowerCase().includes(searchQuery.toLowerCase()) || + order.customer.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesStatus = statusFilter === 'all' || order.status === statusFilter; + const matchesType = typeFilter === 'all' || order.type === typeFilter; + return matchesSearch && matchesStatus && matchesType; + }); + + const getStatusColor = (status: string) => { + const colors: Record = { + draft: '#6b7280', + pending: '#fbbf24', + in_progress: '#3b82f6', + ready: '#10b981', + completed: '#10b981', + voided: '#ef4444', + }; + return colors[status] || '#6b7280'; + }; + + return ( +
+
+

📋 Order History

+

Search and filter all orders

+
+ +
+ setSearchQuery(e.target.value)} + /> + + +
+ +
+ Showing {filteredOrders.length} of {orders.length} orders +
+ +
+
+ + + + + + + + + + + + + + {filteredOrders.map((order) => ( + + + + + + + + + + + ))} + +
Order #CustomerTypeServerStatusTotalTimeActions
{order.orderNumber}{order.customer} + + {order.type.replace('_', ' ')} + + {order.server} + + {order.status.replace('_', ' ')} + + ${order.total.toFixed(2)} + {new Date(order.createdAt).toLocaleTimeString()} + + +
+ + + {filteredOrders.length === 0 && ( +
+ No orders found matching your filters +
+ )} + + ); +} diff --git a/servers/touchbistro/src/ui/react-app/order-grid/index.html b/servers/touchbistro/src/ui/react-app/order-grid/index.html new file mode 100644 index 0000000..2c10b08 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-grid/index.html @@ -0,0 +1,12 @@ + + + + + + Order Grid - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/order-grid/main.tsx b/servers/touchbistro/src/ui/react-app/order-grid/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-grid/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/order-grid/styles.css b/servers/touchbistro/src/ui/react-app/order-grid/styles.css new file mode 100644 index 0000000..c273eef --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/order-grid/styles.css @@ -0,0 +1,197 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.filters { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + flex-wrap: wrap; +} + +.search-input { + flex: 1; + min-width: 300px; + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; +} + +.search-input:focus { + outline: none; + border-color: #3b82f6; +} + +.search-input::placeholder { + color: #64748b; +} + +.filter-select { + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; + cursor: pointer; + min-width: 150px; +} + +.filter-select:focus { + outline: none; + border-color: #3b82f6; +} + +.results-info { + color: #94a3b8; + margin-bottom: 1rem; + font-size: 0.875rem; +} + +.table-container { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + overflow: hidden; +} + +.orders-table { + width: 100%; + border-collapse: collapse; +} + +.orders-table thead { + background: #0f172a; +} + +.orders-table th { + padding: 1rem; + text-align: left; + color: #94a3b8; + font-weight: 600; + font-size: 0.875rem; + text-transform: uppercase; + border-bottom: 2px solid #334155; +} + +.orders-table td { + padding: 1rem; + border-bottom: 1px solid #334155; + color: #cbd5e1; +} + +.orders-table tbody tr:hover { + background: #0f172a; +} + +.orders-table tbody tr:last-child td { + border-bottom: none; +} + +.order-number { + font-weight: 600; + color: #f8fafc; + font-family: 'Courier New', monospace; +} + +.type-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + background: #334155; + color: #cbd5e1; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: capitalize; +} + +.status-badge { + display: inline-block; + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; +} + +.total { + font-weight: 700; + color: #10b981; + font-size: 1.125rem; +} + +.time { + color: #94a3b8; + font-size: 0.875rem; +} + +.action-btn { + padding: 0.5rem 1rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 600; + font-size: 0.875rem; + transition: all 0.2s; +} + +.action-btn:hover { + background: #2563eb; +} + +.no-results { + text-align: center; + padding: 3rem; + color: #64748b; + font-size: 1.125rem; + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + margin-top: 1rem; +} + +@media (max-width: 1024px) { + .table-container { + overflow-x: auto; + } + + .orders-table { + min-width: 800px; + } +} diff --git a/servers/touchbistro/src/ui/react-app/reservation-calendar/App.tsx b/servers/touchbistro/src/ui/react-app/reservation-calendar/App.tsx new file mode 100644 index 0000000..ad4fa5e --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-calendar/App.tsx @@ -0,0 +1,151 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Reservation { + id: string; + time: string; + name: string; + partySize: number; + status: 'confirmed' | 'pending' | 'seated' | 'completed'; + table?: string; + phone: string; +} + +export default function ReservationCalendar() { + const [date, setDate] = useState(new Date().toISOString().split('T')[0]); + const [view, setView] = useState<'day' | 'week'>('day'); + const [reservations, setReservations] = useState([]); + + useEffect(() => { + // Mock data + setReservations([ + { id: '1', time: '11:00', name: 'Sarah Johnson', partySize: 4, status: 'completed', table: 'Table 5', phone: '555-0101' }, + { id: '2', time: '11:30', name: 'Mike Chen', partySize: 2, status: 'completed', table: 'Table 2', phone: '555-0102' }, + { id: '3', time: '12:00', name: 'Emma Davis', partySize: 6, status: 'seated', table: 'Table 8', phone: '555-0103' }, + { id: '4', time: '12:30', name: 'John Smith', partySize: 3, status: 'seated', table: 'Table 4', phone: '555-0104' }, + { id: '5', time: '13:00', name: 'Lisa Park', partySize: 2, status: 'confirmed', phone: '555-0105' }, + { id: '6', time: '13:30', name: 'David Wilson', partySize: 5, status: 'confirmed', phone: '555-0106' }, + { id: '7', time: '14:00', name: 'Amy Chen', partySize: 4, status: 'pending', phone: '555-0107' }, + { id: '8', time: '18:00', name: 'Robert Taylor', partySize: 8, status: 'confirmed', phone: '555-0108' }, + { id: '9', time: '18:30', name: 'Jessica Lee', partySize: 2, status: 'confirmed', phone: '555-0109' }, + { id: '10', time: '19:00', name: 'Michael Brown', partySize: 6, status: 'confirmed', phone: '555-0110' }, + { id: '11', time: '19:30', name: 'Emily White', partySize: 4, status: 'confirmed', phone: '555-0111' }, + { id: '12', time: '20:00', name: 'Tom Anderson', partySize: 2, status: 'pending', phone: '555-0112' }, + ]); + }, []); + + const timeSlots = [ + '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', + '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30', + '19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00' + ]; + + const getStatusColor = (status: string) => { + const colors = { + pending: '#fbbf24', + confirmed: '#3b82f6', + seated: '#10b981', + completed: '#6b7280', + }; + return colors[status as keyof typeof colors] || '#6b7280'; + }; + + const stats = { + total: reservations.length, + pending: reservations.filter(r => r.status === 'pending').length, + confirmed: reservations.filter(r => r.status === 'confirmed').length, + seated: reservations.filter(r => r.status === 'seated').length, + }; + + return ( +
+
+

📅 Reservation Calendar

+

Manage reservations by day or week

+
+ +
+ setDate(e.target.value)} + /> +
+ + +
+ +
+ +
+
+ Total Today + {stats.total} +
+
+ Pending + {stats.pending} +
+
+ Confirmed + {stats.confirmed} +
+
+ Seated + {stats.seated} +
+
+ +
+
+ {timeSlots.map((time) => ( +
+ {time} +
+ ))} +
+
+ {timeSlots.map((time) => { + const slotReservations = reservations.filter(r => r.time === time); + return ( +
+ {slotReservations.map((res) => ( +
+
+ {res.name} + 👥 {res.partySize} +
+
+ + {res.status} + + {res.table && 🪑 {res.table}} +
+
+ ))} +
+ ); + })} +
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/reservation-calendar/index.html b/servers/touchbistro/src/ui/react-app/reservation-calendar/index.html new file mode 100644 index 0000000..0c651ad --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-calendar/index.html @@ -0,0 +1,12 @@ + + + + + + Reservation Calendar - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/reservation-calendar/main.tsx b/servers/touchbistro/src/ui/react-app/reservation-calendar/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-calendar/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/reservation-calendar/styles.css b/servers/touchbistro/src/ui/react-app/reservation-calendar/styles.css new file mode 100644 index 0000000..31c9c26 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-calendar/styles.css @@ -0,0 +1,242 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.controls { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.date-input { + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; + cursor: pointer; +} + +.date-input:focus { + outline: none; + border-color: #3b82f6; +} + +.view-toggle { + display: flex; + background: #1e293b; + border-radius: 0.5rem; + overflow: hidden; +} + +.toggle-btn { + padding: 0.875rem 1.5rem; + background: transparent; + border: none; + color: #cbd5e1; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s; +} + +.toggle-btn:hover { + background: #334155; +} + +.toggle-btn.active { + background: #3b82f6; + color: #fff; +} + +.btn-primary { + padding: 0.875rem 1.5rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; + margin-left: auto; +} + +.btn-primary:hover { + background: #2563eb; +} + +.stats-bar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + padding: 1rem; + text-align: center; +} + +.stat-card.pending { + border-color: #fbbf24; +} + +.stat-card.confirmed { + border-color: #3b82f6; +} + +.stat-card.seated { + border-color: #10b981; +} + +.stat-label { + display: block; + color: #94a3b8; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 0.5rem; +} + +.stat-value { + display: block; + font-size: 1.75rem; + font-weight: 700; + color: #f8fafc; +} + +.calendar-container { + display: grid; + grid-template-columns: 100px 1fr; + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + overflow: hidden; +} + +.time-column { + background: #0f172a; + border-right: 2px solid #334155; +} + +.time-slot { + height: 60px; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 1px solid #334155; + color: #94a3b8; + font-weight: 600; + font-size: 0.875rem; +} + +.time-slot:last-child { + border-bottom: none; +} + +.reservations-column { + overflow-y: auto; + max-height: 700px; +} + +.reservation-slot { + min-height: 60px; + border-bottom: 1px solid #334155; + padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.reservation-slot:last-child { + border-bottom: none; +} + +.reservation-card { + background: #0f172a; + border-left: 4px solid; + border-radius: 0.5rem; + padding: 0.75rem; + cursor: pointer; + transition: all 0.2s; +} + +.reservation-card:hover { + background: #1e293b; + transform: translateX(4px); +} + +.res-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.res-name { + color: #f8fafc; + font-weight: 600; + font-size: 0.9375rem; +} + +.res-party { + color: #cbd5e1; + font-size: 0.875rem; +} + +.res-details { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.res-status { + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; +} + +.res-table { + color: #94a3b8; + font-size: 0.75rem; +} diff --git a/servers/touchbistro/src/ui/react-app/reservation-detail/App.tsx b/servers/touchbistro/src/ui/react-app/reservation-detail/App.tsx new file mode 100644 index 0000000..ec8c152 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-detail/App.tsx @@ -0,0 +1,197 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Reservation { + id: string; + confirmationNumber: string; + customerName: string; + customerEmail: string; + customerPhone: string; + partySize: number; + date: string; + time: string; + duration: number; + status: 'pending' | 'confirmed' | 'seated' | 'completed' | 'cancelled' | 'no_show'; + table?: string; + notes?: string; + specialRequests?: string; + createdAt: string; + seatedAt?: string; +} + +export default function ReservationDetail() { + const [reservation, setReservation] = useState(null); + + useEffect(() => { + // Mock data + setReservation({ + id: '1', + confirmationNumber: 'RES-2024-001', + customerName: 'Sarah Johnson', + customerEmail: 'sarah.johnson@email.com', + customerPhone: '(555) 123-4567', + partySize: 6, + date: '2024-02-15', + time: '19:00', + duration: 90, + status: 'confirmed', + table: 'Table 12', + notes: 'Anniversary celebration - please prepare champagne', + specialRequests: 'Window seating preferred, quiet area', + createdAt: '2024-02-10T14:30:00Z', + }); + }, []); + + if (!reservation) { + return
Loading reservation...
; + } + + const getStatusColor = (status: string) => { + const colors = { + pending: '#fbbf24', + confirmed: '#3b82f6', + seated: '#10b981', + completed: '#6b7280', + cancelled: '#ef4444', + no_show: '#ef4444', + }; + return colors[status as keyof typeof colors] || '#6b7280'; + }; + + return ( +
+
+
+

📋 Reservation Detail

+

{reservation.confirmationNumber}

+
+ + {reservation.status.toUpperCase().replace('_', ' ')} + +
+ +
+
+
+

Guest Information

+
+
+ Guest Name + {reservation.customerName} +
+
+ Email + {reservation.customerEmail} +
+
+ Phone + {reservation.customerPhone} +
+
+ Party Size + + 👥 {reservation.partySize} guests + +
+
+
+ +
+

Reservation Details

+
+
+ Date + + {new Date(reservation.date).toLocaleDateString('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + })} + +
+
+ Time + {reservation.time} +
+
+ Duration + {reservation.duration} minutes +
+
+ Table Assignment + {reservation.table || 'Not assigned'} +
+
+
+ + {reservation.specialRequests && ( +
+

Special Requests

+

{reservation.specialRequests}

+
+ )} + + {reservation.notes && ( +
+

Internal Notes

+

{reservation.notes}

+
+ )} +
+ +
+
+

Timeline

+
+
+
+
+
Reservation Created
+
+ {new Date(reservation.createdAt).toLocaleString()} +
+
+
+ {reservation.status !== 'pending' && ( +
+
+
+
Confirmed
+
Confirmed by guest
+
+
+ )} + {reservation.seatedAt && ( +
+
+
+
Seated
+
+ {new Date(reservation.seatedAt).toLocaleString()} +
+
+
+ )} +
+
+ +
+

Actions

+
+ + + + + + +
+
+
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/reservation-detail/index.html b/servers/touchbistro/src/ui/react-app/reservation-detail/index.html new file mode 100644 index 0000000..d7a1aa9 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Reservation Detail - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/reservation-detail/main.tsx b/servers/touchbistro/src/ui/react-app/reservation-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/reservation-detail/styles.css b/servers/touchbistro/src/ui/react-app/reservation-detail/styles.css new file mode 100644 index 0000000..49d813f --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/reservation-detail/styles.css @@ -0,0 +1,233 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.25rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; + font-family: 'Courier New', monospace; +} + +.status-badge { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-weight: 600; + color: #fff; + font-size: 0.875rem; +} + +.content-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 2rem; +} + +@media (max-width: 1024px) { + .content-grid { + grid-template-columns: 1fr; + } +} + +.main-panel { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.info-card, +.timeline-card, +.actions-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +h2 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1.25rem; +} + +.info-grid { + display: grid; + gap: 1.25rem; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1rem; + background: #0f172a; + border-radius: 0.5rem; +} + +.label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; +} + +.value { + color: #f8fafc; + font-size: 1.125rem; + font-weight: 600; +} + +.party-size { + color: #10b981; + font-size: 1.25rem; +} + +.special-requests, +.notes { + color: #cbd5e1; + font-size: 1rem; + line-height: 1.6; + padding: 1rem; + background: #0f172a; + border-radius: 0.5rem; + border-left: 3px solid #3b82f6; +} + +.notes { + border-left-color: #fbbf24; +} + +.side-panel { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.timeline { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.timeline-item { + display: flex; + gap: 1rem; + position: relative; +} + +.timeline-item::before { + content: ''; + position: absolute; + left: 9px; + top: 30px; + width: 2px; + height: calc(100% + 1.5rem); + background: #334155; +} + +.timeline-item:last-child::before { + display: none; +} + +.timeline-dot { + width: 20px; + height: 20px; + border-radius: 50%; + background: #334155; + border: 3px solid #1e293b; + flex-shrink: 0; + margin-top: 2px; +} + +.timeline-item.active .timeline-dot { + background: #3b82f6; +} + +.timeline-content { + flex: 1; +} + +.timeline-title { + color: #f8fafc; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.timeline-time { + color: #94a3b8; + font-size: 0.875rem; +} + +.actions { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.action-btn { + padding: 0.875rem; + border: none; + border-radius: 0.5rem; + font-weight: 600; + font-size: 0.9375rem; + cursor: pointer; + transition: all 0.2s; + background: #334155; + color: #cbd5e1; +} + +.action-btn:hover { + background: #475569; +} + +.action-btn.primary { + background: #3b82f6; + color: #fff; +} + +.action-btn.primary:hover { + background: #2563eb; +} + +.action-btn.danger { + background: #7f1d1d; + color: #fca5a5; +} + +.action-btn.danger:hover { + background: #991b1b; +} + +.loading { + text-align: center; + padding: 3rem; + color: #94a3b8; + font-size: 1.1rem; +} diff --git a/servers/touchbistro/src/ui/react-app/staff-dashboard/App.tsx b/servers/touchbistro/src/ui/react-app/staff-dashboard/App.tsx new file mode 100644 index 0000000..bcaebf4 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-dashboard/App.tsx @@ -0,0 +1,138 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Staff { + id: string; + name: string; + role: string; + active: boolean; + shift: 'morning' | 'evening' | 'off'; + sales: number; + orders: number; + avgCheck: number; + hours: number; +} + +export default function StaffDashboard() { + const [staff, setStaff] = useState([]); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + // Mock data + setStaff([ + { id: '1', name: 'Mike Chen', role: 'Server', active: true, shift: 'morning', sales: 2847.50, orders: 32, avgCheck: 89.00, hours: 38.5 }, + { id: '2', name: 'Lisa Park', role: 'Server', active: true, shift: 'evening', sales: 3125.75, orders: 45, avgCheck: 69.46, hours: 42.0 }, + { id: '3', name: 'Tom Wilson', role: 'Bartender', active: true, shift: 'evening', sales: 1834.25, orders: 28, avgCheck: 65.51, hours: 40.0 }, + { id: '4', name: 'Sarah Johnson', role: 'Server', active: false, shift: 'off', sales: 1245.00, orders: 18, avgCheck: 69.17, hours: 24.0 }, + { id: '5', name: 'David Kim', role: 'Host', active: true, shift: 'morning', sales: 0, orders: 0, avgCheck: 0, hours: 35.0 }, + { id: '6', name: 'Emma Davis', role: 'Chef', active: true, shift: 'morning', sales: 0, orders: 156, avgCheck: 0, hours: 44.0 }, + { id: '7', name: 'John Smith', role: 'Server', active: true, shift: 'evening', sales: 2567.80, orders: 38, avgCheck: 67.57, hours: 36.5 }, + { id: '8', name: 'Amy Chen', role: 'Busser', active: true, shift: 'evening', sales: 0, orders: 0, avgCheck: 0, hours: 32.0 }, + ]); + }, []); + + const filteredStaff = filter === 'all' ? staff : staff.filter(s => s.shift === filter || (filter === 'off-duty' && !s.active)); + + const totalSales = staff.reduce((sum, s) => sum + s.sales, 0); + const activeStaff = staff.filter(s => s.active).length; + const totalOrders = staff.reduce((sum, s) => sum + s.orders, 0); + + const getRoleBadgeColor = (role: string) => { + const colors: Record = { + Server: '#3b82f6', + Bartender: '#8b5cf6', + Chef: '#ef4444', + Host: '#10b981', + Busser: '#fbbf24', + Manager: '#ec4899', + }; + return colors[role] || '#6b7280'; + }; + + return ( +
+
+

👥 Staff Dashboard

+

Team performance and shifts

+
+ +
+
+
Active Staff
+
{activeStaff}
+
+
+
Total Sales
+
${totalSales.toFixed(2)}
+
+
+
Total Orders
+
{totalOrders}
+
+
+
Avg Check
+
${(totalSales / totalOrders).toFixed(2)}
+
+
+ +
+ {['all', 'morning', 'evening', 'off-duty'].map((shift) => ( + + ))} +
+ +
+ {filteredStaff.map((member) => ( +
+
+
+

{member.name}

+ + {member.role} + +
+
+ {member.active ? member.shift : 'Off Duty'} +
+
+ +
+
+ Sales + ${member.sales.toFixed(2)} +
+
+ Orders + {member.orders} +
+
+ Avg Check + + {member.avgCheck > 0 ? `$${member.avgCheck.toFixed(2)}` : 'N/A'} + +
+
+ Hours + {member.hours}h +
+
+ +
+ + +
+
+ ))} +
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/staff-dashboard/index.html b/servers/touchbistro/src/ui/react-app/staff-dashboard/index.html new file mode 100644 index 0000000..2895628 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Staff Dashboard - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/staff-dashboard/main.tsx b/servers/touchbistro/src/ui/react-app/staff-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/staff-dashboard/styles.css b/servers/touchbistro/src/ui/react-app/staff-dashboard/styles.css new file mode 100644 index 0000000..d066694 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-dashboard/styles.css @@ -0,0 +1,214 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.stat-label { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #f8fafc; +} + +.stat-value.sales { + color: #10b981; +} + +.filters { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.filter-btn { + padding: 0.75rem 1.5rem; + background: #1e293b; + border: 2px solid #334155; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s; + text-transform: capitalize; +} + +.filter-btn:hover { + background: #334155; +} + +.filter-btn.active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.staff-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; +} + +.staff-card { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.2s; +} + +.staff-card.inactive { + opacity: 0.6; +} + +.staff-card:hover { + border-color: #3b82f6; +} + +.staff-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid #334155; +} + +.staff-header h3 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 0.5rem; +} + +.role-badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; +} + +.shift-indicator { + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + text-transform: capitalize; +} + +.shift-indicator.morning { + background: #fef3c7; + color: #92400e; +} + +.shift-indicator.evening { + background: #dbeafe; + color: #1e40af; +} + +.shift-indicator.off { + background: #e5e7eb; + color: #374151; +} + +.staff-stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin-bottom: 1.25rem; +} + +.stat { + background: #0f172a; + padding: 0.875rem; + border-radius: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat .stat-label { + color: #94a3b8; + font-size: 0.75rem; + text-transform: uppercase; +} + +.stat .stat-value { + color: #f8fafc; + font-size: 1.25rem; + font-weight: 700; +} + +.staff-actions { + display: flex; + gap: 0.75rem; +} + +.action-btn { + flex: 1; + padding: 0.75rem; + background: #334155; + border: none; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + font-weight: 600; + transition: all 0.2s; +} + +.action-btn:hover { + background: #475569; +} diff --git a/servers/touchbistro/src/ui/react-app/staff-schedule/App.tsx b/servers/touchbistro/src/ui/react-app/staff-schedule/App.tsx new file mode 100644 index 0000000..ced37a0 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-schedule/App.tsx @@ -0,0 +1,144 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Shift { + staffId: string; + staffName: string; + day: string; + startTime: string; + endTime: string; + role: string; +} + +export default function StaffSchedule() { + const [shifts, setShifts] = useState([]); + const [weekStart, setWeekStart] = useState(new Date().toISOString().split('T')[0]); + + useEffect(() => { + // Mock data + setShifts([ + { staffId: '1', staffName: 'Mike Chen', day: 'Monday', startTime: '09:00', endTime: '17:00', role: 'Server' }, + { staffId: '1', staffName: 'Mike Chen', day: 'Tuesday', startTime: '09:00', endTime: '17:00', role: 'Server' }, + { staffId: '1', staffName: 'Mike Chen', day: 'Thursday', startTime: '09:00', endTime: '17:00', role: 'Server' }, + { staffId: '2', staffName: 'Lisa Park', day: 'Monday', startTime: '17:00', endTime: '23:00', role: 'Server' }, + { staffId: '2', staffName: 'Lisa Park', day: 'Wednesday', startTime: '17:00', endTime: '23:00', role: 'Server' }, + { staffId: '2', staffName: 'Lisa Park', day: 'Friday', startTime: '17:00', endTime: '23:00', role: 'Server' }, + { staffId: '2', staffName: 'Lisa Park', day: 'Saturday', startTime: '17:00', endTime: '23:00', role: 'Server' }, + { staffId: '3', staffName: 'Tom Wilson', day: 'Tuesday', startTime: '18:00', endTime: '02:00', role: 'Bartender' }, + { staffId: '3', staffName: 'Tom Wilson', day: 'Friday', startTime: '18:00', endTime: '02:00', role: 'Bartender' }, + { staffId: '3', staffName: 'Tom Wilson', day: 'Saturday', startTime: '18:00', endTime: '02:00', role: 'Bartender' }, + { staffId: '4', staffName: 'Emma Davis', day: 'Monday', startTime: '06:00', endTime: '14:00', role: 'Chef' }, + { staffId: '4', staffName: 'Emma Davis', day: 'Tuesday', startTime: '06:00', endTime: '14:00', role: 'Chef' }, + { staffId: '4', staffName: 'Emma Davis', day: 'Wednesday', startTime: '06:00', endTime: '14:00', role: 'Chef' }, + { staffId: '4', staffName: 'Emma Davis', day: 'Thursday', startTime: '06:00', endTime: '14:00', role: 'Chef' }, + { staffId: '4', staffName: 'Emma Davis', day: 'Friday', startTime: '06:00', endTime: '14:00', role: 'Chef' }, + { staffId: '5', staffName: 'David Kim', day: 'Wednesday', startTime: '11:00', endTime: '19:00', role: 'Host' }, + { staffId: '5', staffName: 'David Kim', day: 'Thursday', startTime: '11:00', endTime: '19:00', role: 'Host' }, + { staffId: '5', staffName: 'David Kim', day: 'Friday', startTime: '11:00', endTime: '19:00', role: 'Host' }, + { staffId: '5', staffName: 'David Kim', day: 'Saturday', startTime: '11:00', endTime: '19:00', role: 'Host' }, + { staffId: '5', staffName: 'David Kim', day: 'Sunday', startTime: '11:00', endTime: '19:00', role: 'Host' }, + ]); + }, []); + + const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + const staff = Array.from(new Set(shifts.map(s => s.staffName))).sort(); + + const getRoleBadgeColor = (role: string) => { + const colors: Record = { + Server: '#3b82f6', + Bartender: '#8b5cf6', + Chef: '#ef4444', + Host: '#10b981', + Busser: '#fbbf24', + }; + return colors[role] || '#6b7280'; + }; + + return ( +
+
+

📋 Staff Schedule

+

Weekly schedule grid

+
+ +
+ setWeekStart(e.target.value)} + /> + +
+ +
+ + + + + {days.map((day) => ( + + ))} + + + + {staff.map((staffName) => { + const staffShifts = shifts.filter(s => s.staffName === staffName); + const role = staffShifts[0]?.role || ''; + + return ( + + + {days.map((day) => { + const shift = staffShifts.find(s => s.day === day); + return ( + + ); + })} + + ); + })} + +
Staff Member{day}
+
+ {staffName} + + {role} + +
+
+ {shift ? ( +
+
+ {shift.startTime} - {shift.endTime} +
+
+ ) : ( +
+ )} +
+
+ +
+

Weekly Summary

+
+
+ Total Shifts + {shifts.length} +
+
+ Staff Members + {staff.length} +
+
+ Avg Shifts/Person + {(shifts.length / staff.length).toFixed(1)} +
+
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/staff-schedule/index.html b/servers/touchbistro/src/ui/react-app/staff-schedule/index.html new file mode 100644 index 0000000..5bc09b2 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-schedule/index.html @@ -0,0 +1,12 @@ + + + + + + Staff Schedule - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/staff-schedule/main.tsx b/servers/touchbistro/src/ui/react-app/staff-schedule/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-schedule/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/staff-schedule/styles.css b/servers/touchbistro/src/ui/react-app/staff-schedule/styles.css new file mode 100644 index 0000000..dbcca76 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/staff-schedule/styles.css @@ -0,0 +1,230 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1800px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.toolbar { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + align-items: center; +} + +.date-input { + padding: 0.875rem 1.25rem; + background: #1e293b; + border: 2px solid #334155; + color: #f8fafc; + border-radius: 0.5rem; + font-size: 1rem; + cursor: pointer; +} + +.date-input:focus { + outline: none; + border-color: #3b82f6; +} + +.btn-primary { + padding: 0.875rem 1.5rem; + background: #3b82f6; + border: none; + color: #fff; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + cursor: pointer; + transition: all 0.2s; + margin-left: auto; +} + +.btn-primary:hover { + background: #2563eb; +} + +.schedule-container { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + overflow: hidden; + margin-bottom: 2rem; +} + +.schedule-table { + width: 100%; + border-collapse: collapse; +} + +.schedule-table thead { + background: #0f172a; +} + +.schedule-table th { + padding: 1rem; + text-align: left; + color: #f8fafc; + font-weight: 600; + font-size: 0.9375rem; + border-bottom: 2px solid #334155; +} + +.schedule-table th.staff-column { + width: 200px; + min-width: 200px; +} + +.schedule-table td { + padding: 0.75rem; + border-bottom: 1px solid #334155; + border-right: 1px solid #334155; +} + +.schedule-table td:last-child { + border-right: none; +} + +.schedule-table tbody tr:hover { + background: #0f172a; +} + +.schedule-table tbody tr:last-child td { + border-bottom: none; +} + +.staff-column { + background: #0f172a; +} + +.staff-info { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.staff-name { + color: #f8fafc; + font-weight: 600; + font-size: 0.9375rem; +} + +.role-badge { + display: inline-block; + padding: 0.25rem 0.625rem; + border-radius: 0.375rem; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; + width: fit-content; +} + +.shift-cell { + text-align: center; + vertical-align: middle; +} + +.shift-block { + background: #3b82f6; + padding: 0.625rem 0.75rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.shift-block:hover { + background: #2563eb; + transform: scale(1.02); +} + +.shift-time { + color: #fff; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1.4; +} + +.no-shift { + color: #475569; + font-size: 1.25rem; +} + +.summary { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.summary h3 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1rem; +} + +.summary-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; +} + +.summary-stat { + background: #0f172a; + padding: 1.25rem; + border-radius: 0.5rem; + text-align: center; +} + +.summary-stat .stat-label { + display: block; + color: #94a3b8; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + margin-bottom: 0.5rem; +} + +.summary-stat .stat-value { + display: block; + color: #f8fafc; + font-size: 2rem; + font-weight: 700; +} + +@media (max-width: 1400px) { + .schedule-container { + overflow-x: auto; + } + + .schedule-table { + min-width: 1200px; + } +} diff --git a/servers/touchbistro/src/ui/react-app/table-map/App.tsx b/servers/touchbistro/src/ui/react-app/table-map/App.tsx new file mode 100644 index 0000000..bd7167e --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/table-map/App.tsx @@ -0,0 +1,135 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Table { + id: string; + number: string; + status: 'available' | 'occupied' | 'reserved' | 'cleaning'; + seats: number; + server?: string; + guestCount?: number; + seatedAt?: string; + x: number; + y: number; +} + +export default function TableMap() { + const [tables, setTables] = useState([]); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + // Mock data - positioned in grid layout + setTables([ + { id: '1', number: '1', status: 'occupied', seats: 4, server: 'Mike', guestCount: 4, seatedAt: '12:30 PM', x: 50, y: 50 }, + { id: '2', number: '2', status: 'occupied', seats: 2, server: 'Lisa', guestCount: 2, seatedAt: '12:45 PM', x: 200, y: 50 }, + { id: '3', number: '3', status: 'available', seats: 6, x: 350, y: 50 }, + { id: '4', number: '4', status: 'reserved', seats: 4, x: 500, y: 50 }, + { id: '5', number: '5', status: 'occupied', seats: 2, server: 'Tom', guestCount: 2, seatedAt: '1:00 PM', x: 50, y: 200 }, + { id: '6', number: '6', status: 'available', seats: 4, x: 200, y: 200 }, + { id: '7', number: '7', status: 'cleaning', seats: 4, x: 350, y: 200 }, + { id: '8', number: '8', status: 'occupied', seats: 8, server: 'Mike', guestCount: 6, seatedAt: '12:15 PM', x: 500, y: 200 }, + { id: '9', number: '9', status: 'available', seats: 2, x: 50, y: 350 }, + { id: '10', number: '10', status: 'reserved', seats: 6, x: 200, y: 350 }, + { id: '11', number: '11', status: 'occupied', seats: 4, server: 'Lisa', guestCount: 3, seatedAt: '1:15 PM', x: 350, y: 350 }, + { id: '12', number: '12', status: 'available', seats: 2, x: 500, y: 350 }, + ]); + }, []); + + const statusColors = { + available: '#10b981', + occupied: '#ef4444', + reserved: '#fbbf24', + cleaning: '#6b7280', + }; + + const statusCounts = { + available: tables.filter(t => t.status === 'available').length, + occupied: tables.filter(t => t.status === 'occupied').length, + reserved: tables.filter(t => t.status === 'reserved').length, + cleaning: tables.filter(t => t.status === 'cleaning').length, + }; + + const filteredTables = filter === 'all' ? tables : tables.filter(t => t.status === filter); + + return ( +
+
+

🗺️ Table Map

+

Floor plan with live table status

+
+ +
+
+ {statusCounts.available} + Available +
+
+ {statusCounts.occupied} + Occupied +
+
+ {statusCounts.reserved} + Reserved +
+
+ {statusCounts.cleaning} + Cleaning +
+
+ +
+ {['all', 'available', 'occupied', 'reserved', 'cleaning'].map((status) => ( + + ))} +
+ +
+ {filteredTables.map((table) => ( +
+
{table.number}
+
👥 {table.seats}
+
+ {table.status} +
+ {table.status === 'occupied' && ( +
+
Server: {table.server}
+
Guests: {table.guestCount}
+
{table.seatedAt}
+
+ )} +
+ ))} +
+ +
+

Legend

+
+ {Object.entries(statusColors).map(([status, color]) => ( +
+
+ {status.charAt(0).toUpperCase() + status.slice(1)} +
+ ))} +
+
+
+ ); +} diff --git a/servers/touchbistro/src/ui/react-app/table-map/index.html b/servers/touchbistro/src/ui/react-app/table-map/index.html new file mode 100644 index 0000000..f2f4382 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/table-map/index.html @@ -0,0 +1,12 @@ + + + + + + Table Map - TouchBistro MCP + + +
+ + + diff --git a/servers/touchbistro/src/ui/react-app/table-map/main.tsx b/servers/touchbistro/src/ui/react-app/table-map/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/table-map/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/touchbistro/src/ui/react-app/table-map/styles.css b/servers/touchbistro/src/ui/react-app/table-map/styles.css new file mode 100644 index 0000000..553ed58 --- /dev/null +++ b/servers/touchbistro/src/ui/react-app/table-map/styles.css @@ -0,0 +1,208 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.app { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; +} + +.app-header { + margin-bottom: 2rem; +} + +.app-header h1 { + font-size: 2.5rem; + color: #f8fafc; + margin-bottom: 0.5rem; +} + +.app-header p { + color: #94a3b8; + font-size: 1.1rem; +} + +.stats-bar { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.stat-item { + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + padding: 1.25rem; + text-align: center; +} + +.stat-item.available { + border-color: #10b981; +} + +.stat-item.occupied { + border-color: #ef4444; +} + +.stat-item.reserved { + border-color: #fbbf24; +} + +.stat-item.cleaning { + border-color: #6b7280; +} + +.stat-value { + display: block; + font-size: 2rem; + font-weight: 700; + color: #f8fafc; + margin-bottom: 0.25rem; +} + +.stat-label { + color: #94a3b8; + font-size: 0.875rem; + text-transform: uppercase; + font-weight: 600; +} + +.filter-bar { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +.filter-btn { + padding: 0.75rem 1.5rem; + background: #1e293b; + border: 2px solid #334155; + color: #cbd5e1; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + transition: all 0.2s; + text-transform: capitalize; +} + +.filter-btn:hover { + background: #334155; +} + +.filter-btn.active { + background: #3b82f6; + border-color: #3b82f6; + color: #fff; +} + +.floor-plan { + position: relative; + background: #1e293b; + border: 2px solid #334155; + border-radius: 0.75rem; + min-height: 500px; + padding: 2rem; + margin-bottom: 2rem; +} + +.table { + position: absolute; + width: 120px; + background: #0f172a; + border: 3px solid; + border-radius: 0.75rem; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.table:hover { + transform: scale(1.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); + z-index: 10; +} + +.table-number { + font-size: 1.5rem; + font-weight: 700; + color: #f8fafc; + text-align: center; + margin-bottom: 0.5rem; +} + +.table-seats { + text-align: center; + color: #94a3b8; + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.table-status { + padding: 0.375rem; + border-radius: 0.375rem; + text-align: center; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + color: #fff; + margin-bottom: 0.5rem; +} + +.table-info { + font-size: 0.75rem; + color: #94a3b8; + text-align: center; + line-height: 1.4; + padding-top: 0.5rem; + border-top: 1px solid #334155; +} + +.legend { + background: #1e293b; + border: 1px solid #334155; + border-radius: 0.75rem; + padding: 1.5rem; +} + +.legend h3 { + color: #f8fafc; + font-size: 1.25rem; + margin-bottom: 1rem; +} + +.legend-items { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.legend-item { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.legend-color { + width: 24px; + height: 24px; + border-radius: 0.375rem; +} + +.legend-item span { + color: #cbd5e1; + font-weight: 500; + text-transform: capitalize; +} diff --git a/servers/touchbistro/tsconfig.json b/servers/touchbistro/tsconfig.json new file mode 100644 index 0000000..4f9082b --- /dev/null +++ b/servers/touchbistro/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist", "src/ui/*/dist", "src/ui/*/node_modules"] +} diff --git a/servers/wave/src/apps/CustomerManager.tsx b/servers/wave/src/apps/CustomerManager.tsx new file mode 100644 index 0000000..3102bae --- /dev/null +++ b/servers/wave/src/apps/CustomerManager.tsx @@ -0,0 +1,245 @@ +import React, { useState, useEffect } from 'react'; + +interface CustomerManagerProps { + onToolCall: (tool: string, args: any) => Promise; +} + +export function CustomerManager({ onToolCall }: CustomerManagerProps) { + const [customers, setCustomers] = useState([]); + const [loading, setLoading] = useState(true); + const [showForm, setShowForm] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + phone: '', + mobile: '', + website: '', + }); + + useEffect(() => { + loadCustomers(); + }, []); + + async function loadCustomers() { + setLoading(true); + try { + const data = await onToolCall('wave_list_customers', { pageSize: 100 }); + setCustomers(data.customers || []); + } catch (error) { + console.error('Failed to load customers:', error); + } + setLoading(false); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + try { + await onToolCall('wave_create_customer', formData); + setFormData({ name: '', email: '', phone: '', mobile: '', website: '' }); + setShowForm(false); + await loadCustomers(); + } catch (error) { + console.error('Failed to create customer:', error); + alert('Failed to create customer'); + } + } + + return ( +
+
+
+
+

+ Customers +

+

+ Manage your customer database +

+
+ +
+ + {showForm && ( +
+

+ Create New Customer +

+
+
+ setFormData({ ...formData, name: v })} + required + /> + setFormData({ ...formData, email: v })} + /> + setFormData({ ...formData, phone: v })} + /> + setFormData({ ...formData, mobile: v })} + /> + setFormData({ ...formData, website: v })} + /> +
+ +
+
+ )} + +
+ {loading ? ( +
Loading...
+ ) : customers.length === 0 ? ( +
+ No customers found. Create your first customer! +
+ ) : ( +
+ + + + + + + + + + + {customers.map(customer => ( + + + + + + + ))} + +
NameEmailPhoneCurrency
{customer.name}{customer.email || '-'}{customer.phone || customer.mobile || '-'}{customer.currency?.code || '-'}
+
+ )} +
+
+
+ ); +} + +function InputField({ + label, + value, + onChange, + type = 'text', + required = false, +}: { + label: string; + value: string; + onChange: (v: string) => void; + type?: string; + required?: boolean; +}) { + return ( +
+ + onChange(e.target.value)} + required={required} + style={{ + width: '100%', + padding: '10px 12px', + backgroundColor: '#1a1a1a', + border: '1px solid #404040', + borderRadius: '6px', + color: '#e0e0e0', + fontSize: '14px', + }} + /> +
+ ); +} + +const headerStyle: React.CSSProperties = { + textAlign: 'left', + padding: '12px', + fontSize: '13px', + fontWeight: '600', + color: '#888', + textTransform: 'uppercase', +}; + +const cellStyle: React.CSSProperties = { + padding: '12px', + fontSize: '14px', + color: '#e0e0e0', +}; diff --git a/servers/wave/src/apps/Dashboard.tsx b/servers/wave/src/apps/Dashboard.tsx new file mode 100644 index 0000000..ac212c5 --- /dev/null +++ b/servers/wave/src/apps/Dashboard.tsx @@ -0,0 +1,302 @@ +import React, { useState, useEffect } from 'react'; + +interface DashboardProps { + onToolCall: (tool: string, args: any) => Promise; +} + +export function Dashboard({ onToolCall }: DashboardProps) { + const [businesses, setBusinesses] = useState([]); + const [selectedBusiness, setSelectedBusiness] = useState(null); + const [invoices, setInvoices] = useState([]); + const [loading, setLoading] = useState(true); + const [stats, setStats] = useState({ + totalInvoices: 0, + paidInvoices: 0, + overdueInvoices: 0, + totalRevenue: 0, + }); + + useEffect(() => { + loadData(); + }, []); + + async function loadData() { + setLoading(true); + try { + const bizData = await onToolCall('wave_list_businesses', {}); + setBusinesses(bizData); + if (bizData.length > 0) { + setSelectedBusiness(bizData[0]); + await loadBusinessData(bizData[0].id); + } + } catch (error) { + console.error('Failed to load data:', error); + } + setLoading(false); + } + + async function loadBusinessData(businessId: string) { + try { + const invoiceData = await onToolCall('wave_list_invoices', { + businessId, + pageSize: 100 + }); + setInvoices(invoiceData.invoices || []); + + // Calculate stats + const total = invoiceData.invoices?.length || 0; + const paid = invoiceData.invoices?.filter((i: any) => i.status === 'PAID').length || 0; + const overdue = invoiceData.invoices?.filter((i: any) => i.status === 'OVERDUE').length || 0; + const revenue = invoiceData.invoices + ?.filter((i: any) => i.status === 'PAID') + .reduce((sum: number, i: any) => sum + parseFloat(i.total?.value || 0), 0) || 0; + + setStats({ + totalInvoices: total, + paidInvoices: paid, + overdueInvoices: overdue, + totalRevenue: revenue, + }); + } catch (error) { + console.error('Failed to load business data:', error); + } + } + + if (loading) { + return ( +
+
+
Loading Wave dashboard...
+
+
+ ); + } + + return ( +
+
+
+

+ Wave Accounting Dashboard +

+

+ Overview of your business financials +

+
+ + {/* Business Selector */} + {businesses.length > 1 && ( +
+ + +
+ )} + + {selectedBusiness && ( + <> + {/* Stats Grid */} +
+ + + + +
+ + {/* Recent Invoices */} +
+

+ Recent Invoices +

+ + {invoices.length === 0 ? ( +

No invoices found

+ ) : ( +
+ + + + + + + + + + + + + {invoices.slice(0, 10).map(invoice => ( + + + + + + + + + ))} + +
Invoice #CustomerDateStatusAmountAmount Due
{invoice.invoiceNumber}{invoice.customer.name} + {new Date(invoice.invoiceDate).toLocaleDateString()} + + + + {invoice.total.currency.code} {parseFloat(invoice.total.value).toFixed(2)} + + {invoice.amountDue.currency.code} {parseFloat(invoice.amountDue.value).toFixed(2)} +
+
+ )} +
+ + )} +
+
+ ); +} + +function StatCard({ title, value, color }: { title: string; value: string; color: string }) { + return ( +
+
+ {title} +
+
+ {value} +
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + const colors: Record = { + DRAFT: '#6b7280', + SAVED: '#3b82f6', + SENT: '#8b5cf6', + VIEWED: '#06b6d4', + PAID: '#22c55e', + PARTIAL: '#f59e0b', + OVERDUE: '#ef4444', + UNPAID: '#f59e0b', + }; + + return ( + + {status} + + ); +} + +const tableHeaderStyle: React.CSSProperties = { + textAlign: 'left', + padding: '12px', + fontSize: '13px', + fontWeight: '600', + color: '#888', + textTransform: 'uppercase', + letterSpacing: '0.5px', +}; + +const tableCellStyle: React.CSSProperties = { + padding: '12px', + fontSize: '14px', + color: '#e0e0e0', +}; diff --git a/servers/wave/src/apps/InvoiceManager.tsx b/servers/wave/src/apps/InvoiceManager.tsx new file mode 100644 index 0000000..2840e79 --- /dev/null +++ b/servers/wave/src/apps/InvoiceManager.tsx @@ -0,0 +1,190 @@ +import React, { useState, useEffect } from 'react'; + +interface InvoiceManagerProps { + onToolCall: (tool: string, args: any) => Promise; +} + +export function InvoiceManager({ onToolCall }: InvoiceManagerProps) { + const [invoices, setInvoices] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState('ALL'); + + useEffect(() => { + loadInvoices(); + }, []); + + async function loadInvoices() { + setLoading(true); + try { + const data = await onToolCall('wave_list_invoices', { pageSize: 100 }); + setInvoices(data.invoices || []); + } catch (error) { + console.error('Failed to load invoices:', error); + } + setLoading(false); + } + + const filteredInvoices = filter === 'ALL' + ? invoices + : invoices.filter(inv => inv.status === filter); + + const statusCounts = { + ALL: invoices.length, + DRAFT: invoices.filter(i => i.status === 'DRAFT').length, + SENT: invoices.filter(i => i.status === 'SENT').length, + PAID: invoices.filter(i => i.status === 'PAID').length, + OVERDUE: invoices.filter(i => i.status === 'OVERDUE').length, + }; + + return ( +
+
+

+ Invoices +

+ + {/* Filter Tabs */} +
+ {(['ALL', 'DRAFT', 'SENT', 'PAID', 'OVERDUE'] as const).map(status => ( + + ))} +
+ +
+ {loading ? ( +
Loading...
+ ) : filteredInvoices.length === 0 ? ( +
+ No {filter.toLowerCase()} invoices found +
+ ) : ( +
+ + + + + + + + + + + + + + + {filteredInvoices.map(invoice => ( + + + + + + + + + + + ))} + +
Invoice #CustomerDateDue DateStatusTotalAmount DueActions
{invoice.invoiceNumber}{invoice.customer.name} + {new Date(invoice.invoiceDate).toLocaleDateString()} + + {invoice.dueDate ? new Date(invoice.dueDate).toLocaleDateString() : '-'} + + + + {invoice.total.currency.code} {parseFloat(invoice.total.value).toFixed(2)} + + {invoice.amountDue.currency.code} {parseFloat(invoice.amountDue.value).toFixed(2)} + + {invoice.viewUrl && ( + + View + + )} +
+
+ )} +
+
+
+ ); +} + +function StatusBadge({ status }: { status: string }) { + const colors: Record = { + DRAFT: '#6b7280', + SAVED: '#3b82f6', + SENT: '#8b5cf6', + VIEWED: '#06b6d4', + PAID: '#22c55e', + PARTIAL: '#f59e0b', + OVERDUE: '#ef4444', + UNPAID: '#f59e0b', + }; + + return ( + + {status} + + ); +} + +const headerStyle: React.CSSProperties = { + textAlign: 'left', + padding: '12px', + fontSize: '13px', + fontWeight: '600', + color: '#888', + textTransform: 'uppercase', +}; + +const cellStyle: React.CSSProperties = { + padding: '12px', + fontSize: '14px', + color: '#e0e0e0', +}; diff --git a/servers/wave/src/client/wave-client.ts b/servers/wave/src/client/wave-client.ts new file mode 100644 index 0000000..d6b69bc --- /dev/null +++ b/servers/wave/src/client/wave-client.ts @@ -0,0 +1,44 @@ +import { GraphQLClient } from 'graphql-request'; +import type { WaveConfig } from '../types/index.js'; + +export class WaveClient { + private client: GraphQLClient; + private config: WaveConfig; + + constructor(config: WaveConfig) { + this.config = config; + this.client = new GraphQLClient('https://gql.waveapps.com/graphql/public', { + headers: { + Authorization: `Bearer ${config.accessToken}`, + 'Content-Type': 'application/json', + }, + }); + } + + async query(query: string, variables?: any): Promise { + try { + return await this.client.request(query, variables); + } catch (error: any) { + throw new Error(`Wave API Error: ${error.message}`); + } + } + + async mutate(mutation: string, variables?: any): Promise { + try { + return await this.client.request(mutation, variables); + } catch (error: any) { + throw new Error(`Wave API Error: ${error.message}`); + } + } + + getBusinessId(): string { + if (!this.config.businessId) { + throw new Error('Business ID is required but not configured'); + } + return this.config.businessId; + } + + setBusinessId(businessId: string) { + this.config.businessId = businessId; + } +} diff --git a/servers/wave/src/tools/index.ts b/servers/wave/src/tools/index.ts new file mode 100644 index 0000000..12c8657 --- /dev/null +++ b/servers/wave/src/tools/index.ts @@ -0,0 +1,1833 @@ +import { WaveClient } from '../client/wave-client.js'; +import type { + Business, Customer, Product, Invoice, Account, Transaction, + Bill, SalesTax, Vendor, Estimate, Country, Currency +} from '../types/index.js'; + +export const waveTools = { + // ========== BUSINESS TOOLS ========== + + listBusinesses: { + name: 'wave_list_businesses', + description: 'List all businesses associated with the Wave account', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: WaveClient, _args: any) => { + const query = ` + query { + user { + id + businesses(page: 1, pageSize: 50) { + edges { + node { + id + name + currency { + code + symbol + } + organizationSubtype + isClassicAccounting + isClassicInvoicing + isPersonal + } + } + } + } + } + `; + const result = await client.query(query); + return result.user.businesses.edges.map((e: any) => e.node); + }, + }, + + getBusiness: { + name: 'wave_get_business', + description: 'Get details of a specific business', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!) { + business(id: $businessId) { + id + name + currency { + code + symbol + } + organizationSubtype + isClassicAccounting + isClassicInvoicing + isPersonal + } + } + `; + const result = await client.query(query, { businessId }); + return result.business; + }, + }, + + // ========== CUSTOMER TOOLS ========== + + listCustomers: { + name: 'wave_list_customers', + description: 'List all customers for a business', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50, max: 100)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + customers(page: $page, pageSize: $pageSize) { + edges { + node { + id + name + firstName + lastName + email + mobile + phone + currency { + code + } + address { + addressLine1 + addressLine2 + city + postalCode + province { code name } + country { code name } + } + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + customers: result.business.customers.edges.map((e: any) => e.node), + pageInfo: result.business.customers.pageInfo, + }; + }, + }, + + getCustomer: { + name: 'wave_get_customer', + description: 'Get details of a specific customer', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + customerId: { type: 'string', description: 'Customer ID', required: true }, + }, + required: ['customerId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; customerId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $customerId: ID!) { + business(id: $businessId) { + customer(id: $customerId) { + id + name + firstName + lastName + email + mobile + phone + fax + website + internalNotes + currency { code } + address { + addressLine1 + addressLine2 + city + postalCode + province { code name } + country { code name } + } + shippingDetails { + name + phone + instructions + address { + addressLine1 + addressLine2 + city + postalCode + province { code name } + country { code name } + } + } + } + } + } + `; + const result = await client.query(query, { businessId, customerId: args.customerId }); + return result.business.customer; + }, + }, + + createCustomer: { + name: 'wave_create_customer', + description: 'Create a new customer', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + name: { type: 'string', description: 'Customer name', required: true }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + mobile: { type: 'string', description: 'Mobile phone' }, + phone: { type: 'string', description: 'Phone number' }, + fax: { type: 'string', description: 'Fax number' }, + website: { type: 'string', description: 'Website URL' }, + internalNotes: { type: 'string', description: 'Internal notes' }, + currency: { type: 'string', description: 'Currency code (e.g., USD)' }, + address: { + type: 'object', + description: 'Customer address', + properties: { + addressLine1: { type: 'string' }, + addressLine2: { type: 'string' }, + city: { type: 'string' }, + postalCode: { type: 'string' }, + countryCode: { type: 'string' }, + provinceCode: { type: 'string' }, + }, + }, + }, + required: ['name'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: CustomerCreateInput!) { + customerCreate(input: { businessId: $businessId, customer: $input }) { + customer { + id + name + email + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const input: any = { + name: args.name, + firstName: args.firstName, + lastName: args.lastName, + email: args.email, + mobile: args.mobile, + phone: args.phone, + fax: args.fax, + website: args.website, + internalNotes: args.internalNotes, + currency: args.currency, + }; + if (args.address) { + input.address = args.address; + } + const result = await client.mutate(mutation, { businessId, input }); + return result.customerCreate; + }, + }, + + updateCustomer: { + name: 'wave_update_customer', + description: 'Update an existing customer', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + customerId: { type: 'string', description: 'Customer ID', required: true }, + name: { type: 'string', description: 'Customer name' }, + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + email: { type: 'string', description: 'Email address' }, + mobile: { type: 'string', description: 'Mobile phone' }, + phone: { type: 'string', description: 'Phone number' }, + website: { type: 'string', description: 'Website URL' }, + internalNotes: { type: 'string', description: 'Internal notes' }, + }, + required: ['customerId'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $customerId: ID!, $input: CustomerPatchInput!) { + customerPatch(input: { businessId: $businessId, customerId: $customerId, customer: $input }) { + customer { + id + name + email + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, customerId: args.customerId, input: args }); + return result.customerPatch; + }, + }, + + deleteCustomer: { + name: 'wave_delete_customer', + description: 'Delete a customer', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + customerId: { type: 'string', description: 'Customer ID', required: true }, + }, + required: ['customerId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; customerId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $customerId: ID!) { + customerDelete(input: { businessId: $businessId, customerId: $customerId }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, customerId: args.customerId }); + return result.customerDelete; + }, + }, + + // ========== PRODUCT TOOLS ========== + + listProducts: { + name: 'wave_list_products', + description: 'List all products and services', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + products(page: $page, pageSize: $pageSize) { + edges { + node { + id + name + description + unitPrice + isSold + isBought + incomeAccount { id name } + expenseAccount { id name } + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + products: result.business.products.edges.map((e: any) => e.node), + pageInfo: result.business.products.pageInfo, + }; + }, + }, + + getProduct: { + name: 'wave_get_product', + description: 'Get details of a specific product', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + productId: { type: 'string', description: 'Product ID', required: true }, + }, + required: ['productId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; productId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $productId: ID!) { + business(id: $businessId) { + product(id: $productId) { + id + name + description + unitPrice + isSold + isBought + incomeAccount { id name } + expenseAccount { id name } + defaultSalesTaxes { + id + name + rate + } + } + } + } + `; + const result = await client.query(query, { businessId, productId: args.productId }); + return result.business.product; + }, + }, + + createProduct: { + name: 'wave_create_product', + description: 'Create a new product or service', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + name: { type: 'string', description: 'Product name', required: true }, + description: { type: 'string', description: 'Product description' }, + unitPrice: { type: 'number', description: 'Unit price' }, + isSold: { type: 'boolean', description: 'Is this product sold?' }, + isBought: { type: 'boolean', description: 'Is this product bought?' }, + incomeAccountId: { type: 'string', description: 'Income account ID' }, + expenseAccountId: { type: 'string', description: 'Expense account ID' }, + }, + required: ['name'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: ProductCreateInput!) { + productCreate(input: { businessId: $businessId, product: $input }) { + product { + id + name + unitPrice + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.productCreate; + }, + }, + + updateProduct: { + name: 'wave_update_product', + description: 'Update an existing product', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + productId: { type: 'string', description: 'Product ID', required: true }, + name: { type: 'string', description: 'Product name' }, + description: { type: 'string', description: 'Product description' }, + unitPrice: { type: 'number', description: 'Unit price' }, + }, + required: ['productId'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $productId: ID!, $input: ProductPatchInput!) { + productPatch(input: { businessId: $businessId, productId: $productId, product: $input }) { + product { + id + name + unitPrice + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, productId: args.productId, input: args }); + return result.productPatch; + }, + }, + + deleteProduct: { + name: 'wave_delete_product', + description: 'Archive a product', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + productId: { type: 'string', description: 'Product ID', required: true }, + }, + required: ['productId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; productId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $productId: ID!) { + productArchive(input: { businessId: $businessId, productId: $productId }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, productId: args.productId }); + return result.productArchive; + }, + }, + + // ========== INVOICE TOOLS ========== + + listInvoices: { + name: 'wave_list_invoices', + description: 'List all invoices with optional filters', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + status: { + type: 'string', + description: 'Filter by status (DRAFT, SAVED, SENT, VIEWED, APPROVED, PAID, PARTIAL, OVERDUE, UNPAID)', + }, + customerId: { type: 'string', description: 'Filter by customer ID' }, + }, + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + invoices(page: $page, pageSize: $pageSize) { + edges { + node { + id + createdAt + modifiedAt + invoiceNumber + invoiceDate + dueDate + status + customer { + id + name + email + } + total { + value + currency { code } + } + amountDue { + value + currency { code } + } + amountPaid { + value + currency { code } + } + viewUrl + pdfUrl + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + invoices: result.business.invoices.edges.map((e: any) => e.node), + pageInfo: result.business.invoices.pageInfo, + }; + }, + }, + + getInvoice: { + name: 'wave_get_invoice', + description: 'Get detailed information about a specific invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; invoiceId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $invoiceId: ID!) { + business(id: $businessId) { + invoice(id: $invoiceId) { + id + createdAt + modifiedAt + invoiceNumber + invoiceDate + dueDate + status + title + customer { + id + name + email + address { + addressLine1 + city + postalCode + country { name } + } + } + items { + product { id name } + description + quantity + unitPrice + subtotal { value currency { code } } + total { value currency { code } } + taxes { + salesTax { id name rate } + amount { value } + } + } + memo + footer + subtotal { value currency { code } } + total { value currency { code } } + amountDue { value currency { code } } + amountPaid { value currency { code } } + taxTotal { value currency { code } } + viewUrl + pdfUrl + lastSentAt + lastSentVia + lastViewedAt + } + } + } + `; + const result = await client.query(query, { businessId, invoiceId: args.invoiceId }); + return result.business.invoice; + }, + }, + + createInvoice: { + name: 'wave_create_invoice', + description: 'Create a new invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + customerId: { type: 'string', description: 'Customer ID', required: true }, + items: { + type: 'array', + description: 'Invoice line items', + items: { + type: 'object', + properties: { + productId: { type: 'string', description: 'Product ID' }, + description: { type: 'string', description: 'Item description' }, + quantity: { type: 'number', description: 'Quantity' }, + unitPrice: { type: 'number', description: 'Unit price' }, + }, + }, + required: true, + }, + invoiceDate: { type: 'string', description: 'Invoice date (YYYY-MM-DD)' }, + dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' }, + title: { type: 'string', description: 'Invoice title' }, + memo: { type: 'string', description: 'Memo/notes' }, + footer: { type: 'string', description: 'Footer text' }, + }, + required: ['customerId', 'items'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: InvoiceCreateInput!) { + invoiceCreate(input: { businessId: $businessId, invoice: $input }) { + invoice { + id + invoiceNumber + status + total { value currency { code } } + viewUrl + pdfUrl + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.invoiceCreate; + }, + }, + + updateInvoice: { + name: 'wave_update_invoice', + description: 'Update an existing invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + title: { type: 'string', description: 'Invoice title' }, + memo: { type: 'string', description: 'Memo/notes' }, + footer: { type: 'string', description: 'Footer text' }, + dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $invoiceId: ID!, $input: InvoicePatchInput!) { + invoicePatch(input: { businessId: $businessId, invoiceId: $invoiceId, invoice: $input }) { + invoice { + id + status + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, invoiceId: args.invoiceId, input: args }); + return result.invoicePatch; + }, + }, + + deleteInvoice: { + name: 'wave_delete_invoice', + description: 'Delete an invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; invoiceId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $invoiceId: ID!) { + invoiceDelete(input: { businessId: $businessId, invoiceId: $invoiceId }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, invoiceId: args.invoiceId }); + return result.invoiceDelete; + }, + }, + + sendInvoice: { + name: 'wave_send_invoice', + description: 'Send an invoice to customer via email', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + to: { type: 'array', items: { type: 'string' }, description: 'Email addresses to send to' }, + subject: { type: 'string', description: 'Email subject' }, + message: { type: 'string', description: 'Email message' }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $invoiceId: ID!, $input: InvoiceSendInput!) { + invoiceSend(input: { businessId: $businessId, invoiceId: $invoiceId, sendMethod: $input }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { + businessId, + invoiceId: args.invoiceId, + input: { + to: args.to, + subject: args.subject, + message: args.message, + }, + }); + return result.invoiceSend; + }, + }, + + approveInvoice: { + name: 'wave_approve_invoice', + description: 'Approve a draft invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; invoiceId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $invoiceId: ID!) { + invoiceApprove(input: { businessId: $businessId, invoiceId: $invoiceId }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, invoiceId: args.invoiceId }); + return result.invoiceApprove; + }, + }, + + markInvoiceSent: { + name: 'wave_mark_invoice_sent', + description: 'Mark an invoice as sent', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; invoiceId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $invoiceId: ID!) { + invoiceMarkSent(input: { businessId: $businessId, invoiceId: $invoiceId }) { + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, invoiceId: args.invoiceId }); + return result.invoiceMarkSent; + }, + }, + + // ========== ACCOUNT TOOLS ========== + + listAccounts: { + name: 'wave_list_accounts', + description: 'List all accounts (chart of accounts)', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 100)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 100; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + accounts(page: $page, pageSize: $pageSize) { + edges { + node { + id + name + description + displayId + type { + name + normalBalanceType + value + } + subtype { + name + value + } + currency { code } + isArchived + sequence + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + accounts: result.business.accounts.edges.map((e: any) => e.node), + pageInfo: result.business.accounts.pageInfo, + }; + }, + }, + + getAccount: { + name: 'wave_get_account', + description: 'Get details of a specific account', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + accountId: { type: 'string', description: 'Account ID', required: true }, + }, + required: ['accountId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; accountId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $accountId: ID!) { + business(id: $businessId) { + account(id: $accountId) { + id + name + description + displayId + type { + name + normalBalanceType + value + } + subtype { + name + value + } + currency { code } + isArchived + sequence + } + } + } + `; + const result = await client.query(query, { businessId, accountId: args.accountId }); + return result.business.account; + }, + }, + + createAccount: { + name: 'wave_create_account', + description: 'Create a new account', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + name: { type: 'string', description: 'Account name', required: true }, + type: { type: 'string', description: 'Account type', required: true }, + subtype: { type: 'string', description: 'Account subtype', required: true }, + description: { type: 'string', description: 'Account description' }, + currency: { type: 'string', description: 'Currency code' }, + }, + required: ['name', 'type', 'subtype'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: AccountCreateInput!) { + accountCreate(input: { businessId: $businessId, account: $input }) { + account { + id + name + type { name } + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.accountCreate; + }, + }, + + // ========== TRANSACTION TOOLS ========== + + listTransactions: { + name: 'wave_list_transactions', + description: 'List transactions with optional filters', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + accountId: { type: 'string', description: 'Filter by account ID' }, + }, + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + accountTransactions(page: $page, pageSize: $pageSize) { + edges { + node { + id + date + description + source + debits { + account { id name } + amount { value currency { code } } + } + credits { + account { id name } + amount { value currency { code } } + } + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + transactions: result.business.accountTransactions.edges.map((e: any) => e.node), + pageInfo: result.business.accountTransactions.pageInfo, + }; + }, + }, + + getTransaction: { + name: 'wave_get_transaction', + description: 'Get details of a specific transaction', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + transactionId: { type: 'string', description: 'Transaction ID', required: true }, + }, + required: ['transactionId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; transactionId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $transactionId: ID!) { + business(id: $businessId) { + accountTransaction(id: $transactionId) { + id + date + description + notes + source + sourceUrl + debits { + id + account { id name } + amount { value currency { code } } + } + credits { + id + account { id name } + amount { value currency { code } } + } + } + } + } + `; + const result = await client.query(query, { businessId, transactionId: args.transactionId }); + return result.business.accountTransaction; + }, + }, + + createTransaction: { + name: 'wave_create_transaction', + description: 'Create a manual transaction (journal entry)', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + date: { type: 'string', description: 'Transaction date (YYYY-MM-DD)', required: true }, + description: { type: 'string', description: 'Description' }, + debits: { + type: 'array', + description: 'Debit entries', + items: { + type: 'object', + properties: { + accountId: { type: 'string' }, + amount: { type: 'number' }, + }, + }, + required: true, + }, + credits: { + type: 'array', + description: 'Credit entries', + items: { + type: 'object', + properties: { + accountId: { type: 'string' }, + amount: { type: 'number' }, + }, + }, + required: true, + }, + }, + required: ['date', 'debits', 'credits'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: MoneyTransactionCreateInput!) { + moneyTransactionCreate(input: { businessId: $businessId, transaction: $input }) { + transaction { + id + date + description + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.moneyTransactionCreate; + }, + }, + + // ========== BILL TOOLS ========== + + listBills: { + name: 'wave_list_bills', + description: 'List all bills', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + bills(page: $page, pageSize: $pageSize) { + edges { + node { + id + createdAt + status + billNumber + invoiceNumber + billDate + dueDate + vendor { id name } + total { value currency { code } } + amountDue { value currency { code } } + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + bills: result.business.bills.edges.map((e: any) => e.node), + pageInfo: result.business.bills.pageInfo, + }; + }, + }, + + getBill: { + name: 'wave_get_bill', + description: 'Get details of a specific bill', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + billId: { type: 'string', description: 'Bill ID', required: true }, + }, + required: ['billId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; billId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $billId: ID!) { + business(id: $businessId) { + bill(id: $billId) { + id + createdAt + modifiedAt + status + billNumber + invoiceNumber + billDate + dueDate + vendor { id name email } + currency { code } + items { + description + quantity + unitPrice + account { id name } + total { value currency { code } } + taxes { + salesTax { id name } + amount { value } + } + } + memo + subtotal { value currency { code } } + total { value currency { code } } + amountDue { value currency { code } } + } + } + } + `; + const result = await client.query(query, { businessId, billId: args.billId }); + return result.business.bill; + }, + }, + + createBill: { + name: 'wave_create_bill', + description: 'Create a new bill', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + vendorId: { type: 'string', description: 'Vendor ID', required: true }, + billDate: { type: 'string', description: 'Bill date (YYYY-MM-DD)', required: true }, + dueDate: { type: 'string', description: 'Due date (YYYY-MM-DD)' }, + items: { + type: 'array', + description: 'Bill line items', + items: { + type: 'object', + properties: { + description: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + accountId: { type: 'string' }, + }, + }, + required: true, + }, + }, + required: ['vendorId', 'billDate', 'items'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: BillCreateInput!) { + billCreate(input: { businessId: $businessId, bill: $input }) { + bill { + id + billNumber + status + total { value currency { code } } + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.billCreate; + }, + }, + + // ========== VENDOR TOOLS ========== + + listVendors: { + name: 'wave_list_vendors', + description: 'List all vendors', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + vendors(page: $page, pageSize: $pageSize) { + edges { + node { + id + name + email + currency { code } + address { + addressLine1 + city + country { name } + } + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + vendors: result.business.vendors.edges.map((e: any) => e.node), + pageInfo: result.business.vendors.pageInfo, + }; + }, + }, + + createVendor: { + name: 'wave_create_vendor', + description: 'Create a new vendor', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + name: { type: 'string', description: 'Vendor name', required: true }, + email: { type: 'string', description: 'Email address' }, + phone: { type: 'string', description: 'Phone number' }, + website: { type: 'string', description: 'Website URL' }, + currency: { type: 'string', description: 'Currency code' }, + }, + required: ['name'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: VendorCreateInput!) { + vendorCreate(input: { businessId: $businessId, vendor: $input }) { + vendor { + id + name + email + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.vendorCreate; + }, + }, + + // ========== SALES TAX TOOLS ========== + + listSalesTaxes: { + name: 'wave_list_sales_taxes', + description: 'List all sales taxes', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!) { + business(id: $businessId) { + salesTaxes { + id + name + abbreviation + description + rate + isArchived + isCompound + isRecoverable + showTaxNumber + } + } + } + `; + const result = await client.query(query, { businessId }); + return result.business.salesTaxes; + }, + }, + + createSalesTax: { + name: 'wave_create_sales_tax', + description: 'Create a new sales tax', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + name: { type: 'string', description: 'Tax name', required: true }, + abbreviation: { type: 'string', description: 'Tax abbreviation' }, + rate: { type: 'number', description: 'Tax rate (e.g., 0.05 for 5%)', required: true }, + description: { type: 'string', description: 'Tax description' }, + }, + required: ['name', 'rate'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: SalesTaxCreateInput!) { + salesTaxCreate(input: { businessId: $businessId, salesTax: $input }) { + salesTax { + id + name + rate + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.salesTaxCreate; + }, + }, + + // ========== ESTIMATE TOOLS ========== + + listEstimates: { + name: 'wave_list_estimates', + description: 'List all estimates/quotes', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string; page?: number; pageSize?: number }) => { + const businessId = args.businessId || client.getBusinessId(); + const page = args.page || 1; + const pageSize = args.pageSize || 50; + const query = ` + query($businessId: ID!, $page: Int!, $pageSize: Int!) { + business(id: $businessId) { + estimates(page: $page, pageSize: $pageSize) { + edges { + node { + id + createdAt + estimateNumber + estimateDate + expiryDate + status + customer { id name } + total { value currency { code } } + viewUrl + pdfUrl + } + } + pageInfo { + currentPage + totalPages + totalCount + } + } + } + } + `; + const result = await client.query(query, { businessId, page, pageSize }); + return { + estimates: result.business.estimates.edges.map((e: any) => e.node), + pageInfo: result.business.estimates.pageInfo, + }; + }, + }, + + createEstimate: { + name: 'wave_create_estimate', + description: 'Create a new estimate/quote', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + customerId: { type: 'string', description: 'Customer ID', required: true }, + items: { + type: 'array', + description: 'Estimate line items', + items: { + type: 'object', + properties: { + productId: { type: 'string' }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + }, + }, + required: true, + }, + estimateDate: { type: 'string', description: 'Estimate date (YYYY-MM-DD)' }, + expiryDate: { type: 'string', description: 'Expiry date (YYYY-MM-DD)' }, + title: { type: 'string', description: 'Estimate title' }, + memo: { type: 'string', description: 'Memo/notes' }, + }, + required: ['customerId', 'items'], + }, + handler: async (client: WaveClient, args: any) => { + const businessId = args.businessId || client.getBusinessId(); + const mutation = ` + mutation($businessId: ID!, $input: EstimateCreateInput!) { + estimateCreate(input: { businessId: $businessId, estimate: $input }) { + estimate { + id + estimateNumber + status + total { value currency { code } } + } + didSucceed + inputErrors { + path + message + } + } + } + `; + const result = await client.mutate(mutation, { businessId, input: args }); + return result.estimateCreate; + }, + }, + + // ========== REPORTING TOOLS ========== + + getAccountBalances: { + name: 'wave_get_account_balances', + description: 'Get current balances for all accounts', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + }, + }, + handler: async (client: WaveClient, args: { businessId?: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!) { + business(id: $businessId) { + accounts(page: 1, pageSize: 200) { + edges { + node { + id + name + type { name } + subtype { name } + currency { code } + isArchived + } + } + } + } + } + `; + const result = await client.query(query, { businessId }); + return result.business.accounts.edges.map((e: any) => e.node); + }, + }, + + getProfitAndLoss: { + name: 'wave_get_profit_and_loss', + description: 'Get Profit & Loss (Income Statement) report', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)', required: true }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)', required: true }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (client: WaveClient, args: { businessId?: string; startDate: string; endDate: string }) => { + const businessId = args.businessId || client.getBusinessId(); + // This is a simplified version - Wave's actual P&L query is more complex + const query = ` + query($businessId: ID!, $startDate: Date!, $endDate: Date!) { + business(id: $businessId) { + id + name + } + } + `; + const result = await client.query(query, { businessId, startDate: args.startDate, endDate: args.endDate }); + return { + message: 'P&L report generation - requires additional implementation', + business: result.business, + period: { startDate: args.startDate, endDate: args.endDate }, + }; + }, + }, + + getBalanceSheet: { + name: 'wave_get_balance_sheet', + description: 'Get Balance Sheet report', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + date: { type: 'string', description: 'Report date (YYYY-MM-DD)', required: true }, + }, + required: ['date'], + }, + handler: async (client: WaveClient, args: { businessId?: string; date: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!) { + business(id: $businessId) { + id + name + } + } + `; + const result = await client.query(query, { businessId }); + return { + message: 'Balance sheet generation - requires additional implementation', + business: result.business, + date: args.date, + }; + }, + }, + + // ========== UTILITY TOOLS ========== + + listCountries: { + name: 'wave_list_countries', + description: 'List all available countries', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: WaveClient, _args: any) => { + const query = ` + query { + countries { + code + name + currency { + code + symbol + name + } + } + } + `; + const result = await client.query(query); + return result.countries; + }, + }, + + listCurrencies: { + name: 'wave_list_currencies', + description: 'List all available currencies', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: WaveClient, _args: any) => { + const query = ` + query { + currencies { + code + symbol + name + plural + exponent + } + } + `; + const result = await client.query(query); + return result.currencies; + }, + }, + + getInvoicePdf: { + name: 'wave_get_invoice_pdf', + description: 'Get PDF URL for an invoice', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + invoiceId: { type: 'string', description: 'Invoice ID', required: true }, + }, + required: ['invoiceId'], + }, + handler: async (client: WaveClient, args: { businessId?: string; invoiceId: string }) => { + const businessId = args.businessId || client.getBusinessId(); + const query = ` + query($businessId: ID!, $invoiceId: ID!) { + business(id: $businessId) { + invoice(id: $invoiceId) { + id + invoiceNumber + pdfUrl + viewUrl + } + } + } + `; + const result = await client.query(query, { businessId, invoiceId: args.invoiceId }); + return result.business.invoice; + }, + }, + + searchCustomers: { + name: 'wave_search_customers', + description: 'Search customers by name or email', + inputSchema: { + type: 'object', + properties: { + businessId: { type: 'string', description: 'Business ID (optional if set in config)' }, + searchTerm: { type: 'string', description: 'Search term (name or email)', required: true }, + }, + required: ['searchTerm'], + }, + handler: async (client: WaveClient, args: { businessId?: string; searchTerm: string }) => { + const businessId = args.businessId || client.getBusinessId(); + // Note: Wave's GraphQL API doesn't have built-in search, so we'll list and filter + const query = ` + query($businessId: ID!) { + business(id: $businessId) { + customers(page: 1, pageSize: 100) { + edges { + node { + id + name + firstName + lastName + email + } + } + } + } + } + `; + const result = await client.query(query, { businessId }); + const allCustomers = result.business.customers.edges.map((e: any) => e.node); + const searchLower = args.searchTerm.toLowerCase(); + const filtered = allCustomers.filter((c: any) => + c.name?.toLowerCase().includes(searchLower) || + c.email?.toLowerCase().includes(searchLower) || + c.firstName?.toLowerCase().includes(searchLower) || + c.lastName?.toLowerCase().includes(searchLower) + ); + return filtered; + }, + }, +}; + +export type WaveTool = keyof typeof waveTools;