diff --git a/servers/acuity-scheduling/.env.example b/servers/acuity-scheduling/.env.example new file mode 100644 index 0000000..df63b54 --- /dev/null +++ b/servers/acuity-scheduling/.env.example @@ -0,0 +1,11 @@ +# Acuity Scheduling API Credentials +# Get your credentials from: https://secure.acuityscheduling.com/app.php?key=api&action=settings + +# Required: Your Acuity User ID +ACUITY_USER_ID=your_user_id_here + +# Required: Your Acuity API Key +ACUITY_API_KEY=your_api_key_here + +# Optional: OAuth2 Token (if using OAuth2 instead of Basic Auth) +# ACUITY_OAUTH2_TOKEN=your_oauth2_token_here diff --git a/servers/acuity-scheduling/.gitignore b/servers/acuity-scheduling/.gitignore new file mode 100644 index 0000000..4274b51 --- /dev/null +++ b/servers/acuity-scheduling/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.log +.DS_Store diff --git a/servers/acuity-scheduling/README.md b/servers/acuity-scheduling/README.md new file mode 100644 index 0000000..68c00af --- /dev/null +++ b/servers/acuity-scheduling/README.md @@ -0,0 +1,221 @@ +# Acuity Scheduling MCP Server + +A comprehensive Model Context Protocol (MCP) server for Acuity Scheduling API integration. + +## Features + +### 40+ Tools Across 10 Categories + +- **Appointments** (8 tools): List, get, create, update, cancel, reschedule, list types, get type +- **Availability** (4 tools): Get dates, get times, get classes, check availability +- **Clients** (5 tools): List, get, create, update, delete +- **Calendars** (4 tools): List, get, create, update +- **Products** (6 tools): List products by type (add-ons, packages, subscriptions, gift certificates) +- **Forms** (3 tools): List forms, get fields, get intake form responses +- **Labels** (5 tools): List, create, delete, add to appointment, remove from appointment +- **Webhooks** (3 tools): List, create, delete +- **Coupons** (5 tools): List, get, create, update, delete +- **Blocks** (3 tools): List, create, delete time blocks + +### 14 Interactive MCP Apps + +1. **Appointment Dashboard** - Overview dashboard with stats and upcoming appointments +2. **Appointment Detail** - Detailed view of individual appointments +3. **Appointment Grid** - Filterable table view of all appointments +4. **Availability Calendar** - Interactive calendar showing available time slots +5. **Client Directory** - Searchable directory of all clients +6. **Client Detail** - Comprehensive client profiles with appointment history +7. **Calendar Manager** - Manage staff calendars and settings +8. **Product Catalog** - Browse products, packages, and add-ons +9. **Form Responses** - View and manage intake form submissions +10. **Label Manager** - Create and manage appointment labels +11. **Coupon Manager** - Create and track promotional coupons +12. **Booking Flow** - Interactive multi-step booking interface +13. **Schedule Overview** - Week-view schedule across all calendars +14. **Blocked Time Manager** - Manage blocked time slots and staff availability + +## Installation + +```bash +npm install +npm run build +``` + +## Configuration + +Create a `.env` file with your Acuity Scheduling credentials: + +```bash +# Required +ACUITY_USER_ID=your_user_id +ACUITY_API_KEY=your_api_key + +# Optional (for OAuth2) +ACUITY_OAUTH2_TOKEN=your_oauth2_token +``` + +### Getting API Credentials + +1. Log in to your Acuity Scheduling account +2. Go to Business Settings → Integrations → API +3. Enable API access and copy your User ID and API Key + +## Usage + +### As MCP Server + +Add to your MCP client configuration (e.g., Claude Desktop): + +```json +{ + "mcpServers": { + "acuity-scheduling": { + "command": "node", + "args": ["/path/to/acuity-scheduling/dist/main.js"], + "env": { + "ACUITY_USER_ID": "your_user_id", + "ACUITY_API_KEY": "your_api_key" + } + } + } +} +``` + +### Standalone + +```bash +npm start +``` + +## API Reference + +### Authentication + +The server supports two authentication methods: + +1. **Basic Auth** (recommended): Uses User ID and API Key +2. **OAuth2**: Uses OAuth2 access token + +### Tool Examples + +#### List Appointments + +```json +{ + "tool": "acuity_list_appointments", + "arguments": { + "minDate": "2024-01-01", + "maxDate": "2024-01-31", + "calendarID": 1 + } +} +``` + +#### Create Appointment + +```json +{ + "tool": "acuity_create_appointment", + "arguments": { + "appointmentTypeID": 1, + "datetime": "2024-01-15T10:00:00", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "phone": "555-1234" + } +} +``` + +#### Get Availability + +```json +{ + "tool": "acuity_get_availability_times", + "arguments": { + "appointmentTypeID": 1, + "date": "2024-01-15" + } +} +``` + +## MCP Apps + +Access interactive apps through MCP resources: + +- `acuity://apps/appointment-dashboard` +- `acuity://apps/availability-calendar` +- `acuity://apps/client-directory` +- And 11 more... + +Each app provides a rich, interactive UI for managing different aspects of your Acuity Scheduling account. + +## Architecture + +``` +src/ +├── clients/ +│ └── acuity.ts # API client with auth & pagination +├── tools/ +│ ├── appointments-tools.ts +│ ├── availability-tools.ts +│ ├── clients-tools.ts +│ ├── calendars-tools.ts +│ ├── products-tools.ts +│ ├── forms-tools.ts +│ ├── labels-tools.ts +│ ├── webhooks-tools.ts +│ ├── coupons-tools.ts +│ └── blocks-tools.ts +├── types/ +│ └── index.ts # TypeScript type definitions +├── ui/ +│ └── react-app/ # 14 interactive MCP apps +├── server.ts # MCP server implementation +└── main.ts # Entry point +``` + +## Development + +```bash +# Build +npm run build + +# Watch mode +npm run dev + +# Run server +npm start +``` + +## Error Handling + +The server includes comprehensive error handling: + +- API rate limit detection +- Network error retries +- Invalid credential detection +- Missing required parameters validation + +## Rate Limits + +Acuity Scheduling API has rate limits. The client handles: + +- Automatic pagination for large result sets +- Error responses with status codes +- Request throttling (if needed) + +## Support + +For issues or questions: + +- Acuity Scheduling API Documentation: https://developers.acuityscheduling.com/ +- MCP Documentation: https://modelcontextprotocol.io/ + +## License + +MIT + +## Version + +1.0.0 diff --git a/servers/acuity-scheduling/package.json b/servers/acuity-scheduling/package.json index 1100df5..99492d7 100644 --- a/servers/acuity-scheduling/package.json +++ b/servers/acuity-scheduling/package.json @@ -1,20 +1,34 @@ { - "name": "mcp-server-acuity-scheduling", + "name": "@mcpengine/acuity-scheduling-server", "version": "1.0.0", + "description": "MCP server for Acuity Scheduling API", "type": "module", - "main": "dist/index.js", + "main": "./dist/main.js", + "bin": { + "acuity-scheduling-mcp": "./dist/main.js" + }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "dev": "tsc --watch", + "start": "node dist/main.js", + "prepare": "npm run build" }, + "keywords": [ + "mcp", + "acuity", + "scheduling", + "appointments" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "dotenv": "^16.4.7", + "zod": "^3.24.1" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "typescript": "^5.7.2" } } diff --git a/servers/acuity-scheduling/src/clients/acuity.ts b/servers/acuity-scheduling/src/clients/acuity.ts new file mode 100644 index 0000000..f4a2067 --- /dev/null +++ b/servers/acuity-scheduling/src/clients/acuity.ts @@ -0,0 +1,362 @@ +import axios, { AxiosInstance, AxiosError } from 'axios'; +import type { + AcuityCredentials, + Appointment, + AppointmentType, + AvailabilityDate, + Client, + Calendar, + Product, + Form, + FormField, + Label, + Webhook, + Coupon, + Block, + PaginatedResponse, + ErrorResponse +} from '../types/index.js'; + +export class AcuityClient { + private client: AxiosInstance; + private credentials: AcuityCredentials; + + constructor(credentials: AcuityCredentials) { + this.credentials = credentials; + + const headers: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + + if (credentials.oauth2Token) { + headers['Authorization'] = `Bearer ${credentials.oauth2Token}`; + } + + this.client = axios.create({ + baseURL: 'https://acuityscheduling.com/api/v1', + headers, + ...(credentials.oauth2Token ? {} : { + auth: { + username: credentials.userId, + password: credentials.apiKey + } + }) + }); + + // Error interceptor + this.client.interceptors.response.use( + response => response, + (error: AxiosError) => { + if (error.response?.data) { + const err = new Error(error.response.data.message || error.response.data.error); + (err as any).statusCode = error.response.status; + throw err; + } + throw error; + } + ); + } + + // Generic paginated GET request + private async paginatedGet( + endpoint: string, + params: Record = {} + ): Promise { + const allResults: T[] = []; + let page = 0; + const limit = params.limit || 100; + + while (true) { + const response = await this.client.get(endpoint, { + params: { ...params, limit, offset: page * limit } + }); + + allResults.push(...response.data); + + if (response.data.length < limit) { + break; + } + page++; + } + + return allResults; + } + + // Appointments + async listAppointments(params?: { + minDate?: string; + maxDate?: string; + calendarID?: number; + canceled?: boolean; + }): Promise { + return this.paginatedGet('/appointments', params); + } + + async getAppointment(id: number): Promise { + const response = await this.client.get(`/appointments/${id}`); + return response.data; + } + + async createAppointment(data: { + appointmentTypeID: number; + datetime: string; + firstName: string; + lastName: string; + email: string; + phone?: string; + fields?: Array<{ id: number; value: string }>; + calendarID?: number; + certificate?: string; + }): Promise { + const response = await this.client.post('/appointments', data); + return response.data; + } + + async updateAppointment(id: number, data: Partial): Promise { + const response = await this.client.put(`/appointments/${id}`, data); + return response.data; + } + + async cancelAppointment(id: number, params?: { noEmail?: boolean }): Promise { + const response = await this.client.put(`/appointments/${id}/cancel`, null, { params }); + return response.data; + } + + async rescheduleAppointment(id: number, datetime: string): Promise { + const response = await this.client.put(`/appointments/${id}/reschedule`, { datetime }); + return response.data; + } + + async listAppointmentTypes(): Promise { + const response = await this.client.get('/appointment-types'); + return response.data; + } + + async getAppointmentType(id: number): Promise { + const response = await this.client.get(`/appointment-types/${id}`); + return response.data; + } + + // Availability + async getAvailabilityDates(params: { + appointmentTypeID: number; + month?: string; + calendarID?: number; + timezone?: string; + }): Promise { + const response = await this.client.get('/availability/dates', { params }); + return response.data; + } + + async getAvailabilityTimes(params: { + appointmentTypeID: number; + date: string; + calendarID?: number; + timezone?: string; + }): Promise<{ time: string }[]> { + const response = await this.client.get<{ time: string }[]>('/availability/times', { params }); + return response.data; + } + + async getAvailabilityClasses(params?: { + appointmentTypeID?: number; + minDate?: string; + maxDate?: string; + }): Promise { + const response = await this.client.get('/availability/classes', { params }); + return response.data; + } + + async checkAvailability(params: { + appointmentTypeID: number; + date: string; + time: string; + calendarID?: number; + }): Promise<{ available: boolean }> { + const response = await this.client.get<{ available: boolean }>('/availability/check-times', { params }); + return response.data; + } + + // Clients + async listClients(): Promise { + return this.paginatedGet('/clients'); + } + + async getClient(id: number): Promise { + const response = await this.client.get(`/clients/${id}`); + return response.data; + } + + async createClient(data: { + firstName: string; + lastName: string; + email: string; + phone?: string; + notes?: string; + }): Promise { + const response = await this.client.post('/clients', data); + return response.data; + } + + async updateClient(id: number, data: Partial): Promise { + const response = await this.client.put(`/clients/${id}`, data); + return response.data; + } + + async deleteClient(id: number): Promise { + await this.client.delete(`/clients/${id}`); + } + + // Calendars + async listCalendars(): Promise { + const response = await this.client.get('/calendars'); + return response.data; + } + + async getCalendar(id: number): Promise { + const response = await this.client.get(`/calendars/${id}`); + return response.data; + } + + async createCalendar(data: { + name: string; + email?: string; + location?: string; + timezone?: string; + }): Promise { + const response = await this.client.post('/calendars', data); + return response.data; + } + + async updateCalendar(id: number, data: Partial): Promise { + const response = await this.client.put(`/calendars/${id}`, data); + return response.data; + } + + // Products + async listProducts(type?: 'addons' | 'packages' | 'subscriptions' | 'certificates'): Promise { + const endpoint = type ? `/products/${type}` : '/products'; + const response = await this.client.get(endpoint); + return response.data; + } + + async getProduct(id: number): Promise { + const response = await this.client.get(`/products/${id}`); + return response.data; + } + + // Forms + async listForms(): Promise { + const response = await this.client.get('/forms'); + return response.data; + } + + async getFormFields(formId: number): Promise { + const response = await this.client.get(`/forms/${formId}/fields`); + return response.data; + } + + async getIntakeFormResponses(appointmentId: number): Promise { + const response = await this.client.get(`/appointments/${appointmentId}/forms`); + return response.data; + } + + // Labels + async listLabels(): Promise { + const response = await this.client.get('/labels'); + return response.data; + } + + async createLabel(data: { name: string; color?: string }): Promise