UNPKG

appointment-mcp-server

Version:

Customer-focused MCP Server for appointment management with comprehensive service discovery, availability checking, and booking capabilities

1,003 lines (1,002 loc) 142 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { createAppointment, getAppointments, getAppointment, deleteAppointment, ensureBusinessExists, getBusinessDetails, verifyDatabaseConnection, createCustomer, getCustomer, searchCustomers, updateCustomer, getServices, getService, getServiceByName, searchServicesFuzzy, searchServicesComprehensive, getCustomerAppointments, getBusinessHours, getStaff, getCustomerReviews, createReview, getStaffAvailability, getAvailableTimeSlots, getAllStaffInfo, getStaffMember, getStaffTimeOff, checkServiceAvailability, getServiceTimeSlots, checkBusinessHours, checkAppointmentConflict, // New Phase 1 functions updateAppointment, cancelAppointment, rescheduleAppointment, confirmAppointment, completeAppointment, getStaffAvailabilityCalendar, checkRealTimeAvailability, // Phase 2 Customer-Focused Functions createCustomerValidated, updateCustomerProfile, getCustomerPreferences, getCustomerStatistics, createBookingValidated, getBookingConfirmation, getAvailableBookingSlots, // Additional Customer-Focused Service Discovery Functions getServicesByPriceRange, getServicesByDuration, getServicesByStaff, getServicesByTimeAvailability, getPopularServices, // Helper functions createCustomerIfNotExists } from "./database.js"; // Get BUSINESS_ID from environment variables const DEFAULT_BUSINESS_ID = process.env.BUSINESS_ID; if (!DEFAULT_BUSINESS_ID) { console.warn('Warning: BUSINESS_ID environment variable not set. All operations will require explicit business_id parameter.'); } // Helper function to get business ID (use provided or default) function getBusinessId(providedBusinessId) { const businessId = providedBusinessId || DEFAULT_BUSINESS_ID; if (!businessId) { throw new Error('Business ID is required. Either provide business_id parameter or set BUSINESS_ID environment variable.'); } return businessId; } // Create server instance const server = new Server({ name: "appointment-mcp-server", version: "1.7.0", }, { capabilities: { tools: {}, }, }); // Helper function to validate date format (YYYY-MM-DD) function isValidDate(dateString) { const regex = /^\d{4}-\d{2}-\d{2}$/; if (!regex.test(dateString)) return false; const date = new Date(dateString); return date instanceof Date && !isNaN(date.getTime()); } // Helper function to validate time format (HH:MM) function isValidTime(timeString) { const regex = /^([01]?\d|2[0-3]):[0-5]\d$/; return regex.test(timeString); } // Tool: Create appointment server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case "create_appointment": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), service_id: z.string().min(1, "Service ID is required"), staff_id: z.string().optional(), start_time: z.string().min(1, "Start time is required"), end_time: z.string().min(1, "End time is required"), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const appointmentData = { ...parsedArgs }; // Ensure business exists await ensureBusinessExists(); const appointment = await createAppointment(appointmentData); return { content: [ { type: "text", text: `Appointment created successfully!\n\nID: ${appointment.id}\nCustomer ID: ${appointment.customer_id}\nService ID: ${appointment.service_id}\nStart Time: ${appointment.start_time}\nEnd Time: ${appointment.end_time}${appointment.notes ? `\nNotes: ${appointment.notes}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "list_appointments": { const schema = z.object({ customer_id: z.string().optional(), service_id: z.string().optional(), staff_id: z.string().optional(), status: z.string().optional(), start_date: z.string().optional(), end_date: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const filters = parsedArgs; const appointments = await getAppointments(filters); if (!appointments || appointments.length === 0) { return { content: [ { type: "text", text: "No appointments found.", }, ], }; } const appointmentList = appointments .map((apt) => `ID: ${apt.id}\nCustomer: ${apt.customer_first_name} ${apt.customer_last_name}\nService: ${apt.service_name}\nStaff: ${apt.staff_first_name ? `${apt.staff_first_name} ${apt.staff_last_name}` : 'Not assigned'}\nStart: ${apt.start_time}\nEnd: ${apt.end_time}\nStatus: ${apt.status}\n---`) .join("\n"); return { content: [ { type: "text", text: `Found ${appointments.length} appointment(s):\n\n${appointmentList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error listing appointments: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_appointment": { const schema = z.object({ id: z.string().min(1, "Appointment ID is required"), }); try { const parsedArgs = schema.parse(args); const { id } = parsedArgs; const appointment = await getAppointment(id); return { content: [ { type: "text", text: `Appointment Details:\n\nID: ${appointment.id}\nCustomer: ${appointment.customer_first_name} ${appointment.customer_last_name}\nEmail: ${appointment.customer_email}\nPhone: ${appointment.customer_phone || 'Not provided'}\nService: ${appointment.service_name}\nDescription: ${appointment.service_description || 'No description'}\nDuration: ${appointment.duration_minutes} minutes\nPrice: $${(appointment.price_cents / 100).toFixed(2)}\nStaff: ${appointment.staff_first_name ? `${appointment.staff_first_name} ${appointment.staff_last_name}` : 'Not assigned'}\nStart Time: ${appointment.start_time}\nEnd Time: ${appointment.end_time}\nStatus: ${appointment.status}\nNotes: ${appointment.notes || 'No notes'}\nCreated: ${new Date(appointment.created_at).toLocaleString()}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "delete_appointment": { const schema = z.object({ id: z.string().min(1, "Appointment ID is required"), }); try { const parsedArgs = schema.parse(args); const { id } = parsedArgs; const deletedAppointment = await deleteAppointment(id); return { content: [ { type: "text", text: `Appointment deleted successfully!\n\nDeleted appointment ID: ${deletedAppointment.id}\nCustomer ID: ${deletedAppointment.customer_id}\nService ID: ${deletedAppointment.service_id}\nStart Time: ${deletedAppointment.start_time}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error deleting appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // Business Information Tools case "get_business": { const schema = z.object({}); try { const parsedArgs = schema.parse(args); const business = await getBusinessDetails(); return { content: [ { type: "text", text: `Business Details:\n\nID: ${business.id}\nName: ${business.name}\nDescription: ${business.description || 'No description'}\nAddress: ${business.address || 'Not provided'}\nPhone: ${business.phone || 'Not provided'}\nEmail: ${business.email || 'Not provided'}\nWebsite: ${business.website || 'Not provided'}\nTimezone: ${business.timezone || 'Not specified'}\nCreated: ${new Date(business.created_at).toLocaleString()}\nUpdated: ${new Date(business.updated_at).toLocaleString()}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving business details: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // Customer Management Tools case "create_customer": { const schema = z.object({ first_name: z.string().optional(), last_name: z.string().optional(), email: z.string().optional(), phone: z.string(), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const customerData = parsedArgs; await ensureBusinessExists(); const customer = await createCustomer(customerData); return { content: [ { type: "text", text: `Customer created successfully!\n\nID: ${customer.id}\nName: ${customer.first_name} ${customer.last_name}\nEmail: ${customer.email}\nPhone: ${customer.phone_number || 'Not provided'}\nNotes: ${customer.notes || 'No notes'}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating customer: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_customer": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), }); try { const parsedArgs = schema.parse(args); const { customer_id } = parsedArgs; const customer = await getCustomer(customer_id); return { content: [ { type: "text", text: `Customer Details:\n\nID: ${customer.id}\nName: ${customer.first_name} ${customer.last_name}\nEmail: ${customer.email}\nPhone: ${customer.phone_number || 'Not provided'}\nNotes: ${customer.notes || 'No notes'}\nCreated: ${new Date(customer.created_at).toLocaleString()}\nUpdated: ${new Date(customer.updated_at).toLocaleString()}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving customer: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "search_customers": { const schema = z.object({ search_term: z.string().min(1, "Search term is required"), }); try { const parsedArgs = schema.parse(args); const { search_term } = parsedArgs; const customers = await searchCustomers(search_term); if (!customers || customers.length === 0) { return { content: [ { type: "text", text: `No customers found matching "${search_term}".`, }, ], }; } const customerList = customers .map((customer) => `ID: ${customer.id}\nName: ${customer.first_name} ${customer.last_name}\nEmail: ${customer.email}\nPhone: ${customer.phone_number || 'Not provided'}\n---`) .join("\n"); return { content: [ { type: "text", text: `Found ${customers.length} customer(s) matching "${search_term}":\n\n${customerList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error searching customers: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // Service Information Tools case "get_services": { const schema = z.object({}); try { const parsedArgs = schema.parse(args); const services = await getServices(); if (!services || services.length === 0) { return { content: [ { type: "text", text: "No services found.", }, ], }; } const serviceList = services .map((service) => `ID: ${service.id}\nName: ${service.name}\nDescription: ${service.description || 'No description'}\nDuration: ${service.duration_minutes} minutes\nPrice: $${(service.price_cents / 100).toFixed(2)}\nCategory: ${service.category_name || 'Uncategorized'}\n---`) .join("\n"); return { content: [ { type: "text", text: `Found ${services.length} service(s):\n\n${serviceList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving services: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_service": { const schema = z.object({ service_id: z.string().min(1, "Service ID is required"), }); try { const parsedArgs = schema.parse(args); const { service_id } = parsedArgs; const service = await getService(service_id); const staffList = service.staff && service.staff.length > 0 ? service.staff.map((staff) => `${staff.first_name} ${staff.last_name}`).join(', ') : 'No staff assigned'; return { content: [ { type: "text", text: `Service Details:\n\nID: ${service.id}\nName: ${service.name}\nDescription: ${service.description || 'No description'}\nDuration: ${service.duration_minutes} minutes\nPrice: $${(service.price_cents / 100).toFixed(2)}\nCategory: ${service.category_name || 'Uncategorized'}\nCategory Description: ${service.category_description || 'No category description'}\nAvailable Staff: ${staffList}\nActive: ${service.is_active ? 'Yes' : 'No'}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving service: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_service_by_name": { const schema = z.object({ service_name: z.string().min(1, "Service name is required"), }); try { const parsedArgs = schema.parse(args); const { service_name } = parsedArgs; const services = await getServiceByName(service_name); if (!services || services.length === 0) { return { content: [ { type: "text", text: `No services found matching "${service_name}".`, }, ], }; } const serviceList = services .map((service) => { const staffList = service.staff && service.staff.length > 0 ? service.staff.map((staff) => `${staff.first_name} ${staff.last_name}`).join(', ') : 'No staff assigned'; return `ID: ${service.id}\nName: ${service.name}\nDescription: ${service.description || 'No description'}\nDuration: ${service.duration_minutes} minutes\nPrice: $${(service.price_cents / 100).toFixed(2)}\nCategory: ${service.category_name || 'Uncategorized'}\nAvailable Staff: ${staffList}\nStaff Count: ${service.staff_count}\nActive: ${service.is_active ? 'Yes' : 'No'}\n---`; }) .join("\n"); return { content: [ { type: "text", text: `Found ${services.length} service(s) matching "${service_name}":\n\n${serviceList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error retrieving service by name: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "search_services_fuzzy": { const schema = z.object({ service_name: z.string().min(1, "Service name is required"), similarity_threshold: z.number().min(0).max(1).optional(), }); try { const parsedArgs = schema.parse(args); const { service_name, similarity_threshold = 0.3 } = parsedArgs; const services = await searchServicesFuzzy(service_name, similarity_threshold); if (!services || services.length === 0) { return { content: [ { type: "text", text: `No services found matching "${service_name}" with similarity threshold ${similarity_threshold}.`, }, ], }; } const serviceList = services .map((service) => { const staffList = service.staff && service.staff.length > 0 ? service.staff.map((staff) => `${staff.first_name} ${staff.last_name}`).join(', ') : 'No staff assigned'; return `ID: ${service.service_id}\nName: ${service.name}\nDescription: ${service.description || 'No description'}\nDuration: ${service.duration_minutes} minutes\nPrice: $${(service.price_cents / 100).toFixed(2)}\nSimilarity Score: ${(service.similarity_score * 100).toFixed(1)}%\nCategory: ${service.category?.name || 'Uncategorized'}\nAvailable Staff: ${staffList}\nStaff Count: ${service.staff_count}\nActive: ${service.is_active ? 'Yes' : 'No'}\n---`; }) .join("\n"); return { content: [ { type: "text", text: `Found ${services.length} service(s) matching "${service_name}" (fuzzy search):\n\n${serviceList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error searching services with fuzzy matching: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "search_services_comprehensive": { const schema = z.object({ search_term: z.string().min(1, "Search term is required"), similarity_threshold: z.number().min(0).max(1).optional(), }); try { const parsedArgs = schema.parse(args); const { search_term, similarity_threshold = 0.3 } = parsedArgs; const services = await searchServicesComprehensive(search_term, similarity_threshold); if (!services || services.length === 0) { return { content: [ { type: "text", text: `No services found matching "${search_term}" with comprehensive search.`, }, ], }; } const serviceList = services .map((service) => { const staffList = service.staff && service.staff.length > 0 ? service.staff.map((staff) => `${staff.first_name} ${staff.last_name}`).join(', ') : 'No staff assigned'; return `ID: ${service.service_id}\nName: ${service.name}\nDescription: ${service.description || 'No description'}\nDuration: ${service.duration_minutes} minutes\nPrice: $${(service.price_cents / 100).toFixed(2)}\nSearch Score: ${service.search_score}/100\nMatch Type: ${service.match_type}\nMatched Field: ${service.matched_field}\nCategory: ${service.category?.name || 'Uncategorized'}\nAvailable Staff: ${staffList}\nStaff Count: ${service.staff_count}\nActive: ${service.is_active ? 'Yes' : 'No'}\n---`; }) .join("\n"); return { content: [ { type: "text", text: `Found ${services.length} service(s) matching "${search_term}" (comprehensive search):\n\n${serviceList}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error searching services comprehensively: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // Customer-Focused Appointment Management case "update_appointment": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), customer_id: z.string().min(1, "Customer ID is required"), service_id: z.string().min(1, "Service ID is required"), staff_id: z.string().min(1, "Staff ID is required"), start_time: z.string().min(1, "Start time is required"), end_time: z.string().min(1, "End time is required"), status: z.string().min(1, "Status is required"), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const result = await updateAppointment(parsedArgs.appointment_id, parsedArgs.customer_id, parsedArgs.service_id, parsedArgs.staff_id, parsedArgs.start_time, parsedArgs.end_time, parsedArgs.status, parsedArgs.notes); return { content: [ { type: "text", text: `✅ Appointment updated successfully!\n\nAppointment ID: ${result.appointment.id}\nStatus: ${result.appointment.status}\nUpdated at: ${result.appointment.updated_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error updating appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "cancel_appointment": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), cancellation_reason: z.string().min(1, "Cancellation reason is required"), cancelled_by: z.string().min(1, "Cancelled by is required"), }); try { const parsedArgs = schema.parse(args); const result = await cancelAppointment(parsedArgs.appointment_id, parsedArgs.cancellation_reason, parsedArgs.cancelled_by); return { content: [ { type: "text", text: `✅ Appointment cancelled successfully!\n\nAppointment ID: ${result.cancellation.appointment_id}\nCancellation Reason: ${parsedArgs.cancellation_reason}\nCancelled by: ${parsedArgs.cancelled_by}\nCancelled at: ${result.cancellation.cancelled_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error cancelling appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "reschedule_appointment": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), new_start_time: z.string().min(1, "New start time is required"), new_end_time: z.string().min(1, "New end time is required"), rescheduled_by: z.string().min(1, "Rescheduled by is required"), }); try { const parsedArgs = schema.parse(args); const result = await rescheduleAppointment(parsedArgs.appointment_id, parsedArgs.new_start_time, parsedArgs.new_end_time, parsedArgs.rescheduled_by); return { content: [ { type: "text", text: `✅ Appointment rescheduled successfully!\n\nAppointment ID: ${result.reschedule.appointment_id}\nOld Start Time: ${result.reschedule.old_start_time}\nNew Start Time: ${result.reschedule.new_start_time}\nRescheduled by: ${parsedArgs.rescheduled_by}\nRescheduled at: ${result.reschedule.rescheduled_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error rescheduling appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "confirm_appointment": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), confirmed_by: z.string().min(1, "Confirmed by is required"), }); try { const parsedArgs = schema.parse(args); const result = await confirmAppointment(parsedArgs.appointment_id, parsedArgs.confirmed_by); return { content: [ { type: "text", text: `✅ Appointment confirmed successfully!\n\nAppointment ID: ${result.confirmation.appointment_id}\nStatus: ${result.confirmation.status}\nConfirmed by: ${parsedArgs.confirmed_by}\nConfirmed at: ${result.confirmation.confirmed_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error confirming appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "complete_appointment": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), completed_by: z.string().min(1, "Completed by is required"), completion_notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const result = await completeAppointment(parsedArgs.appointment_id, parsedArgs.completed_by, parsedArgs.completion_notes); return { content: [ { type: "text", text: `✅ Appointment completed successfully!\n\nAppointment ID: ${result.completion.appointment_id}\nStatus: ${result.completion.status}\nCompletion Notes: ${parsedArgs.completion_notes || 'None'}\nCompleted by: ${parsedArgs.completed_by}\nCompleted at: ${result.completion.completed_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error completing appointment: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "check_real_time_availability": { const schema = z.object({ service_id: z.string().min(1, "Service ID is required"), date: z.string().min(1, "Date is required"), time: z.string().min(1, "Time is required"), }); try { const parsedArgs = schema.parse(args); const result = await checkRealTimeAvailability(parsedArgs.service_id, parsedArgs.date, parsedArgs.time); const availabilityText = result.available ? `✅ Available! ${result.reason}\n\nRemaining slots: ${result.remaining_slots}\nAvailable staff: ${result.available_staff}` : `❌ Not available: ${result.reason}\n\nExisting bookings: ${result.existing_bookings}\nMax bookings: ${result.max_bookings}\nAvailable staff: ${result.available_staff}`; return { content: [ { type: "text", text: `Real-time availability check for ${parsedArgs.date} at ${parsedArgs.time}:\n\n${availabilityText}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error checking real-time availability: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_staff_availability_calendar": { const schema = z.object({ staff_id: z.string().min(1, "Staff ID is required"), start_date: z.string().min(1, "Start date is required"), end_date: z.string().min(1, "End date is required"), }); try { const parsedArgs = schema.parse(args); const result = await getStaffAvailabilityCalendar(parsedArgs.staff_id, parsedArgs.start_date, parsedArgs.end_date); if (!result.availability || result.availability.length === 0) { return { content: [ { type: "text", text: `No availability data found for staff member from ${parsedArgs.start_date} to ${parsedArgs.end_date}.`, }, ], }; } const calendarText = result.availability .map((day) => { const status = day.is_available ? '✅ Available' : '❌ Not Available'; const appointments = day.appointments && day.appointments.length > 0 ? day.appointments.map((apt) => `${apt.service_name} with ${apt.customer_name} (${apt.start_time})`).join(', ') : 'No appointments'; return `${day.date} (${day.day_name}): ${status}\nWorking Hours: ${day.working_hours || 'Not set'}\nAppointments: ${appointments}\n---`; }) .join("\n"); return { content: [ { type: "text", text: `Staff Availability Calendar for ${parsedArgs.start_date} to ${parsedArgs.end_date}:\n\n${calendarText}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting staff availability calendar: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } // Phase 2 Customer-Focused Tools case "create_customer_validated": { const schema = z.object({ first_name: z.string().min(1, "First name is required"), last_name: z.string().min(1, "Last name is required"), email: z.string().email("Invalid email format"), phone: z.string().min(10, "Phone number must be at least 10 digits"), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const result = await createCustomerValidated(parsedArgs.first_name, parsedArgs.last_name, parsedArgs.email, parsedArgs.phone, parsedArgs.notes); return { content: [ { type: "text", text: `✅ Customer created successfully!\n\nCustomer ID: ${result.customer.id}\nName: ${result.customer.first_name} ${result.customer.last_name}\nEmail: ${result.customer.email}\nPhone: ${result.customer.phone_number}\nCreated: ${result.customer.created_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating customer: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "update_customer_profile": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), first_name: z.string().min(1, "First name is required"), last_name: z.string().min(1, "Last name is required"), email: z.string().email("Invalid email format"), phone: z.string().min(10, "Phone number must be at least 10 digits"), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const result = await updateCustomerProfile(parsedArgs.customer_id, parsedArgs.first_name, parsedArgs.last_name, parsedArgs.email, parsedArgs.phone, parsedArgs.notes); return { content: [ { type: "text", text: `✅ Customer profile updated successfully!\n\nCustomer ID: ${result.customer.id}\nName: ${result.customer.first_name} ${result.customer.last_name}\nEmail: ${result.customer.email}\nPhone: ${result.customer.phone_number}\nUpdated: ${result.customer.updated_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error updating customer profile: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_customer_preferences": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), }); try { const parsedArgs = schema.parse(args); const result = await getCustomerPreferences(parsedArgs.customer_id); const preferences = result.preferences; const preferredServices = preferences.preferred_services || []; const preferredStaff = preferences.preferred_staff || []; const preferredTimeSlots = preferences.preferred_time_slots || []; let preferencesText = `Customer Preferences for ${parsedArgs.customer_id}:\n\n`; if (preferredServices.length > 0) { preferencesText += `Preferred Services:\n${preferredServices.map((service) => `- ${service.service_name} (${service.booking_count} bookings, last: ${service.last_booking})`).join('\n')}\n\n`; } if (preferredStaff.length > 0) { preferencesText += `Preferred Staff:\n${preferredStaff.map((staff) => `- ${staff.staff_name} (${staff.booking_count} bookings, last: ${staff.last_booking})`).join('\n')}\n\n`; } if (preferredTimeSlots.length > 0) { preferencesText += `Preferred Time Slots:\n${preferredTimeSlots.map((slot) => `- ${slot.time_slot} (${slot.booking_count} bookings)`).join('\n')}\n\n`; } preferencesText += `Total Appointments: ${preferences.total_appointments}\n`; preferencesText += `Completed Appointments: ${preferences.completed_appointments}\n`; preferencesText += `Average Rating: ${preferences.average_rating.toFixed(1)}/5`; return { content: [ { type: "text", text: preferencesText, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting customer preferences: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_customer_statistics": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), }); try { const parsedArgs = schema.parse(args); const result = await getCustomerStatistics(parsedArgs.customer_id); const stats = result.statistics; const aptStats = stats.appointment_stats; const serviceStats = stats.service_stats; const staffStats = stats.staff_stats; const reviewStats = stats.review_stats; const loyaltyStats = stats.loyalty_stats; let statsText = `Customer Statistics for ${parsedArgs.customer_id}:\n\n`; statsText += `📊 Appointment Statistics:\n`; statsText += `- Total Appointments: ${aptStats.total_appointments}\n`; statsText += `- Completed: ${aptStats.completed_appointments}\n`; statsText += `- Cancelled: ${aptStats.cancelled_appointments}\n`; statsText += `- Upcoming: ${aptStats.upcoming_appointments}\n`; statsText += `- First Appointment: ${aptStats.first_appointment || 'N/A'}\n`; statsText += `- Last Appointment: ${aptStats.last_appointment || 'N/A'}\n\n`; statsText += `💰 Service Statistics:\n`; statsText += `- Services Used: ${serviceStats.total_services_used}\n`; statsText += `- Most Used Service: ${serviceStats.most_used_service || 'N/A'}\n`; statsText += `- Total Spent: $${(serviceStats.total_spent / 100).toFixed(2)}\n\n`; statsText += `👥 Staff Statistics:\n`; statsText += `- Staff Seen: ${staffStats.total_staff_seen}\n`; statsText += `- Preferred Staff: ${staffStats.preferred_staff || 'N/A'}\n\n`; statsText += `⭐ Review Statistics:\n`; statsText += `- Total Reviews: ${reviewStats.total_reviews}\n`; statsText += `- Average Rating: ${reviewStats.average_rating.toFixed(1)}/5\n`; statsText += `- Last Review: ${reviewStats.last_review_date || 'N/A'}\n\n`; statsText += `🎯 Loyalty Statistics:\n`; statsText += `- Customer Since: ${loyaltyStats.customer_since}\n`; statsText += `- Days Since Last Visit: ${loyaltyStats.days_since_last_visit || 'N/A'}\n`; statsText += `- Visit Frequency: ${loyaltyStats.visit_frequency ? loyaltyStats.visit_frequency.toFixed(1) + ' days' : 'N/A'}`; return { content: [ { type: "text", text: statsText, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error getting customer statistics: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "create_booking_validated": { const schema = z.object({ customer_id: z.string().min(1, "Customer ID is required"), service_id: z.string().min(1, "Service ID is required"), staff_id: z.string().min(1, "Staff ID is required"), start_time: z.string().min(1, "Start time is required"), notes: z.string().optional(), }); try { const parsedArgs = schema.parse(args); const result = await createBookingValidated(parsedArgs.customer_id, parsedArgs.service_id, parsedArgs.staff_id, parsedArgs.start_time, parsedArgs.notes); return { content: [ { type: "text", text: `✅ Booking created successfully!\n\nAppointment ID: ${result.booking.appointment_id}\nCustomer ID: ${result.booking.customer_id}\nService ID: ${result.booking.service_id}\nStaff ID: ${result.booking.staff_id}\nStart Time: ${result.booking.start_time}\nEnd Time: ${result.booking.end_time}\nStatus: ${result.booking.status}\nCreated: ${result.booking.created_at}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating booking: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } } case "get_booking_confirmation": { const schema = z.object({ appointment_id: z.string().min(1, "Appointment ID is required"), }); try { const parsedArgs = schema.parse(args); const result = await getBookingConfirmation(parsedArgs.appointment_id); const confirmation = result.confirmation; const customer = confirmation.customer; const service = confirmation.service; const staff = confirmation.staff; const appointment = confirmation.appointment; const business = confirmation.business; let confirmationText = `📋 Booking Confirmation\n\n`; confirmationText += `Confirmation Code: ${confirmation.confirmation_code}\n\n`; confirmationText += `👤 Customer:\n`; confirmationText += `- Name: ${customer.name}\n`; confirmationText += `- Email: ${customer.email}\n`; confirmationText += `- Phone: ${customer.phone}\n\n`; confirmationText += `🛠️ Service:\n`; confirmationText += `- Name: ${service.name}\n`; confirmationText += `- Description: ${service.description || 'N/A'}\n`;