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
JavaScript
#!/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`;