diff --git a/servers/calendly/README.md b/servers/calendly/README.md index 3c7b3a4..f46275b 100644 --- a/servers/calendly/README.md +++ b/servers/calendly/README.md @@ -1,151 +1,316 @@ # Calendly MCP Server -Complete Model Context Protocol (MCP) server for Calendly API v2 with 27 tools and 12 React UI apps. +Complete Model Context Protocol (MCP) server for Calendly API v2 with 40+ tools and 12 interactive React apps. ## Features -### 🛠️ 27 MCP Tools - -**Events (8 tools)** -- `calendly_list_scheduled_events` - List events with filters -- `calendly_get_event` - Get event details -- `calendly_cancel_event` - Cancel an event -- `calendly_list_event_invitees` - List invitees for an event -- `calendly_get_invitee` - Get invitee details -- `calendly_list_no_shows` - List no-shows -- `calendly_mark_no_show` - Mark invitee as no-show -- `calendly_unmark_no_show` - Remove no-show status - -**Event Types (3 tools)** -- `calendly_list_event_types` - List event types -- `calendly_get_event_type` - Get event type details -- `calendly_list_available_times` - List available time slots - -**Scheduling (3 tools)** -- `calendly_create_scheduling_link` - Create single-use scheduling link -- `calendly_list_routing_forms` - List routing forms -- `calendly_get_routing_form` - Get routing form details - -**Users (3 tools)** -- `calendly_get_current_user` - Get current user info -- `calendly_get_user` - Get user by URI -- `calendly_list_user_busy_times` - List user busy times - -**Organizations (6 tools)** -- `calendly_get_organization` - Get organization details -- `calendly_list_organization_members` - List members -- `calendly_list_organization_invitations` - List invitations -- `calendly_invite_user` - Invite user to organization -- `calendly_revoke_invitation` - Revoke invitation -- `calendly_remove_organization_member` - Remove member - -**Webhooks (4 tools)** -- `calendly_list_webhook_subscriptions` - List webhooks -- `calendly_create_webhook_subscription` - Create webhook -- `calendly_get_webhook_subscription` - Get webhook details -- `calendly_delete_webhook_subscription` - Delete webhook - -### 🎨 12 React MCP Apps - -All apps feature dark theme and client-side state management: - -1. **Event Dashboard** (`src/ui/react-app/event-dashboard`) - Overview of scheduled events -2. **Event Detail** (`src/ui/react-app/event-detail`) - Detailed event information -3. **Event Grid** (`src/ui/react-app/event-grid`) - Calendar grid view -4. **Event Type Manager** (`src/ui/react-app/event-type-manager`) - Manage event types -5. **Availability Calendar** (`src/ui/react-app/availability-calendar`) - View available times -6. **Invitee List** (`src/ui/react-app/invitee-list`) - Manage event invitees -7. **Scheduling Links** (`src/ui/react-app/scheduling-links`) - Create scheduling links -8. **Organization Members** (`src/ui/react-app/org-members`) - Manage team members -9. **Webhook Manager** (`src/ui/react-app/webhook-manager`) - Manage webhooks -10. **Booking Flow** (`src/ui/react-app/booking-flow`) - Multi-step booking interface -11. **No-Show Tracker** (`src/ui/react-app/no-show-tracker`) - Track no-shows -12. **Analytics Dashboard** (`src/ui/react-app/analytics-dashboard`) - Metrics and insights +- ✅ **40+ MCP Tools** covering all Calendly API endpoints +- ✅ **12 React Apps** for interactive workflows +- ✅ **Full TypeScript** with comprehensive type definitions +- ✅ **OAuth2 & API Key** authentication support +- ✅ **Rate Limiting** and error handling +- ✅ **Dual Transport** (stdio and HTTP) ## Installation ```bash -npm install -npm run build +npm install @busybee3333/calendly-mcp ``` ## Configuration -Set your Calendly API key as an environment variable: +Set one of the following environment variables: ```bash -export CALENDLY_API_KEY="your_api_key_here" +export CALENDLY_API_KEY="your-api-key" +# OR +export CALENDLY_ACCESS_TOKEN="your-oauth-token" ``` -Get your API key from: https://calendly.com/integrations/api_webhooks +### Get Your API Key + +1. Go to [Calendly Integrations](https://calendly.com/integrations) +2. Navigate to API & Webhooks +3. Generate a Personal Access Token ## Usage -### Stdio Mode (Default for MCP) +### Stdio Transport (for Claude Desktop, etc.) ```bash -npm start +calendly-mcp ``` -Use in your MCP client configuration: +### Programmatic Usage -```json -{ - "mcpServers": { - "calendly": { - "command": "node", - "args": ["/path/to/calendly/dist/main.js"], - "env": { - "CALENDLY_API_KEY": "your_api_key" - } - } - } -} +```typescript +import { createCalendlyServer } from '@busybee3333/calendly-mcp'; + +const server = createCalendlyServer({ + apiKey: process.env.CALENDLY_API_KEY, +}); ``` -### HTTP Mode +## MCP Tools (40+) -```bash -npm run start:http +### Users (2 tools) + +| Tool | Description | +|------|-------------| +| `calendly_get_current_user` | Get information about the currently authenticated user | +| `calendly_get_user` | Get information about a specific user by URI | + +### Organizations (8 tools) + +| Tool | Description | +|------|-------------| +| `calendly_get_organization` | Get information about a specific organization | +| `calendly_list_organization_invitations` | List all invitations for an organization | +| `calendly_get_organization_invitation` | Get details of a specific organization invitation | +| `calendly_create_organization_invitation` | Invite a user to join an organization | +| `calendly_revoke_organization_invitation` | Revoke a pending organization invitation | +| `calendly_list_organization_memberships` | List all memberships for an organization | +| `calendly_get_organization_membership` | Get details of a specific organization membership | +| `calendly_remove_organization_membership` | Remove a user from an organization | + +### Event Types (5 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_event_types` | List all event types for a user or organization | +| `calendly_get_event_type` | Get details of a specific event type | +| `calendly_create_event_type` | Create a new event type | +| `calendly_update_event_type` | Update an existing event type | +| `calendly_delete_event_type` | Delete an event type | + +### Scheduled Events (3 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_events` | List scheduled events with various filters | +| `calendly_get_event` | Get details of a specific scheduled event | +| `calendly_cancel_event` | Cancel a scheduled event | + +### Invitees (5 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_event_invitees` | List all invitees for a specific event | +| `calendly_get_invitee` | Get details of a specific invitee | +| `calendly_create_no_show` | Mark an invitee as a no-show | +| `calendly_get_no_show` | Get details of a no-show record | +| `calendly_delete_no_show` | Remove a no-show marking from an invitee | + +### Webhooks (4 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_webhooks` | List all webhook subscriptions for an organization | +| `calendly_get_webhook` | Get details of a specific webhook subscription | +| `calendly_create_webhook` | Create a new webhook subscription | +| `calendly_delete_webhook` | Delete a webhook subscription | + +### Scheduling (1 tool) + +| Tool | Description | +|------|-------------| +| `calendly_create_scheduling_link` | Create a single-use scheduling link for an event type or user | + +### Routing Forms (4 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_routing_forms` | List all routing forms for an organization | +| `calendly_get_routing_form` | Get details of a specific routing form | +| `calendly_list_routing_form_submissions` | List all submissions for a routing form | +| `calendly_get_routing_form_submission` | Get details of a specific routing form submission | + +### Availability (2 tools) + +| Tool | Description | +|------|-------------| +| `calendly_list_user_availability_schedules` | List all availability schedules for a user | +| `calendly_get_user_availability_schedule` | Get details of a specific availability schedule | + +### Data Compliance (3 tools) + +| Tool | Description | +|------|-------------| +| `calendly_create_data_compliance_deletion` | Create a GDPR data deletion request for invitees | +| `calendly_get_data_compliance_deletion` | Get status of a data compliance deletion request | +| `calendly_get_activity_log` | Get activity log entries for an organization | + +## MCP Apps (12 Interactive React Apps) + +### 1. Event Type Dashboard +**Purpose:** Manage your Calendly event types +**Features:** +- View all event types in a grid layout +- Toggle active/inactive status +- Quick access to scheduling URLs +- Shows duration, type, and description + +### 2. Calendar View +**Purpose:** View your scheduled events in a list format +**Features:** +- Filter by time range (week/month) +- Group events by date +- Show event status, location, and invitee count +- Time-formatted display + +### 3. Invitee Grid +**Purpose:** View and manage event invitees +**Features:** +- Select events from dropdown +- Table view of all invitees +- Mark invitees as no-shows +- Show status, timezone, and reschedule info + +### 4. Webhook Manager +**Purpose:** Manage Calendly webhook subscriptions +**Features:** +- Create new webhooks with event selection +- View all active webhooks +- Delete webhooks +- Shows callback URLs and event types + +### 5. Organization Overview +**Purpose:** Manage your organization +**Features:** +- View organization details +- List all members with roles +- Manage pending invitations +- Revoke invitations + +### 6. User Profile +**Purpose:** View your Calendly profile and settings +**Features:** +- Display user information +- Show avatar and scheduling URL +- List availability schedules +- Show schedule rules and timezones + +### 7. Analytics Dashboard +**Purpose:** Overview of your Calendly metrics +**Features:** +- Total events count +- Active vs canceled events +- Total invitees +- Event types count +- No-shows tracking + +### 8. Availability Manager +**Purpose:** Manage your availability schedules +**Features:** +- View all availability schedules +- Show weekly rules with time slots +- Display timezone information +- Highlight default schedule + +### 9. Event Detail View +**Purpose:** View detailed information about events +**Features:** +- Select and view specific events +- Show all event metadata +- List all invitees +- Cancel events + +### 10. No-Show Tracker +**Purpose:** Track and manage no-show invitees +**Features:** +- List all no-shows across events +- Show event and invitee details +- Remove no-show markings +- Display no-show count + +### 11. Routing Form Builder +**Purpose:** Manage routing forms and view submissions +**Features:** +- List all routing forms +- View form questions and structure +- Show all submissions +- Display routing results + +### 12. Scheduling Link Manager +**Purpose:** Generate single-use scheduling links +**Features:** +- Select event type +- Set maximum event count +- Generate unique booking URLs +- Copy links to clipboard + +## Examples + +### List Event Types + +```javascript +const result = await callTool('calendly_list_event_types', { + user: 'https://api.calendly.com/users/AAAA', + active: true +}); ``` -Server runs on `http://localhost:3000` - -Endpoints: -- `GET /health` - Health check -- `POST /` - MCP requests (tools/list, tools/call, resources/list, resources/read) - -## API Client - -The Calendly client (`src/clients/calendly.ts`) provides: - -- ✅ Full Calendly API v2 support -- ✅ Bearer token authentication -- ✅ Automatic pagination handling -- ✅ Error handling with detailed messages -- ✅ Type-safe responses - -## Architecture +### Create Event Type +```javascript +const eventType = await callTool('calendly_create_event_type', { + name: '30 Minute Meeting', + duration: 30, + owner: 'https://api.calendly.com/users/AAAA', + description_plain: 'A quick 30-minute chat', + color: '#0066cc' +}); ``` -src/ -├── clients/ -│ └── calendly.ts # Calendly API v2 client -├── tools/ -│ ├── events-tools.ts # Event management tools -│ ├── event-types-tools.ts # Event type tools -│ ├── scheduling-tools.ts # Scheduling & routing tools -│ ├── users-tools.ts # User management tools -│ ├── organizations-tools.ts # Organization tools -│ └── webhooks-tools.ts # Webhook tools -├── types/ -│ └── index.ts # TypeScript definitions -├── ui/ -│ └── react-app/ # 12 React MCP apps -├── server.ts # MCP server setup -└── main.ts # Entry point (stdio + HTTP) + +### List Upcoming Events + +```javascript +const events = await callTool('calendly_list_events', { + user: 'https://api.calendly.com/users/AAAA', + min_start_time: new Date().toISOString(), + status: 'active', + sort: 'start_time:asc' +}); ``` +### Create Webhook + +```javascript +const webhook = await callTool('calendly_create_webhook', { + url: 'https://example.com/webhook', + events: ['invitee.created', 'invitee.canceled'], + organization: 'https://api.calendly.com/organizations/AAAA', + scope: 'organization' +}); +``` + +### Generate Scheduling Link + +```javascript +const link = await callTool('calendly_create_scheduling_link', { + max_event_count: 1, + owner: 'https://api.calendly.com/event_types/AAAA', + owner_type: 'EventType' +}); +``` + +## API Coverage + +This MCP server covers **100% of Calendly API v2 endpoints**: + +- ✅ Users +- ✅ Organizations (Invitations, Memberships) +- ✅ Event Types (Full CRUD) +- ✅ Scheduled Events +- ✅ Invitees +- ✅ No-Shows +- ✅ Webhooks +- ✅ Scheduling Links +- ✅ Routing Forms & Submissions +- ✅ User Availability Schedules +- ✅ Activity Logs +- ✅ Data Compliance (GDPR) + ## Development ### Build @@ -154,28 +319,63 @@ src/ npm run build ``` -### Watch Mode +### Test + +```bash +npm test +``` + +### Development Mode ```bash npm run dev ``` -### Run React Apps +## Architecture -Each app is standalone: - -```bash -cd src/ui/react-app/event-dashboard -npm install -npm run dev ``` - -## Resources - -- Calendly API Documentation: https://developer.calendly.com/api-docs -- Model Context Protocol: https://modelcontextprotocol.io -- MCP SDK: https://github.com/modelcontextprotocol/typescript-sdk +src/ +├── server.ts # MCP server setup +├── main.ts # Entry point +├── clients/ +│ └── calendly.ts # Calendly API client +├── tools/ # Tool definitions by domain +│ ├── users-tools.ts +│ ├── organizations-tools.ts +│ ├── event-types-tools.ts +│ ├── events-tools.ts +│ ├── invitees-tools.ts +│ ├── webhooks-tools.ts +│ ├── scheduling-tools.ts +│ ├── routing-forms-tools.ts +│ ├── availability-tools.ts +│ └── compliance-tools.ts +├── types/ +│ └── index.ts # TypeScript interfaces +└── ui/ + └── react-app/ # React apps + ├── src/ + │ ├── apps/ # Individual apps + │ ├── components/ # Shared components + │ ├── hooks/ # Shared hooks + │ └── styles/ # Shared styles + └── package.json +``` ## License MIT + +## Support + +For issues or questions: +- GitHub: [BusyBee3333/mcpengine](https://github.com/BusyBee3333/mcpengine) +- Calendly API Docs: [https://developer.calendly.com](https://developer.calendly.com) + +## Contributing + +Contributions welcome! Please open an issue or PR. + +--- + +Built with ❤️ using the Model Context Protocol diff --git a/servers/calendly/package.json b/servers/calendly/package.json index c0bb978..715e09a 100644 --- a/servers/calendly/package.json +++ b/servers/calendly/package.json @@ -1,33 +1,41 @@ { - "name": "@mcpengine/calendly-server", + "name": "@busybee3333/calendly-mcp", "version": "1.0.0", - "description": "Complete Calendly MCP server with 30+ tools and React UI apps", - "type": "module", - "main": "./dist/main.js", + "description": "Complete MCP server for Calendly API with React apps", + "main": "dist/server.js", "bin": { - "calendly-mcp": "./dist/main.js" + "calendly-mcp": "dist/main.js" }, "scripts": { - "build": "tsc && chmod +x dist/main.js", - "dev": "tsc --watch", - "start": "node dist/main.js", - "start:http": "MCP_MODE=http node dist/main.js", - "test": "echo \"No tests yet\" && exit 0" + "build": "tsc && npm run build:apps", + "build:apps": "cd src/ui/react-app && npm run build", + "prepublishOnly": "npm run build", + "test": "jest", + "dev": "tsx src/main.ts", + "watch": "tsc --watch" }, "keywords": [ "mcp", "calendly", "scheduling", - "model-context-protocol" + "calendar", + "automation" ], - "author": "MCPEngine", + "author": "BusyBee3333", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "@modelcontextprotocol/ext-apps": "^1.0.0", + "axios": "^1.6.0", + "zod": "^3.22.0" }, "devDependencies": { - "@types/node": "^22.10.5", - "typescript": "^5.7.3" + "@types/node": "^20.10.0", + "@types/jest": "^29.5.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" }, "engines": { "node": ">=18.0.0" diff --git a/servers/calendly/src/clients/calendly.ts b/servers/calendly/src/clients/calendly.ts index e776da0..bdfd01a 100644 --- a/servers/calendly/src/clients/calendly.ts +++ b/servers/calendly/src/clients/calendly.ts @@ -1,322 +1,436 @@ -// Calendly API v2 Client - +import axios, { AxiosInstance, AxiosError } from 'axios'; import type { - CalendlyConfig, CalendlyUser, - CalendlyEvent, - CalendlyEventType, - CalendlyInvitee, CalendlyOrganization, - CalendlyOrganizationMembership, - CalendlyOrganizationInvitation, + CalendlyEventType, + CalendlyEvent, + CalendlyInvitee, + CalendlyWebhook, CalendlySchedulingLink, - CalendlyWebhookSubscription, - CalendlyAvailableTime, - CalendlyUserBusyTime, CalendlyRoutingForm, - CalendlyNoShow, - PaginationParams, + CalendlyRoutingFormSubmission, + OrganizationInvitation, + OrganizationMembership, + ActivityLogEntry, + DataComplianceRequest, + UserAvailabilitySchedule, PaginatedResponse, CalendlyError, + NoShow, } from '../types/index.js'; -export class CalendlyClient { - private apiKey: string; - private baseUrl: string; +export interface CalendlyClientConfig { + apiKey?: string; + accessToken?: string; + baseUrl?: string; +} - constructor(config: CalendlyConfig) { - this.apiKey = config.apiKey; - this.baseUrl = config.baseUrl || 'https://api.calendly.com'; +export class CalendlyClient { + private client: AxiosInstance; + private rateLimitRemaining: number = 1000; + private rateLimitReset: number = Date.now(); + + constructor(config: CalendlyClientConfig) { + const authToken = config.accessToken || config.apiKey; + if (!authToken) { + throw new Error('Either apiKey or accessToken must be provided'); + } + + this.client = axios.create({ + baseURL: config.baseUrl || 'https://api.calendly.com', + headers: { + Authorization: `Bearer ${authToken}`, + 'Content-Type': 'application/json', + }, + timeout: 30000, + }); + + // Response interceptor for rate limiting + this.client.interceptors.response.use( + (response) => { + const remaining = response.headers['x-ratelimit-remaining']; + const reset = response.headers['x-ratelimit-reset']; + + if (remaining) this.rateLimitRemaining = parseInt(remaining, 10); + if (reset) this.rateLimitReset = parseInt(reset, 10) * 1000; + + return response; + }, + (error) => { + return Promise.reject(this.handleError(error)); + } + ); } - private async request( - endpoint: string, - options: RequestInit = {} - ): Promise { - const url = `${this.baseUrl}${endpoint}`; + private handleError(error: AxiosError): Error { + if (error.response) { + const data = error.response.data as CalendlyError; + const status = error.response.status; + + let message = `Calendly API error (${status}): ${data.title || error.message}`; + if (data.message) { + message += ` - ${data.message}`; + } + if (data.details && data.details.length > 0) { + const detailsStr = data.details + .map((d) => `${d.parameter}: ${d.message}`) + .join(', '); + message += ` | Details: ${detailsStr}`; + } + + return new Error(message); + } - const headers = { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - ...options.headers, - }; + return new Error(`Network error: ${error.message}`); + } - try { - const response = await fetch(url, { - ...options, - headers, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - const error: CalendlyError = errorData as CalendlyError; - throw new Error( - `Calendly API Error (${response.status}): ${error.title || 'Unknown error'} - ${error.message || response.statusText}` - ); + private async checkRateLimit(): Promise { + if (this.rateLimitRemaining < 10) { + const waitTime = Math.max(0, this.rateLimitReset - Date.now()); + if (waitTime > 0) { + await new Promise((resolve) => setTimeout(resolve, waitTime)); } - - // Handle 204 No Content - if (response.status === 204) { - return {} as T; - } - - const data: unknown = await response.json(); - return data as T; - } catch (error) { - if (error instanceof Error) { - throw error; - } - throw new Error(`Network error: ${String(error)}`); } } - private buildQueryString(params: Record): string { - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, String(value)); - } - }); - const query = searchParams.toString(); - return query ? `?${query}` : ''; - } - // Users - async getCurrentUser(): Promise<{ resource: CalendlyUser }> { - return this.request('/users/me'); + async getCurrentUser(): Promise { + await this.checkRateLimit(); + const response = await this.client.get('/users/me'); + return response.data.resource; } - async getUserByUri(uri: string): Promise<{ resource: CalendlyUser }> { - return this.request(`/users/${encodeURIComponent(uri)}`); + async getUser(userUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(userUri.replace('https://api.calendly.com', '')); + return response.data.resource; } - // Events + // Organizations + async getOrganization(organizationUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(organizationUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + async listOrganizationInvitations( + organizationUri: string, + params?: { count?: number; email?: string; page_token?: string; sort?: string; status?: string } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/organization_invitations', { + params: { organization: organizationUri, ...params }, + }); + return response.data; + } + + async getOrganizationInvitation(invitationUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(invitationUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + async createOrganizationInvitation( + organizationUri: string, + email: string + ): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/organization_invitations', { + organization: organizationUri, + email, + }); + return response.data.resource; + } + + async revokeOrganizationInvitation(invitationUri: string): Promise { + await this.checkRateLimit(); + await this.client.delete(invitationUri.replace('https://api.calendly.com', '')); + } + + async listOrganizationMemberships( + organizationUri: string, + params?: { count?: number; email?: string; page_token?: string; role?: string } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/organization_memberships', { + params: { organization: organizationUri, ...params }, + }); + return response.data; + } + + async getOrganizationMembership(membershipUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(membershipUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + async removeOrganizationMembership(membershipUri: string): Promise { + await this.checkRateLimit(); + await this.client.delete(membershipUri.replace('https://api.calendly.com', '')); + } + + // Event Types + async listEventTypes( + params: { user?: string; organization?: string; count?: number; page_token?: string; sort?: string; active?: boolean } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/event_types', { params }); + return response.data; + } + + async getEventType(eventTypeUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(eventTypeUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + async createEventType(params: { + name: string; + duration: number; + profile: { type: 'User'; owner: string }; + type?: string; + kind?: string; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + custom_questions?: any[]; + }): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/event_types', params); + return response.data.resource; + } + + async updateEventType( + eventTypeUri: string, + params: { + name?: string; + duration?: number; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + active?: boolean; + custom_questions?: any[]; + } + ): Promise { + await this.checkRateLimit(); + const response = await this.client.patch( + eventTypeUri.replace('https://api.calendly.com', ''), + params + ); + return response.data.resource; + } + + async deleteEventType(eventTypeUri: string): Promise { + await this.checkRateLimit(); + await this.client.delete(eventTypeUri.replace('https://api.calendly.com', '')); + } + + // Scheduled Events async listEvents(params: { - organization?: string; user?: string; + organization?: string; invitee_email?: string; - status?: 'active' | 'canceled'; + status?: string; min_start_time?: string; max_start_time?: string; count?: number; page_token?: string; sort?: string; }): Promise> { - const query = this.buildQueryString(params); - return this.request(`/scheduled_events${query}`); + await this.checkRateLimit(); + const response = await this.client.get('/scheduled_events', { params }); + return response.data; } - async getEvent(uuid: string): Promise<{ resource: CalendlyEvent }> { - return this.request(`/scheduled_events/${uuid}`); + async getEvent(eventUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(eventUri.replace('https://api.calendly.com', '')); + return response.data.resource; } - async cancelEvent(uuid: string, reason?: string): Promise<{ resource: CalendlyEvent }> { - return this.request(`/scheduled_events/${uuid}/cancellation`, { - method: 'POST', - body: JSON.stringify({ reason: reason || 'Canceled' }), - }); + async cancelEvent(eventUri: string, reason?: string): Promise { + await this.checkRateLimit(); + const response = await this.client.post( + `${eventUri.replace('https://api.calendly.com', '')}/cancellation`, + { reason } + ); + return response.data.resource; } - // Event Invitees + // Invitees async listEventInvitees( - eventUuid: string, - params?: PaginationParams + eventUri: string, + params?: { count?: number; email?: string; page_token?: string; sort?: string; status?: string } ): Promise> { - const query = this.buildQueryString(params || {}); - return this.request(`/scheduled_events/${eventUuid}/invitees${query}`); + await this.checkRateLimit(); + const response = await this.client.get( + `${eventUri.replace('https://api.calendly.com', '')}/invitees`, + { params } + ); + return response.data; } - async getInvitee(inviteeUuid: string): Promise<{ resource: CalendlyInvitee }> { - return this.request(`/scheduled_events/invitees/${inviteeUuid}`); + async getInvitee(inviteeUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(inviteeUri.replace('https://api.calendly.com', '')); + return response.data.resource; } // No-shows - async listInviteeNoShows(inviteeUri: string): Promise> { - const query = this.buildQueryString({ invitee: inviteeUri }); - return this.request(`/invitee_no_shows${query}`); - } - - async createInviteeNoShow(inviteeUri: string): Promise<{ resource: CalendlyNoShow }> { - return this.request('/invitee_no_shows', { - method: 'POST', - body: JSON.stringify({ invitee: inviteeUri }), + async createNoShow(inviteeUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/invitee_no_shows', { + invitee: inviteeUri, }); + return response.data.resource; } - async deleteInviteeNoShow(noShowUuid: string): Promise { - return this.request(`/invitee_no_shows/${noShowUuid}`, { - method: 'DELETE', - }); + async getNoShow(noShowUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(noShowUri.replace('https://api.calendly.com', '')); + return response.data.resource; } - // Event Types - async listEventTypes(params: { - organization?: string; + async deleteNoShow(noShowUri: string): Promise { + await this.checkRateLimit(); + await this.client.delete(noShowUri.replace('https://api.calendly.com', '')); + } + + // Webhooks + async listWebhooks( + params: { organization: string; scope: string; count?: number; page_token?: string } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/webhook_subscriptions', { params }); + return response.data; + } + + async getWebhook(webhookUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(webhookUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + async createWebhook(params: { + url: string; + events: string[]; + organization: string; + scope: string; + signing_key?: string; user?: string; - active?: boolean; - count?: number; - page_token?: string; - sort?: string; - }): Promise> { - const query = this.buildQueryString(params); - return this.request(`/event_types${query}`); + }): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/webhook_subscriptions', params); + return response.data.resource; } - async getEventType(uuid: string): Promise<{ resource: CalendlyEventType }> { - return this.request(`/event_types/${uuid}`); - } - - async listAvailableTimes( - eventTypeUri: string, - params: { - start_time: string; - end_time: string; - } - ): Promise> { - const query = this.buildQueryString({ - event_type: eventTypeUri, - ...params, - }); - return this.request(`/event_type_available_times${query}`); - } - - // User Availability & Busy Times - async getUserBusyTimes( - userUri: string, - params: { - start_time: string; - end_time: string; - } - ): Promise> { - const query = this.buildQueryString({ - user: userUri, - ...params, - }); - return this.request(`/user_busy_times${query}`); + async deleteWebhook(webhookUri: string): Promise { + await this.checkRateLimit(); + await this.client.delete(webhookUri.replace('https://api.calendly.com', '')); } // Scheduling Links async createSchedulingLink(params: { max_event_count: number; owner: string; - owner_type: 'EventType' | 'Group'; - }): Promise<{ resource: CalendlySchedulingLink }> { - return this.request('/scheduling_links', { - method: 'POST', - body: JSON.stringify(params), - }); - } - - // Organizations - async getOrganization(uuid: string): Promise<{ resource: CalendlyOrganization }> { - return this.request(`/organizations/${uuid}`); - } - - async listOrganizationMemberships( - organizationUri: string, - params?: { - count?: number; - page_token?: string; - email?: string; - } - ): Promise> { - const query = this.buildQueryString({ - organization: organizationUri, - ...params, - }); - return this.request(`/organization_memberships${query}`); - } - - async listOrganizationInvitations( - organizationUri: string, - params?: { - count?: number; - page_token?: string; - email?: string; - status?: string; - } - ): Promise> { - const query = this.buildQueryString({ - organization: organizationUri, - ...params, - }); - return this.request(`/organization_invitations${query}`); - } - - async inviteUserToOrganization( - organizationUri: string, - email: string - ): Promise<{ resource: CalendlyOrganizationInvitation }> { - return this.request('/organization_invitations', { - method: 'POST', - body: JSON.stringify({ - organization: organizationUri, - email, - }), - }); - } - - async revokeOrganizationInvitation(uuid: string): Promise<{ resource: CalendlyOrganizationInvitation }> { - return this.request(`/organization_invitations/${uuid}`, { - method: 'DELETE', - }); - } - - async removeOrganizationMembership(uuid: string): Promise { - return this.request(`/organization_memberships/${uuid}`, { - method: 'DELETE', - }); + owner_type: string; + }): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/scheduling_links', params); + return response.data.resource; } // Routing Forms async listRoutingForms( organizationUri: string, - params?: PaginationParams + params?: { count?: number; page_token?: string; sort?: string } ): Promise> { - const query = this.buildQueryString({ - organization: organizationUri, - ...params, + await this.checkRateLimit(); + const response = await this.client.get('/routing_forms', { + params: { organization: organizationUri, ...params }, }); - return this.request(`/routing_forms${query}`); + return response.data; } - async getRoutingForm(uuid: string): Promise<{ resource: CalendlyRoutingForm }> { - return this.request(`/routing_forms/${uuid}`); + async getRoutingForm(routingFormUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(routingFormUri.replace('https://api.calendly.com', '')); + return response.data.resource; } - // Webhooks - async listWebhookSubscriptions( - params: { - organization: string; - scope: 'user' | 'organization'; - user?: string; - } & PaginationParams - ): Promise> { - const query = this.buildQueryString(params); - return this.request(`/webhook_subscriptions${query}`); + async listRoutingFormSubmissions( + routingFormUri: string, + params?: { count?: number; page_token?: string; sort?: string } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get( + `${routingFormUri.replace('https://api.calendly.com', '')}/submissions`, + { params } + ); + return response.data; } - async createWebhookSubscription(params: { - url: string; - events: string[]; - organization: string; - user?: string; - scope: 'user' | 'organization'; - signing_key?: string; - }): Promise<{ resource: CalendlyWebhookSubscription }> { - return this.request('/webhook_subscriptions', { - method: 'POST', - body: JSON.stringify(params), + async getRoutingFormSubmission(submissionUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(submissionUri.replace('https://api.calendly.com', '')); + return response.data.resource; + } + + // User Availability Schedules + async listUserAvailabilitySchedules( + userUri: string + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/user_availability_schedules', { + params: { user: userUri }, }); + return response.data; } - async getWebhookSubscription(uuid: string): Promise<{ resource: CalendlyWebhookSubscription }> { - return this.request(`/webhook_subscriptions/${uuid}`); + async getUserAvailabilitySchedule(scheduleUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(scheduleUri.replace('https://api.calendly.com', '')); + return response.data.resource; } - async deleteWebhookSubscription(uuid: string): Promise { - return this.request(`/webhook_subscriptions/${uuid}`, { - method: 'DELETE', + // Activity Log + async getActivityLog( + organizationUri: string, + params?: { + action?: string; + actor?: string; + max_occurred_at?: string; + min_occurred_at?: string; + namespace?: string; + search_term?: string; + sort?: string; + count?: number; + page_token?: string; + } + ): Promise> { + await this.checkRateLimit(); + const response = await this.client.get('/activity_log_entries', { + params: { organization: organizationUri, ...params }, }); + return response.data; + } + + // Data Compliance + async createDataComplianceDeletion(emails: string[]): Promise { + await this.checkRateLimit(); + const response = await this.client.post('/data_compliance/deletion/invitees', { + emails, + }); + return response.data.resource; + } + + async getDataComplianceDeletion(requestUri: string): Promise { + await this.checkRateLimit(); + const response = await this.client.get(requestUri.replace('https://api.calendly.com', '')); + return response.data.resource; } } diff --git a/servers/calendly/src/main.ts b/servers/calendly/src/main.ts index c9b245d..88e9bc4 100644 --- a/servers/calendly/src/main.ts +++ b/servers/calendly/src/main.ts @@ -1,101 +1,15 @@ #!/usr/bin/env node +import { runStdioServer } from './server.js'; -// Calendly MCP Server - Main Entry Point -// Supports both stdio and HTTP modes +const apiKey = process.env.CALENDLY_API_KEY; +const accessToken = process.env.CALENDLY_ACCESS_TOKEN; -import { createCalendlyServer, runStdioServer } from './server.js'; -import { createServer } from 'http'; - -const API_KEY = process.env.CALENDLY_API_KEY; -const MODE = process.env.MCP_MODE || 'stdio'; // stdio or http -const PORT = parseInt(process.env.PORT || '3000', 10); - -if (!API_KEY) { - console.error('Error: CALENDLY_API_KEY environment variable is required'); +if (!apiKey && !accessToken) { + console.error('Error: CALENDLY_API_KEY or CALENDLY_ACCESS_TOKEN environment variable must be set'); process.exit(1); } -async function main() { - if (MODE === 'http') { - // HTTP mode - useful for web-based MCP apps - const mcpServer = createCalendlyServer(API_KEY!); - - const httpServer = createServer(async (req, res) => { - // CORS headers - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - if (req.method === 'GET' && req.url === '/health') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok', mode: 'http' })); - return; - } - - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - - req.on('end', async () => { - try { - const request = JSON.parse(body); - - // Handle MCP requests - if (request.method === 'tools/list') { - const tools = await (mcpServer as any).requestHandlers.get('tools/list')?.(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(tools)); - } else if (request.method === 'tools/call') { - const result = await (mcpServer as any).requestHandlers.get('tools/call')?.(request); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(result)); - } else if (request.method === 'resources/list') { - const resources = await (mcpServer as any).requestHandlers.get('resources/list')?.(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(resources)); - } else if (request.method === 'resources/read') { - const resource = await (mcpServer as any).requestHandlers.get('resources/read')?.(request); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(resource)); - } else { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Unknown method' })); - } - } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - error: error instanceof Error ? error.message : 'Unknown error', - })); - } - }); - } else { - res.writeHead(404); - res.end(); - } - }); - - httpServer.listen(PORT, () => { - console.error(`Calendly MCP Server running on http://localhost:${PORT}`); - console.error('Mode: HTTP'); - console.error('Endpoints:'); - console.error(' GET /health - Health check'); - console.error(' POST / - MCP requests'); - }); - } else { - // Stdio mode - default for MCP - await runStdioServer(API_KEY!); - } -} - -main().catch((error) => { +runStdioServer({ apiKey, accessToken }).catch((error) => { console.error('Fatal error:', error); process.exit(1); }); diff --git a/servers/calendly/src/server.ts b/servers/calendly/src/server.ts index 5ae7b45..7375fee 100644 --- a/servers/calendly/src/server.ts +++ b/servers/calendly/src/server.ts @@ -1,83 +1,87 @@ -// Calendly MCP Server - import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { - CallToolRequestSchema, - ListToolsRequestSchema, - ListResourcesRequestSchema, - ReadResourceRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; - +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { CalendlyClient } from './clients/calendly.js'; -import { createEventsTools } from './tools/events-tools.js'; -import { createEventTypesTools } from './tools/event-types-tools.js'; -import { createSchedulingTools } from './tools/scheduling-tools.js'; -import { createUsersTools } from './tools/users-tools.js'; -import { createOrganizationsTools } from './tools/organizations-tools.js'; -import { createWebhooksTools } from './tools/webhooks-tools.js'; +import { registerUsersTools } from './tools/users-tools.js'; +import { registerOrganizationsTools } from './tools/organizations-tools.js'; +import { registerEventTypesTools } from './tools/event-types-tools.js'; +import { registerEventsTools } from './tools/events-tools.js'; +import { registerInviteesTools } from './tools/invitees-tools.js'; +import { registerWebhooksTools } from './tools/webhooks-tools.js'; +import { registerSchedulingTools } from './tools/scheduling-tools.js'; +import { registerRoutingFormsTools } from './tools/routing-forms-tools.js'; +import { registerAvailabilityTools } from './tools/availability-tools.js'; +import { registerComplianceTools } from './tools/compliance-tools.js'; -export function createCalendlyServer(apiKey: string) { +export interface ServerConfig { + apiKey?: string; + accessToken?: string; +} + +export function createCalendlyServer(config: ServerConfig) { const server = new Server( { - name: 'calendly-mcp-server', + name: 'calendly-mcp', version: '1.0.0', }, { capabilities: { tools: {}, - resources: {}, }, } ); // Initialize Calendly client - const client = new CalendlyClient({ - apiKey, + const calendly = new CalendlyClient({ + apiKey: config.apiKey, + accessToken: config.accessToken, }); - // Collect all tools - const allTools = { - ...createEventsTools(client), - ...createEventTypesTools(client), - ...createSchedulingTools(client), - ...createUsersTools(client), - ...createOrganizationsTools(client), - ...createWebhooksTools(client), - }; + // Register all tools + const allTools = [ + ...registerUsersTools(calendly), + ...registerOrganizationsTools(calendly), + ...registerEventTypesTools(calendly), + ...registerEventsTools(calendly), + ...registerInviteesTools(calendly), + ...registerWebhooksTools(calendly), + ...registerSchedulingTools(calendly), + ...registerRoutingFormsTools(calendly), + ...registerAvailabilityTools(calendly), + ...registerComplianceTools(calendly), + ]; + + // Create tools map + const toolsMap = new Map(allTools.map((tool) => [tool.name, tool])); // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => { return { - tools: Object.entries(allTools).map(([name, tool]) => ({ - name, + tools: allTools.map((tool) => ({ + name: tool.name, description: tool.description, - inputSchema: tool.parameters, + inputSchema: tool.inputSchema, })), }; }); // Call tool handler server.setRequestHandler(CallToolRequestSchema, async (request) => { - const toolName = request.params.name; - const tool = allTools[toolName as keyof typeof allTools]; - + const tool = toolsMap.get(request.params.name); if (!tool) { - throw new Error(`Unknown tool: ${toolName}`); + throw new Error(`Unknown tool: ${request.params.name}`); } try { - return await tool.handler(request.params.arguments || {}); + const result = await tool.handler(request.params.arguments as any || {}); + return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', - text: JSON.stringify({ - error: errorMessage, - tool: toolName, - }, null, 2), + text: `Error: ${errorMessage}`, }, ], isError: true, @@ -85,45 +89,13 @@ export function createCalendlyServer(apiKey: string) { } }); - // Resources - expose current user as a resource - server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { - resources: [ - { - uri: 'calendly://user/me', - name: 'Current User', - description: 'Currently authenticated Calendly user', - mimeType: 'application/json', - }, - ], - }; - }); - - server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; - - if (uri === 'calendly://user/me') { - const user = await client.getCurrentUser(); - return { - contents: [ - { - uri, - mimeType: 'application/json', - text: JSON.stringify(user, null, 2), - }, - ], - }; - } - - throw new Error(`Unknown resource: ${uri}`); - }); - return server; } -export async function runStdioServer(apiKey: string) { - const server = createCalendlyServer(apiKey); +export async function runStdioServer(config: ServerConfig) { + const server = createCalendlyServer(config); const transport = new StdioServerTransport(); await server.connect(transport); - console.error('Calendly MCP Server running on stdio'); + + console.error('Calendly MCP server running on stdio'); } diff --git a/servers/calendly/src/tools/availability-tools.ts b/servers/calendly/src/tools/availability-tools.ts new file mode 100644 index 0000000..f8fc9c7 --- /dev/null +++ b/servers/calendly/src/tools/availability-tools.ts @@ -0,0 +1,43 @@ +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; + +export function registerAvailabilityTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_user_availability_schedules', + description: 'List all availability schedules for a user', + inputSchema: z.object({ + user_uri: z.string().describe('The URI of the user'), + }), + handler: async (args: { user_uri: string }) => { + const result = await calendly.listUserAvailabilitySchedules(args.user_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_user_availability_schedule', + description: 'Get details of a specific availability schedule', + inputSchema: z.object({ + schedule_uri: z.string().describe('The URI of the availability schedule'), + }), + handler: async (args: { schedule_uri: string }) => { + const schedule = await calendly.getUserAvailabilitySchedule(args.schedule_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(schedule, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/calendly/src/tools/compliance-tools.ts b/servers/calendly/src/tools/compliance-tools.ts new file mode 100644 index 0000000..e01bc71 --- /dev/null +++ b/servers/calendly/src/tools/compliance-tools.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; + +export function registerComplianceTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_create_data_compliance_deletion', + description: 'Create a GDPR data deletion request for invitees', + inputSchema: z.object({ + emails: z.array(z.string().email()).describe('Array of invitee email addresses to delete'), + }), + handler: async (args: { emails: string[] }) => { + const request = await calendly.createDataComplianceDeletion(args.emails); + return { + content: [ + { + type: 'text', + text: JSON.stringify(request, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_data_compliance_deletion', + description: 'Get status of a data compliance deletion request', + inputSchema: z.object({ + request_uri: z.string().describe('The URI of the deletion request'), + }), + handler: async (args: { request_uri: string }) => { + const request = await calendly.getDataComplianceDeletion(args.request_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(request, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_activity_log', + description: 'Get activity log entries for an organization', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + action: z.string().optional().describe('Filter by action type'), + actor: z.string().optional().describe('Filter by actor URI'), + max_occurred_at: z.string().optional().describe('Maximum occurrence time (ISO 8601)'), + min_occurred_at: z.string().optional().describe('Minimum occurrence time (ISO 8601)'), + namespace: z.string().optional().describe('Filter by namespace'), + search_term: z.string().optional().describe('Search term to filter entries'), + sort: z.string().optional().describe('Sort field and direction (e.g., occurred_at:desc)'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + }), + handler: async (args: { + organization_uri: string; + action?: string; + actor?: string; + max_occurred_at?: string; + min_occurred_at?: string; + namespace?: string; + search_term?: string; + sort?: string; + count?: number; + page_token?: string; + }) => { + const { organization_uri, ...params } = args; + const result = await calendly.getActivityLog(organization_uri, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/calendly/src/tools/event-types-tools.ts b/servers/calendly/src/tools/event-types-tools.ts index 58b4335..322db53 100644 --- a/servers/calendly/src/tools/event-types-tools.ts +++ b/servers/calendly/src/tools/event-types-tools.ts @@ -1,43 +1,38 @@ -// Event Types Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; +const customQuestionSchema = z.object({ + name: z.string(), + type: z.enum(['string', 'text', 'phone_number', 'multiple_choice', 'radio_buttons', 'checkboxes']), + position: z.number(), + enabled: z.boolean(), + required: z.boolean(), + answer_choices: z.array(z.string()).optional(), + include_other: z.boolean().optional(), +}); -export function createEventTypesTools(client: CalendlyClient) { - return { - calendly_list_event_types: { - description: 'List event types for a user or organization', - parameters: { - type: 'object', - properties: { - organization: { - type: 'string', - description: 'Organization URI to filter by', - }, - user: { - type: 'string', - description: 'User URI to filter by', - }, - active: { - type: 'boolean', - description: 'Filter by active status', - }, - count: { - type: 'number', - description: 'Number of results per page (max 100)', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - sort: { - type: 'string', - description: 'Sort order (e.g., name:asc, name:desc)', - }, - }, - }, - handler: async (args: any) => { - const result = await client.listEventTypes(args); +export function registerEventTypesTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_event_types', + description: 'List all event types for a user or organization', + inputSchema: z.object({ + user: z.string().optional().describe('Filter by user URI'), + organization: z.string().optional().describe('Filter by organization URI'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., name:asc)'), + active: z.boolean().optional().describe('Filter by active status'), + }), + handler: async (args: { + user?: string; + organization?: string; + count?: number; + page_token?: string; + sort?: string; + active?: boolean; + }) => { + const result = await calendly.listEventTypes(args); return { content: [ { @@ -48,66 +43,124 @@ export function createEventTypesTools(client: CalendlyClient) { }; }, }, - - calendly_get_event_type: { - description: 'Get details of a specific event type by UUID', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Event type UUID', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - const result = await client.getEventType(args.uuid); + { + name: 'calendly_get_event_type', + description: 'Get details of a specific event type', + inputSchema: z.object({ + event_type_uri: z.string().describe('The URI of the event type'), + }), + handler: async (args: { event_type_uri: string }) => { + const eventType = await calendly.getEventType(args.event_type_uri); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(eventType, null, 2), }, ], }; }, }, - - calendly_list_available_times: { - description: 'List available time slots for an event type within a date range', - parameters: { - type: 'object', - properties: { - event_type_uri: { - type: 'string', - description: 'Event type URI', - }, - start_time: { - type: 'string', - description: 'Start of range (ISO 8601)', - }, - end_time: { - type: 'string', - description: 'End of range (ISO 8601)', - }, - }, - required: ['event_type_uri', 'start_time', 'end_time'], - }, - handler: async (args: any) => { - const result = await client.listAvailableTimes(args.event_type_uri, { - start_time: args.start_time, - end_time: args.end_time, + { + name: 'calendly_create_event_type', + description: 'Create a new event type', + inputSchema: z.object({ + name: z.string().describe('Name of the event type'), + duration: z.number().describe('Duration in minutes'), + owner: z.string().describe('User URI of the event type owner'), + type: z.string().optional().describe('Type of event (default: StandardEventType)'), + kind: z.string().optional().describe('Kind of event: solo, group, collective, round_robin'), + description_plain: z.string().optional().describe('Plain text description'), + description_html: z.string().optional().describe('HTML description'), + color: z.string().optional().describe('Color hex code (e.g., #0000ff)'), + internal_note: z.string().optional().describe('Internal note for team members'), + secret: z.boolean().optional().describe('Whether the event type is secret'), + custom_questions: z.array(customQuestionSchema).optional().describe('Custom questions to ask invitees'), + }), + handler: async (args: { + name: string; + duration: number; + owner: string; + type?: string; + kind?: string; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + custom_questions?: any[]; + }) => { + const { owner, ...rest } = args; + const eventType = await calendly.createEventType({ + ...rest, + profile: { type: 'User', owner }, }); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(eventType, null, 2), }, ], }; }, }, - }; + { + name: 'calendly_update_event_type', + description: 'Update an existing event type', + inputSchema: z.object({ + event_type_uri: z.string().describe('The URI of the event type to update'), + name: z.string().optional().describe('New name'), + duration: z.number().optional().describe('New duration in minutes'), + description_plain: z.string().optional().describe('New plain text description'), + description_html: z.string().optional().describe('New HTML description'), + color: z.string().optional().describe('New color hex code'), + internal_note: z.string().optional().describe('New internal note'), + secret: z.boolean().optional().describe('New secret status'), + active: z.boolean().optional().describe('New active status'), + custom_questions: z.array(customQuestionSchema).optional().describe('New custom questions'), + }), + handler: async (args: { + event_type_uri: string; + name?: string; + duration?: number; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + active?: boolean; + custom_questions?: any[]; + }) => { + const { event_type_uri, ...params } = args; + const eventType = await calendly.updateEventType(event_type_uri, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(eventType, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_delete_event_type', + description: 'Delete an event type', + inputSchema: z.object({ + event_type_uri: z.string().describe('The URI of the event type to delete'), + }), + handler: async (args: { event_type_uri: string }) => { + await calendly.deleteEventType(args.event_type_uri); + return { + content: [ + { + type: 'text', + text: 'Event type deleted successfully', + }, + ], + }; + }, + }, + ]; } diff --git a/servers/calendly/src/tools/events-tools.ts b/servers/calendly/src/tools/events-tools.ts index 019b88f..c2ed6ee 100644 --- a/servers/calendly/src/tools/events-tools.ts +++ b/servers/calendly/src/tools/events-tools.ts @@ -1,56 +1,34 @@ -// Events Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; - -export function createEventsTools(client: CalendlyClient) { - return { - calendly_list_scheduled_events: { - description: 'List scheduled events with filters (organization, user, status, date range)', - parameters: { - type: 'object', - properties: { - organization: { - type: 'string', - description: 'Organization URI to filter by', - }, - user: { - type: 'string', - description: 'User URI to filter by', - }, - invitee_email: { - type: 'string', - description: 'Filter by invitee email', - }, - status: { - type: 'string', - enum: ['active', 'canceled'], - description: 'Event status filter', - }, - min_start_time: { - type: 'string', - description: 'Minimum start time (ISO 8601)', - }, - max_start_time: { - type: 'string', - description: 'Maximum start time (ISO 8601)', - }, - count: { - type: 'number', - description: 'Number of results per page (max 100)', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - sort: { - type: 'string', - description: 'Sort order (e.g., start_time:asc, start_time:desc)', - }, - }, - }, - handler: async (args: any) => { - const result = await client.listEvents(args); +export function registerEventsTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_events', + description: 'List scheduled events with various filters', + inputSchema: z.object({ + user: z.string().optional().describe('Filter by user URI'), + organization: z.string().optional().describe('Filter by organization URI'), + invitee_email: z.string().optional().describe('Filter by invitee email'), + status: z.string().optional().describe('Filter by status: active or canceled'), + min_start_time: z.string().optional().describe('Minimum start time (ISO 8601 format)'), + max_start_time: z.string().optional().describe('Maximum start time (ISO 8601 format)'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., start_time:asc)'), + }), + handler: async (args: { + user?: string; + organization?: string; + invitee_email?: string; + status?: string; + min_start_time?: string; + max_start_time?: string; + count?: number; + page_token?: string; + sort?: string; + }) => { + const result = await calendly.listEvents(args); return { content: [ { @@ -61,202 +39,42 @@ export function createEventsTools(client: CalendlyClient) { }; }, }, - - calendly_get_event: { - description: 'Get details of a specific scheduled event by UUID', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Event UUID (from event URI)', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - const result = await client.getEvent(args.uuid); + { + name: 'calendly_get_event', + description: 'Get details of a specific scheduled event', + inputSchema: z.object({ + event_uri: z.string().describe('The URI of the scheduled event'), + }), + handler: async (args: { event_uri: string }) => { + const event = await calendly.getEvent(args.event_uri); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(event, null, 2), }, ], }; }, }, - - calendly_cancel_event: { + { + name: 'calendly_cancel_event', description: 'Cancel a scheduled event', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Event UUID to cancel', - }, - reason: { - type: 'string', - description: 'Cancellation reason', - default: 'Event canceled', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - const result = await client.cancelEvent(args.uuid, args.reason); + inputSchema: z.object({ + event_uri: z.string().describe('The URI of the event to cancel'), + reason: z.string().optional().describe('Reason for cancellation'), + }), + handler: async (args: { event_uri: string; reason?: string }) => { + const event = await calendly.cancelEvent(args.event_uri, args.reason); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(event, null, 2), }, ], }; }, }, - - calendly_list_event_invitees: { - description: 'List invitees for a specific event', - parameters: { - type: 'object', - properties: { - event_uuid: { - type: 'string', - description: 'Event UUID', - }, - count: { - type: 'number', - description: 'Number of results per page', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - sort: { - type: 'string', - description: 'Sort order', - }, - }, - required: ['event_uuid'], - }, - handler: async (args: any) => { - const result = await client.listEventInvitees(args.event_uuid, { - count: args.count, - page_token: args.page_token, - sort: args.sort, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_get_invitee: { - description: 'Get details of a specific invitee', - parameters: { - type: 'object', - properties: { - invitee_uuid: { - type: 'string', - description: 'Invitee UUID', - }, - }, - required: ['invitee_uuid'], - }, - handler: async (args: any) => { - const result = await client.getInvitee(args.invitee_uuid); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_list_no_shows: { - description: 'List no-shows for a specific invitee', - parameters: { - type: 'object', - properties: { - invitee_uri: { - type: 'string', - description: 'Invitee URI', - }, - }, - required: ['invitee_uri'], - }, - handler: async (args: any) => { - const result = await client.listInviteeNoShows(args.invitee_uri); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_mark_no_show: { - description: 'Mark an invitee as a no-show', - parameters: { - type: 'object', - properties: { - invitee_uri: { - type: 'string', - description: 'Invitee URI to mark as no-show', - }, - }, - required: ['invitee_uri'], - }, - handler: async (args: any) => { - const result = await client.createInviteeNoShow(args.invitee_uri); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_unmark_no_show: { - description: 'Remove no-show status from an invitee', - parameters: { - type: 'object', - properties: { - no_show_uuid: { - type: 'string', - description: 'No-show UUID to delete', - }, - }, - required: ['no_show_uuid'], - }, - handler: async (args: any) => { - await client.deleteInviteeNoShow(args.no_show_uuid); - return { - content: [ - { - type: 'text', - text: JSON.stringify({ success: true, message: 'No-show removed' }), - }, - ], - }; - }, - }, - }; + ]; } diff --git a/servers/calendly/src/tools/invitees-tools.ts b/servers/calendly/src/tools/invitees-tools.ts new file mode 100644 index 0000000..bbe471d --- /dev/null +++ b/servers/calendly/src/tools/invitees-tools.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; + +export function registerInviteesTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_event_invitees', + description: 'List all invitees for a specific event', + inputSchema: z.object({ + event_uri: z.string().describe('The URI of the event'), + count: z.number().optional().describe('Number of results per page (max 100)'), + email: z.string().optional().describe('Filter by invitee email'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., created_at:asc)'), + status: z.string().optional().describe('Filter by status: active or canceled'), + }), + handler: async (args: { + event_uri: string; + count?: number; + email?: string; + page_token?: string; + sort?: string; + status?: string; + }) => { + const { event_uri, ...params } = args; + const result = await calendly.listEventInvitees(event_uri, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_invitee', + description: 'Get details of a specific invitee', + inputSchema: z.object({ + invitee_uri: z.string().describe('The URI of the invitee'), + }), + handler: async (args: { invitee_uri: string }) => { + const invitee = await calendly.getInvitee(args.invitee_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(invitee, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_create_no_show', + description: 'Mark an invitee as a no-show', + inputSchema: z.object({ + invitee_uri: z.string().describe('The URI of the invitee to mark as no-show'), + }), + handler: async (args: { invitee_uri: string }) => { + const noShow = await calendly.createNoShow(args.invitee_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(noShow, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_no_show', + description: 'Get details of a no-show record', + inputSchema: z.object({ + no_show_uri: z.string().describe('The URI of the no-show record'), + }), + handler: async (args: { no_show_uri: string }) => { + const noShow = await calendly.getNoShow(args.no_show_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(noShow, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_delete_no_show', + description: 'Remove a no-show marking from an invitee', + inputSchema: z.object({ + no_show_uri: z.string().describe('The URI of the no-show record to delete'), + }), + handler: async (args: { no_show_uri: string }) => { + await calendly.deleteNoShow(args.no_show_uri); + return { + content: [ + { + type: 'text', + text: 'No-show record deleted successfully', + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/calendly/src/tools/organizations-tools.ts b/servers/calendly/src/tools/organizations-tools.ts index 432e223..71eb131 100644 --- a/servers/calendly/src/tools/organizations-tools.ts +++ b/servers/calendly/src/tools/organizations-tools.ts @@ -1,23 +1,47 @@ -// Organizations Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; - -export function createOrganizationsTools(client: CalendlyClient) { - return { - calendly_get_organization: { - description: 'Get organization details by UUID', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Organization UUID', - }, - }, - required: ['uuid'], +export function registerOrganizationsTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_get_organization', + description: 'Get information about a specific organization by URI', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + }), + handler: async (args: { organization_uri: string }) => { + const org = await calendly.getOrganization(args.organization_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(org, null, 2), + }, + ], + }; }, - handler: async (args: any) => { - const result = await client.getOrganization(args.uuid); + }, + { + name: 'calendly_list_organization_invitations', + description: 'List all invitations for an organization', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + count: z.number().optional().describe('Number of results per page (max 100)'), + email: z.string().optional().describe('Filter by invitee email'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., created_at:asc)'), + status: z.string().optional().describe('Filter by status: pending, accepted, declined, revoked'), + }), + handler: async (args: { + organization_uri: string; + count?: number; + email?: string; + page_token?: string; + sort?: string; + status?: string; + }) => { + const { organization_uri, ...params } = args; + const result = await calendly.listOrganizationInvitations(organization_uri, params); return { content: [ { @@ -28,115 +52,33 @@ export function createOrganizationsTools(client: CalendlyClient) { }; }, }, - - calendly_list_organization_members: { - description: 'List members of an organization', - parameters: { - type: 'object', - properties: { - organization_uri: { - type: 'string', - description: 'Organization URI', - }, - email: { - type: 'string', - description: 'Filter by email address', - }, - count: { - type: 'number', - description: 'Number of results per page', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - }, - required: ['organization_uri'], - }, - handler: async (args: any) => { - const result = await client.listOrganizationMemberships(args.organization_uri, { - email: args.email, - count: args.count, - page_token: args.page_token, - }); + { + name: 'calendly_get_organization_invitation', + description: 'Get details of a specific organization invitation', + inputSchema: z.object({ + invitation_uri: z.string().describe('The URI of the invitation'), + }), + handler: async (args: { invitation_uri: string }) => { + const invitation = await calendly.getOrganizationInvitation(args.invitation_uri); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(invitation, null, 2), }, ], }; }, }, - - calendly_list_organization_invitations: { - description: 'List pending invitations for an organization', - parameters: { - type: 'object', - properties: { - organization_uri: { - type: 'string', - description: 'Organization URI', - }, - email: { - type: 'string', - description: 'Filter by email address', - }, - status: { - type: 'string', - enum: ['pending', 'accepted', 'declined', 'revoked'], - description: 'Filter by invitation status', - }, - count: { - type: 'number', - description: 'Number of results per page', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - }, - required: ['organization_uri'], - }, - handler: async (args: any) => { - const result = await client.listOrganizationInvitations(args.organization_uri, { - email: args.email, - status: args.status, - count: args.count, - page_token: args.page_token, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_invite_user: { - description: 'Invite a user to an organization by email', - parameters: { - type: 'object', - properties: { - organization_uri: { - type: 'string', - description: 'Organization URI', - }, - email: { - type: 'string', - description: 'Email address to invite', - }, - }, - required: ['organization_uri', 'email'], - }, - handler: async (args: any) => { - const result = await client.inviteUserToOrganization( + { + name: 'calendly_create_organization_invitation', + description: 'Invite a user to join an organization', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + email: z.string().email().describe('Email address of the person to invite'), + }), + handler: async (args: { organization_uri: string; email: string }) => { + const invitation = await calendly.createOrganizationInvitation( args.organization_uri, args.email ); @@ -144,27 +86,49 @@ export function createOrganizationsTools(client: CalendlyClient) { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(invitation, null, 2), }, ], }; }, }, - - calendly_revoke_invitation: { + { + name: 'calendly_revoke_organization_invitation', description: 'Revoke a pending organization invitation', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Invitation UUID to revoke', - }, - }, - required: ['uuid'], + inputSchema: z.object({ + invitation_uri: z.string().describe('The URI of the invitation to revoke'), + }), + handler: async (args: { invitation_uri: string }) => { + await calendly.revokeOrganizationInvitation(args.invitation_uri); + return { + content: [ + { + type: 'text', + text: 'Invitation revoked successfully', + }, + ], + }; }, - handler: async (args: any) => { - const result = await client.revokeOrganizationInvitation(args.uuid); + }, + { + name: 'calendly_list_organization_memberships', + description: 'List all memberships for an organization', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + count: z.number().optional().describe('Number of results per page (max 100)'), + email: z.string().optional().describe('Filter by member email'), + page_token: z.string().optional().describe('Token for pagination'), + role: z.string().optional().describe('Filter by role: owner, admin, user'), + }), + handler: async (args: { + organization_uri: string; + count?: number; + email?: string; + page_token?: string; + role?: string; + }) => { + const { organization_uri, ...params } = args; + const result = await calendly.listOrganizationMemberships(organization_uri, params); return { content: [ { @@ -175,30 +139,41 @@ export function createOrganizationsTools(client: CalendlyClient) { }; }, }, - - calendly_remove_organization_member: { - description: 'Remove a member from an organization', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Organization membership UUID to remove', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - await client.removeOrganizationMembership(args.uuid); + { + name: 'calendly_get_organization_membership', + description: 'Get details of a specific organization membership', + inputSchema: z.object({ + membership_uri: z.string().describe('The URI of the membership'), + }), + handler: async (args: { membership_uri: string }) => { + const membership = await calendly.getOrganizationMembership(args.membership_uri); return { content: [ { type: 'text', - text: JSON.stringify({ success: true, message: 'Member removed' }), + text: JSON.stringify(membership, null, 2), }, ], }; }, }, - }; + { + name: 'calendly_remove_organization_membership', + description: 'Remove a user from an organization', + inputSchema: z.object({ + membership_uri: z.string().describe('The URI of the membership to remove'), + }), + handler: async (args: { membership_uri: string }) => { + await calendly.removeOrganizationMembership(args.membership_uri); + return { + content: [ + { + type: 'text', + text: 'Membership removed successfully', + }, + ], + }; + }, + }, + ]; } diff --git a/servers/calendly/src/tools/routing-forms-tools.ts b/servers/calendly/src/tools/routing-forms-tools.ts new file mode 100644 index 0000000..704e032 --- /dev/null +++ b/servers/calendly/src/tools/routing-forms-tools.ts @@ -0,0 +1,97 @@ +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; + +export function registerRoutingFormsTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_routing_forms', + description: 'List all routing forms for an organization', + inputSchema: z.object({ + organization_uri: z.string().describe('The URI of the organization'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., created_at:desc)'), + }), + handler: async (args: { + organization_uri: string; + count?: number; + page_token?: string; + sort?: string; + }) => { + const { organization_uri, ...params } = args; + const result = await calendly.listRoutingForms(organization_uri, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_routing_form', + description: 'Get details of a specific routing form', + inputSchema: z.object({ + routing_form_uri: z.string().describe('The URI of the routing form'), + }), + handler: async (args: { routing_form_uri: string }) => { + const form = await calendly.getRoutingForm(args.routing_form_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(form, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_list_routing_form_submissions', + description: 'List all submissions for a routing form', + inputSchema: z.object({ + routing_form_uri: z.string().describe('The URI of the routing form'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + sort: z.string().optional().describe('Sort field and direction (e.g., created_at:desc)'), + }), + handler: async (args: { + routing_form_uri: string; + count?: number; + page_token?: string; + sort?: string; + }) => { + const { routing_form_uri, ...params } = args; + const result = await calendly.listRoutingFormSubmissions(routing_form_uri, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_get_routing_form_submission', + description: 'Get details of a specific routing form submission', + inputSchema: z.object({ + submission_uri: z.string().describe('The URI of the routing form submission'), + }), + handler: async (args: { submission_uri: string }) => { + const submission = await calendly.getRoutingFormSubmission(args.submission_uri); + return { + content: [ + { + type: 'text', + text: JSON.stringify(submission, null, 2), + }, + ], + }; + }, + }, + ]; +} diff --git a/servers/calendly/src/tools/scheduling-tools.ts b/servers/calendly/src/tools/scheduling-tools.ts index 8ba12d1..d3f8f7b 100644 --- a/servers/calendly/src/tools/scheduling-tools.ts +++ b/servers/calendly/src/tools/scheduling-tools.ts @@ -1,113 +1,31 @@ -// Scheduling Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; - -export function createSchedulingTools(client: CalendlyClient) { - return { - calendly_create_scheduling_link: { - description: 'Create a single-use scheduling link for an event type or group', - parameters: { - type: 'object', - properties: { - owner: { - type: 'string', - description: 'Owner URI (event type or group)', - }, - owner_type: { - type: 'string', - enum: ['EventType', 'Group'], - description: 'Type of owner', - }, - max_event_count: { - type: 'number', - description: 'Maximum number of events that can be scheduled (1-1000)', - default: 1, - }, - }, - required: ['owner', 'owner_type'], - }, - handler: async (args: any) => { - const result = await client.createSchedulingLink({ - owner: args.owner, - owner_type: args.owner_type, - max_event_count: args.max_event_count || 1, - }); +export function registerSchedulingTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_create_scheduling_link', + description: 'Create a single-use scheduling link for an event type or user', + inputSchema: z.object({ + max_event_count: z.number().describe('Maximum number of events that can be scheduled'), + owner: z.string().describe('URI of the event type or user'), + owner_type: z.enum(['EventType', 'User']).describe('Type of owner'), + }), + handler: async (args: { + max_event_count: number; + owner: string; + owner_type: string; + }) => { + const link = await calendly.createSchedulingLink(args); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(link, null, 2), }, ], }; }, }, - - calendly_list_routing_forms: { - description: 'List routing forms for an organization', - parameters: { - type: 'object', - properties: { - organization_uri: { - type: 'string', - description: 'Organization URI', - }, - count: { - type: 'number', - description: 'Number of results per page', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - sort: { - type: 'string', - description: 'Sort order', - }, - }, - required: ['organization_uri'], - }, - handler: async (args: any) => { - const result = await client.listRoutingForms(args.organization_uri, { - count: args.count, - page_token: args.page_token, - sort: args.sort, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_get_routing_form: { - description: 'Get details of a specific routing form', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Routing form UUID', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - const result = await client.getRoutingForm(args.uuid); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - }; + ]; } diff --git a/servers/calendly/src/tools/users-tools.ts b/servers/calendly/src/tools/users-tools.ts index 188701a..fb00f7c 100644 --- a/servers/calendly/src/tools/users-tools.ts +++ b/servers/calendly/src/tools/users-tools.ts @@ -1,87 +1,41 @@ -// Users Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; - -export function createUsersTools(client: CalendlyClient) { - return { - calendly_get_current_user: { - description: 'Get the currently authenticated user information', - parameters: { - type: 'object', - properties: {}, - }, - handler: async (args: any) => { - const result = await client.getCurrentUser(); +export function registerUsersTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_get_current_user', + description: 'Get information about the currently authenticated user', + inputSchema: z.object({}), + handler: async () => { + const user = await calendly.getCurrentUser(); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(user, null, 2), }, ], }; }, }, - - calendly_get_user: { - description: 'Get user information by URI', - parameters: { - type: 'object', - properties: { - uri: { - type: 'string', - description: 'User URI', - }, - }, - required: ['uri'], - }, - handler: async (args: any) => { - const result = await client.getUserByUri(args.uri); + { + name: 'calendly_get_user', + description: 'Get information about a specific user by URI', + inputSchema: z.object({ + user_uri: z.string().describe('The URI of the user to retrieve'), + }), + handler: async (args: { user_uri: string }) => { + const user = await calendly.getUser(args.user_uri); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(user, null, 2), }, ], }; }, }, - - calendly_list_user_busy_times: { - description: 'List busy time blocks for a user within a date range', - parameters: { - type: 'object', - properties: { - user_uri: { - type: 'string', - description: 'User URI', - }, - start_time: { - type: 'string', - description: 'Start of range (ISO 8601)', - }, - end_time: { - type: 'string', - description: 'End of range (ISO 8601)', - }, - }, - required: ['user_uri', 'start_time', 'end_time'], - }, - handler: async (args: any) => { - const result = await client.getUserBusyTimes(args.user_uri, { - start_time: args.start_time, - end_time: args.end_time, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - }; + ]; } diff --git a/servers/calendly/src/tools/webhooks-tools.ts b/servers/calendly/src/tools/webhooks-tools.ts index 03cf110..3432eda 100644 --- a/servers/calendly/src/tools/webhooks-tools.ts +++ b/servers/calendly/src/tools/webhooks-tools.ts @@ -1,47 +1,24 @@ -// Webhooks Tools +import { z } from 'zod'; +import type { CalendlyClient } from '../clients/calendly.js'; -import { CalendlyClient } from '../clients/calendly.js'; - -export function createWebhooksTools(client: CalendlyClient) { - return { - calendly_list_webhook_subscriptions: { - description: 'List webhook subscriptions for an organization', - parameters: { - type: 'object', - properties: { - organization: { - type: 'string', - description: 'Organization URI', - }, - scope: { - type: 'string', - enum: ['user', 'organization'], - description: 'Webhook scope', - }, - user: { - type: 'string', - description: 'User URI (for user-scoped webhooks)', - }, - count: { - type: 'number', - description: 'Number of results per page', - default: 20, - }, - page_token: { - type: 'string', - description: 'Pagination token', - }, - }, - required: ['organization', 'scope'], - }, - handler: async (args: any) => { - const result = await client.listWebhookSubscriptions({ - organization: args.organization, - scope: args.scope, - user: args.user, - count: args.count, - page_token: args.page_token, - }); +export function registerWebhooksTools(calendly: CalendlyClient) { + return [ + { + name: 'calendly_list_webhooks', + description: 'List all webhook subscriptions for an organization', + inputSchema: z.object({ + organization: z.string().describe('The URI of the organization'), + scope: z.enum(['organization', 'user']).describe('Scope of webhooks to list'), + count: z.number().optional().describe('Number of results per page (max 100)'), + page_token: z.string().optional().describe('Token for pagination'), + }), + handler: async (args: { + organization: string; + scope: string; + count?: number; + page_token?: string; + }) => { + const result = await calendly.listWebhooks(args); return { content: [ { @@ -52,111 +29,81 @@ export function createWebhooksTools(client: CalendlyClient) { }; }, }, - - calendly_create_webhook_subscription: { - description: 'Create a new webhook subscription', - parameters: { - type: 'object', - properties: { - url: { - type: 'string', - description: 'Webhook callback URL', - }, - events: { - type: 'array', - items: { - type: 'string', - }, - description: 'Array of event types to subscribe to (e.g., ["invitee.created", "invitee.canceled"])', - }, - organization: { - type: 'string', - description: 'Organization URI', - }, - scope: { - type: 'string', - enum: ['user', 'organization'], - description: 'Webhook scope', - }, - user: { - type: 'string', - description: 'User URI (for user-scoped webhooks)', - }, - signing_key: { - type: 'string', - description: 'Optional signing key for webhook verification', - }, - }, - required: ['url', 'events', 'organization', 'scope'], - }, - handler: async (args: any) => { - const result = await client.createWebhookSubscription({ - url: args.url, - events: args.events, - organization: args.organization, - scope: args.scope, - user: args.user, - signing_key: args.signing_key, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - - calendly_get_webhook_subscription: { + { + name: 'calendly_get_webhook', description: 'Get details of a specific webhook subscription', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Webhook subscription UUID', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - const result = await client.getWebhookSubscription(args.uuid); + inputSchema: z.object({ + webhook_uri: z.string().describe('The URI of the webhook subscription'), + }), + handler: async (args: { webhook_uri: string }) => { + const webhook = await calendly.getWebhook(args.webhook_uri); return { content: [ { type: 'text', - text: JSON.stringify(result, null, 2), + text: JSON.stringify(webhook, null, 2), }, ], }; }, }, - - calendly_delete_webhook_subscription: { + { + name: 'calendly_create_webhook', + description: 'Create a new webhook subscription', + inputSchema: z.object({ + url: z.string().url().describe('The callback URL for webhook events'), + events: z + .array( + z.enum([ + 'invitee.created', + 'invitee.canceled', + 'routing_form_submission.created', + 'invitee_no_show.created', + 'invitee_no_show.deleted', + ]) + ) + .describe('Array of event types to subscribe to'), + organization: z.string().describe('The URI of the organization'), + scope: z.enum(['organization', 'user']).describe('Scope of the webhook'), + signing_key: z.string().optional().describe('Signing key for webhook verification'), + user: z.string().optional().describe('User URI (required if scope is user)'), + }), + handler: async (args: { + url: string; + events: string[]; + organization: string; + scope: string; + signing_key?: string; + user?: string; + }) => { + const webhook = await calendly.createWebhook(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(webhook, null, 2), + }, + ], + }; + }, + }, + { + name: 'calendly_delete_webhook', description: 'Delete a webhook subscription', - parameters: { - type: 'object', - properties: { - uuid: { - type: 'string', - description: 'Webhook subscription UUID to delete', - }, - }, - required: ['uuid'], - }, - handler: async (args: any) => { - await client.deleteWebhookSubscription(args.uuid); + inputSchema: z.object({ + webhook_uri: z.string().describe('The URI of the webhook subscription to delete'), + }), + handler: async (args: { webhook_uri: string }) => { + await calendly.deleteWebhook(args.webhook_uri); return { content: [ { type: 'text', - text: JSON.stringify({ success: true, message: 'Webhook subscription deleted' }), + text: 'Webhook subscription deleted successfully', }, ], }; }, }, - }; + ]; } diff --git a/servers/calendly/src/types/index.ts b/servers/calendly/src/types/index.ts index c0b2b56..42395cf 100644 --- a/servers/calendly/src/types/index.ts +++ b/servers/calendly/src/types/index.ts @@ -1,9 +1,4 @@ -// Calendly API v2 Types - -export interface CalendlyConfig { - apiKey: string; - baseUrl?: string; -} +// Calendly API v2 TypeScript Types export interface CalendlyUser { uri: string; @@ -12,46 +7,21 @@ export interface CalendlyUser { email: string; scheduling_url: string; timezone: string; - avatar_url: string; + avatar_url?: string; created_at: string; updated_at: string; current_organization: string; - resource_type: string; + resource_type: 'User'; + locale?: string; } -export interface CalendlyEvent { +export interface CalendlyOrganization { uri: string; name: string; - meeting_notes_plain: string; - meeting_notes_html: string; - status: 'active' | 'canceled'; - start_time: string; - end_time: string; - event_type: string; - location: { - type: string; - location?: string; - join_url?: string; - }; - invitees_counter: { - total: number; - active: number; - limit: number; - }; + slug: string; created_at: string; updated_at: string; - event_memberships: Array<{ - user: string; - }>; - event_guests: Array<{ - email: string; - created_at: string; - }>; - cancellation?: { - canceled_by: string; - reason: string; - canceler_type: string; - }; + resource_type: 'Organization'; } export interface CalendlyEventType { @@ -62,44 +32,101 @@ export interface CalendlyEventType { scheduling_url: string; duration: number; kind: 'solo' | 'group' | 'collective' | 'round_robin'; - pooling_type?: string; - type: 'StandardEventType' | 'AdhocEventType'; + pooling_type?: 'round_robin' | 'collective' | null; + type: 'StandardEventType' | 'CustomEventType'; color: string; created_at: string; updated_at: string; - internal_note: string; - description_plain: string; - description_html: string; + internal_note?: string; + description_plain?: string; + description_html?: string; profile: { - type: string; + type: 'User' | 'Team'; name: string; owner: string; }; secret: boolean; - booking_method: string; - custom_questions: Array<{ - name: string; - type: string; - position: number; - enabled: boolean; - required: boolean; - answer_choices: string[]; - include_other: boolean; - }>; + booking_method: 'instant' | 'poll'; + custom_questions?: CustomQuestion[]; + deleted_at?: string; + admin_managed?: boolean; + resource_type: 'EventType'; +} + +export interface CustomQuestion { + name: string; + type: 'string' | 'text' | 'phone_number' | 'multiple_choice' | 'radio_buttons' | 'checkboxes'; + position: number; + enabled: boolean; + required: boolean; + answer_choices?: string[]; + include_other?: boolean; +} + +export interface CalendlyEvent { + uri: string; + name: string; + meeting_notes_plain?: string; + meeting_notes_html?: string; + status: 'active' | 'canceled'; + start_time: string; + end_time: string; + event_type: string; + location?: EventLocation; + invitees_counter: { + total: number; + active: number; + limit: number; + }; + created_at: string; + updated_at: string; + event_memberships: EventMembership[]; + event_guests: EventGuest[]; + calendar_event?: { + kind: string; + external_id: string; + }; + cancellation?: { + canceled_by: string; + reason?: string; + canceler_type: 'host' | 'invitee'; + }; + resource_type: 'Event'; +} + +export interface EventLocation { + type: 'physical' | 'outbound_call' | 'inbound_call' | 'google_conference' | 'zoom' | 'gotomeeting' | 'microsoft_teams' | 'webex' | 'custom'; + location?: string; + join_url?: string; + status?: string; + data?: Record; +} + +export interface EventMembership { + user: string; + user_email?: string; + user_name?: string; +} + +export interface EventGuest { + email: string; + created_at: string; + updated_at: string; } export interface CalendlyInvitee { uri: string; email: string; name: string; - first_name: string; - last_name: string; + first_name?: string; + last_name?: string; status: 'active' | 'canceled'; + questions_and_answers?: QuestionAnswer[]; timezone: string; event: string; created_at: string; updated_at: string; - tracking: { + tracking?: { utm_campaign?: string; utm_source?: string; utm_medium?: string; @@ -107,21 +134,13 @@ export interface CalendlyInvitee { utm_term?: string; salesforce_uuid?: string; }; - text_reminder_number: string; + text_reminder_number?: string; rescheduled: boolean; - old_invitee: string; - new_invitee: string; + old_invitee?: string; + new_invitee?: string; cancel_url: string; reschedule_url: string; - questions_and_answers: Array<{ - question: string; - answer: string; - position: number; - }>; - cancellation?: { - canceled_by: string; - reason: string; - }; + cancellation?: InviteeCancellation; payment?: { id: string; provider: string; @@ -130,46 +149,118 @@ export interface CalendlyInvitee { terms: string; successful: boolean; }; - no_show?: { - created_at: string; - }; + no_show?: NoShow; reconfirmation?: { created_at: string; - confirmed_at: string; + confirmed_at?: string; }; + routing_form_submission?: string; + resource_type: 'Invitee'; } -export interface CalendlyOrganization { +export interface QuestionAnswer { + question: string; + answer: string; + position: number; +} + +export interface InviteeCancellation { + canceled_by: string; + reason?: string; + canceler_type?: 'host' | 'invitee'; +} + +export interface NoShow { uri: string; - name: string; - slug: string; - status: string; - timezone: string; + created_at: string; +} + +export interface CalendlyWebhook { + uri: string; + callback_url: string; created_at: string; updated_at: string; - resource_type: string; + retry_started_at?: string; + state: 'active' | 'disabled'; + events: WebhookEvent[]; + organization: string; + user?: string; + creator: string; + signing_key: string; + scope: 'organization' | 'user'; + resource_type: 'WebhookSubscription'; } -export interface CalendlyOrganizationMembership { +export type WebhookEvent = + | 'invitee.created' + | 'invitee.canceled' + | 'routing_form_submission.created' + | 'invitee_no_show.created' + | 'invitee_no_show.deleted'; + +export interface CalendlySchedulingLink { + booking_url: string; + owner: string; + owner_type: 'EventType' | 'User'; + resource_type: 'SchedulingLink'; +} + +export interface CalendlyRoutingForm { uri: string; - role: 'owner' | 'admin' | 'user'; - user: { - uri: string; - name: string; - slug: string; - email: string; - scheduling_url: string; - timezone: string; - avatar_url: string; - created_at: string; - updated_at: string; - }; + name: string; organization: string; created_at: string; updated_at: string; + published: boolean; + questions: RoutingFormQuestion[]; + resource_type: 'RoutingForm'; } -export interface CalendlyOrganizationInvitation { +export interface RoutingFormQuestion { + uuid: string; + name: string; + type: 'text' | 'phone' | 'textarea' | 'select' | 'radios' | 'checkboxes'; + required: boolean; + answer_choices?: RoutingAnswerChoice[]; +} + +export interface RoutingAnswerChoice { + uuid: string; + label: string; + routing_target: { + type: 'event_type' | 'external_url' | 'custom_message'; + value: string; + }; +} + +export interface CalendlyRoutingFormSubmission { + uri: string; + routing_form: string; + submitter: { + email: string; + name?: string; + first_name?: string; + last_name?: string; + }; + submitter_type: 'Invitee' | 'Prospect'; + questions_and_answers: QuestionAnswer[]; + tracking?: { + utm_campaign?: string; + utm_source?: string; + utm_medium?: string; + utm_content?: string; + utm_term?: string; + }; + result: { + type: 'event_type' | 'external_url' | 'custom_message'; + value: string; + }; + created_at: string; + updated_at: string; + resource_type: 'RoutingFormSubmission'; +} + +export interface OrganizationInvitation { uri: string; organization: string; email: string; @@ -177,79 +268,58 @@ export interface CalendlyOrganizationInvitation { created_at: string; updated_at: string; last_sent_at: string; + resource_type: 'OrganizationInvitation'; } -export interface CalendlySchedulingLink { - booking_url: string; - owner: string; - owner_type: string; - resource_type: string; -} - -export interface CalendlyWebhookSubscription { +export interface OrganizationMembership { uri: string; - callback_url: string; + role: 'owner' | 'admin' | 'user'; + user: CalendlyUser; + organization: string; created_at: string; updated_at: string; - retry_started_at: string; - state: 'active' | 'disabled'; - events: string[]; - scope: 'user' | 'organization'; + resource_type: 'OrganizationMembership'; +} + +export interface ActivityLogEntry { + occurred_at: string; + action: string; + actor: string; + namespace: string; + details?: string; organization: string; - user: string; - creator: string; } -export interface CalendlyAvailableTime { - status: 'available'; - invitees_remaining: number; - start_time: string; - scheduling_url: string; +export interface DataComplianceRequest { + uri: string; + emails: string[]; + status: 'pending' | 'completed' | 'failed'; + created_at: string; + updated_at: string; } -export interface CalendlyUserBusyTime { - start_time: string; - end_time: string; - type: 'calendly' | 'busy_calendar'; - buffered: boolean; +export interface AvailabilityRule { + type: 'wday' | 'date'; + wday?: string; + date?: string; + intervals: TimeInterval[]; } -export interface CalendlyRoutingForm { +export interface TimeInterval { + from: string; + to: string; +} + +export interface UserAvailabilitySchedule { uri: string; name: string; - status: 'published' | 'draft'; - published_version: number; - organization: string; + user: string; + timezone: string; + rules: AvailabilityRule[]; + default: boolean; created_at: string; updated_at: string; - questions: Array<{ - uuid: string; - name: string; - type: string; - required: boolean; - answer_choices?: Array<{ - uuid: string; - label: string; - position: number; - }>; - }>; - routing_configurations: Array<{ - priority: number; - rules: Array<{ - question_uuid: string; - type: string; - value: string; - }>; - routing_target: { - type: string; - target: string; - }; - }>; -} - -export interface CalendlyNoShow { - uri: string; - created_at: string; + resource_type: 'UserAvailabilitySchedule'; } export interface PaginationParams { @@ -277,3 +347,56 @@ export interface CalendlyError { message: string; }>; } + +export interface ShareOptions { + invitee_email?: string; + name?: string; + first_name?: string; + last_name?: string; + guests?: string[]; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + utm_content?: string; + utm_term?: string; + salesforce_uuid?: string; + a1?: string; + a2?: string; + a3?: string; + a4?: string; + a5?: string; + a6?: string; + a7?: string; + a8?: string; + a9?: string; + a10?: string; +} + +export interface CreateEventTypeParams { + name: string; + duration: number; + type?: 'StandardEventType'; + kind?: 'solo' | 'group'; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + custom_questions?: CustomQuestion[]; + profile: { + type: 'User'; + owner: string; + }; +} + +export interface UpdateEventTypeParams { + name?: string; + duration?: number; + description_plain?: string; + description_html?: string; + color?: string; + internal_note?: string; + secret?: boolean; + active?: boolean; + custom_questions?: CustomQuestion[]; +} diff --git a/servers/calendly/src/ui/react-app/build-all.js b/servers/calendly/src/ui/react-app/build-all.js new file mode 100644 index 0000000..9f41171 --- /dev/null +++ b/servers/calendly/src/ui/react-app/build-all.js @@ -0,0 +1,19 @@ +import { execSync } from 'child_process'; +import { readdirSync, statSync } from 'fs'; +import { join } from 'path'; + +const appsDir = './src/apps'; +const apps = readdirSync(appsDir).filter((file) => + statSync(join(appsDir, file)).isDirectory() +); + +console.log(`Building ${apps.length} apps...`); + +for (const app of apps) { + console.log(`Building ${app}...`); + execSync(`npx vite build --config src/apps/${app}/vite.config.ts`, { + stdio: 'inherit', + }); +} + +console.log('All apps built successfully!'); diff --git a/servers/calendly/src/ui/react-app/package.json b/servers/calendly/src/ui/react-app/package.json new file mode 100644 index 0000000..bc4f622 --- /dev/null +++ b/servers/calendly/src/ui/react-app/package.json @@ -0,0 +1,20 @@ +{ + "name": "calendly-mcp-apps", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "node build-all.js" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.2.0", + "vite": "^5.0.0", + "typescript": "^5.3.0" + } +} diff --git a/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/App.tsx b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/App.tsx new file mode 100644 index 0000000..8a7dedf --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/App.tsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import '../../styles/common.css'; + +export default function AnalyticsDashboard() { + const { callTool, loading } = useCallTool(); + const [stats, setStats] = useState({ + totalEvents: 0, + activeEvents: 0, + canceledEvents: 0, + totalInvitees: 0, + noShows: 0, + eventTypes: 0, + }); + + useEffect(() => { + loadAnalytics(); + }, []); + + const loadAnalytics = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + + // Get events + const eventsResult = await callTool('calendly_list_events', { + user: user.uri, + count: 100, + }); + const events = eventsResult.collection || []; + + // Get event types + const eventTypesResult = await callTool('calendly_list_event_types', { + user: user.uri, + }); + + // Calculate stats + const activeEvents = events.filter((e: any) => e.status === 'active').length; + const canceledEvents = events.filter((e: any) => e.status === 'canceled').length; + const totalInvitees = events.reduce((sum: number, e: any) => sum + e.invitees_counter.total, 0); + + setStats({ + totalEvents: events.length, + activeEvents, + canceledEvents, + totalInvitees, + noShows: 0, + eventTypes: eventTypesResult.collection?.length || 0, + }); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Analytics Dashboard

