calendly: complete MCP server with 40+ tools + 12 apps
This commit is contained in:
parent
df6c795500
commit
3244868c07
@ -1,151 +1,316 @@
|
|||||||
# Calendly MCP Server
|
# 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
|
## Features
|
||||||
|
|
||||||
### 🛠️ 27 MCP Tools
|
- ✅ **40+ MCP Tools** covering all Calendly API endpoints
|
||||||
|
- ✅ **12 React Apps** for interactive workflows
|
||||||
**Events (8 tools)**
|
- ✅ **Full TypeScript** with comprehensive type definitions
|
||||||
- `calendly_list_scheduled_events` - List events with filters
|
- ✅ **OAuth2 & API Key** authentication support
|
||||||
- `calendly_get_event` - Get event details
|
- ✅ **Rate Limiting** and error handling
|
||||||
- `calendly_cancel_event` - Cancel an event
|
- ✅ **Dual Transport** (stdio and HTTP)
|
||||||
- `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
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install @busybee3333/calendly-mcp
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Set your Calendly API key as an environment variable:
|
Set one of the following environment variables:
|
||||||
|
|
||||||
```bash
|
```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
|
## Usage
|
||||||
|
|
||||||
### Stdio Mode (Default for MCP)
|
### Stdio Transport (for Claude Desktop, etc.)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
calendly-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
Use in your MCP client configuration:
|
### Programmatic Usage
|
||||||
|
|
||||||
```json
|
```typescript
|
||||||
{
|
import { createCalendlyServer } from '@busybee3333/calendly-mcp';
|
||||||
"mcpServers": {
|
|
||||||
"calendly": {
|
const server = createCalendlyServer({
|
||||||
"command": "node",
|
apiKey: process.env.CALENDLY_API_KEY,
|
||||||
"args": ["/path/to/calendly/dist/main.js"],
|
});
|
||||||
"env": {
|
|
||||||
"CALENDLY_API_KEY": "your_api_key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### HTTP Mode
|
## MCP Tools (40+)
|
||||||
|
|
||||||
```bash
|
### Users (2 tools)
|
||||||
npm run start:http
|
|
||||||
|
| 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`
|
### Create Event Type
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
```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/
|
### List Upcoming Events
|
||||||
│ └── calendly.ts # Calendly API v2 client
|
|
||||||
├── tools/
|
```javascript
|
||||||
│ ├── events-tools.ts # Event management tools
|
const events = await callTool('calendly_list_events', {
|
||||||
│ ├── event-types-tools.ts # Event type tools
|
user: 'https://api.calendly.com/users/AAAA',
|
||||||
│ ├── scheduling-tools.ts # Scheduling & routing tools
|
min_start_time: new Date().toISOString(),
|
||||||
│ ├── users-tools.ts # User management tools
|
status: 'active',
|
||||||
│ ├── organizations-tools.ts # Organization tools
|
sort: 'start_time:asc'
|
||||||
│ └── 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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
## Development
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
@ -154,28 +319,63 @@ src/
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Watch Mode
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run React Apps
|
## Architecture
|
||||||
|
|
||||||
Each app is standalone:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd src/ui/react-app/event-dashboard
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
src/
|
||||||
## Resources
|
├── server.ts # MCP server setup
|
||||||
|
├── main.ts # Entry point
|
||||||
- Calendly API Documentation: https://developer.calendly.com/api-docs
|
├── clients/
|
||||||
- Model Context Protocol: https://modelcontextprotocol.io
|
│ └── calendly.ts # Calendly API client
|
||||||
- MCP SDK: https://github.com/modelcontextprotocol/typescript-sdk
|
├── 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
|
## License
|
||||||
|
|
||||||
MIT
|
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
|
||||||
|
|||||||
@ -1,33 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "@mcpengine/calendly-server",
|
"name": "@busybee3333/calendly-mcp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Complete Calendly MCP server with 30+ tools and React UI apps",
|
"description": "Complete MCP server for Calendly API with React apps",
|
||||||
"type": "module",
|
"main": "dist/server.js",
|
||||||
"main": "./dist/main.js",
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"calendly-mcp": "./dist/main.js"
|
"calendly-mcp": "dist/main.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && chmod +x dist/main.js",
|
"build": "tsc && npm run build:apps",
|
||||||
"dev": "tsc --watch",
|
"build:apps": "cd src/ui/react-app && npm run build",
|
||||||
"start": "node dist/main.js",
|
"prepublishOnly": "npm run build",
|
||||||
"start:http": "MCP_MODE=http node dist/main.js",
|
"test": "jest",
|
||||||
"test": "echo \"No tests yet\" && exit 0"
|
"dev": "tsx src/main.ts",
|
||||||
|
"watch": "tsc --watch"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
"calendly",
|
"calendly",
|
||||||
"scheduling",
|
"scheduling",
|
||||||
"model-context-protocol"
|
"calendar",
|
||||||
|
"automation"
|
||||||
],
|
],
|
||||||
"author": "MCPEngine",
|
"author": "BusyBee3333",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^20.10.0",
|
||||||
"typescript": "^5.7.3"
|
"@types/jest": "^29.5.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"typescript": "^5.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
@ -1,322 +1,436 @@
|
|||||||
// Calendly API v2 Client
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CalendlyConfig,
|
|
||||||
CalendlyUser,
|
CalendlyUser,
|
||||||
CalendlyEvent,
|
|
||||||
CalendlyEventType,
|
|
||||||
CalendlyInvitee,
|
|
||||||
CalendlyOrganization,
|
CalendlyOrganization,
|
||||||
CalendlyOrganizationMembership,
|
CalendlyEventType,
|
||||||
CalendlyOrganizationInvitation,
|
CalendlyEvent,
|
||||||
|
CalendlyInvitee,
|
||||||
|
CalendlyWebhook,
|
||||||
CalendlySchedulingLink,
|
CalendlySchedulingLink,
|
||||||
CalendlyWebhookSubscription,
|
|
||||||
CalendlyAvailableTime,
|
|
||||||
CalendlyUserBusyTime,
|
|
||||||
CalendlyRoutingForm,
|
CalendlyRoutingForm,
|
||||||
CalendlyNoShow,
|
CalendlyRoutingFormSubmission,
|
||||||
PaginationParams,
|
OrganizationInvitation,
|
||||||
|
OrganizationMembership,
|
||||||
|
ActivityLogEntry,
|
||||||
|
DataComplianceRequest,
|
||||||
|
UserAvailabilitySchedule,
|
||||||
PaginatedResponse,
|
PaginatedResponse,
|
||||||
CalendlyError,
|
CalendlyError,
|
||||||
|
NoShow,
|
||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
|
|
||||||
export class CalendlyClient {
|
export interface CalendlyClientConfig {
|
||||||
private apiKey: string;
|
apiKey?: string;
|
||||||
private baseUrl: string;
|
accessToken?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(config: CalendlyConfig) {
|
export class CalendlyClient {
|
||||||
this.apiKey = config.apiKey;
|
private client: AxiosInstance;
|
||||||
this.baseUrl = config.baseUrl || 'https://api.calendly.com';
|
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<T>(
|
private handleError(error: AxiosError): Error {
|
||||||
endpoint: string,
|
if (error.response) {
|
||||||
options: RequestInit = {}
|
const data = error.response.data as CalendlyError;
|
||||||
): Promise<T> {
|
const status = error.response.status;
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
|
||||||
|
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 = {
|
return new Error(`Network error: ${error.message}`);
|
||||||
'Authorization': `Bearer ${this.apiKey}`,
|
}
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
private async checkRateLimit(): Promise<void> {
|
||||||
const response = await fetch(url, {
|
if (this.rateLimitRemaining < 10) {
|
||||||
...options,
|
const waitTime = Math.max(0, this.rateLimitReset - Date.now());
|
||||||
headers,
|
if (waitTime > 0) {
|
||||||
});
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||||
|
|
||||||
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}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, any>): 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
|
// Users
|
||||||
async getCurrentUser(): Promise<{ resource: CalendlyUser }> {
|
async getCurrentUser(): Promise<CalendlyUser> {
|
||||||
return this.request('/users/me');
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get('/users/me');
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserByUri(uri: string): Promise<{ resource: CalendlyUser }> {
|
async getUser(userUri: string): Promise<CalendlyUser> {
|
||||||
return this.request(`/users/${encodeURIComponent(uri)}`);
|
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<CalendlyOrganization> {
|
||||||
|
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<PaginatedResponse<OrganizationInvitation>> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get('/organization_invitations', {
|
||||||
|
params: { organization: organizationUri, ...params },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationInvitation(invitationUri: string): Promise<OrganizationInvitation> {
|
||||||
|
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<OrganizationInvitation> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.post('/organization_invitations', {
|
||||||
|
organization: organizationUri,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return response.data.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeOrganizationInvitation(invitationUri: string): Promise<void> {
|
||||||
|
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<PaginatedResponse<OrganizationMembership>> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get('/organization_memberships', {
|
||||||
|
params: { organization: organizationUri, ...params },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationMembership(membershipUri: string): Promise<OrganizationMembership> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get(membershipUri.replace('https://api.calendly.com', ''));
|
||||||
|
return response.data.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeOrganizationMembership(membershipUri: string): Promise<void> {
|
||||||
|
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<PaginatedResponse<CalendlyEventType>> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get('/event_types', { params });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventType(eventTypeUri: string): Promise<CalendlyEventType> {
|
||||||
|
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<CalendlyEventType> {
|
||||||
|
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<CalendlyEventType> {
|
||||||
|
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<void> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
await this.client.delete(eventTypeUri.replace('https://api.calendly.com', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheduled Events
|
||||||
async listEvents(params: {
|
async listEvents(params: {
|
||||||
organization?: string;
|
|
||||||
user?: string;
|
user?: string;
|
||||||
|
organization?: string;
|
||||||
invitee_email?: string;
|
invitee_email?: string;
|
||||||
status?: 'active' | 'canceled';
|
status?: string;
|
||||||
min_start_time?: string;
|
min_start_time?: string;
|
||||||
max_start_time?: string;
|
max_start_time?: string;
|
||||||
count?: number;
|
count?: number;
|
||||||
page_token?: string;
|
page_token?: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
}): Promise<PaginatedResponse<CalendlyEvent>> {
|
}): Promise<PaginatedResponse<CalendlyEvent>> {
|
||||||
const query = this.buildQueryString(params);
|
await this.checkRateLimit();
|
||||||
return this.request(`/scheduled_events${query}`);
|
const response = await this.client.get('/scheduled_events', { params });
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvent(uuid: string): Promise<{ resource: CalendlyEvent }> {
|
async getEvent(eventUri: string): Promise<CalendlyEvent> {
|
||||||
return this.request(`/scheduled_events/${uuid}`);
|
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 }> {
|
async cancelEvent(eventUri: string, reason?: string): Promise<CalendlyEvent> {
|
||||||
return this.request(`/scheduled_events/${uuid}/cancellation`, {
|
await this.checkRateLimit();
|
||||||
method: 'POST',
|
const response = await this.client.post(
|
||||||
body: JSON.stringify({ reason: reason || 'Canceled' }),
|
`${eventUri.replace('https://api.calendly.com', '')}/cancellation`,
|
||||||
});
|
{ reason }
|
||||||
|
);
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event Invitees
|
// Invitees
|
||||||
async listEventInvitees(
|
async listEventInvitees(
|
||||||
eventUuid: string,
|
eventUri: string,
|
||||||
params?: PaginationParams
|
params?: { count?: number; email?: string; page_token?: string; sort?: string; status?: string }
|
||||||
): Promise<PaginatedResponse<CalendlyInvitee>> {
|
): Promise<PaginatedResponse<CalendlyInvitee>> {
|
||||||
const query = this.buildQueryString(params || {});
|
await this.checkRateLimit();
|
||||||
return this.request(`/scheduled_events/${eventUuid}/invitees${query}`);
|
const response = await this.client.get(
|
||||||
|
`${eventUri.replace('https://api.calendly.com', '')}/invitees`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInvitee(inviteeUuid: string): Promise<{ resource: CalendlyInvitee }> {
|
async getInvitee(inviteeUri: string): Promise<CalendlyInvitee> {
|
||||||
return this.request(`/scheduled_events/invitees/${inviteeUuid}`);
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get(inviteeUri.replace('https://api.calendly.com', ''));
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No-shows
|
// No-shows
|
||||||
async listInviteeNoShows(inviteeUri: string): Promise<PaginatedResponse<CalendlyNoShow>> {
|
async createNoShow(inviteeUri: string): Promise<NoShow> {
|
||||||
const query = this.buildQueryString({ invitee: inviteeUri });
|
await this.checkRateLimit();
|
||||||
return this.request(`/invitee_no_shows${query}`);
|
const response = await this.client.post('/invitee_no_shows', {
|
||||||
}
|
invitee: inviteeUri,
|
||||||
|
|
||||||
async createInviteeNoShow(inviteeUri: string): Promise<{ resource: CalendlyNoShow }> {
|
|
||||||
return this.request('/invitee_no_shows', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ invitee: inviteeUri }),
|
|
||||||
});
|
});
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteInviteeNoShow(noShowUuid: string): Promise<void> {
|
async getNoShow(noShowUri: string): Promise<NoShow> {
|
||||||
return this.request(`/invitee_no_shows/${noShowUuid}`, {
|
await this.checkRateLimit();
|
||||||
method: 'DELETE',
|
const response = await this.client.get(noShowUri.replace('https://api.calendly.com', ''));
|
||||||
});
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event Types
|
async deleteNoShow(noShowUri: string): Promise<void> {
|
||||||
async listEventTypes(params: {
|
await this.checkRateLimit();
|
||||||
organization?: string;
|
await this.client.delete(noShowUri.replace('https://api.calendly.com', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
async listWebhooks(
|
||||||
|
params: { organization: string; scope: string; count?: number; page_token?: string }
|
||||||
|
): Promise<PaginatedResponse<CalendlyWebhook>> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get('/webhook_subscriptions', { params });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebhook(webhookUri: string): Promise<CalendlyWebhook> {
|
||||||
|
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;
|
user?: string;
|
||||||
active?: boolean;
|
}): Promise<CalendlyWebhook> {
|
||||||
count?: number;
|
await this.checkRateLimit();
|
||||||
page_token?: string;
|
const response = await this.client.post('/webhook_subscriptions', params);
|
||||||
sort?: string;
|
return response.data.resource;
|
||||||
}): Promise<PaginatedResponse<CalendlyEventType>> {
|
|
||||||
const query = this.buildQueryString(params);
|
|
||||||
return this.request(`/event_types${query}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventType(uuid: string): Promise<{ resource: CalendlyEventType }> {
|
async deleteWebhook(webhookUri: string): Promise<void> {
|
||||||
return this.request(`/event_types/${uuid}`);
|
await this.checkRateLimit();
|
||||||
}
|
await this.client.delete(webhookUri.replace('https://api.calendly.com', ''));
|
||||||
|
|
||||||
async listAvailableTimes(
|
|
||||||
eventTypeUri: string,
|
|
||||||
params: {
|
|
||||||
start_time: string;
|
|
||||||
end_time: string;
|
|
||||||
}
|
|
||||||
): Promise<PaginatedResponse<CalendlyAvailableTime>> {
|
|
||||||
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<PaginatedResponse<CalendlyUserBusyTime>> {
|
|
||||||
const query = this.buildQueryString({
|
|
||||||
user: userUri,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
return this.request(`/user_busy_times${query}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scheduling Links
|
// Scheduling Links
|
||||||
async createSchedulingLink(params: {
|
async createSchedulingLink(params: {
|
||||||
max_event_count: number;
|
max_event_count: number;
|
||||||
owner: string;
|
owner: string;
|
||||||
owner_type: 'EventType' | 'Group';
|
owner_type: string;
|
||||||
}): Promise<{ resource: CalendlySchedulingLink }> {
|
}): Promise<CalendlySchedulingLink> {
|
||||||
return this.request('/scheduling_links', {
|
await this.checkRateLimit();
|
||||||
method: 'POST',
|
const response = await this.client.post('/scheduling_links', params);
|
||||||
body: JSON.stringify(params),
|
return response.data.resource;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<PaginatedResponse<CalendlyOrganizationMembership>> {
|
|
||||||
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<PaginatedResponse<CalendlyOrganizationInvitation>> {
|
|
||||||
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<void> {
|
|
||||||
return this.request(`/organization_memberships/${uuid}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routing Forms
|
// Routing Forms
|
||||||
async listRoutingForms(
|
async listRoutingForms(
|
||||||
organizationUri: string,
|
organizationUri: string,
|
||||||
params?: PaginationParams
|
params?: { count?: number; page_token?: string; sort?: string }
|
||||||
): Promise<PaginatedResponse<CalendlyRoutingForm>> {
|
): Promise<PaginatedResponse<CalendlyRoutingForm>> {
|
||||||
const query = this.buildQueryString({
|
await this.checkRateLimit();
|
||||||
organization: organizationUri,
|
const response = await this.client.get('/routing_forms', {
|
||||||
...params,
|
params: { organization: organizationUri, ...params },
|
||||||
});
|
});
|
||||||
return this.request(`/routing_forms${query}`);
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRoutingForm(uuid: string): Promise<{ resource: CalendlyRoutingForm }> {
|
async getRoutingForm(routingFormUri: string): Promise<CalendlyRoutingForm> {
|
||||||
return this.request(`/routing_forms/${uuid}`);
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get(routingFormUri.replace('https://api.calendly.com', ''));
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webhooks
|
async listRoutingFormSubmissions(
|
||||||
async listWebhookSubscriptions(
|
routingFormUri: string,
|
||||||
params: {
|
params?: { count?: number; page_token?: string; sort?: string }
|
||||||
organization: string;
|
): Promise<PaginatedResponse<CalendlyRoutingFormSubmission>> {
|
||||||
scope: 'user' | 'organization';
|
await this.checkRateLimit();
|
||||||
user?: string;
|
const response = await this.client.get(
|
||||||
} & PaginationParams
|
`${routingFormUri.replace('https://api.calendly.com', '')}/submissions`,
|
||||||
): Promise<PaginatedResponse<CalendlyWebhookSubscription>> {
|
{ params }
|
||||||
const query = this.buildQueryString(params);
|
);
|
||||||
return this.request(`/webhook_subscriptions${query}`);
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createWebhookSubscription(params: {
|
async getRoutingFormSubmission(submissionUri: string): Promise<CalendlyRoutingFormSubmission> {
|
||||||
url: string;
|
await this.checkRateLimit();
|
||||||
events: string[];
|
const response = await this.client.get(submissionUri.replace('https://api.calendly.com', ''));
|
||||||
organization: string;
|
return response.data.resource;
|
||||||
user?: string;
|
}
|
||||||
scope: 'user' | 'organization';
|
|
||||||
signing_key?: string;
|
// User Availability Schedules
|
||||||
}): Promise<{ resource: CalendlyWebhookSubscription }> {
|
async listUserAvailabilitySchedules(
|
||||||
return this.request('/webhook_subscriptions', {
|
userUri: string
|
||||||
method: 'POST',
|
): Promise<PaginatedResponse<UserAvailabilitySchedule>> {
|
||||||
body: JSON.stringify(params),
|
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 }> {
|
async getUserAvailabilitySchedule(scheduleUri: string): Promise<UserAvailabilitySchedule> {
|
||||||
return this.request(`/webhook_subscriptions/${uuid}`);
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get(scheduleUri.replace('https://api.calendly.com', ''));
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWebhookSubscription(uuid: string): Promise<void> {
|
// Activity Log
|
||||||
return this.request(`/webhook_subscriptions/${uuid}`, {
|
async getActivityLog(
|
||||||
method: 'DELETE',
|
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<PaginatedResponse<ActivityLogEntry>> {
|
||||||
|
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<DataComplianceRequest> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.post('/data_compliance/deletion/invitees', {
|
||||||
|
emails,
|
||||||
|
});
|
||||||
|
return response.data.resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDataComplianceDeletion(requestUri: string): Promise<DataComplianceRequest> {
|
||||||
|
await this.checkRateLimit();
|
||||||
|
const response = await this.client.get(requestUri.replace('https://api.calendly.com', ''));
|
||||||
|
return response.data.resource;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,101 +1,15 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import { runStdioServer } from './server.js';
|
||||||
|
|
||||||
// Calendly MCP Server - Main Entry Point
|
const apiKey = process.env.CALENDLY_API_KEY;
|
||||||
// Supports both stdio and HTTP modes
|
const accessToken = process.env.CALENDLY_ACCESS_TOKEN;
|
||||||
|
|
||||||
import { createCalendlyServer, runStdioServer } from './server.js';
|
if (!apiKey && !accessToken) {
|
||||||
import { createServer } from 'http';
|
console.error('Error: CALENDLY_API_KEY or CALENDLY_ACCESS_TOKEN environment variable must be set');
|
||||||
|
|
||||||
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');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
runStdioServer({ apiKey, accessToken }).catch((error) => {
|
||||||
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) => {
|
|
||||||
console.error('Fatal error:', error);
|
console.error('Fatal error:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,83 +1,87 @@
|
|||||||
// Calendly MCP Server
|
|
||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||||
CallToolRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
ListResourcesRequestSchema,
|
|
||||||
ReadResourceRequestSchema,
|
|
||||||
} from '@modelcontextprotocol/sdk/types.js';
|
|
||||||
|
|
||||||
import { CalendlyClient } from './clients/calendly.js';
|
import { CalendlyClient } from './clients/calendly.js';
|
||||||
import { createEventsTools } from './tools/events-tools.js';
|
import { registerUsersTools } from './tools/users-tools.js';
|
||||||
import { createEventTypesTools } from './tools/event-types-tools.js';
|
import { registerOrganizationsTools } from './tools/organizations-tools.js';
|
||||||
import { createSchedulingTools } from './tools/scheduling-tools.js';
|
import { registerEventTypesTools } from './tools/event-types-tools.js';
|
||||||
import { createUsersTools } from './tools/users-tools.js';
|
import { registerEventsTools } from './tools/events-tools.js';
|
||||||
import { createOrganizationsTools } from './tools/organizations-tools.js';
|
import { registerInviteesTools } from './tools/invitees-tools.js';
|
||||||
import { createWebhooksTools } from './tools/webhooks-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(
|
const server = new Server(
|
||||||
{
|
{
|
||||||
name: 'calendly-mcp-server',
|
name: 'calendly-mcp',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
capabilities: {
|
capabilities: {
|
||||||
tools: {},
|
tools: {},
|
||||||
resources: {},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize Calendly client
|
// Initialize Calendly client
|
||||||
const client = new CalendlyClient({
|
const calendly = new CalendlyClient({
|
||||||
apiKey,
|
apiKey: config.apiKey,
|
||||||
|
accessToken: config.accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect all tools
|
// Register all tools
|
||||||
const allTools = {
|
const allTools = [
|
||||||
...createEventsTools(client),
|
...registerUsersTools(calendly),
|
||||||
...createEventTypesTools(client),
|
...registerOrganizationsTools(calendly),
|
||||||
...createSchedulingTools(client),
|
...registerEventTypesTools(calendly),
|
||||||
...createUsersTools(client),
|
...registerEventsTools(calendly),
|
||||||
...createOrganizationsTools(client),
|
...registerInviteesTools(calendly),
|
||||||
...createWebhooksTools(client),
|
...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
|
// List tools handler
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
return {
|
return {
|
||||||
tools: Object.entries(allTools).map(([name, tool]) => ({
|
tools: allTools.map((tool) => ({
|
||||||
name,
|
name: tool.name,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: tool.parameters,
|
inputSchema: tool.inputSchema,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call tool handler
|
// Call tool handler
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
const toolName = request.params.name;
|
const tool = toolsMap.get(request.params.name);
|
||||||
const tool = allTools[toolName as keyof typeof allTools];
|
|
||||||
|
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
throw new Error(`Unknown tool: ${toolName}`);
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await tool.handler(request.params.arguments || {});
|
const result = await tool.handler(request.params.arguments as any || {});
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify({
|
text: `Error: ${errorMessage}`,
|
||||||
error: errorMessage,
|
|
||||||
tool: toolName,
|
|
||||||
}, null, 2),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
isError: true,
|
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;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runStdioServer(apiKey: string) {
|
export async function runStdioServer(config: ServerConfig) {
|
||||||
const server = createCalendlyServer(apiKey);
|
const server = createCalendlyServer(config);
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
console.error('Calendly MCP Server running on stdio');
|
|
||||||
|
console.error('Calendly MCP server running on stdio');
|
||||||
}
|
}
|
||||||
|
|||||||
43
servers/calendly/src/tools/availability-tools.ts
Normal file
43
servers/calendly/src/tools/availability-tools.ts
Normal file
@ -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),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
82
servers/calendly/src/tools/compliance-tools.ts
Normal file
82
servers/calendly/src/tools/compliance-tools.ts
Normal file
@ -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),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -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) {
|
export function registerEventTypesTools(calendly: CalendlyClient) {
|
||||||
return {
|
return [
|
||||||
calendly_list_event_types: {
|
{
|
||||||
description: 'List event types for a user or organization',
|
name: 'calendly_list_event_types',
|
||||||
parameters: {
|
description: 'List all event types for a user or organization',
|
||||||
type: 'object',
|
inputSchema: z.object({
|
||||||
properties: {
|
user: z.string().optional().describe('Filter by user URI'),
|
||||||
organization: {
|
organization: z.string().optional().describe('Filter by organization URI'),
|
||||||
type: 'string',
|
count: z.number().optional().describe('Number of results per page (max 100)'),
|
||||||
description: 'Organization URI to filter by',
|
page_token: z.string().optional().describe('Token for pagination'),
|
||||||
},
|
sort: z.string().optional().describe('Sort field and direction (e.g., name:asc)'),
|
||||||
user: {
|
active: z.boolean().optional().describe('Filter by active status'),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'User URI to filter by',
|
handler: async (args: {
|
||||||
},
|
user?: string;
|
||||||
active: {
|
organization?: string;
|
||||||
type: 'boolean',
|
count?: number;
|
||||||
description: 'Filter by active status',
|
page_token?: string;
|
||||||
},
|
sort?: string;
|
||||||
count: {
|
active?: boolean;
|
||||||
type: 'number',
|
}) => {
|
||||||
description: 'Number of results per page (max 100)',
|
const result = await calendly.listEventTypes(args);
|
||||||
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);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -48,66 +43,124 @@ export function createEventTypesTools(client: CalendlyClient) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_get_event_type: {
|
name: 'calendly_get_event_type',
|
||||||
description: 'Get details of a specific event type by UUID',
|
description: 'Get details of a specific event type',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
event_type_uri: z.string().describe('The URI of the event type'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { event_type_uri: string }) => {
|
||||||
type: 'string',
|
const eventType = await calendly.getEventType(args.event_type_uri);
|
||||||
description: 'Event type UUID',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.getEventType(args.uuid);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify(result, null, 2),
|
text: JSON.stringify(eventType, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_list_available_times: {
|
name: 'calendly_create_event_type',
|
||||||
description: 'List available time slots for an event type within a date range',
|
description: 'Create a new event type',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
name: z.string().describe('Name of the event type'),
|
||||||
properties: {
|
duration: z.number().describe('Duration in minutes'),
|
||||||
event_type_uri: {
|
owner: z.string().describe('User URI of the event type owner'),
|
||||||
type: 'string',
|
type: z.string().optional().describe('Type of event (default: StandardEventType)'),
|
||||||
description: 'Event type URI',
|
kind: z.string().optional().describe('Kind of event: solo, group, collective, round_robin'),
|
||||||
},
|
description_plain: z.string().optional().describe('Plain text description'),
|
||||||
start_time: {
|
description_html: z.string().optional().describe('HTML description'),
|
||||||
type: 'string',
|
color: z.string().optional().describe('Color hex code (e.g., #0000ff)'),
|
||||||
description: 'Start of range (ISO 8601)',
|
internal_note: z.string().optional().describe('Internal note for team members'),
|
||||||
},
|
secret: z.boolean().optional().describe('Whether the event type is secret'),
|
||||||
end_time: {
|
custom_questions: z.array(customQuestionSchema).optional().describe('Custom questions to ask invitees'),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'End of range (ISO 8601)',
|
handler: async (args: {
|
||||||
},
|
name: string;
|
||||||
},
|
duration: number;
|
||||||
required: ['event_type_uri', 'start_time', 'end_time'],
|
owner: string;
|
||||||
},
|
type?: string;
|
||||||
handler: async (args: any) => {
|
kind?: string;
|
||||||
const result = await client.listAvailableTimes(args.event_type_uri, {
|
description_plain?: string;
|
||||||
start_time: args.start_time,
|
description_html?: string;
|
||||||
end_time: args.end_time,
|
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 {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 registerEventsTools(calendly: CalendlyClient) {
|
||||||
|
return [
|
||||||
export function createEventsTools(client: CalendlyClient) {
|
{
|
||||||
return {
|
name: 'calendly_list_events',
|
||||||
calendly_list_scheduled_events: {
|
description: 'List scheduled events with various filters',
|
||||||
description: 'List scheduled events with filters (organization, user, status, date range)',
|
inputSchema: z.object({
|
||||||
parameters: {
|
user: z.string().optional().describe('Filter by user URI'),
|
||||||
type: 'object',
|
organization: z.string().optional().describe('Filter by organization URI'),
|
||||||
properties: {
|
invitee_email: z.string().optional().describe('Filter by invitee email'),
|
||||||
organization: {
|
status: z.string().optional().describe('Filter by status: active or canceled'),
|
||||||
type: 'string',
|
min_start_time: z.string().optional().describe('Minimum start time (ISO 8601 format)'),
|
||||||
description: 'Organization URI to filter by',
|
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)'),
|
||||||
user: {
|
page_token: z.string().optional().describe('Token for pagination'),
|
||||||
type: 'string',
|
sort: z.string().optional().describe('Sort field and direction (e.g., start_time:asc)'),
|
||||||
description: 'User URI to filter by',
|
}),
|
||||||
},
|
handler: async (args: {
|
||||||
invitee_email: {
|
user?: string;
|
||||||
type: 'string',
|
organization?: string;
|
||||||
description: 'Filter by invitee email',
|
invitee_email?: string;
|
||||||
},
|
status?: string;
|
||||||
status: {
|
min_start_time?: string;
|
||||||
type: 'string',
|
max_start_time?: string;
|
||||||
enum: ['active', 'canceled'],
|
count?: number;
|
||||||
description: 'Event status filter',
|
page_token?: string;
|
||||||
},
|
sort?: string;
|
||||||
min_start_time: {
|
}) => {
|
||||||
type: 'string',
|
const result = await calendly.listEvents(args);
|
||||||
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);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -61,202 +39,42 @@ export function createEventsTools(client: CalendlyClient) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_get_event: {
|
name: 'calendly_get_event',
|
||||||
description: 'Get details of a specific scheduled event by UUID',
|
description: 'Get details of a specific scheduled event',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
event_uri: z.string().describe('The URI of the scheduled event'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { event_uri: string }) => {
|
||||||
type: 'string',
|
const event = await calendly.getEvent(args.event_uri);
|
||||||
description: 'Event UUID (from event URI)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.getEvent(args.uuid);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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',
|
description: 'Cancel a scheduled event',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
event_uri: z.string().describe('The URI of the event to cancel'),
|
||||||
properties: {
|
reason: z.string().optional().describe('Reason for cancellation'),
|
||||||
uuid: {
|
}),
|
||||||
type: 'string',
|
handler: async (args: { event_uri: string; reason?: string }) => {
|
||||||
description: 'Event UUID to cancel',
|
const event = await calendly.cancelEvent(args.event_uri, args.reason);
|
||||||
},
|
|
||||||
reason: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Cancellation reason',
|
|
||||||
default: 'Event canceled',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.cancelEvent(args.uuid, args.reason);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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' }),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
110
servers/calendly/src/tools/invitees-tools.ts
Normal file
110
servers/calendly/src/tools/invitees-tools.ts
Normal file
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -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 registerOrganizationsTools(calendly: CalendlyClient) {
|
||||||
|
return [
|
||||||
export function createOrganizationsTools(client: CalendlyClient) {
|
{
|
||||||
return {
|
name: 'calendly_get_organization',
|
||||||
calendly_get_organization: {
|
description: 'Get information about a specific organization by URI',
|
||||||
description: 'Get organization details by UUID',
|
inputSchema: z.object({
|
||||||
parameters: {
|
organization_uri: z.string().describe('The URI of the organization'),
|
||||||
type: 'object',
|
}),
|
||||||
properties: {
|
handler: async (args: { organization_uri: string }) => {
|
||||||
uuid: {
|
const org = await calendly.getOrganization(args.organization_uri);
|
||||||
type: 'string',
|
return {
|
||||||
description: 'Organization UUID',
|
content: [
|
||||||
},
|
{
|
||||||
},
|
type: 'text',
|
||||||
required: ['uuid'],
|
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 {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -28,115 +52,33 @@ export function createOrganizationsTools(client: CalendlyClient) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_list_organization_members: {
|
name: 'calendly_get_organization_invitation',
|
||||||
description: 'List members of an organization',
|
description: 'Get details of a specific organization invitation',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
invitation_uri: z.string().describe('The URI of the invitation'),
|
||||||
properties: {
|
}),
|
||||||
organization_uri: {
|
handler: async (args: { invitation_uri: string }) => {
|
||||||
type: 'string',
|
const invitation = await calendly.getOrganizationInvitation(args.invitation_uri);
|
||||||
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,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify(result, null, 2),
|
text: JSON.stringify(invitation, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_list_organization_invitations: {
|
name: 'calendly_create_organization_invitation',
|
||||||
description: 'List pending invitations for an organization',
|
description: 'Invite a user to join an organization',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
organization_uri: z.string().describe('The URI of the organization'),
|
||||||
properties: {
|
email: z.string().email().describe('Email address of the person to invite'),
|
||||||
organization_uri: {
|
}),
|
||||||
type: 'string',
|
handler: async (args: { organization_uri: string; email: string }) => {
|
||||||
description: 'Organization URI',
|
const invitation = await calendly.createOrganizationInvitation(
|
||||||
},
|
|
||||||
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(
|
|
||||||
args.organization_uri,
|
args.organization_uri,
|
||||||
args.email
|
args.email
|
||||||
);
|
);
|
||||||
@ -144,27 +86,49 @@ export function createOrganizationsTools(client: CalendlyClient) {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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',
|
description: 'Revoke a pending organization invitation',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
invitation_uri: z.string().describe('The URI of the invitation to revoke'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { invitation_uri: string }) => {
|
||||||
type: 'string',
|
await calendly.revokeOrganizationInvitation(args.invitation_uri);
|
||||||
description: 'Invitation UUID to revoke',
|
return {
|
||||||
},
|
content: [
|
||||||
},
|
{
|
||||||
required: ['uuid'],
|
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 {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -175,30 +139,41 @@ export function createOrganizationsTools(client: CalendlyClient) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_remove_organization_member: {
|
name: 'calendly_get_organization_membership',
|
||||||
description: 'Remove a member from an organization',
|
description: 'Get details of a specific organization membership',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
membership_uri: z.string().describe('The URI of the membership'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { membership_uri: string }) => {
|
||||||
type: 'string',
|
const membership = await calendly.getOrganizationMembership(args.membership_uri);
|
||||||
description: 'Organization membership UUID to remove',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
await client.removeOrganizationMembership(args.uuid);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
97
servers/calendly/src/tools/routing-forms-tools.ts
Normal file
97
servers/calendly/src/tools/routing-forms-tools.ts
Normal file
@ -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),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -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 registerSchedulingTools(calendly: CalendlyClient) {
|
||||||
|
return [
|
||||||
export function createSchedulingTools(client: CalendlyClient) {
|
{
|
||||||
return {
|
name: 'calendly_create_scheduling_link',
|
||||||
calendly_create_scheduling_link: {
|
description: 'Create a single-use scheduling link for an event type or user',
|
||||||
description: 'Create a single-use scheduling link for an event type or group',
|
inputSchema: z.object({
|
||||||
parameters: {
|
max_event_count: z.number().describe('Maximum number of events that can be scheduled'),
|
||||||
type: 'object',
|
owner: z.string().describe('URI of the event type or user'),
|
||||||
properties: {
|
owner_type: z.enum(['EventType', 'User']).describe('Type of owner'),
|
||||||
owner: {
|
}),
|
||||||
type: 'string',
|
handler: async (args: {
|
||||||
description: 'Owner URI (event type or group)',
|
max_event_count: number;
|
||||||
},
|
owner: string;
|
||||||
owner_type: {
|
owner_type: string;
|
||||||
type: 'string',
|
}) => {
|
||||||
enum: ['EventType', 'Group'],
|
const link = await calendly.createSchedulingLink(args);
|
||||||
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,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 registerUsersTools(calendly: CalendlyClient) {
|
||||||
|
return [
|
||||||
export function createUsersTools(client: CalendlyClient) {
|
{
|
||||||
return {
|
name: 'calendly_get_current_user',
|
||||||
calendly_get_current_user: {
|
description: 'Get information about the currently authenticated user',
|
||||||
description: 'Get the currently authenticated user information',
|
inputSchema: z.object({}),
|
||||||
parameters: {
|
handler: async () => {
|
||||||
type: 'object',
|
const user = await calendly.getCurrentUser();
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.getCurrentUser();
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify(result, null, 2),
|
text: JSON.stringify(user, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_get_user: {
|
name: 'calendly_get_user',
|
||||||
description: 'Get user information by URI',
|
description: 'Get information about a specific user by URI',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
user_uri: z.string().describe('The URI of the user to retrieve'),
|
||||||
properties: {
|
}),
|
||||||
uri: {
|
handler: async (args: { user_uri: string }) => {
|
||||||
type: 'string',
|
const user = await calendly.getUser(args.user_uri);
|
||||||
description: 'User URI',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uri'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.getUserByUri(args.uri);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 registerWebhooksTools(calendly: CalendlyClient) {
|
||||||
|
return [
|
||||||
export function createWebhooksTools(client: CalendlyClient) {
|
{
|
||||||
return {
|
name: 'calendly_list_webhooks',
|
||||||
calendly_list_webhook_subscriptions: {
|
description: 'List all webhook subscriptions for an organization',
|
||||||
description: 'List webhook subscriptions for an organization',
|
inputSchema: z.object({
|
||||||
parameters: {
|
organization: z.string().describe('The URI of the organization'),
|
||||||
type: 'object',
|
scope: z.enum(['organization', 'user']).describe('Scope of webhooks to list'),
|
||||||
properties: {
|
count: z.number().optional().describe('Number of results per page (max 100)'),
|
||||||
organization: {
|
page_token: z.string().optional().describe('Token for pagination'),
|
||||||
type: 'string',
|
}),
|
||||||
description: 'Organization URI',
|
handler: async (args: {
|
||||||
},
|
organization: string;
|
||||||
scope: {
|
scope: string;
|
||||||
type: 'string',
|
count?: number;
|
||||||
enum: ['user', 'organization'],
|
page_token?: string;
|
||||||
description: 'Webhook scope',
|
}) => {
|
||||||
},
|
const result = await calendly.listWebhooks(args);
|
||||||
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,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -52,111 +29,81 @@ export function createWebhooksTools(client: CalendlyClient) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
calendly_create_webhook_subscription: {
|
name: 'calendly_get_webhook',
|
||||||
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: {
|
|
||||||
description: 'Get details of a specific webhook subscription',
|
description: 'Get details of a specific webhook subscription',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
webhook_uri: z.string().describe('The URI of the webhook subscription'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { webhook_uri: string }) => {
|
||||||
type: 'string',
|
const webhook = await calendly.getWebhook(args.webhook_uri);
|
||||||
description: 'Webhook subscription UUID',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
const result = await client.getWebhookSubscription(args.uuid);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
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',
|
description: 'Delete a webhook subscription',
|
||||||
parameters: {
|
inputSchema: z.object({
|
||||||
type: 'object',
|
webhook_uri: z.string().describe('The URI of the webhook subscription to delete'),
|
||||||
properties: {
|
}),
|
||||||
uuid: {
|
handler: async (args: { webhook_uri: string }) => {
|
||||||
type: 'string',
|
await calendly.deleteWebhook(args.webhook_uri);
|
||||||
description: 'Webhook subscription UUID to delete',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['uuid'],
|
|
||||||
},
|
|
||||||
handler: async (args: any) => {
|
|
||||||
await client.deleteWebhookSubscription(args.uuid);
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: JSON.stringify({ success: true, message: 'Webhook subscription deleted' }),
|
text: 'Webhook subscription deleted successfully',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
// Calendly API v2 Types
|
// Calendly API v2 TypeScript Types
|
||||||
|
|
||||||
export interface CalendlyConfig {
|
|
||||||
apiKey: string;
|
|
||||||
baseUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CalendlyUser {
|
export interface CalendlyUser {
|
||||||
uri: string;
|
uri: string;
|
||||||
@ -12,46 +7,21 @@ export interface CalendlyUser {
|
|||||||
email: string;
|
email: string;
|
||||||
scheduling_url: string;
|
scheduling_url: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
avatar_url: string;
|
avatar_url?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
current_organization: string;
|
current_organization: string;
|
||||||
resource_type: string;
|
resource_type: 'User';
|
||||||
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlyEvent {
|
export interface CalendlyOrganization {
|
||||||
uri: string;
|
uri: string;
|
||||||
name: string;
|
name: string;
|
||||||
meeting_notes_plain: string;
|
slug: 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;
|
|
||||||
};
|
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
event_memberships: Array<{
|
resource_type: 'Organization';
|
||||||
user: string;
|
|
||||||
}>;
|
|
||||||
event_guests: Array<{
|
|
||||||
email: string;
|
|
||||||
created_at: string;
|
|
||||||
}>;
|
|
||||||
cancellation?: {
|
|
||||||
canceled_by: string;
|
|
||||||
reason: string;
|
|
||||||
canceler_type: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlyEventType {
|
export interface CalendlyEventType {
|
||||||
@ -62,44 +32,101 @@ export interface CalendlyEventType {
|
|||||||
scheduling_url: string;
|
scheduling_url: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
kind: 'solo' | 'group' | 'collective' | 'round_robin';
|
kind: 'solo' | 'group' | 'collective' | 'round_robin';
|
||||||
pooling_type?: string;
|
pooling_type?: 'round_robin' | 'collective' | null;
|
||||||
type: 'StandardEventType' | 'AdhocEventType';
|
type: 'StandardEventType' | 'CustomEventType';
|
||||||
color: string;
|
color: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
internal_note: string;
|
internal_note?: string;
|
||||||
description_plain: string;
|
description_plain?: string;
|
||||||
description_html: string;
|
description_html?: string;
|
||||||
profile: {
|
profile: {
|
||||||
type: string;
|
type: 'User' | 'Team';
|
||||||
name: string;
|
name: string;
|
||||||
owner: string;
|
owner: string;
|
||||||
};
|
};
|
||||||
secret: boolean;
|
secret: boolean;
|
||||||
booking_method: string;
|
booking_method: 'instant' | 'poll';
|
||||||
custom_questions: Array<{
|
custom_questions?: CustomQuestion[];
|
||||||
name: string;
|
deleted_at?: string;
|
||||||
type: string;
|
admin_managed?: boolean;
|
||||||
position: number;
|
resource_type: 'EventType';
|
||||||
enabled: boolean;
|
}
|
||||||
required: boolean;
|
|
||||||
answer_choices: string[];
|
export interface CustomQuestion {
|
||||||
include_other: boolean;
|
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<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface CalendlyInvitee {
|
||||||
uri: string;
|
uri: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
first_name: string;
|
first_name?: string;
|
||||||
last_name: string;
|
last_name?: string;
|
||||||
status: 'active' | 'canceled';
|
status: 'active' | 'canceled';
|
||||||
|
questions_and_answers?: QuestionAnswer[];
|
||||||
timezone: string;
|
timezone: string;
|
||||||
event: string;
|
event: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
tracking: {
|
tracking?: {
|
||||||
utm_campaign?: string;
|
utm_campaign?: string;
|
||||||
utm_source?: string;
|
utm_source?: string;
|
||||||
utm_medium?: string;
|
utm_medium?: string;
|
||||||
@ -107,21 +134,13 @@ export interface CalendlyInvitee {
|
|||||||
utm_term?: string;
|
utm_term?: string;
|
||||||
salesforce_uuid?: string;
|
salesforce_uuid?: string;
|
||||||
};
|
};
|
||||||
text_reminder_number: string;
|
text_reminder_number?: string;
|
||||||
rescheduled: boolean;
|
rescheduled: boolean;
|
||||||
old_invitee: string;
|
old_invitee?: string;
|
||||||
new_invitee: string;
|
new_invitee?: string;
|
||||||
cancel_url: string;
|
cancel_url: string;
|
||||||
reschedule_url: string;
|
reschedule_url: string;
|
||||||
questions_and_answers: Array<{
|
cancellation?: InviteeCancellation;
|
||||||
question: string;
|
|
||||||
answer: string;
|
|
||||||
position: number;
|
|
||||||
}>;
|
|
||||||
cancellation?: {
|
|
||||||
canceled_by: string;
|
|
||||||
reason: string;
|
|
||||||
};
|
|
||||||
payment?: {
|
payment?: {
|
||||||
id: string;
|
id: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
@ -130,46 +149,118 @@ export interface CalendlyInvitee {
|
|||||||
terms: string;
|
terms: string;
|
||||||
successful: boolean;
|
successful: boolean;
|
||||||
};
|
};
|
||||||
no_show?: {
|
no_show?: NoShow;
|
||||||
created_at: string;
|
|
||||||
};
|
|
||||||
reconfirmation?: {
|
reconfirmation?: {
|
||||||
created_at: string;
|
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;
|
uri: string;
|
||||||
name: string;
|
created_at: string;
|
||||||
slug: string;
|
}
|
||||||
status: string;
|
|
||||||
timezone: string;
|
export interface CalendlyWebhook {
|
||||||
|
uri: string;
|
||||||
|
callback_url: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_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;
|
uri: string;
|
||||||
role: 'owner' | 'admin' | 'user';
|
name: string;
|
||||||
user: {
|
|
||||||
uri: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
email: string;
|
|
||||||
scheduling_url: string;
|
|
||||||
timezone: string;
|
|
||||||
avatar_url: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
organization: string;
|
organization: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_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;
|
uri: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -177,79 +268,58 @@ export interface CalendlyOrganizationInvitation {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
last_sent_at: string;
|
last_sent_at: string;
|
||||||
|
resource_type: 'OrganizationInvitation';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlySchedulingLink {
|
export interface OrganizationMembership {
|
||||||
booking_url: string;
|
|
||||||
owner: string;
|
|
||||||
owner_type: string;
|
|
||||||
resource_type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CalendlyWebhookSubscription {
|
|
||||||
uri: string;
|
uri: string;
|
||||||
callback_url: string;
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
user: CalendlyUser;
|
||||||
|
organization: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
retry_started_at: string;
|
resource_type: 'OrganizationMembership';
|
||||||
state: 'active' | 'disabled';
|
}
|
||||||
events: string[];
|
|
||||||
scope: 'user' | 'organization';
|
export interface ActivityLogEntry {
|
||||||
|
occurred_at: string;
|
||||||
|
action: string;
|
||||||
|
actor: string;
|
||||||
|
namespace: string;
|
||||||
|
details?: string;
|
||||||
organization: string;
|
organization: string;
|
||||||
user: string;
|
|
||||||
creator: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlyAvailableTime {
|
export interface DataComplianceRequest {
|
||||||
status: 'available';
|
uri: string;
|
||||||
invitees_remaining: number;
|
emails: string[];
|
||||||
start_time: string;
|
status: 'pending' | 'completed' | 'failed';
|
||||||
scheduling_url: string;
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlyUserBusyTime {
|
export interface AvailabilityRule {
|
||||||
start_time: string;
|
type: 'wday' | 'date';
|
||||||
end_time: string;
|
wday?: string;
|
||||||
type: 'calendly' | 'busy_calendar';
|
date?: string;
|
||||||
buffered: boolean;
|
intervals: TimeInterval[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalendlyRoutingForm {
|
export interface TimeInterval {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAvailabilitySchedule {
|
||||||
uri: string;
|
uri: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'published' | 'draft';
|
user: string;
|
||||||
published_version: number;
|
timezone: string;
|
||||||
organization: string;
|
rules: AvailabilityRule[];
|
||||||
|
default: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
questions: Array<{
|
resource_type: 'UserAvailabilitySchedule';
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationParams {
|
export interface PaginationParams {
|
||||||
@ -277,3 +347,56 @@ export interface CalendlyError {
|
|||||||
message: string;
|
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[];
|
||||||
|
}
|
||||||
|
|||||||
19
servers/calendly/src/ui/react-app/build-all.js
vendored
Normal file
19
servers/calendly/src/ui/react-app/build-all.js
vendored
Normal file
@ -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!');
|
||||||
20
servers/calendly/src/ui/react-app/package.json
Normal file
20
servers/calendly/src/ui/react-app/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Analytics Dashboard</h1>
|
||||||
|
<p>Overview of your Calendly metrics</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<div className="grid grid-3">
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.totalEvents}</div>
|
||||||
|
<div className="stat-label">Total Events</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.activeEvents}</div>
|
||||||
|
<div className="stat-label">Active Events</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.canceledEvents}</div>
|
||||||
|
<div className="stat-label">Canceled Events</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.totalInvitees}</div>
|
||||||
|
<div className="stat-label">Total Invitees</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.eventTypes}</div>
|
||||||
|
<div className="stat-label">Event Types</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{stats.noShows}</div>
|
||||||
|
<div className="stat-label">No-Shows</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>analytics-dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
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<string, string> = {
|
||||||
|
sunday: 'Sunday',
|
||||||
|
monday: 'Monday',
|
||||||
|
tuesday: 'Tuesday',
|
||||||
|
wednesday: 'Wednesday',
|
||||||
|
thursday: 'Thursday',
|
||||||
|
friday: 'Friday',
|
||||||
|
saturday: 'Saturday',
|
||||||
|
};
|
||||||
|
return days[wday] || wday;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Availability Manager</h1>
|
||||||
|
<p>Manage your availability schedules</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
{schedules.map((schedule) => (
|
||||||
|
<Card key={schedule.uri}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<h3 style={{ display: 'inline', marginRight: '8px' }}>{schedule.name}</h3>
|
||||||
|
{schedule.default && <Badge variant="success">Default</Badge>}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '14px', color: '#666', marginBottom: '16px' }}>
|
||||||
|
Timezone: {schedule.timezone}
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<strong style={{ fontSize: '14px' }}>Availability Rules:</strong>
|
||||||
|
{schedule.rules.map((rule: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
marginTop: '8px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rule.type === 'wday' ? (
|
||||||
|
<div style={{ fontWeight: '500', marginBottom: '4px' }}>
|
||||||
|
{getDayName(rule.wday)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontWeight: '500', marginBottom: '4px' }}>
|
||||||
|
Date: {rule.date}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{rule.intervals.map((interval: any, i: number) => (
|
||||||
|
<div key={i} style={{ fontSize: '14px', color: '#666', marginLeft: '12px' }}>
|
||||||
|
{formatTime(interval.from)} - {formatTime(interval.to)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{schedules.length === 0 && !loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No availability schedules found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>availability-manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
const [selectedEventType, setSelectedEventType] = useState<any>(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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Booking Page Preview</h1>
|
||||||
|
<p>Preview your event type booking pages</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<label className="label">Select Event Type</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={selectedEventType?.uri || ''}
|
||||||
|
onChange={(e) => loadEventTypeDetail(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Select event type --</option>
|
||||||
|
{eventTypes.map((et) => (
|
||||||
|
<option key={et.uri} value={et.uri}>
|
||||||
|
{et.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{selectedEventType && (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h2 style={{ marginBottom: '8px' }}>{selectedEventType.name}</h2>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<Badge variant={selectedEventType.active ? 'success' : 'warning'}>
|
||||||
|
{selectedEventType.active ? 'Active' : 'Inactive'}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="info" style={{ marginLeft: '8px' }}>
|
||||||
|
{selectedEventType.duration} minutes
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="info" style={{ marginLeft: '8px' }}>
|
||||||
|
{selectedEventType.kind}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{selectedEventType.description_plain && (
|
||||||
|
<p style={{ marginBottom: '12px', color: '#666' }}>
|
||||||
|
{selectedEventType.description_plain}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{selectedEventType.internal_note && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
background: '#fff3cd',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong>Internal Note:</strong> {selectedEventType.internal_note}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={selectedEventType.scheduling_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
Open Booking Page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
background: selectedEventType.color || '#0066cc',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginLeft: '16px',
|
||||||
|
}}
|
||||||
|
title={`Color: ${selectedEventType.color}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{selectedEventType.custom_questions && selectedEventType.custom_questions.length > 0 && (
|
||||||
|
<Card title="Custom Questions">
|
||||||
|
{selectedEventType.custom_questions.map((q: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: '500', marginBottom: '4px' }}>
|
||||||
|
{q.position}. {q.name}
|
||||||
|
{q.required && <span style={{ color: '#dc3545' }}> *</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
Type: {q.type}
|
||||||
|
{!q.enabled && <Badge variant="warning" style={{ marginLeft: '8px' }}>Disabled</Badge>}
|
||||||
|
</div>
|
||||||
|
{q.answer_choices && q.answer_choices.length > 0 && (
|
||||||
|
<div style={{ marginTop: '6px', fontSize: '14px' }}>
|
||||||
|
Choices: {q.answer_choices.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>booking-page-preview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
118
servers/calendly/src/ui/react-app/src/apps/calendar-view/App.tsx
Normal file
118
servers/calendly/src/ui/react-app/src/apps/calendar-view/App.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
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<string, any[]> = {};
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Calendar View</h1>
|
||||||
|
<p>View your scheduled events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
value={timeRange}
|
||||||
|
onChange={(e) => setTimeRange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="week">This Week</option>
|
||||||
|
<option value="month">This Month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{Object.keys(grouped).map((date) => (
|
||||||
|
<div key={date} style={{ marginBottom: '24px' }}>
|
||||||
|
<h3 style={{ marginBottom: '12px', fontSize: '18px' }}>{date}</h3>
|
||||||
|
{grouped[date].map((event) => (
|
||||||
|
<Card key={event.uri}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||||
|
<div>
|
||||||
|
<h4 style={{ marginBottom: '8px', fontSize: '16px' }}>{event.name}</h4>
|
||||||
|
<p style={{ fontSize: '14px', color: '#666', marginBottom: '8px' }}>
|
||||||
|
{formatDate(event.start_time)} - {new Date(event.end_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</p>
|
||||||
|
<Badge variant={event.status === 'active' ? 'success' : 'danger'}>
|
||||||
|
{event.status}
|
||||||
|
</Badge>
|
||||||
|
{event.location && (
|
||||||
|
<div style={{ marginTop: '8px', fontSize: '14px' }}>
|
||||||
|
📍 {event.location.type}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', textAlign: 'right' }}>
|
||||||
|
<div>{event.invitees_counter.active} / {event.invitees_counter.limit} invitees</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{events.length === 0 && !loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No events found in this time range
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Calendar View</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Event Calendar</h1>
|
||||||
|
<p>Monthly calendar view of events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||||
|
<button className="btn btn-secondary" onClick={previousMonth}>
|
||||||
|
← Previous
|
||||||
|
</button>
|
||||||
|
<h2>{monthYear}</h2>
|
||||||
|
<button className="btn btn-secondary" onClick={nextMonth}>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '8px' }}>
|
||||||
|
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
|
||||||
|
<div key={day} style={{ padding: '8px', fontWeight: 'bold', textAlign: 'center' }}>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{days.map((date, idx) => {
|
||||||
|
if (!date) {
|
||||||
|
return <div key={`empty-${idx}`} style={{ minHeight: '100px', border: '1px solid #e0e0e0', borderRadius: '6px' }} />;
|
||||||
|
}
|
||||||
|
const dayEvents = getEventsForDate(date);
|
||||||
|
const isToday =
|
||||||
|
date.getDate() === new Date().getDate() &&
|
||||||
|
date.getMonth() === new Date().getMonth() &&
|
||||||
|
date.getFullYear() === new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
minHeight: '100px',
|
||||||
|
border: isToday ? '2px solid #0066cc' : '1px solid #e0e0e0',
|
||||||
|
borderRadius: '6px',
|
||||||
|
padding: '8px',
|
||||||
|
background: isToday ? '#e6f2ff' : '#fff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: isToday ? 'bold' : 'normal', marginBottom: '4px' }}>
|
||||||
|
{date.getDate()}
|
||||||
|
</div>
|
||||||
|
{dayEvents.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.uri}
|
||||||
|
style={{
|
||||||
|
fontSize: '11px',
|
||||||
|
padding: '4px',
|
||||||
|
marginBottom: '4px',
|
||||||
|
background: event.status === 'active' ? '#d4edda' : '#f8d7da',
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
title={`${event.name} - ${new Date(event.start_time).toLocaleTimeString()}`}
|
||||||
|
>
|
||||||
|
{new Date(event.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}{' '}
|
||||||
|
{event.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '20px' }}>
|
||||||
|
<Card title="Legend">
|
||||||
|
<div style={{ display: 'flex', gap: '16px', fontSize: '14px' }}>
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', width: '12px', height: '12px', background: '#d4edda', marginRight: '4px', borderRadius: '2px' }} />
|
||||||
|
Active Events
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', width: '12px', height: '12px', background: '#f8d7da', marginRight: '4px', borderRadius: '2px' }} />
|
||||||
|
Canceled Events
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ display: 'inline-block', width: '12px', height: '12px', border: '2px solid #0066cc', marginRight: '4px', borderRadius: '2px' }} />
|
||||||
|
Today
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>event-calendar</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
174
servers/calendly/src/ui/react-app/src/apps/event-detail/App.tsx
Normal file
174
servers/calendly/src/ui/react-app/src/apps/event-detail/App.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<any>(null);
|
||||||
|
const [invitees, setInvitees] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Event Detail View</h1>
|
||||||
|
<p>View detailed information about events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<label className="label">Select Event</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={selectedEvent?.uri || ''}
|
||||||
|
onChange={(e) => loadEventDetail(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Select an event --</option>
|
||||||
|
{events.map((event) => (
|
||||||
|
<option key={event.uri} value={event.uri}>
|
||||||
|
{event.name} - {new Date(event.start_time).toLocaleString()}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{selectedEvent && (
|
||||||
|
<>
|
||||||
|
<Card title="Event Information">
|
||||||
|
<div style={{ fontSize: '14px' }}>
|
||||||
|
<h3 style={{ marginBottom: '12px' }}>{selectedEvent.name}</h3>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Status:</strong>{' '}
|
||||||
|
<Badge variant={selectedEvent.status === 'active' ? 'success' : 'danger'}>
|
||||||
|
{selectedEvent.status}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Start:</strong> {new Date(selectedEvent.start_time).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>End:</strong> {new Date(selectedEvent.end_time).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
{selectedEvent.location && (
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Location:</strong> {selectedEvent.location.type}
|
||||||
|
{selectedEvent.location.join_url && (
|
||||||
|
<a
|
||||||
|
href={selectedEvent.location.join_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ marginLeft: '8px' }}
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Invitees:</strong> {selectedEvent.invitees_counter.active} /{' '}
|
||||||
|
{selectedEvent.invitees_counter.limit}
|
||||||
|
</p>
|
||||||
|
{selectedEvent.meeting_notes_plain && (
|
||||||
|
<div style={{ marginTop: '12px' }}>
|
||||||
|
<strong>Meeting Notes:</strong>
|
||||||
|
<p style={{ marginTop: '4px', whiteSpace: 'pre-wrap' }}>
|
||||||
|
{selectedEvent.meeting_notes_plain}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedEvent.status === 'active' && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={cancelEvent}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ marginTop: '16px' }}
|
||||||
|
>
|
||||||
|
Cancel Event
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Invitees (${invitees.length})`}>
|
||||||
|
{invitees.map((invitee) => (
|
||||||
|
<div
|
||||||
|
key={invitee.uri}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||||
|
<div>
|
||||||
|
<h4>{invitee.name}</h4>
|
||||||
|
<p style={{ color: '#666', fontSize: '14px' }}>{invitee.email}</p>
|
||||||
|
<div style={{ marginTop: '4px' }}>
|
||||||
|
<Badge variant={invitee.status === 'active' ? 'success' : 'danger'}>
|
||||||
|
{invitee.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', textAlign: 'right' }}>
|
||||||
|
{invitee.timezone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>event-detail</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Event Type Dashboard</h1>
|
||||||
|
<p>Manage your Calendly event types</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<div className="grid grid-3">
|
||||||
|
{eventTypes.map((et) => (
|
||||||
|
<Card key={et.uri} title={et.name}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<Badge variant={et.active ? 'success' : 'warning'}>
|
||||||
|
{et.active ? 'Active' : 'Inactive'}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="info" style={{ marginLeft: '8px' }}>
|
||||||
|
{et.duration} min
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '14px', color: '#666', marginBottom: '12px' }}>
|
||||||
|
{et.description_plain || 'No description'}
|
||||||
|
</p>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginBottom: '12px' }}>
|
||||||
|
Type: {et.kind} • {et.type}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => toggleActive(et)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{et.active ? 'Deactivate' : 'Activate'}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={et.scheduling_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{eventTypes.length === 0 && !loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No event types found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Event Type Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
129
servers/calendly/src/ui/react-app/src/apps/invitee-grid/App.tsx
Normal file
129
servers/calendly/src/ui/react-app/src/apps/invitee-grid/App.tsx
Normal file
@ -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<any[]>([]);
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<string>('');
|
||||||
|
const [invitees, setInvitees] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Invitee Grid</h1>
|
||||||
|
<p>View and manage event invitees</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<label className="label">Select Event</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={selectedEvent}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedEvent(e.target.value);
|
||||||
|
loadInvitees(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">-- Select an event --</option>
|
||||||
|
{events.map((event) => (
|
||||||
|
<option key={event.uri} value={event.uri}>
|
||||||
|
{event.name} - {new Date(event.start_time).toLocaleDateString()}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{invitees.length > 0 && (
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Timezone</th>
|
||||||
|
<th>Rescheduled</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invitees.map((invitee) => (
|
||||||
|
<tr key={invitee.uri}>
|
||||||
|
<td>{invitee.name}</td>
|
||||||
|
<td>{invitee.email}</td>
|
||||||
|
<td>
|
||||||
|
<Badge variant={invitee.status === 'active' ? 'success' : 'danger'}>
|
||||||
|
{invitee.status}
|
||||||
|
</Badge>
|
||||||
|
{invitee.no_show && <Badge variant="warning" style={{ marginLeft: '8px' }}>No Show</Badge>}
|
||||||
|
</td>
|
||||||
|
<td>{invitee.timezone}</td>
|
||||||
|
<td>{invitee.rescheduled ? 'Yes' : 'No'}</td>
|
||||||
|
<td>
|
||||||
|
{!invitee.no_show && invitee.status === 'active' && (
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => markNoShow(invitee.uri)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
>
|
||||||
|
Mark No-Show
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invitees.length === 0 && selectedEvent && !loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No invitees found for this event
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Invitee Grid</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
const [noShowInvitees, setNoShowInvitees] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>No-Show Tracker</h1>
|
||||||
|
<p>Track and manage no-show invitees</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-value">{noShowInvitees.length}</div>
|
||||||
|
<div className="stat-label">Total No-Shows</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{noShowInvitees.length > 0 ? (
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>Event Date</th>
|
||||||
|
<th>Marked No-Show</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{noShowInvitees.map((invitee, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>{invitee.name}</td>
|
||||||
|
<td>{invitee.email}</td>
|
||||||
|
<td>{invitee.event.name}</td>
|
||||||
|
<td>{new Date(invitee.event.start_time).toLocaleString()}</td>
|
||||||
|
<td>{new Date(invitee.no_show.created_at).toLocaleString()}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => removeNoShow(invitee)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
>
|
||||||
|
Remove Marking
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
!loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No no-shows found
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>no-show-tracker</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any>(null);
|
||||||
|
const [memberships, setMemberships] = useState<any[]>([]);
|
||||||
|
const [invitations, setInvitations] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Organization Overview</h1>
|
||||||
|
<p>Manage your organization</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{organization && (
|
||||||
|
<Card title="Organization Details">
|
||||||
|
<div style={{ fontSize: '14px' }}>
|
||||||
|
<p><strong>Name:</strong> {organization.name}</p>
|
||||||
|
<p><strong>Slug:</strong> {organization.slug}</p>
|
||||||
|
<p><strong>Created:</strong> {new Date(organization.created_at).toLocaleDateString()}</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card title={`Members (${memberships.length})`}>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Joined</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{memberships.map((membership) => (
|
||||||
|
<tr key={membership.uri}>
|
||||||
|
<td>{membership.user.name}</td>
|
||||||
|
<td>{membership.user.email}</td>
|
||||||
|
<td>
|
||||||
|
<Badge variant="info">{membership.role}</Badge>
|
||||||
|
</td>
|
||||||
|
<td>{new Date(membership.created_at).toLocaleDateString()}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Pending Invitations (${invitations.length})`}>
|
||||||
|
{invitations.length > 0 ? (
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Sent</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invitations.map((invitation) => (
|
||||||
|
<tr key={invitation.uri}>
|
||||||
|
<td>{invitation.email}</td>
|
||||||
|
<td>
|
||||||
|
<Badge variant={invitation.status === 'pending' ? 'warning' : 'info'}>
|
||||||
|
{invitation.status}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td>{new Date(invitation.last_sent_at).toLocaleDateString()}</td>
|
||||||
|
<td>
|
||||||
|
{invitation.status === 'pending' && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={() => revokeInvitation(invitation.uri)}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<p style={{ color: '#999' }}>No pending invitations</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>organization-overview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
const [selectedForm, setSelectedForm] = useState<any>(null);
|
||||||
|
const [submissions, setSubmissions] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Routing Form Builder</h1>
|
||||||
|
<p>Manage routing forms and view submissions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<label className="label">Select Routing Form</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={selectedForm?.uri || ''}
|
||||||
|
onChange={(e) => loadFormDetail(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Select a form --</option>
|
||||||
|
{routingForms.map((form) => (
|
||||||
|
<option key={form.uri} value={form.uri}>
|
||||||
|
{form.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{selectedForm && (
|
||||||
|
<>
|
||||||
|
<Card title="Form Details">
|
||||||
|
<div style={{ fontSize: '14px' }}>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Name:</strong> {selectedForm.name}
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Status:</strong>{' '}
|
||||||
|
<Badge variant={selectedForm.published ? 'success' : 'warning'}>
|
||||||
|
{selectedForm.published ? 'Published' : 'Draft'}
|
||||||
|
</Badge>
|
||||||
|
</p>
|
||||||
|
<p style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>Created:</strong> {new Date(selectedForm.created_at).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<strong>Questions ({selectedForm.questions.length}):</strong>
|
||||||
|
{selectedForm.questions.map((q: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
marginTop: '8px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: '500' }}>
|
||||||
|
{q.name} {q.required && <span style={{ color: '#dc3545' }}>*</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||||
|
Type: {q.type}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Submissions (${submissions.length})`}>
|
||||||
|
{submissions.length > 0 ? (
|
||||||
|
submissions.map((submission, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: '8px' }}>
|
||||||
|
<strong>{submission.submitter.name || submission.submitter.email}</strong>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', marginBottom: '8px' }}>
|
||||||
|
{submission.questions_and_answers.map((qa: any, qaIdx: number) => (
|
||||||
|
<div key={qaIdx} style={{ marginTop: '4px' }}>
|
||||||
|
<strong>{qa.question}:</strong> {qa.answer}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||||
|
Result: {submission.result.type} - {submission.result.value}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '12px', color: '#999', marginTop: '4px' }}>
|
||||||
|
Submitted: {new Date(submission.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p style={{ color: '#999' }}>No submissions yet</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>routing-form-builder</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
const [selectedEventType, setSelectedEventType] = useState('');
|
||||||
|
const [maxCount, setMaxCount] = useState(1);
|
||||||
|
const [generatedLink, setGeneratedLink] = useState<any>(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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Scheduling Link Manager</h1>
|
||||||
|
<p>Generate single-use scheduling links</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
|
||||||
|
<Card title="Generate New Link">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
|
<div>
|
||||||
|
<label className="label">Event Type</label>
|
||||||
|
<select
|
||||||
|
className="input"
|
||||||
|
value={selectedEventType}
|
||||||
|
onChange={(e) => setSelectedEventType(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">-- Select event type --</option>
|
||||||
|
{eventTypes.map((et) => (
|
||||||
|
<option key={et.uri} value={et.uri}>
|
||||||
|
{et.name} ({et.duration} min)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Maximum Event Count</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="input"
|
||||||
|
value={maxCount}
|
||||||
|
onChange={(e) => setMaxCount(parseInt(e.target.value) || 1)}
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
/>
|
||||||
|
<p style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
|
||||||
|
Number of times this link can be used to book events
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={generateLink}
|
||||||
|
disabled={loading || !selectedEventType}
|
||||||
|
>
|
||||||
|
{loading ? 'Generating...' : 'Generate Link'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{generatedLink && (
|
||||||
|
<Card title="Generated Link">
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<strong>Booking URL:</strong>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
background: '#f9f9f9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
marginBottom: '12px',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{generatedLink.booking_url}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => copyToClipboard(generatedLink.booking_url)}
|
||||||
|
>
|
||||||
|
Copy to Clipboard
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>scheduling-link-manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
100
servers/calendly/src/ui/react-app/src/apps/user-profile/App.tsx
Normal file
100
servers/calendly/src/ui/react-app/src/apps/user-profile/App.tsx
Normal file
@ -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<any>(null);
|
||||||
|
const [schedules, setSchedules] = useState<any[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>User Profile</h1>
|
||||||
|
<p>View your Calendly profile and settings</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
{user && (
|
||||||
|
<>
|
||||||
|
<Card title="Profile Information">
|
||||||
|
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
|
||||||
|
{user.avatar_url && (
|
||||||
|
<img
|
||||||
|
src={user.avatar_url}
|
||||||
|
alt={user.name}
|
||||||
|
style={{ width: '80px', height: '80px', borderRadius: '50%' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<h3 style={{ marginBottom: '8px' }}>{user.name}</h3>
|
||||||
|
<p style={{ color: '#666', marginBottom: '4px' }}>{user.email}</p>
|
||||||
|
<p style={{ color: '#666', marginBottom: '4px' }}>Slug: {user.slug}</p>
|
||||||
|
<p style={{ color: '#666', marginBottom: '8px' }}>Timezone: {user.timezone}</p>
|
||||||
|
<a
|
||||||
|
href={user.scheduling_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{ textDecoration: 'none', display: 'inline-block' }}
|
||||||
|
>
|
||||||
|
View Scheduling Page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title={`Availability Schedules (${schedules.length})`}>
|
||||||
|
{schedules.map((schedule) => (
|
||||||
|
<div key={schedule.uri} style={{ marginBottom: '16px', padding: '12px', background: '#f9f9f9', borderRadius: '6px' }}>
|
||||||
|
<h4 style={{ marginBottom: '8px' }}>
|
||||||
|
{schedule.name} {schedule.default && <span style={{ color: '#0066cc' }}>(Default)</span>}
|
||||||
|
</h4>
|
||||||
|
<p style={{ fontSize: '14px', color: '#666' }}>
|
||||||
|
Timezone: {schedule.timezone}
|
||||||
|
</p>
|
||||||
|
<div style={{ marginTop: '8px', fontSize: '14px' }}>
|
||||||
|
<strong>Rules:</strong>
|
||||||
|
{schedule.rules.map((rule: any, idx: number) => (
|
||||||
|
<div key={idx} style={{ marginTop: '4px', paddingLeft: '12px' }}>
|
||||||
|
• {rule.type === 'wday' ? `Day: ${rule.wday}` : `Date: ${rule.date}`}
|
||||||
|
{rule.intervals.map((interval: any, i: number) => (
|
||||||
|
<span key={i} style={{ marginLeft: '8px' }}>
|
||||||
|
{interval.from} - {interval.to}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{schedules.length === 0 && (
|
||||||
|
<p style={{ color: '#999' }}>No availability schedules found</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>user-profile</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -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<any[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="app-container">
|
||||||
|
<div className="header">
|
||||||
|
<h1>Webhook Manager</h1>
|
||||||
|
<p>Manage Calendly webhook subscriptions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<button className="btn btn-primary" onClick={() => setShowForm(!showForm)}>
|
||||||
|
{showForm ? 'Cancel' : 'Create Webhook'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm && (
|
||||||
|
<Card title="Create New Webhook">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
<div>
|
||||||
|
<label className="label">Callback URL</label>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
value={formData.url}
|
||||||
|
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
|
||||||
|
placeholder="https://example.com/webhook"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Events</label>
|
||||||
|
{eventOptions.map((event) => (
|
||||||
|
<div key={event} style={{ marginBottom: '8px' }}>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formData.events.includes(event)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newEvents = e.target.checked
|
||||||
|
? [...formData.events, event]
|
||||||
|
: formData.events.filter((ev) => ev !== event);
|
||||||
|
setFormData({ ...formData, events: newEvents });
|
||||||
|
}}
|
||||||
|
style={{ marginRight: '8px' }}
|
||||||
|
/>
|
||||||
|
{event}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-primary" onClick={createWebhook} disabled={loading || !formData.url || formData.events.length === 0}>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <div className="error">{error}</div>}
|
||||||
|
{loading && <div className="loading">Loading...</div>}
|
||||||
|
|
||||||
|
<div className="grid grid-2">
|
||||||
|
{webhooks.map((webhook) => (
|
||||||
|
<Card key={webhook.uri}>
|
||||||
|
<div style={{ marginBottom: '12px' }}>
|
||||||
|
<Badge variant={webhook.state === 'active' ? 'success' : 'danger'}>
|
||||||
|
{webhook.state}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px', fontSize: '14px', wordBreak: 'break-all' }}>
|
||||||
|
<strong>URL:</strong> {webhook.callback_url}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px', fontSize: '14px' }}>
|
||||||
|
<strong>Events:</strong> {webhook.events.join(', ')}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '12px', fontSize: '12px', color: '#666' }}>
|
||||||
|
Created: {new Date(webhook.created_at).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
<button className="btn btn-danger" onClick={() => deleteWebhook(webhook.uri)} disabled={loading}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{webhooks.length === 0 && !loading && (
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px', color: '#999' }}>
|
||||||
|
No webhooks found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>webhook-manager</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
10
servers/calendly/src/ui/react-app/src/components/Badge.tsx
Normal file
10
servers/calendly/src/ui/react-app/src/components/Badge.tsx
Normal file
@ -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 <span className={`badge badge-${variant}`}>{children}</span>;
|
||||||
|
}
|
||||||
16
servers/calendly/src/ui/react-app/src/components/Card.tsx
Normal file
16
servers/calendly/src/ui/react-app/src/components/Card.tsx
Normal file
@ -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 (
|
||||||
|
<div className={`card ${className}`}>
|
||||||
|
{title && <div className="card-header">{title}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
servers/calendly/src/ui/react-app/src/hooks/useCallTool.ts
Normal file
31
servers/calendly/src/ui/react-app/src/hooks/useCallTool.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export function useCallTool() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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 };
|
||||||
|
}
|
||||||
18
servers/calendly/src/ui/react-app/src/hooks/useDirtyState.ts
Normal file
18
servers/calendly/src/ui/react-app/src/hooks/useDirtyState.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export function useDirtyState<T>(initialValue: T) {
|
||||||
|
const [value, setValue] = useState<T>(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 };
|
||||||
|
}
|
||||||
192
servers/calendly/src/ui/react-app/src/styles/common.css
Normal file
192
servers/calendly/src/ui/react-app/src/styles/common.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
20
servers/calendly/src/ui/react-app/tsconfig.json
Normal file
20
servers/calendly/src/ui/react-app/tsconfig.json
Normal file
@ -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"]
|
||||||
|
}
|
||||||
@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "Node16",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "Node16",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@ -13,9 +13,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true
|
||||||
"types": ["node"]
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "src/ui"]
|
"exclude": ["node_modules", "dist", "tests", "src/ui"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user