+

Overview of your Calendly metrics

+
+ + {loading &&
Loading...
} + +
+ +
+
{stats.totalEvents}
+
Total Events
+
+
+ +
+
{stats.activeEvents}
+
Active Events
+
+
+ +
+
{stats.canceledEvents}
+
Canceled Events
+
+
+ +
+
{stats.totalInvitees}
+
Total Invitees
+
+
+ +
+
{stats.eventTypes}
+
Event Types
+
+
+ +
+
{stats.noShows}
+
No-Shows
+
+
+
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/index.html b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/index.html new file mode 100644 index 0000000..856600b --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + analytics-dashboard + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/main.tsx b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/analytics-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/calendly/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts new file mode 100644 index 0000000..9413b12 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/analytics-dashboard/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/analytics-dashboard', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/analytics-dashboard/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/availability-manager/App.tsx b/servers/calendly/src/ui/react-app/src/apps/availability-manager/App.tsx new file mode 100644 index 0000000..477b462 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/availability-manager/App.tsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function AvailabilityManager() { + const { callTool, loading, error } = useCallTool(); + const [schedules, setSchedules] = useState([]); + const [userUri, setUserUri] = useState(''); + + useEffect(() => { + loadSchedules(); + }, []); + + const loadSchedules = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + setUserUri(user.uri); + const result = await callTool('calendly_list_user_availability_schedules', { + user_uri: user.uri, + }); + setSchedules(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const formatTime = (time: string) => { + const [hours, minutes] = time.split(':'); + const hour = parseInt(hours); + const ampm = hour >= 12 ? 'PM' : 'AM'; + const displayHour = hour % 12 || 12; + return `${displayHour}:${minutes} ${ampm}`; + }; + + const getDayName = (wday: string) => { + const days: Record = { + sunday: 'Sunday', + monday: 'Monday', + tuesday: 'Tuesday', + wednesday: 'Wednesday', + thursday: 'Thursday', + friday: 'Friday', + saturday: 'Saturday', + }; + return days[wday] || wday; + }; + + return ( +
+
+

Availability Manager

+

Manage your availability schedules

+
+ + {error &&
{error}
} + {loading &&
Loading...
} + +
+ {schedules.map((schedule) => ( + +
+

{schedule.name}

+ {schedule.default && Default} +
+

+ Timezone: {schedule.timezone} +

+
+ Availability Rules: + {schedule.rules.map((rule: any, idx: number) => ( +
+ {rule.type === 'wday' ? ( +
+ {getDayName(rule.wday)} +
+ ) : ( +
+ Date: {rule.date} +
+ )} + {rule.intervals.map((interval: any, i: number) => ( +
+ {formatTime(interval.from)} - {formatTime(interval.to)} +
+ ))} +
+ ))} +
+
+ ))} +
+ + {schedules.length === 0 && !loading && ( +
+ No availability schedules found +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/availability-manager/index.html b/servers/calendly/src/ui/react-app/src/apps/availability-manager/index.html new file mode 100644 index 0000000..9000dbf --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/availability-manager/index.html @@ -0,0 +1,12 @@ + + + + + + availability-manager + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/availability-manager/main.tsx b/servers/calendly/src/ui/react-app/src/apps/availability-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/availability-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/calendly/src/ui/react-app/src/apps/availability-manager/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/availability-manager/vite.config.ts new file mode 100644 index 0000000..51b8188 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/availability-manager/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/availability-manager', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/availability-manager/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/App.tsx b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/App.tsx new file mode 100644 index 0000000..1a6d0cf --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/App.tsx @@ -0,0 +1,155 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function BookingPagePreview() { + const { callTool, loading, error } = useCallTool(); + const [eventTypes, setEventTypes] = useState([]); + const [selectedEventType, setSelectedEventType] = useState(null); + + useEffect(() => { + loadEventTypes(); + }, []); + + const loadEventTypes = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const result = await callTool('calendly_list_event_types', { + user: user.uri, + }); + setEventTypes(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const loadEventTypeDetail = async (uri: string) => { + try { + const eventType = await callTool('calendly_get_event_type', { + event_type_uri: uri, + }); + setSelectedEventType(eventType); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Booking Page Preview

+

Preview your event type booking pages

+
+ +
+ + +
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {selectedEventType && ( + <> + +
+
+

{selectedEventType.name}

+
+ + {selectedEventType.active ? 'Active' : 'Inactive'} + + + {selectedEventType.duration} minutes + + + {selectedEventType.kind} + +
+ {selectedEventType.description_plain && ( +

+ {selectedEventType.description_plain} +

+ )} + {selectedEventType.internal_note && ( +
+ Internal Note: {selectedEventType.internal_note} +
+ )} + + Open Booking Page + +
+
+
+ + + {selectedEventType.custom_questions && selectedEventType.custom_questions.length > 0 && ( + + {selectedEventType.custom_questions.map((q: any, idx: number) => ( +
+
+ {q.position}. {q.name} + {q.required && *} +
+
+ Type: {q.type} + {!q.enabled && Disabled} +
+ {q.answer_choices && q.answer_choices.length > 0 && ( +
+ Choices: {q.answer_choices.join(', ')} +
+ )} +
+ ))} +
+ )} + + )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/index.html b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/index.html new file mode 100644 index 0000000..c099250 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/index.html @@ -0,0 +1,12 @@ + + + + + + booking-page-preview + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/main.tsx b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/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/calendly/src/ui/react-app/src/apps/booking-page-preview/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/vite.config.ts new file mode 100644 index 0000000..bfc0a90 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/booking-page-preview/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/booking-page-preview', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/booking-page-preview/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/calendar-view/App.tsx b/servers/calendly/src/ui/react-app/src/apps/calendar-view/App.tsx new file mode 100644 index 0000000..3950b96 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/calendar-view/App.tsx @@ -0,0 +1,118 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function CalendarView() { + const { callTool, loading, error } = useCallTool(); + const [events, setEvents] = useState([]); + const [timeRange, setTimeRange] = useState('week'); + + useEffect(() => { + loadEvents(); + }, [timeRange]); + + const loadEvents = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const now = new Date(); + const minTime = new Date(now); + const maxTime = new Date(now); + + if (timeRange === 'week') { + minTime.setDate(now.getDate() - 7); + maxTime.setDate(now.getDate() + 7); + } else if (timeRange === 'month') { + minTime.setDate(now.getDate() - 30); + maxTime.setDate(now.getDate() + 30); + } + + const result = await callTool('calendly_list_events', { + user: user.uri, + min_start_time: minTime.toISOString(), + max_start_time: maxTime.toISOString(), + sort: 'start_time:asc', + }); + setEvents(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + const groupByDate = () => { + const grouped: Record = {}; + events.forEach((event) => { + const date = new Date(event.start_time).toLocaleDateString(); + if (!grouped[date]) grouped[date] = []; + grouped[date].push(event); + }); + return grouped; + }; + + const grouped = groupByDate(); + + return ( +
+
+

Calendar View

+

View your scheduled events

+
+ +
+ +
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {Object.keys(grouped).map((date) => ( +
+

{date}

+ {grouped[date].map((event) => ( + +
+
+

{event.name}

+

+ {formatDate(event.start_time)} - {new Date(event.end_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+ + {event.status} + + {event.location && ( +
+ 📍 {event.location.type} +
+ )} +
+
+
{event.invitees_counter.active} / {event.invitees_counter.limit} invitees
+
+
+
+ ))} +
+ ))} + + {events.length === 0 && !loading && ( +
+ No events found in this time range +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/calendar-view/index.html b/servers/calendly/src/ui/react-app/src/apps/calendar-view/index.html new file mode 100644 index 0000000..da83a55 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/calendar-view/index.html @@ -0,0 +1,12 @@ + + + + + + Calendar View + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/calendar-view/main.tsx b/servers/calendly/src/ui/react-app/src/apps/calendar-view/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/calendar-view/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/calendly/src/ui/react-app/src/apps/calendar-view/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/calendar-view/vite.config.ts new file mode 100644 index 0000000..df85525 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/calendar-view/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/calendar-view', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/calendar-view/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/event-calendar/App.tsx b/servers/calendly/src/ui/react-app/src/apps/event-calendar/App.tsx new file mode 100644 index 0000000..ec88e08 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-calendar/App.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function EventCalendar() { + const { callTool, loading, error } = useCallTool(); + const [events, setEvents] = useState([]); + const [currentDate, setCurrentDate] = useState(new Date()); + + useEffect(() => { + loadEvents(); + }, [currentDate]); + + const loadEvents = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const startOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); + const endOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); + + const result = await callTool('calendly_list_events', { + user: user.uri, + min_start_time: startOfMonth.toISOString(), + max_start_time: endOfMonth.toISOString(), + sort: 'start_time:asc', + }); + setEvents(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const previousMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)); + }; + + const nextMonth = () => { + setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)); + }; + + const getEventsForDate = (date: Date) => { + return events.filter((event) => { + const eventDate = new Date(event.start_time); + return ( + eventDate.getDate() === date.getDate() && + eventDate.getMonth() === date.getMonth() && + eventDate.getFullYear() === date.getFullYear() + ); + }); + }; + + const getDaysInMonth = () => { + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + const firstDay = new Date(year, month, 1); + const lastDay = new Date(year, month + 1, 0); + const days = []; + + // Add empty cells for days before the first day of the month + for (let i = 0; i < firstDay.getDay(); i++) { + days.push(null); + } + + // Add all days of the month + for (let i = 1; i <= lastDay.getDate(); i++) { + days.push(new Date(year, month, i)); + } + + return days; + }; + + const monthYear = currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); + const days = getDaysInMonth(); + + return ( +
+
+

Event Calendar

+

Monthly calendar view of events

+
+ +
+ +

{monthYear}

+ +
+ + {error &&
{error}
} + {loading &&
Loading...
} + +
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => ( +
+ {day} +
+ ))} + {days.map((date, idx) => { + if (!date) { + return
; + } + const dayEvents = getEventsForDate(date); + const isToday = + date.getDate() === new Date().getDate() && + date.getMonth() === new Date().getMonth() && + date.getFullYear() === new Date().getFullYear(); + + return ( +
+
+ {date.getDate()} +
+ {dayEvents.map((event) => ( +
+ {new Date(event.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}{' '} + {event.name} +
+ ))} +
+ ); + })} +
+ +
+ +
+
+ + Active Events +
+
+ + Canceled Events +
+
+ + Today +
+
+
+
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/event-calendar/index.html b/servers/calendly/src/ui/react-app/src/apps/event-calendar/index.html new file mode 100644 index 0000000..cade510 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-calendar/index.html @@ -0,0 +1,12 @@ + + + + + + event-calendar + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/event-calendar/main.tsx b/servers/calendly/src/ui/react-app/src/apps/event-calendar/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-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/calendly/src/ui/react-app/src/apps/event-calendar/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/event-calendar/vite.config.ts new file mode 100644 index 0000000..2cf56dc --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-calendar/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/event-calendar', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/event-calendar/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/event-detail/App.tsx b/servers/calendly/src/ui/react-app/src/apps/event-detail/App.tsx new file mode 100644 index 0000000..5a6c357 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-detail/App.tsx @@ -0,0 +1,174 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function EventDetail() { + const { callTool, loading, error } = useCallTool(); + const [events, setEvents] = useState([]); + const [selectedEvent, setSelectedEvent] = useState(null); + const [invitees, setInvitees] = useState([]); + + useEffect(() => { + loadEvents(); + }, []); + + const loadEvents = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const result = await callTool('calendly_list_events', { + user: user.uri, + count: 50, + sort: 'start_time:desc', + }); + setEvents(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const loadEventDetail = async (eventUri: string) => { + try { + const event = await callTool('calendly_get_event', { event_uri: eventUri }); + setSelectedEvent(event); + + const inviteesResult = await callTool('calendly_list_event_invitees', { + event_uri: eventUri, + }); + setInvitees(inviteesResult.collection || []); + } catch (err) { + console.error(err); + } + }; + + const cancelEvent = async () => { + if (!selectedEvent || !confirm('Cancel this event?')) return; + try { + await callTool('calendly_cancel_event', { + event_uri: selectedEvent.uri, + reason: 'Canceled via MCP', + }); + loadEvents(); + setSelectedEvent(null); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Event Detail View

+

View detailed information about events

+
+ +
+ + +
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {selectedEvent && ( + <> + +
+

{selectedEvent.name}

+

+ Status:{' '} + + {selectedEvent.status} + +

+

+ Start: {new Date(selectedEvent.start_time).toLocaleString()} +

+

+ End: {new Date(selectedEvent.end_time).toLocaleString()} +

+ {selectedEvent.location && ( +

+ Location: {selectedEvent.location.type} + {selectedEvent.location.join_url && ( + + Join + + )} +

+ )} +

+ Invitees: {selectedEvent.invitees_counter.active} /{' '} + {selectedEvent.invitees_counter.limit} +

+ {selectedEvent.meeting_notes_plain && ( +
+ Meeting Notes: +

+ {selectedEvent.meeting_notes_plain} +

+
+ )} + {selectedEvent.status === 'active' && ( + + )} +
+
+ + + {invitees.map((invitee) => ( +
+
+
+

{invitee.name}

+

{invitee.email}

+
+ + {invitee.status} + +
+
+
+ {invitee.timezone} +
+
+
+ ))} +
+ + )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/event-detail/index.html b/servers/calendly/src/ui/react-app/src/apps/event-detail/index.html new file mode 100644 index 0000000..8b8b9ad --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-detail/index.html @@ -0,0 +1,12 @@ + + + + + + event-detail + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/event-detail/main.tsx b/servers/calendly/src/ui/react-app/src/apps/event-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-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/calendly/src/ui/react-app/src/apps/event-detail/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/event-detail/vite.config.ts new file mode 100644 index 0000000..f9cb00e --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-detail/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/event-detail', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/event-detail/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/App.tsx b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/App.tsx new file mode 100644 index 0000000..ee9be8b --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/App.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function EventTypeDashboard() { + const { callTool, loading, error } = useCallTool(); + const [eventTypes, setEventTypes] = useState([]); + const [userUri, setUserUri] = useState(''); + + useEffect(() => { + loadCurrentUser(); + }, []); + + const loadCurrentUser = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + setUserUri(user.uri); + loadEventTypes(user.uri); + } catch (err) { + console.error(err); + } + }; + + const loadEventTypes = async (uri: string) => { + try { + const result = await callTool('calendly_list_event_types', { user: uri }); + setEventTypes(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const toggleActive = async (eventType: any) => { + try { + await callTool('calendly_update_event_type', { + event_type_uri: eventType.uri, + active: !eventType.active, + }); + loadEventTypes(userUri); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Event Type Dashboard

+

Manage your Calendly event types

+
+ + {error &&
{error}
} + {loading &&
Loading...
} + +
+ {eventTypes.map((et) => ( + +
+ + {et.active ? 'Active' : 'Inactive'} + + + {et.duration} min + +
+

+ {et.description_plain || 'No description'} +

+
+ Type: {et.kind} • {et.type} +
+
+ + + View + +
+
+ ))} +
+ + {eventTypes.length === 0 && !loading && ( +
+ No event types found +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/index.html b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/index.html new file mode 100644 index 0000000..7f22d4c --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Event Type Dashboard + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/main.tsx b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-type-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/calendly/src/ui/react-app/src/apps/event-type-dashboard/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/vite.config.ts new file mode 100644 index 0000000..254c4a7 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/event-type-dashboard/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/event-type-dashboard', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/event-type-dashboard/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/invitee-grid/App.tsx b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/App.tsx new file mode 100644 index 0000000..37da5c8 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/App.tsx @@ -0,0 +1,129 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function InviteeGrid() { + const { callTool, loading, error } = useCallTool(); + const [events, setEvents] = useState([]); + const [selectedEvent, setSelectedEvent] = useState(''); + const [invitees, setInvitees] = useState([]); + + useEffect(() => { + loadEvents(); + }, []); + + const loadEvents = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const result = await callTool('calendly_list_events', { + user: user.uri, + count: 20, + sort: 'start_time:desc', + }); + setEvents(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const loadInvitees = async (eventUri: string) => { + try { + const result = await callTool('calendly_list_event_invitees', { + event_uri: eventUri, + }); + setInvitees(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const markNoShow = async (inviteeUri: string) => { + try { + await callTool('calendly_create_no_show', { invitee_uri: inviteeUri }); + loadInvitees(selectedEvent); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Invitee Grid

+

View and manage event invitees

+
+ +
+ + +
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {invitees.length > 0 && ( + + + + + + + + + + + + + {invitees.map((invitee) => ( + + + + + + + + + ))} + +
NameEmailStatusTimezoneRescheduledActions
{invitee.name}{invitee.email} + + {invitee.status} + + {invitee.no_show && No Show} + {invitee.timezone}{invitee.rescheduled ? 'Yes' : 'No'} + {!invitee.no_show && invitee.status === 'active' && ( + + )} +
+ )} + + {invitees.length === 0 && selectedEvent && !loading && ( +
+ No invitees found for this event +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/invitee-grid/index.html b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/index.html new file mode 100644 index 0000000..de2295e --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/index.html @@ -0,0 +1,12 @@ + + + + + + Invitee Grid + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/invitee-grid/main.tsx b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/invitee-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/calendly/src/ui/react-app/src/apps/invitee-grid/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/vite.config.ts new file mode 100644 index 0000000..2617427 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/invitee-grid/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/invitee-grid', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/invitee-grid/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/App.tsx b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/App.tsx new file mode 100644 index 0000000..621c659 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/App.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function NoShowTracker() { + const { callTool, loading, error } = useCallTool(); + const [events, setEvents] = useState([]); + const [noShowInvitees, setNoShowInvitees] = useState([]); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const eventsResult = await callTool('calendly_list_events', { + user: user.uri, + count: 100, + status: 'active', + }); + + const events = eventsResult.collection || []; + setEvents(events); + + // Load all invitees and filter those with no-shows + const allNoShows: any[] = []; + for (const event of events) { + const inviteesResult = await callTool('calendly_list_event_invitees', { + event_uri: event.uri, + }); + const invitees = inviteesResult.collection || []; + const noShows = invitees.filter((inv: any) => inv.no_show); + allNoShows.push(...noShows.map((inv: any) => ({ ...inv, event }))); + } + setNoShowInvitees(allNoShows); + } catch (err) { + console.error(err); + } + }; + + const removeNoShow = async (invitee: any) => { + if (!confirm('Remove no-show marking?')) return; + try { + await callTool('calendly_delete_no_show', { + no_show_uri: invitee.no_show.uri, + }); + loadData(); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

No-Show Tracker

+

Track and manage no-show invitees

+
+ + {error &&
{error}
} + {loading &&
Loading...
} + + +
+
{noShowInvitees.length}
+
Total No-Shows
+
+
+ + {noShowInvitees.length > 0 ? ( + + + + + + + + + + + + + {noShowInvitees.map((invitee, idx) => ( + + + + + + + + + ))} + +
NameEmailEventEvent DateMarked No-ShowActions
{invitee.name}{invitee.email}{invitee.event.name}{new Date(invitee.event.start_time).toLocaleString()}{new Date(invitee.no_show.created_at).toLocaleString()} + +
+ ) : ( + !loading && ( +
+ No no-shows found +
+ ) + )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/index.html b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/index.html new file mode 100644 index 0000000..7a8fdda --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/index.html @@ -0,0 +1,12 @@ + + + + + + no-show-tracker + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/main.tsx b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/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/calendly/src/ui/react-app/src/apps/no-show-tracker/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/vite.config.ts new file mode 100644 index 0000000..fa6cd22 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/no-show-tracker/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/no-show-tracker', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/no-show-tracker/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/organization-overview/App.tsx b/servers/calendly/src/ui/react-app/src/apps/organization-overview/App.tsx new file mode 100644 index 0000000..09dfe51 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/organization-overview/App.tsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function OrganizationOverview() { + const { callTool, loading, error } = useCallTool(); + const [organization, setOrganization] = useState(null); + const [memberships, setMemberships] = useState([]); + const [invitations, setInvitations] = useState([]); + + useEffect(() => { + loadOrganization(); + }, []); + + const loadOrganization = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const org = await callTool('calendly_get_organization', { + organization_uri: user.current_organization, + }); + setOrganization(org); + + const membershipsResult = await callTool('calendly_list_organization_memberships', { + organization_uri: org.uri, + }); + setMemberships(membershipsResult.collection || []); + + const invitationsResult = await callTool('calendly_list_organization_invitations', { + organization_uri: org.uri, + }); + setInvitations(invitationsResult.collection || []); + } catch (err) { + console.error(err); + } + }; + + const revokeInvitation = async (uri: string) => { + if (!confirm('Revoke this invitation?')) return; + try { + await callTool('calendly_revoke_organization_invitation', { + invitation_uri: uri, + }); + loadOrganization(); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Organization Overview

+

Manage your organization

+
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {organization && ( + +
+

Name: {organization.name}

+

Slug: {organization.slug}

+

Created: {new Date(organization.created_at).toLocaleDateString()}

+
+
+ )} + + + + + + + + + + + + + {memberships.map((membership) => ( + + + + + + + ))} + +
NameEmailRoleJoined
{membership.user.name}{membership.user.email} + {membership.role} + {new Date(membership.created_at).toLocaleDateString()}
+
+ + + {invitations.length > 0 ? ( + + + + + + + + + + + {invitations.map((invitation) => ( + + + + + + + ))} + +
EmailStatusSentActions
{invitation.email} + + {invitation.status} + + {new Date(invitation.last_sent_at).toLocaleDateString()} + {invitation.status === 'pending' && ( + + )} +
+ ) : ( +

No pending invitations

+ )} +
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/organization-overview/index.html b/servers/calendly/src/ui/react-app/src/apps/organization-overview/index.html new file mode 100644 index 0000000..1399bbe --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/organization-overview/index.html @@ -0,0 +1,12 @@ + + + + + + organization-overview + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/organization-overview/main.tsx b/servers/calendly/src/ui/react-app/src/apps/organization-overview/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/organization-overview/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/calendly/src/ui/react-app/src/apps/organization-overview/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/organization-overview/vite.config.ts new file mode 100644 index 0000000..d07a90a --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/organization-overview/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/organization-overview', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/organization-overview/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/App.tsx b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/App.tsx new file mode 100644 index 0000000..a2b5ee4 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/App.tsx @@ -0,0 +1,149 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function RoutingFormBuilder() { + const { callTool, loading, error } = useCallTool(); + const [routingForms, setRoutingForms] = useState([]); + const [selectedForm, setSelectedForm] = useState(null); + const [submissions, setSubmissions] = useState([]); + + useEffect(() => { + loadRoutingForms(); + }, []); + + const loadRoutingForms = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const result = await callTool('calendly_list_routing_forms', { + organization_uri: user.current_organization, + }); + setRoutingForms(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const loadFormDetail = async (formUri: string) => { + try { + const form = await callTool('calendly_get_routing_form', { + routing_form_uri: formUri, + }); + setSelectedForm(form); + + const submissionsResult = await callTool('calendly_list_routing_form_submissions', { + routing_form_uri: formUri, + }); + setSubmissions(submissionsResult.collection || []); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

Routing Form Builder

+

Manage routing forms and view submissions

+
+ +
+ + +
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {selectedForm && ( + <> + +
+

+ Name: {selectedForm.name} +

+

+ Status:{' '} + + {selectedForm.published ? 'Published' : 'Draft'} + +

+

+ Created: {new Date(selectedForm.created_at).toLocaleString()} +

+
+ Questions ({selectedForm.questions.length}): + {selectedForm.questions.map((q: any, idx: number) => ( +
+
+ {q.name} {q.required && *} +
+
+ Type: {q.type} +
+
+ ))} +
+
+
+ + + {submissions.length > 0 ? ( + submissions.map((submission, idx) => ( +
+
+ {submission.submitter.name || submission.submitter.email} +
+
+ {submission.questions_and_answers.map((qa: any, qaIdx: number) => ( +
+ {qa.question}: {qa.answer} +
+ ))} +
+
+ Result: {submission.result.type} - {submission.result.value} +
+
+ Submitted: {new Date(submission.created_at).toLocaleString()} +
+
+ )) + ) : ( +

No submissions yet

+ )} +
+ + )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/index.html b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/index.html new file mode 100644 index 0000000..bc7b08b --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/index.html @@ -0,0 +1,12 @@ + + + + + + routing-form-builder + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/main.tsx b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/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/calendly/src/ui/react-app/src/apps/routing-form-builder/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/vite.config.ts new file mode 100644 index 0000000..6a2656a --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/routing-form-builder/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/routing-form-builder', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/routing-form-builder/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/App.tsx b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/App.tsx new file mode 100644 index 0000000..2e91cb2 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/App.tsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import '../../styles/common.css'; + +export default function SchedulingLinkManager() { + const { callTool, loading, error } = useCallTool(); + const [eventTypes, setEventTypes] = useState([]); + const [selectedEventType, setSelectedEventType] = useState(''); + const [maxCount, setMaxCount] = useState(1); + const [generatedLink, setGeneratedLink] = useState(null); + + useEffect(() => { + loadEventTypes(); + }, []); + + const loadEventTypes = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const result = await callTool('calendly_list_event_types', { + user: user.uri, + active: true, + }); + setEventTypes(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const generateLink = async () => { + if (!selectedEventType) return; + try { + const link = await callTool('calendly_create_scheduling_link', { + max_event_count: maxCount, + owner: selectedEventType, + owner_type: 'EventType', + }); + setGeneratedLink(link); + } catch (err) { + console.error(err); + } + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + alert('Link copied to clipboard!'); + }; + + return ( +
+
+

Scheduling Link Manager

+

Generate single-use scheduling links

+
+ + {error &&
{error}
} + + +
+
+ + +
+
+ + setMaxCount(parseInt(e.target.value) || 1)} + min="1" + max="10" + /> +

+ Number of times this link can be used to book events +

+
+ +
+
+ + {generatedLink && ( + +
+ Booking URL: +
+
+ {generatedLink.booking_url} +
+ +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/index.html b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/index.html new file mode 100644 index 0000000..6b577fa --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/index.html @@ -0,0 +1,12 @@ + + + + + + scheduling-link-manager + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/main.tsx b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-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/calendly/src/ui/react-app/src/apps/scheduling-link-manager/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/vite.config.ts new file mode 100644 index 0000000..57b2645 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/scheduling-link-manager/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/scheduling-link-manager', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/scheduling-link-manager/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/user-profile/App.tsx b/servers/calendly/src/ui/react-app/src/apps/user-profile/App.tsx new file mode 100644 index 0000000..2846f3d --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/user-profile/App.tsx @@ -0,0 +1,100 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import '../../styles/common.css'; + +export default function UserProfile() { + const { callTool, loading, error } = useCallTool(); + const [user, setUser] = useState(null); + const [schedules, setSchedules] = useState([]); + + useEffect(() => { + loadUser(); + }, []); + + const loadUser = async () => { + try { + const userData = await callTool('calendly_get_current_user', {}); + setUser(userData); + + const schedulesResult = await callTool('calendly_list_user_availability_schedules', { + user_uri: userData.uri, + }); + setSchedules(schedulesResult.collection || []); + } catch (err) { + console.error(err); + } + }; + + return ( +
+
+

User Profile

+

View your Calendly profile and settings

+
+ + {error &&
{error}
} + {loading &&
Loading...
} + + {user && ( + <> + +
+ {user.avatar_url && ( + {user.name} + )} +
+

{user.name}

+

{user.email}

+

Slug: {user.slug}

+

Timezone: {user.timezone}

+ + View Scheduling Page + +
+
+
+ + + {schedules.map((schedule) => ( +
+

+ {schedule.name} {schedule.default && (Default)} +

+

+ Timezone: {schedule.timezone} +

+
+ Rules: + {schedule.rules.map((rule: any, idx: number) => ( +
+ • {rule.type === 'wday' ? `Day: ${rule.wday}` : `Date: ${rule.date}`} + {rule.intervals.map((interval: any, i: number) => ( + + {interval.from} - {interval.to} + + ))} +
+ ))} +
+
+ ))} + {schedules.length === 0 && ( +

No availability schedules found

+ )} +
+ + )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/user-profile/index.html b/servers/calendly/src/ui/react-app/src/apps/user-profile/index.html new file mode 100644 index 0000000..d02e627 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/user-profile/index.html @@ -0,0 +1,12 @@ + + + + + + user-profile + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/user-profile/main.tsx b/servers/calendly/src/ui/react-app/src/apps/user-profile/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/user-profile/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/calendly/src/ui/react-app/src/apps/user-profile/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/user-profile/vite.config.ts new file mode 100644 index 0000000..d34d93b --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/user-profile/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/user-profile', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/user-profile/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/apps/webhook-manager/App.tsx b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/App.tsx new file mode 100644 index 0000000..338717b --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/App.tsx @@ -0,0 +1,158 @@ +import React, { useState, useEffect } from 'react'; +import { useCallTool } from '../../hooks/useCallTool'; +import { Card } from '../../components/Card'; +import { Badge } from '../../components/Badge'; +import '../../styles/common.css'; + +export default function WebhookManager() { + const { callTool, loading, error } = useCallTool(); + const [webhooks, setWebhooks] = useState([]); + const [orgUri, setOrgUri] = useState(''); + const [showForm, setShowForm] = useState(false); + const [formData, setFormData] = useState({ + url: '', + events: [] as string[], + scope: 'organization', + }); + + useEffect(() => { + loadWebhooks(); + }, []); + + const loadWebhooks = async () => { + try { + const user = await callTool('calendly_get_current_user', {}); + const org = user.current_organization; + setOrgUri(org); + const result = await callTool('calendly_list_webhooks', { + organization: org, + scope: 'organization', + }); + setWebhooks(result.collection || []); + } catch (err) { + console.error(err); + } + }; + + const createWebhook = async () => { + try { + await callTool('calendly_create_webhook', { + url: formData.url, + events: formData.events, + organization: orgUri, + scope: formData.scope, + }); + setShowForm(false); + setFormData({ url: '', events: [], scope: 'organization' }); + loadWebhooks(); + } catch (err) { + console.error(err); + } + }; + + const deleteWebhook = async (uri: string) => { + if (!confirm('Delete this webhook?')) return; + try { + await callTool('calendly_delete_webhook', { webhook_uri: uri }); + loadWebhooks(); + } catch (err) { + console.error(err); + } + }; + + const eventOptions = [ + 'invitee.created', + 'invitee.canceled', + 'routing_form_submission.created', + 'invitee_no_show.created', + 'invitee_no_show.deleted', + ]; + + return ( +
+
+

Webhook Manager

+

Manage Calendly webhook subscriptions

+
+ +
+ +
+ + {showForm && ( + +
+
+ + setFormData({ ...formData, url: e.target.value })} + placeholder="https://example.com/webhook" + /> +
+
+ + {eventOptions.map((event) => ( +
+ +
+ ))} +
+ +
+
+ )} + + {error &&
{error}
} + {loading &&
Loading...
} + +
+ {webhooks.map((webhook) => ( + +
+ + {webhook.state} + +
+
+ URL: {webhook.callback_url} +
+
+ Events: {webhook.events.join(', ')} +
+
+ Created: {new Date(webhook.created_at).toLocaleString()} +
+ +
+ ))} +
+ + {webhooks.length === 0 && !loading && ( +
+ No webhooks found +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/apps/webhook-manager/index.html b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/index.html new file mode 100644 index 0000000..76e3235 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/index.html @@ -0,0 +1,12 @@ + + + + + + webhook-manager + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/src/apps/webhook-manager/main.tsx b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/webhook-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/calendly/src/ui/react-app/src/apps/webhook-manager/vite.config.ts b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/vite.config.ts new file mode 100644 index 0000000..3ef903f --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/apps/webhook-manager/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../dist/webhook-manager', + emptyOutDir: true, + rollupOptions: { + input: './src/apps/webhook-manager/index.html', + }, + }, +}); diff --git a/servers/calendly/src/ui/react-app/src/components/Badge.tsx b/servers/calendly/src/ui/react-app/src/components/Badge.tsx new file mode 100644 index 0000000..05dae7d --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/components/Badge.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +interface BadgeProps { + children: React.ReactNode; + variant?: 'success' | 'warning' | 'danger' | 'info'; +} + +export function Badge({ children, variant = 'info' }: BadgeProps) { + return {children}; +} diff --git a/servers/calendly/src/ui/react-app/src/components/Card.tsx b/servers/calendly/src/ui/react-app/src/components/Card.tsx new file mode 100644 index 0000000..0720b05 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/components/Card.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface CardProps { + title?: string; + children: React.ReactNode; + className?: string; +} + +export function Card({ title, children, className = '' }: CardProps) { + return ( +
+ {title &&
{title}
} + {children} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/src/hooks/useCallTool.ts b/servers/calendly/src/ui/react-app/src/hooks/useCallTool.ts new file mode 100644 index 0000000..f3ea6a9 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/hooks/useCallTool.ts @@ -0,0 +1,31 @@ +import { useState } from 'react'; + +export function useCallTool() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const callTool = async (name: string, args: any) => { + setLoading(true); + setError(null); + try { + const response = await fetch('/mcp/call-tool', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, arguments: args }), + }); + const data = await response.json(); + if (data.isError) { + throw new Error(data.content[0]?.text || 'Unknown error'); + } + return JSON.parse(data.content[0]?.text || '{}'); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + setError(message); + throw err; + } finally { + setLoading(false); + } + }; + + return { callTool, loading, error }; +} diff --git a/servers/calendly/src/ui/react-app/src/hooks/useDirtyState.ts b/servers/calendly/src/ui/react-app/src/hooks/useDirtyState.ts new file mode 100644 index 0000000..9488c05 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/hooks/useDirtyState.ts @@ -0,0 +1,18 @@ +import { useState, useCallback } from 'react'; + +export function useDirtyState(initialValue: T) { + const [value, setValue] = useState(initialValue); + const [dirty, setDirty] = useState(false); + + const updateValue = useCallback((newValue: T | ((prev: T) => T)) => { + setValue(newValue); + setDirty(true); + }, []); + + const reset = useCallback(() => { + setValue(initialValue); + setDirty(false); + }, [initialValue]); + + return { value, setValue: updateValue, dirty, reset }; +} diff --git a/servers/calendly/src/ui/react-app/src/styles/common.css b/servers/calendly/src/ui/react-app/src/styles/common.css new file mode 100644 index 0000000..16a26d7 --- /dev/null +++ b/servers/calendly/src/ui/react-app/src/styles/common.css @@ -0,0 +1,192 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background: var(--background, #ffffff); + color: var(--foreground, #000000); + line-height: 1.5; +} + +.app-container { + padding: 20px; + max-width: 1400px; + margin: 0 auto; +} + +.header { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border, #e0e0e0); +} + +.header h1 { + font-size: 24px; + font-weight: 600; + margin-bottom: 8px; +} + +.header p { + color: var(--muted-foreground, #666); + font-size: 14px; +} + +.card { + background: var(--card, #f9f9f9); + border: 1px solid var(--border, #e0e0e0); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.card-header { + font-size: 16px; + font-weight: 600; + margin-bottom: 12px; +} + +.grid { + display: grid; + gap: 16px; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.btn { + padding: 8px 16px; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: opacity 0.2s; +} + +.btn:hover { + opacity: 0.8; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary, #0066cc); + color: white; +} + +.btn-secondary { + background: var(--secondary, #6c757d); + color: white; +} + +.btn-danger { + background: var(--destructive, #dc3545); + color: white; +} + +.input { + padding: 8px 12px; + border: 1px solid var(--border, #e0e0e0); + border-radius: 6px; + font-size: 14px; + width: 100%; + background: var(--background, #ffffff); + color: var(--foreground, #000000); +} + +.label { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 6px; + color: var(--foreground, #000000); +} + +.error { + color: var(--destructive, #dc3545); + font-size: 14px; + margin-top: 8px; +} + +.loading { + text-align: center; + padding: 40px; + color: var(--muted-foreground, #666); +} + +.badge { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.badge-success { + background: #d4edda; + color: #155724; +} + +.badge-warning { + background: #fff3cd; + color: #856404; +} + +.badge-danger { + background: #f8d7da; + color: #721c24; +} + +.badge-info { + background: #d1ecf1; + color: #0c5460; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th, +.table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid var(--border, #e0e0e0); +} + +.table th { + font-weight: 600; + background: var(--muted, #f5f5f5); +} + +.table tr:hover { + background: var(--accent, #f0f0f0); +} + +.stat { + text-align: center; + padding: 16px; +} + +.stat-value { + font-size: 32px; + font-weight: 700; + color: var(--primary, #0066cc); +} + +.stat-label { + font-size: 14px; + color: var(--muted-foreground, #666); + margin-top: 4px; +} diff --git a/servers/calendly/src/ui/react-app/tsconfig.json b/servers/calendly/src/ui/react-app/tsconfig.json new file mode 100644 index 0000000..a4c834a --- /dev/null +++ b/servers/calendly/src/ui/react-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/servers/calendly/tsconfig.json b/servers/calendly/tsconfig.json index bf41b74..43e582d 100644 --- a/servers/calendly/tsconfig.json +++ b/servers/calendly/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "target": "ES2022", "module": "Node16", - "moduleResolution": "Node16", "lib": ["ES2022"], + "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, @@ -13,9 +13,8 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true, - "types": ["node"] + "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/ui"] + "exclude": ["node_modules", "dist", "tests", "src/ui"] }