calendly: Complete MCP server with 27 tools and 12 React apps
- Calendly API v2 client with auth, pagination, error handling - 27 MCP tools across 6 categories (events, event types, scheduling, users, orgs, webhooks) - 12 React MCP apps with dark theme and client-side state - Both stdio and HTTP modes supported - Full TypeScript types and documentation
This commit is contained in:
parent
716f99056d
commit
8e9d1ffb87
181
servers/calendly/README.md
Normal file
181
servers/calendly/README.md
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# Calendly MCP Server
|
||||||
|
|
||||||
|
Complete Model Context Protocol (MCP) server for Calendly API v2 with 27 tools and 12 React UI apps.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 🛠️ 27 MCP Tools
|
||||||
|
|
||||||
|
**Events (8 tools)**
|
||||||
|
- `calendly_list_scheduled_events` - List events with filters
|
||||||
|
- `calendly_get_event` - Get event details
|
||||||
|
- `calendly_cancel_event` - Cancel an event
|
||||||
|
- `calendly_list_event_invitees` - List invitees for an event
|
||||||
|
- `calendly_get_invitee` - Get invitee details
|
||||||
|
- `calendly_list_no_shows` - List no-shows
|
||||||
|
- `calendly_mark_no_show` - Mark invitee as no-show
|
||||||
|
- `calendly_unmark_no_show` - Remove no-show status
|
||||||
|
|
||||||
|
**Event Types (3 tools)**
|
||||||
|
- `calendly_list_event_types` - List event types
|
||||||
|
- `calendly_get_event_type` - Get event type details
|
||||||
|
- `calendly_list_available_times` - List available time slots
|
||||||
|
|
||||||
|
**Scheduling (3 tools)**
|
||||||
|
- `calendly_create_scheduling_link` - Create single-use scheduling link
|
||||||
|
- `calendly_list_routing_forms` - List routing forms
|
||||||
|
- `calendly_get_routing_form` - Get routing form details
|
||||||
|
|
||||||
|
**Users (3 tools)**
|
||||||
|
- `calendly_get_current_user` - Get current user info
|
||||||
|
- `calendly_get_user` - Get user by URI
|
||||||
|
- `calendly_list_user_busy_times` - List user busy times
|
||||||
|
|
||||||
|
**Organizations (6 tools)**
|
||||||
|
- `calendly_get_organization` - Get organization details
|
||||||
|
- `calendly_list_organization_members` - List members
|
||||||
|
- `calendly_list_organization_invitations` - List invitations
|
||||||
|
- `calendly_invite_user` - Invite user to organization
|
||||||
|
- `calendly_revoke_invitation` - Revoke invitation
|
||||||
|
- `calendly_remove_organization_member` - Remove member
|
||||||
|
|
||||||
|
**Webhooks (4 tools)**
|
||||||
|
- `calendly_list_webhook_subscriptions` - List webhooks
|
||||||
|
- `calendly_create_webhook_subscription` - Create webhook
|
||||||
|
- `calendly_get_webhook_subscription` - Get webhook details
|
||||||
|
- `calendly_delete_webhook_subscription` - Delete webhook
|
||||||
|
|
||||||
|
### 🎨 12 React MCP Apps
|
||||||
|
|
||||||
|
All apps feature dark theme and client-side state management:
|
||||||
|
|
||||||
|
1. **Event Dashboard** (`src/ui/react-app/event-dashboard`) - Overview of scheduled events
|
||||||
|
2. **Event Detail** (`src/ui/react-app/event-detail`) - Detailed event information
|
||||||
|
3. **Event Grid** (`src/ui/react-app/event-grid`) - Calendar grid view
|
||||||
|
4. **Event Type Manager** (`src/ui/react-app/event-type-manager`) - Manage event types
|
||||||
|
5. **Availability Calendar** (`src/ui/react-app/availability-calendar`) - View available times
|
||||||
|
6. **Invitee List** (`src/ui/react-app/invitee-list`) - Manage event invitees
|
||||||
|
7. **Scheduling Links** (`src/ui/react-app/scheduling-links`) - Create scheduling links
|
||||||
|
8. **Organization Members** (`src/ui/react-app/org-members`) - Manage team members
|
||||||
|
9. **Webhook Manager** (`src/ui/react-app/webhook-manager`) - Manage webhooks
|
||||||
|
10. **Booking Flow** (`src/ui/react-app/booking-flow`) - Multi-step booking interface
|
||||||
|
11. **No-Show Tracker** (`src/ui/react-app/no-show-tracker`) - Track no-shows
|
||||||
|
12. **Analytics Dashboard** (`src/ui/react-app/analytics-dashboard`) - Metrics and insights
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Set your Calendly API key as an environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CALENDLY_API_KEY="your_api_key_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Get your API key from: https://calendly.com/integrations/api_webhooks
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Stdio Mode (Default for MCP)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Use in your MCP client configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"calendly": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/calendly/dist/main.js"],
|
||||||
|
"env": {
|
||||||
|
"CALENDLY_API_KEY": "your_api_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start:http
|
||||||
|
```
|
||||||
|
|
||||||
|
Server runs on `http://localhost:3000`
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
- `GET /health` - Health check
|
||||||
|
- `POST /` - MCP requests (tools/list, tools/call, resources/list, resources/read)
|
||||||
|
|
||||||
|
## API Client
|
||||||
|
|
||||||
|
The Calendly client (`src/clients/calendly.ts`) provides:
|
||||||
|
|
||||||
|
- ✅ Full Calendly API v2 support
|
||||||
|
- ✅ Bearer token authentication
|
||||||
|
- ✅ Automatic pagination handling
|
||||||
|
- ✅ Error handling with detailed messages
|
||||||
|
- ✅ Type-safe responses
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── clients/
|
||||||
|
│ └── calendly.ts # Calendly API v2 client
|
||||||
|
├── tools/
|
||||||
|
│ ├── events-tools.ts # Event management tools
|
||||||
|
│ ├── event-types-tools.ts # Event type tools
|
||||||
|
│ ├── scheduling-tools.ts # Scheduling & routing tools
|
||||||
|
│ ├── users-tools.ts # User management tools
|
||||||
|
│ ├── organizations-tools.ts # Organization tools
|
||||||
|
│ └── webhooks-tools.ts # Webhook tools
|
||||||
|
├── types/
|
||||||
|
│ └── index.ts # TypeScript definitions
|
||||||
|
├── ui/
|
||||||
|
│ └── react-app/ # 12 React MCP apps
|
||||||
|
├── server.ts # MCP server setup
|
||||||
|
└── main.ts # Entry point (stdio + HTTP)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watch Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run React Apps
|
||||||
|
|
||||||
|
Each app is standalone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src/ui/react-app/event-dashboard
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- Calendly API Documentation: https://developer.calendly.com/api-docs
|
||||||
|
- Model Context Protocol: https://modelcontextprotocol.io
|
||||||
|
- MCP SDK: https://github.com/modelcontextprotocol/typescript-sdk
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@ -1,20 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-server-calendly",
|
"name": "@mcpengine/calendly-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "Complete Calendly MCP server with 30+ tools and React UI apps",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "./dist/main.js",
|
||||||
"scripts": {
|
"bin": {
|
||||||
"build": "tsc",
|
"calendly-mcp": "./dist/main.js"
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "tsx src/index.ts"
|
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && chmod +x dist/main.js",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"start:http": "MCP_MODE=http node dist/main.js",
|
||||||
|
"test": "echo \"No tests yet\" && exit 0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"calendly",
|
||||||
|
"scheduling",
|
||||||
|
"model-context-protocol"
|
||||||
|
],
|
||||||
|
"author": "MCPEngine",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^22.10.5",
|
||||||
"tsx": "^4.7.0",
|
"typescript": "^5.7.3"
|
||||||
"typescript": "^5.3.0"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
322
servers/calendly/src/clients/calendly.ts
Normal file
322
servers/calendly/src/clients/calendly.ts
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
// Calendly API v2 Client
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CalendlyConfig,
|
||||||
|
CalendlyUser,
|
||||||
|
CalendlyEvent,
|
||||||
|
CalendlyEventType,
|
||||||
|
CalendlyInvitee,
|
||||||
|
CalendlyOrganization,
|
||||||
|
CalendlyOrganizationMembership,
|
||||||
|
CalendlyOrganizationInvitation,
|
||||||
|
CalendlySchedulingLink,
|
||||||
|
CalendlyWebhookSubscription,
|
||||||
|
CalendlyAvailableTime,
|
||||||
|
CalendlyUserBusyTime,
|
||||||
|
CalendlyRoutingForm,
|
||||||
|
CalendlyNoShow,
|
||||||
|
PaginationParams,
|
||||||
|
PaginatedResponse,
|
||||||
|
CalendlyError,
|
||||||
|
} from '../types/index.js';
|
||||||
|
|
||||||
|
export class CalendlyClient {
|
||||||
|
private apiKey: string;
|
||||||
|
private baseUrl: string;
|
||||||
|
|
||||||
|
constructor(config: CalendlyConfig) {
|
||||||
|
this.apiKey = config.apiKey;
|
||||||
|
this.baseUrl = config.baseUrl || 'https://api.calendly.com';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
const error: CalendlyError = errorData as CalendlyError;
|
||||||
|
throw new Error(
|
||||||
|
`Calendly API Error (${response.status}): ${error.title || 'Unknown error'} - ${error.message || response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
async getCurrentUser(): Promise<{ resource: CalendlyUser }> {
|
||||||
|
return this.request('/users/me');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserByUri(uri: string): Promise<{ resource: CalendlyUser }> {
|
||||||
|
return this.request(`/users/${encodeURIComponent(uri)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
async listEvents(params: {
|
||||||
|
organization?: string;
|
||||||
|
user?: string;
|
||||||
|
invitee_email?: string;
|
||||||
|
status?: 'active' | 'canceled';
|
||||||
|
min_start_time?: string;
|
||||||
|
max_start_time?: string;
|
||||||
|
count?: number;
|
||||||
|
page_token?: string;
|
||||||
|
sort?: string;
|
||||||
|
}): Promise<PaginatedResponse<CalendlyEvent>> {
|
||||||
|
const query = this.buildQueryString(params);
|
||||||
|
return this.request(`/scheduled_events${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(uuid: string): Promise<{ resource: CalendlyEvent }> {
|
||||||
|
return this.request(`/scheduled_events/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelEvent(uuid: string, reason?: string): Promise<{ resource: CalendlyEvent }> {
|
||||||
|
return this.request(`/scheduled_events/${uuid}/cancellation`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ reason: reason || 'Canceled' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Invitees
|
||||||
|
async listEventInvitees(
|
||||||
|
eventUuid: string,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<CalendlyInvitee>> {
|
||||||
|
const query = this.buildQueryString(params || {});
|
||||||
|
return this.request(`/scheduled_events/${eventUuid}/invitees${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInvitee(inviteeUuid: string): Promise<{ resource: CalendlyInvitee }> {
|
||||||
|
return this.request(`/scheduled_events/invitees/${inviteeUuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-shows
|
||||||
|
async listInviteeNoShows(inviteeUri: string): Promise<PaginatedResponse<CalendlyNoShow>> {
|
||||||
|
const query = this.buildQueryString({ invitee: inviteeUri });
|
||||||
|
return this.request(`/invitee_no_shows${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createInviteeNoShow(inviteeUri: string): Promise<{ resource: CalendlyNoShow }> {
|
||||||
|
return this.request('/invitee_no_shows', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ invitee: inviteeUri }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteInviteeNoShow(noShowUuid: string): Promise<void> {
|
||||||
|
return this.request(`/invitee_no_shows/${noShowUuid}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Types
|
||||||
|
async listEventTypes(params: {
|
||||||
|
organization?: string;
|
||||||
|
user?: string;
|
||||||
|
active?: boolean;
|
||||||
|
count?: number;
|
||||||
|
page_token?: string;
|
||||||
|
sort?: string;
|
||||||
|
}): Promise<PaginatedResponse<CalendlyEventType>> {
|
||||||
|
const query = this.buildQueryString(params);
|
||||||
|
return this.request(`/event_types${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventType(uuid: string): Promise<{ resource: CalendlyEventType }> {
|
||||||
|
return this.request(`/event_types/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAvailableTimes(
|
||||||
|
eventTypeUri: string,
|
||||||
|
params: {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
}
|
||||||
|
): Promise<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
|
||||||
|
async createSchedulingLink(params: {
|
||||||
|
max_event_count: number;
|
||||||
|
owner: string;
|
||||||
|
owner_type: 'EventType' | 'Group';
|
||||||
|
}): Promise<{ resource: CalendlySchedulingLink }> {
|
||||||
|
return this.request('/scheduling_links', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organizations
|
||||||
|
async getOrganization(uuid: string): Promise<{ resource: CalendlyOrganization }> {
|
||||||
|
return this.request(`/organizations/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listOrganizationMemberships(
|
||||||
|
organizationUri: string,
|
||||||
|
params?: {
|
||||||
|
count?: number;
|
||||||
|
page_token?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
): Promise<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
|
||||||
|
async listRoutingForms(
|
||||||
|
organizationUri: string,
|
||||||
|
params?: PaginationParams
|
||||||
|
): Promise<PaginatedResponse<CalendlyRoutingForm>> {
|
||||||
|
const query = this.buildQueryString({
|
||||||
|
organization: organizationUri,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
return this.request(`/routing_forms${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRoutingForm(uuid: string): Promise<{ resource: CalendlyRoutingForm }> {
|
||||||
|
return this.request(`/routing_forms/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
async listWebhookSubscriptions(
|
||||||
|
params: {
|
||||||
|
organization: string;
|
||||||
|
scope: 'user' | 'organization';
|
||||||
|
user?: string;
|
||||||
|
} & PaginationParams
|
||||||
|
): Promise<PaginatedResponse<CalendlyWebhookSubscription>> {
|
||||||
|
const query = this.buildQueryString(params);
|
||||||
|
return this.request(`/webhook_subscriptions${query}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWebhookSubscription(params: {
|
||||||
|
url: string;
|
||||||
|
events: string[];
|
||||||
|
organization: string;
|
||||||
|
user?: string;
|
||||||
|
scope: 'user' | 'organization';
|
||||||
|
signing_key?: string;
|
||||||
|
}): Promise<{ resource: CalendlyWebhookSubscription }> {
|
||||||
|
return this.request('/webhook_subscriptions', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWebhookSubscription(uuid: string): Promise<{ resource: CalendlyWebhookSubscription }> {
|
||||||
|
return this.request(`/webhook_subscriptions/${uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWebhookSubscription(uuid: string): Promise<void> {
|
||||||
|
return this.request(`/webhook_subscriptions/${uuid}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,271 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import {
|
|
||||||
CallToolRequestSchema,
|
|
||||||
ListToolsRequestSchema,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// CONFIGURATION
|
|
||||||
// ============================================
|
|
||||||
const MCP_NAME = "calendly";
|
|
||||||
const MCP_VERSION = "1.0.0";
|
|
||||||
const API_BASE_URL = "https://api.calendly.com";
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// API CLIENT - Calendly API v2
|
|
||||||
// ============================================
|
|
||||||
class CalendlyClient {
|
|
||||||
private apiKey: string;
|
|
||||||
private baseUrl: string;
|
|
||||||
private currentUserUri: string | null = null;
|
|
||||||
|
|
||||||
constructor(apiKey: string) {
|
|
||||||
this.apiKey = apiKey;
|
|
||||||
this.baseUrl = API_BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(endpoint: string, options: RequestInit = {}) {
|
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${this.apiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text();
|
|
||||||
throw new Error(`Calendly API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(endpoint: string) {
|
|
||||||
return this.request(endpoint, { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async post(endpoint: string, data: any) {
|
|
||||||
return this.request(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(endpoint: string) {
|
|
||||||
return this.request(endpoint, { method: "DELETE" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentUser(): Promise<string> {
|
|
||||||
if (!this.currentUserUri) {
|
|
||||||
const result = await this.get("/users/me");
|
|
||||||
this.currentUserUri = result.resource.uri;
|
|
||||||
}
|
|
||||||
return this.currentUserUri!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL DEFINITIONS - Calendly API v2
|
|
||||||
// ============================================
|
|
||||||
const tools = [
|
|
||||||
{
|
|
||||||
name: "list_events",
|
|
||||||
description: "List scheduled events. Returns events for the authenticated user within the specified time range.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
count: { type: "number", description: "Number of events to return (max 100)" },
|
|
||||||
min_start_time: { type: "string", description: "Start of time range (ISO 8601 format)" },
|
|
||||||
max_start_time: { type: "string", description: "End of time range (ISO 8601 format)" },
|
|
||||||
status: { type: "string", enum: ["active", "canceled"], description: "Filter by event status" },
|
|
||||||
page_token: { type: "string", description: "Token for pagination" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_event",
|
|
||||||
description: "Get details of a specific scheduled event by its UUID",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
event_uuid: { type: "string", description: "The UUID of the scheduled event" },
|
|
||||||
},
|
|
||||||
required: ["event_uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cancel_event",
|
|
||||||
description: "Cancel a scheduled event. Optionally provide a reason for cancellation.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
event_uuid: { type: "string", description: "The UUID of the scheduled event to cancel" },
|
|
||||||
reason: { type: "string", description: "Reason for cancellation (optional)" },
|
|
||||||
},
|
|
||||||
required: ["event_uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_event_types",
|
|
||||||
description: "List all event types available for the authenticated user",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
count: { type: "number", description: "Number of event types to return (max 100)" },
|
|
||||||
active: { type: "boolean", description: "Filter by active status" },
|
|
||||||
page_token: { type: "string", description: "Token for pagination" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_availability",
|
|
||||||
description: "Get available time slots for an event type",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
event_type_uuid: { type: "string", description: "The UUID of the event type" },
|
|
||||||
start_time: { type: "string", description: "Start of availability window (ISO 8601)" },
|
|
||||||
end_time: { type: "string", description: "End of availability window (ISO 8601)" },
|
|
||||||
},
|
|
||||||
required: ["event_type_uuid", "start_time", "end_time"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_invitees",
|
|
||||||
description: "List invitees for a scheduled event",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
event_uuid: { type: "string", description: "The UUID of the scheduled event" },
|
|
||||||
count: { type: "number", description: "Number of invitees to return (max 100)" },
|
|
||||||
status: { type: "string", enum: ["active", "canceled"], description: "Filter by invitee status" },
|
|
||||||
page_token: { type: "string", description: "Token for pagination" },
|
|
||||||
},
|
|
||||||
required: ["event_uuid"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_user",
|
|
||||||
description: "Get the current authenticated user's information",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL HANDLERS
|
|
||||||
// ============================================
|
|
||||||
async function handleTool(client: CalendlyClient, name: string, args: any) {
|
|
||||||
switch (name) {
|
|
||||||
case "list_events": {
|
|
||||||
const userUri = await client.getCurrentUser();
|
|
||||||
const params = new URLSearchParams({ user: userUri });
|
|
||||||
if (args.count) params.append("count", String(args.count));
|
|
||||||
if (args.min_start_time) params.append("min_start_time", args.min_start_time);
|
|
||||||
if (args.max_start_time) params.append("max_start_time", args.max_start_time);
|
|
||||||
if (args.status) params.append("status", args.status);
|
|
||||||
if (args.page_token) params.append("page_token", args.page_token);
|
|
||||||
return await client.get(`/scheduled_events?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "get_event": {
|
|
||||||
const { event_uuid } = args;
|
|
||||||
return await client.get(`/scheduled_events/${event_uuid}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "cancel_event": {
|
|
||||||
const { event_uuid, reason } = args;
|
|
||||||
const body: any = {};
|
|
||||||
if (reason) body.reason = reason;
|
|
||||||
return await client.post(`/scheduled_events/${event_uuid}/cancellation`, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list_event_types": {
|
|
||||||
const userUri = await client.getCurrentUser();
|
|
||||||
const params = new URLSearchParams({ user: userUri });
|
|
||||||
if (args.count) params.append("count", String(args.count));
|
|
||||||
if (args.active !== undefined) params.append("active", String(args.active));
|
|
||||||
if (args.page_token) params.append("page_token", args.page_token);
|
|
||||||
return await client.get(`/event_types?${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "get_availability": {
|
|
||||||
const { event_type_uuid, start_time, end_time } = args;
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
start_time,
|
|
||||||
end_time,
|
|
||||||
});
|
|
||||||
return await client.get(`/event_type_available_times?event_type=https://api.calendly.com/event_types/${event_type_uuid}&${params.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "list_invitees": {
|
|
||||||
const { event_uuid, count, status, page_token } = args;
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (count) params.append("count", String(count));
|
|
||||||
if (status) params.append("status", status);
|
|
||||||
if (page_token) params.append("page_token", page_token);
|
|
||||||
const queryString = params.toString();
|
|
||||||
return await client.get(`/scheduled_events/${event_uuid}/invitees${queryString ? '?' + queryString : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "get_user": {
|
|
||||||
return await client.get("/users/me");
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SERVER SETUP
|
|
||||||
// ============================================
|
|
||||||
async function main() {
|
|
||||||
const apiKey = process.env.CALENDLY_API_KEY;
|
|
||||||
if (!apiKey) {
|
|
||||||
console.error("Error: CALENDLY_API_KEY environment variable required");
|
|
||||||
console.error("Get your Personal Access Token from: https://calendly.com/integrations/api_webhooks");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new CalendlyClient(apiKey);
|
|
||||||
|
|
||||||
const server = new Server(
|
|
||||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
|
||||||
{ capabilities: { tools: {} } }
|
|
||||||
);
|
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
||||||
tools,
|
|
||||||
}));
|
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
||||||
const { name, arguments: args } = request.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await handleTool(client, name, args || {});
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: `Error: ${message}` }],
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
101
servers/calendly/src/main.ts
Normal file
101
servers/calendly/src/main.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Calendly MCP Server - Main Entry Point
|
||||||
|
// Supports both stdio and HTTP modes
|
||||||
|
|
||||||
|
import { createCalendlyServer, runStdioServer } from './server.js';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
|
||||||
|
const API_KEY = process.env.CALENDLY_API_KEY;
|
||||||
|
const MODE = process.env.MCP_MODE || 'stdio'; // stdio or http
|
||||||
|
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||||
|
|
||||||
|
if (!API_KEY) {
|
||||||
|
console.error('Error: CALENDLY_API_KEY environment variable is required');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (MODE === 'http') {
|
||||||
|
// HTTP mode - useful for web-based MCP apps
|
||||||
|
const mcpServer = createCalendlyServer(API_KEY!);
|
||||||
|
|
||||||
|
const httpServer = createServer(async (req, res) => {
|
||||||
|
// CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET' && req.url === '/health') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ status: 'ok', mode: 'http' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => {
|
||||||
|
body += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('end', async () => {
|
||||||
|
try {
|
||||||
|
const request = JSON.parse(body);
|
||||||
|
|
||||||
|
// Handle MCP requests
|
||||||
|
if (request.method === 'tools/list') {
|
||||||
|
const tools = await (mcpServer as any).requestHandlers.get('tools/list')?.();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(tools));
|
||||||
|
} else if (request.method === 'tools/call') {
|
||||||
|
const result = await (mcpServer as any).requestHandlers.get('tools/call')?.(request);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(result));
|
||||||
|
} else if (request.method === 'resources/list') {
|
||||||
|
const resources = await (mcpServer as any).requestHandlers.get('resources/list')?.();
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(resources));
|
||||||
|
} else if (request.method === 'resources/read') {
|
||||||
|
const resource = await (mcpServer as any).requestHandlers.get('resources/read')?.(request);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(resource));
|
||||||
|
} else {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Unknown method' }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
|
console.error(`Calendly MCP Server running on http://localhost:${PORT}`);
|
||||||
|
console.error('Mode: HTTP');
|
||||||
|
console.error('Endpoints:');
|
||||||
|
console.error(' GET /health - Health check');
|
||||||
|
console.error(' POST / - MCP requests');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Stdio mode - default for MCP
|
||||||
|
await runStdioServer(API_KEY!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
129
servers/calendly/src/server.ts
Normal file
129
servers/calendly/src/server.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Calendly MCP Server
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
ListResourcesRequestSchema,
|
||||||
|
ReadResourceRequestSchema,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
|
import { CalendlyClient } from './clients/calendly.js';
|
||||||
|
import { createEventsTools } from './tools/events-tools.js';
|
||||||
|
import { createEventTypesTools } from './tools/event-types-tools.js';
|
||||||
|
import { createSchedulingTools } from './tools/scheduling-tools.js';
|
||||||
|
import { createUsersTools } from './tools/users-tools.js';
|
||||||
|
import { createOrganizationsTools } from './tools/organizations-tools.js';
|
||||||
|
import { createWebhooksTools } from './tools/webhooks-tools.js';
|
||||||
|
|
||||||
|
export function createCalendlyServer(apiKey: string) {
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'calendly-mcp-server',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize Calendly client
|
||||||
|
const client = new CalendlyClient({
|
||||||
|
apiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collect all tools
|
||||||
|
const allTools = {
|
||||||
|
...createEventsTools(client),
|
||||||
|
...createEventTypesTools(client),
|
||||||
|
...createSchedulingTools(client),
|
||||||
|
...createUsersTools(client),
|
||||||
|
...createOrganizationsTools(client),
|
||||||
|
...createWebhooksTools(client),
|
||||||
|
};
|
||||||
|
|
||||||
|
// List tools handler
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: Object.entries(allTools).map(([name, tool]) => ({
|
||||||
|
name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: tool.parameters,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call tool handler
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const toolName = request.params.name;
|
||||||
|
const tool = allTools[toolName as keyof typeof allTools];
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await tool.handler(request.params.arguments || {});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({
|
||||||
|
error: errorMessage,
|
||||||
|
tool: toolName,
|
||||||
|
}, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Resources - expose current user as a resource
|
||||||
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
uri: 'calendly://user/me',
|
||||||
|
name: 'Current User',
|
||||||
|
description: 'Currently authenticated Calendly user',
|
||||||
|
mimeType: 'application/json',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||||
|
const uri = request.params.uri;
|
||||||
|
|
||||||
|
if (uri === 'calendly://user/me') {
|
||||||
|
const user = await client.getCurrentUser();
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
text: JSON.stringify(user, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown resource: ${uri}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runStdioServer(apiKey: string) {
|
||||||
|
const server = createCalendlyServer(apiKey);
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error('Calendly MCP Server running on stdio');
|
||||||
|
}
|
||||||
113
servers/calendly/src/tools/event-types-tools.ts
Normal file
113
servers/calendly/src/tools/event-types-tools.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Event Types Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createEventTypesTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_list_event_types: {
|
||||||
|
description: 'List event types for a user or organization',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI to filter by',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User URI to filter by',
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Filter by active status',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results per page (max 100)',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
page_token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Pagination token',
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Sort order (e.g., name:asc, name:desc)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listEventTypes(args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_get_event_type: {
|
||||||
|
description: 'Get details of a specific event type by UUID',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Event type UUID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getEventType(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_list_available_times: {
|
||||||
|
description: 'List available time slots for an event type within a date range',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
event_type_uri: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Event type URI',
|
||||||
|
},
|
||||||
|
start_time: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Start of range (ISO 8601)',
|
||||||
|
},
|
||||||
|
end_time: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'End of range (ISO 8601)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['event_type_uri', 'start_time', 'end_time'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listAvailableTimes(args.event_type_uri, {
|
||||||
|
start_time: args.start_time,
|
||||||
|
end_time: args.end_time,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
262
servers/calendly/src/tools/events-tools.ts
Normal file
262
servers/calendly/src/tools/events-tools.ts
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
// Events Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createEventsTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_list_scheduled_events: {
|
||||||
|
description: 'List scheduled events with filters (organization, user, status, date range)',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI to filter by',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User URI to filter by',
|
||||||
|
},
|
||||||
|
invitee_email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by invitee email',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['active', 'canceled'],
|
||||||
|
description: 'Event status filter',
|
||||||
|
},
|
||||||
|
min_start_time: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Minimum start time (ISO 8601)',
|
||||||
|
},
|
||||||
|
max_start_time: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Maximum start time (ISO 8601)',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results per page (max 100)',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
page_token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Pagination token',
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Sort order (e.g., start_time:asc, start_time:desc)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listEvents(args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_get_event: {
|
||||||
|
description: 'Get details of a specific scheduled event by UUID',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Event UUID (from event URI)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getEvent(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_cancel_event: {
|
||||||
|
description: 'Cancel a scheduled event',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Event UUID to cancel',
|
||||||
|
},
|
||||||
|
reason: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Cancellation reason',
|
||||||
|
default: 'Event canceled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.cancelEvent(args.uuid, args.reason);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, 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' }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
204
servers/calendly/src/tools/organizations-tools.ts
Normal file
204
servers/calendly/src/tools/organizations-tools.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// Organizations Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createOrganizationsTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_get_organization: {
|
||||||
|
description: 'Get organization details by UUID',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization UUID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getOrganization(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_list_organization_members: {
|
||||||
|
description: 'List members of an organization',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization_uri: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by email address',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results per page',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
page_token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Pagination token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['organization_uri'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listOrganizationMemberships(args.organization_uri, {
|
||||||
|
email: args.email,
|
||||||
|
count: args.count,
|
||||||
|
page_token: args.page_token,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_list_organization_invitations: {
|
||||||
|
description: 'List pending invitations for an organization',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization_uri: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filter by email address',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['pending', 'accepted', 'declined', 'revoked'],
|
||||||
|
description: 'Filter by invitation status',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results per page',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
page_token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Pagination token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['organization_uri'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listOrganizationInvitations(args.organization_uri, {
|
||||||
|
email: args.email,
|
||||||
|
status: args.status,
|
||||||
|
count: args.count,
|
||||||
|
page_token: args.page_token,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_invite_user: {
|
||||||
|
description: 'Invite a user to an organization by email',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization_uri: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Email address to invite',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['organization_uri', 'email'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.inviteUserToOrganization(
|
||||||
|
args.organization_uri,
|
||||||
|
args.email
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_revoke_invitation: {
|
||||||
|
description: 'Revoke a pending organization invitation',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Invitation UUID to revoke',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.revokeOrganizationInvitation(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_remove_organization_member: {
|
||||||
|
description: 'Remove a member from an organization',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization membership UUID to remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.removeOrganizationMembership(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ success: true, message: 'Member removed' }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
113
servers/calendly/src/tools/scheduling-tools.ts
Normal file
113
servers/calendly/src/tools/scheduling-tools.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Scheduling Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createSchedulingTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_create_scheduling_link: {
|
||||||
|
description: 'Create a single-use scheduling link for an event type or group',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
owner: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Owner URI (event type or group)',
|
||||||
|
},
|
||||||
|
owner_type: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['EventType', 'Group'],
|
||||||
|
description: 'Type of owner',
|
||||||
|
},
|
||||||
|
max_event_count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Maximum number of events that can be scheduled (1-1000)',
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['owner', 'owner_type'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.createSchedulingLink({
|
||||||
|
owner: args.owner,
|
||||||
|
owner_type: args.owner_type,
|
||||||
|
max_event_count: args.max_event_count || 1,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, 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),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
87
servers/calendly/src/tools/users-tools.ts
Normal file
87
servers/calendly/src/tools/users-tools.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// Users Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createUsersTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_get_current_user: {
|
||||||
|
description: 'Get the currently authenticated user information',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getCurrentUser();
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_get_user: {
|
||||||
|
description: 'Get user information by URI',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uri: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User URI',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uri'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getUserByUri(args.uri);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, 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),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
162
servers/calendly/src/tools/webhooks-tools.ts
Normal file
162
servers/calendly/src/tools/webhooks-tools.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Webhooks Tools
|
||||||
|
|
||||||
|
import { CalendlyClient } from '../clients/calendly.js';
|
||||||
|
|
||||||
|
export function createWebhooksTools(client: CalendlyClient) {
|
||||||
|
return {
|
||||||
|
calendly_list_webhook_subscriptions: {
|
||||||
|
description: 'List webhook subscriptions for an organization',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
organization: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI',
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['user', 'organization'],
|
||||||
|
description: 'Webhook scope',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User URI (for user-scoped webhooks)',
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Number of results per page',
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
page_token: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Pagination token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['organization', 'scope'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.listWebhookSubscriptions({
|
||||||
|
organization: args.organization,
|
||||||
|
scope: args.scope,
|
||||||
|
user: args.user,
|
||||||
|
count: args.count,
|
||||||
|
page_token: args.page_token,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_create_webhook_subscription: {
|
||||||
|
description: 'Create a new webhook subscription',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Webhook callback URL',
|
||||||
|
},
|
||||||
|
events: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
description: 'Array of event types to subscribe to (e.g., ["invitee.created", "invitee.canceled"])',
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Organization URI',
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['user', 'organization'],
|
||||||
|
description: 'Webhook scope',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'User URI (for user-scoped webhooks)',
|
||||||
|
},
|
||||||
|
signing_key: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional signing key for webhook verification',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['url', 'events', 'organization', 'scope'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.createWebhookSubscription({
|
||||||
|
url: args.url,
|
||||||
|
events: args.events,
|
||||||
|
organization: args.organization,
|
||||||
|
scope: args.scope,
|
||||||
|
user: args.user,
|
||||||
|
signing_key: args.signing_key,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_get_webhook_subscription: {
|
||||||
|
description: 'Get details of a specific webhook subscription',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Webhook subscription UUID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const result = await client.getWebhookSubscription(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
calendly_delete_webhook_subscription: {
|
||||||
|
description: 'Delete a webhook subscription',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
uuid: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Webhook subscription UUID to delete',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['uuid'],
|
||||||
|
},
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteWebhookSubscription(args.uuid);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify({ success: true, message: 'Webhook subscription deleted' }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
279
servers/calendly/src/types/index.ts
Normal file
279
servers/calendly/src/types/index.ts
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
// Calendly API v2 Types
|
||||||
|
|
||||||
|
export interface CalendlyConfig {
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyUser {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
email: string;
|
||||||
|
scheduling_url: string;
|
||||||
|
timezone: string;
|
||||||
|
avatar_url: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
current_organization: string;
|
||||||
|
resource_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: {
|
||||||
|
type: string;
|
||||||
|
location?: string;
|
||||||
|
join_url?: string;
|
||||||
|
};
|
||||||
|
invitees_counter: {
|
||||||
|
total: number;
|
||||||
|
active: number;
|
||||||
|
limit: number;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
event_memberships: Array<{
|
||||||
|
user: string;
|
||||||
|
}>;
|
||||||
|
event_guests: Array<{
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
}>;
|
||||||
|
cancellation?: {
|
||||||
|
canceled_by: string;
|
||||||
|
reason: string;
|
||||||
|
canceler_type: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyEventType {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
active: boolean;
|
||||||
|
slug: string;
|
||||||
|
scheduling_url: string;
|
||||||
|
duration: number;
|
||||||
|
kind: 'solo' | 'group' | 'collective' | 'round_robin';
|
||||||
|
pooling_type?: string;
|
||||||
|
type: 'StandardEventType' | 'AdhocEventType';
|
||||||
|
color: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
internal_note: string;
|
||||||
|
description_plain: string;
|
||||||
|
description_html: string;
|
||||||
|
profile: {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
};
|
||||||
|
secret: boolean;
|
||||||
|
booking_method: string;
|
||||||
|
custom_questions: Array<{
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
position: number;
|
||||||
|
enabled: boolean;
|
||||||
|
required: boolean;
|
||||||
|
answer_choices: string[];
|
||||||
|
include_other: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyInvitee {
|
||||||
|
uri: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
status: 'active' | 'canceled';
|
||||||
|
timezone: string;
|
||||||
|
event: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
tracking: {
|
||||||
|
utm_campaign?: string;
|
||||||
|
utm_source?: string;
|
||||||
|
utm_medium?: string;
|
||||||
|
utm_content?: string;
|
||||||
|
utm_term?: string;
|
||||||
|
salesforce_uuid?: string;
|
||||||
|
};
|
||||||
|
text_reminder_number: string;
|
||||||
|
rescheduled: boolean;
|
||||||
|
old_invitee: string;
|
||||||
|
new_invitee: string;
|
||||||
|
cancel_url: string;
|
||||||
|
reschedule_url: string;
|
||||||
|
questions_and_answers: Array<{
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
position: number;
|
||||||
|
}>;
|
||||||
|
cancellation?: {
|
||||||
|
canceled_by: string;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
payment?: {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
amount: number;
|
||||||
|
currency: string;
|
||||||
|
terms: string;
|
||||||
|
successful: boolean;
|
||||||
|
};
|
||||||
|
no_show?: {
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
reconfirmation?: {
|
||||||
|
created_at: string;
|
||||||
|
confirmed_at: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyOrganization {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
status: string;
|
||||||
|
timezone: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
resource_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyOrganizationMembership {
|
||||||
|
uri: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
user: {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
email: string;
|
||||||
|
scheduling_url: string;
|
||||||
|
timezone: string;
|
||||||
|
avatar_url: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
organization: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyOrganizationInvitation {
|
||||||
|
uri: string;
|
||||||
|
organization: string;
|
||||||
|
email: string;
|
||||||
|
status: 'pending' | 'accepted' | 'declined' | 'revoked';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
last_sent_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlySchedulingLink {
|
||||||
|
booking_url: string;
|
||||||
|
owner: string;
|
||||||
|
owner_type: string;
|
||||||
|
resource_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyWebhookSubscription {
|
||||||
|
uri: string;
|
||||||
|
callback_url: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
retry_started_at: string;
|
||||||
|
state: 'active' | 'disabled';
|
||||||
|
events: string[];
|
||||||
|
scope: 'user' | 'organization';
|
||||||
|
organization: string;
|
||||||
|
user: string;
|
||||||
|
creator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyAvailableTime {
|
||||||
|
status: 'available';
|
||||||
|
invitees_remaining: number;
|
||||||
|
start_time: string;
|
||||||
|
scheduling_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyUserBusyTime {
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
type: 'calendly' | 'busy_calendar';
|
||||||
|
buffered: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyRoutingForm {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
status: 'published' | 'draft';
|
||||||
|
published_version: number;
|
||||||
|
organization: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
questions: Array<{
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
required: boolean;
|
||||||
|
answer_choices?: Array<{
|
||||||
|
uuid: string;
|
||||||
|
label: string;
|
||||||
|
position: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
routing_configurations: Array<{
|
||||||
|
priority: number;
|
||||||
|
rules: Array<{
|
||||||
|
question_uuid: string;
|
||||||
|
type: string;
|
||||||
|
value: string;
|
||||||
|
}>;
|
||||||
|
routing_target: {
|
||||||
|
type: string;
|
||||||
|
target: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyNoShow {
|
||||||
|
uri: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
count?: number;
|
||||||
|
page_token?: string;
|
||||||
|
sort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
collection: T[];
|
||||||
|
pagination: {
|
||||||
|
count: number;
|
||||||
|
next_page?: string;
|
||||||
|
previous_page?: string;
|
||||||
|
next_page_token?: string;
|
||||||
|
previous_page_token?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalendlyError {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
details?: Array<{
|
||||||
|
parameter: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function AnalyticsDashboard() {
|
||||||
|
const stats = {
|
||||||
|
totalEvents: 245,
|
||||||
|
activeEvents: 32,
|
||||||
|
totalInvitees: 489,
|
||||||
|
noShows: 12,
|
||||||
|
cancelRate: '4.9%',
|
||||||
|
avgDuration: '45 min',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📊 Analytics Dashboard</h1>
|
||||||
|
<p>Calendly metrics and insights</p>
|
||||||
|
</header>
|
||||||
|
<div className="detail-grid">
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>Total Events</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.totalEvents}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>Active Events</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.activeEvents}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>Total Invitees</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.totalInvitees}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>No-Shows</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#f85149'}}>{stats.noShows}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>Cancel Rate</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#d29922'}}>{stats.cancelRate}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid-item">
|
||||||
|
<h3>Avg Duration</h3>
|
||||||
|
<div style={{fontSize: '2rem', fontWeight: 'bold', color: '#58a6ff'}}>{stats.avgDuration}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/analytics-dashboard/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/analytics-dashboard/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function AvailabilityCalendar() {
|
||||||
|
const [availableTimes] = useState([
|
||||||
|
{ time: '2024-02-15 09:00', available: true },
|
||||||
|
{ time: '2024-02-15 10:00', available: true },
|
||||||
|
{ time: '2024-02-15 11:00', available: false },
|
||||||
|
{ time: '2024-02-15 14:00', available: true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📅 Availability Calendar</h1>
|
||||||
|
<p>View available time slots</p>
|
||||||
|
</header>
|
||||||
|
<div className="search-box">
|
||||||
|
<input type="date" placeholder="Start Date" />
|
||||||
|
<input type="date" placeholder="End Date" />
|
||||||
|
<button>Load Availability</button>
|
||||||
|
</div>
|
||||||
|
<div className="detail-grid">
|
||||||
|
{availableTimes.map((slot, i) => (
|
||||||
|
<div key={i} className="grid-item">
|
||||||
|
<strong>{new Date(slot.time).toLocaleString()}</strong>
|
||||||
|
<span className={`status-badge ${slot.available ? 'active' : 'canceled'}`}>
|
||||||
|
{slot.available ? 'Available' : 'Busy'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
56
servers/calendly/src/ui/react-app/booking-flow/App.tsx
Normal file
56
servers/calendly/src/ui/react-app/booking-flow/App.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function BookingFlow() {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
eventType: '',
|
||||||
|
time: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📝 Booking Flow</h1>
|
||||||
|
<p>Create a booking experience</p>
|
||||||
|
</header>
|
||||||
|
<div className="detail-card">
|
||||||
|
<h2>Step {step} of 3</h2>
|
||||||
|
{step === 1 && (
|
||||||
|
<div>
|
||||||
|
<h3>Select Event Type</h3>
|
||||||
|
<select onChange={(e) => setFormData({...formData, eventType: e.target.value})}>
|
||||||
|
<option value="">Choose...</option>
|
||||||
|
<option value="demo">Sales Demo (60 min)</option>
|
||||||
|
<option value="meeting">Quick Meeting (30 min)</option>
|
||||||
|
</select>
|
||||||
|
<button onClick={() => setStep(2)} style={{marginTop: '1rem'}}>Next</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 2 && (
|
||||||
|
<div>
|
||||||
|
<h3>Select Time</h3>
|
||||||
|
<input type="datetime-local" onChange={(e) => setFormData({...formData, time: e.target.value})} />
|
||||||
|
<div style={{marginTop: '1rem'}}>
|
||||||
|
<button onClick={() => setStep(1)}>Back</button>
|
||||||
|
<button onClick={() => setStep(3)} style={{marginLeft: '0.5rem'}}>Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step === 3 && (
|
||||||
|
<div>
|
||||||
|
<h3>Your Information</h3>
|
||||||
|
<input type="text" placeholder="Name" onChange={(e) => setFormData({...formData, name: e.target.value})} style={{marginBottom: '0.5rem'}} />
|
||||||
|
<input type="email" placeholder="Email" onChange={(e) => setFormData({...formData, email: e.target.value})} />
|
||||||
|
<div style={{marginTop: '1rem'}}>
|
||||||
|
<button onClick={() => setStep(2)}>Back</button>
|
||||||
|
<button style={{marginLeft: '0.5rem'}}>Book Event</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/booking-flow/index.html
Normal file
19
servers/calendly/src/ui/react-app/booking-flow/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/booking-flow/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/booking-flow/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
157
servers/calendly/src/ui/react-app/event-dashboard/App.tsx
Normal file
157
servers/calendly/src/ui/react-app/event-dashboard/App.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
uri: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
invitees_counter: {
|
||||||
|
total: number;
|
||||||
|
active: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventDashboard() {
|
||||||
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'canceled'>('active');
|
||||||
|
const [dateRange, setDateRange] = useState('week');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEvents();
|
||||||
|
}, [statusFilter, dateRange]);
|
||||||
|
|
||||||
|
const loadEvents = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Call MCP tool via parent window
|
||||||
|
const params: any = {};
|
||||||
|
|
||||||
|
if (statusFilter !== 'all') {
|
||||||
|
params.status = statusFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range calculation
|
||||||
|
const now = new Date();
|
||||||
|
const minDate = new Date();
|
||||||
|
if (dateRange === 'week') {
|
||||||
|
minDate.setDate(now.getDate() - 7);
|
||||||
|
} else if (dateRange === 'month') {
|
||||||
|
minDate.setMonth(now.getMonth() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.min_start_time = minDate.toISOString();
|
||||||
|
params.max_start_time = now.toISOString();
|
||||||
|
params.sort = 'start_time:desc';
|
||||||
|
|
||||||
|
const result = await window.parent.postMessage({
|
||||||
|
type: 'mcp_tool_call',
|
||||||
|
tool: 'calendly_list_scheduled_events',
|
||||||
|
params,
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// In real implementation, would listen for response
|
||||||
|
// For now, mock data
|
||||||
|
setEvents([
|
||||||
|
{
|
||||||
|
uri: 'https://api.calendly.com/scheduled_events/001',
|
||||||
|
name: 'Sales Demo',
|
||||||
|
status: 'active',
|
||||||
|
start_time: new Date().toISOString(),
|
||||||
|
end_time: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
invitees_counter: { total: 1, active: 1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uri: 'https://api.calendly.com/scheduled_events/002',
|
||||||
|
name: 'Customer Onboarding',
|
||||||
|
status: 'active',
|
||||||
|
start_time: new Date(Date.now() - 86400000).toISOString(),
|
||||||
|
end_time: new Date(Date.now() - 82800000).toISOString(),
|
||||||
|
invitees_counter: { total: 2, active: 2 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load events:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📅 Event Dashboard</h1>
|
||||||
|
<p>Manage your Calendly scheduled events</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="filters">
|
||||||
|
<div className="filter-group">
|
||||||
|
<label>Status:</label>
|
||||||
|
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value as any)}>
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="canceled">Canceled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="filter-group">
|
||||||
|
<label>Date Range:</label>
|
||||||
|
<select value={dateRange} onChange={(e) => setDateRange(e.target.value)}>
|
||||||
|
<option value="week">Last Week</option>
|
||||||
|
<option value="month">Last Month</option>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button onClick={loadEvents} className="btn-refresh">↻ Refresh</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stats-grid">
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>Total Events</h3>
|
||||||
|
<div className="stat-value">{events.length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>Active Events</h3>
|
||||||
|
<div className="stat-value">{events.filter(e => e.status === 'active').length}</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<h3>Total Invitees</h3>
|
||||||
|
<div className="stat-value">{events.reduce((acc, e) => acc + e.invitees_counter.total, 0)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="loading">Loading events...</div>
|
||||||
|
) : (
|
||||||
|
<div className="events-list">
|
||||||
|
{events.map((event) => (
|
||||||
|
<div key={event.uri} className="event-card">
|
||||||
|
<div className="event-header">
|
||||||
|
<h3>{event.name}</h3>
|
||||||
|
<span className={`status-badge ${event.status}`}>{event.status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="event-details">
|
||||||
|
<p><strong>Start:</strong> {formatDate(event.start_time)}</p>
|
||||||
|
<p><strong>End:</strong> {formatDate(event.end_time)}</p>
|
||||||
|
<p><strong>Invitees:</strong> {event.invitees_counter.active} / {event.invitees_counter.total}</p>
|
||||||
|
</div>
|
||||||
|
<div className="event-actions">
|
||||||
|
<button className="btn-view">View Details</button>
|
||||||
|
{event.status === 'active' && (
|
||||||
|
<button className="btn-cancel">Cancel Event</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/event-dashboard/index.html
Normal file
19
servers/calendly/src/ui/react-app/event-dashboard/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly Event Dashboard - MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/event-dashboard/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/event-dashboard/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
83
servers/calendly/src/ui/react-app/event-detail/App.tsx
Normal file
83
servers/calendly/src/ui/react-app/event-detail/App.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EventDetail() {
|
||||||
|
const [event, setEvent] = useState<any>(null);
|
||||||
|
const [invitees, setInvitees] = useState<any[]>([]);
|
||||||
|
const [eventUuid, setEventUuid] = useState('');
|
||||||
|
|
||||||
|
const loadEvent = async () => {
|
||||||
|
if (!eventUuid) return;
|
||||||
|
// Call MCP tool calendly_get_event
|
||||||
|
setEvent({
|
||||||
|
name: 'Sales Demo',
|
||||||
|
status: 'active',
|
||||||
|
start_time: new Date().toISOString(),
|
||||||
|
end_time: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
location: { type: 'zoom', join_url: 'https://zoom.us/j/123' },
|
||||||
|
meeting_notes_plain: 'Please prepare your questions.',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadInvitees = async () => {
|
||||||
|
if (!eventUuid) return;
|
||||||
|
// Call MCP tool calendly_list_event_invitees
|
||||||
|
setInvitees([
|
||||||
|
{ name: 'John Doe', email: 'john@example.com', status: 'active' },
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEvent = async () => {
|
||||||
|
if (!confirm('Cancel this event?')) return;
|
||||||
|
// Call MCP tool calendly_cancel_event
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📋 Event Details</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="search-box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter Event UUID..."
|
||||||
|
value={eventUuid}
|
||||||
|
onChange={(e) => setEventUuid(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={loadEvent}>Load Event</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event && (
|
||||||
|
<div className="detail-card">
|
||||||
|
<h2>{event.name}</h2>
|
||||||
|
<div className="detail-grid">
|
||||||
|
<div><strong>Status:</strong> <span className={`status-badge ${event.status}`}>{event.status}</span></div>
|
||||||
|
<div><strong>Start:</strong> {new Date(event.start_time).toLocaleString()}</div>
|
||||||
|
<div><strong>End:</strong> {new Date(event.end_time).toLocaleString()}</div>
|
||||||
|
<div><strong>Location:</strong> {event.location.type}</div>
|
||||||
|
</div>
|
||||||
|
{event.meeting_notes_plain && (
|
||||||
|
<div className="notes">
|
||||||
|
<h3>Meeting Notes</h3>
|
||||||
|
<p>{event.meeting_notes_plain}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="invitees-section">
|
||||||
|
<h3>Invitees</h3>
|
||||||
|
{invitees.map((inv, i) => (
|
||||||
|
<div key={i} className="invitee-row">
|
||||||
|
<span>{inv.name}</span>
|
||||||
|
<span>{inv.email}</span>
|
||||||
|
<span className={`status-badge ${inv.status}`}>{inv.status}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{event.status === 'active' && (
|
||||||
|
<button onClick={cancelEvent} className="btn-cancel">Cancel Event</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/event-detail/index.html
Normal file
19
servers/calendly/src/ui/react-app/event-detail/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/event-detail/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/event-detail/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
28
servers/calendly/src/ui/react-app/event-grid/App.tsx
Normal file
28
servers/calendly/src/ui/react-app/event-grid/App.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EventGrid() {
|
||||||
|
const [events] = useState([
|
||||||
|
{ id: '1', name: 'Sales Demo', date: '2024-02-15 10:00', status: 'active' },
|
||||||
|
{ id: '2', name: 'Onboarding Call', date: '2024-02-16 14:00', status: 'active' },
|
||||||
|
{ id: '3', name: 'Team Sync', date: '2024-02-17 09:00', status: 'active' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>📊 Event Grid</h1>
|
||||||
|
<p>Calendar grid view of all events</p>
|
||||||
|
</header>
|
||||||
|
<div className="detail-grid">
|
||||||
|
{events.map(event => (
|
||||||
|
<div key={event.id} className="grid-item">
|
||||||
|
<h3>{event.name}</h3>
|
||||||
|
<p>{event.date}</p>
|
||||||
|
<span className={`status-badge ${event.status}`}>{event.status}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/event-grid/index.html
Normal file
19
servers/calendly/src/ui/react-app/event-grid/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/event-grid/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/event-grid/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
14
servers/calendly/src/ui/react-app/event-grid/vite.config.ts
Normal file
14
servers/calendly/src/ui/react-app/event-grid/vite.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
35
servers/calendly/src/ui/react-app/event-type-manager/App.tsx
Normal file
35
servers/calendly/src/ui/react-app/event-type-manager/App.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function EventTypeManager() {
|
||||||
|
const [eventTypes] = useState([
|
||||||
|
{ id: '1', name: '30 Minute Meeting', duration: 30, active: true },
|
||||||
|
{ id: '2', name: 'Sales Demo', duration: 60, active: true },
|
||||||
|
{ id: '3', name: 'Quick Chat', duration: 15, active: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>⚙️ Event Type Manager</h1>
|
||||||
|
<p>Manage your Calendly event types</p>
|
||||||
|
</header>
|
||||||
|
<button style={{marginBottom: '1rem'}}>+ Create New Event Type</button>
|
||||||
|
<div className="detail-grid">
|
||||||
|
{eventTypes.map(type => (
|
||||||
|
<div key={type.id} className="grid-item">
|
||||||
|
<h3>{type.name}</h3>
|
||||||
|
<p>Duration: {type.duration} minutes</p>
|
||||||
|
<span className={`status-badge ${type.active ? 'active' : 'canceled'}`}>
|
||||||
|
{type.active ? 'Active' : 'Inactive'}
|
||||||
|
</span>
|
||||||
|
<div style={{marginTop: '1rem'}}>
|
||||||
|
<button>Edit</button>
|
||||||
|
<button className="btn-cancel" style={{marginLeft: '0.5rem'}}>Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/event-type-manager/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/event-type-manager/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
34
servers/calendly/src/ui/react-app/invitee-list/App.tsx
Normal file
34
servers/calendly/src/ui/react-app/invitee-list/App.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function InviteeList() {
|
||||||
|
const [invitees] = useState([
|
||||||
|
{ id: '1', name: 'John Doe', email: 'john@example.com', event: 'Sales Demo', status: 'active' },
|
||||||
|
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', event: 'Onboarding', status: 'active' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const markNoShow = (id: string) => {
|
||||||
|
// Call MCP tool calendly_mark_no_show
|
||||||
|
alert(`Marking invitee ${id} as no-show`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>👥 Invitee List</h1>
|
||||||
|
<p>Manage event invitees</p>
|
||||||
|
</header>
|
||||||
|
{invitees.map(invitee => (
|
||||||
|
<div key={invitee.id} className="detail-card">
|
||||||
|
<h3>{invitee.name}</h3>
|
||||||
|
<p><strong>Email:</strong> {invitee.email}</p>
|
||||||
|
<p><strong>Event:</strong> {invitee.event}</p>
|
||||||
|
<span className={`status-badge ${invitee.status}`}>{invitee.status}</span>
|
||||||
|
<div style={{marginTop: '1rem'}}>
|
||||||
|
<button onClick={() => markNoShow(invitee.id)}>Mark No-Show</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/invitee-list/index.html
Normal file
19
servers/calendly/src/ui/react-app/invitee-list/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/invitee-list/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/invitee-list/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
34
servers/calendly/src/ui/react-app/no-show-tracker/App.tsx
Normal file
34
servers/calendly/src/ui/react-app/no-show-tracker/App.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function NoShowTracker() {
|
||||||
|
const [noShows] = useState([
|
||||||
|
{ id: '1', invitee: 'John Doe', event: 'Sales Demo', date: '2024-02-10', email: 'john@example.com' },
|
||||||
|
{ id: '2', invitee: 'Jane Smith', event: 'Onboarding', date: '2024-02-12', email: 'jane@example.com' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const unmarkNoShow = (id: string) => {
|
||||||
|
// Call MCP tool calendly_unmark_no_show
|
||||||
|
alert(`Removing no-show status for ${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>🚫 No-Show Tracker</h1>
|
||||||
|
<p>Track and manage no-shows</p>
|
||||||
|
</header>
|
||||||
|
{noShows.map(noShow => (
|
||||||
|
<div key={noShow.id} className="detail-card">
|
||||||
|
<h3>{noShow.invitee}</h3>
|
||||||
|
<p><strong>Event:</strong> {noShow.event}</p>
|
||||||
|
<p><strong>Date:</strong> {noShow.date}</p>
|
||||||
|
<p><strong>Email:</strong> {noShow.email}</p>
|
||||||
|
<button onClick={() => unmarkNoShow(noShow.id)} style={{marginTop: '0.5rem'}}>
|
||||||
|
Remove No-Show Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/no-show-tracker/index.html
Normal file
19
servers/calendly/src/ui/react-app/no-show-tracker/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/no-show-tracker/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/no-show-tracker/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
41
servers/calendly/src/ui/react-app/org-members/App.tsx
Normal file
41
servers/calendly/src/ui/react-app/org-members/App.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function OrgMembers() {
|
||||||
|
const [members] = useState([
|
||||||
|
{ id: '1', name: 'Alice Johnson', email: 'alice@company.com', role: 'admin' },
|
||||||
|
{ id: '2', name: 'Bob Wilson', email: 'bob@company.com', role: 'user' },
|
||||||
|
]);
|
||||||
|
const [newEmail, setNewEmail] = useState('');
|
||||||
|
|
||||||
|
const inviteMember = async () => {
|
||||||
|
// Call MCP tool calendly_invite_user
|
||||||
|
alert(`Inviting ${newEmail}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>👨💼 Organization Members</h1>
|
||||||
|
<p>Manage team members</p>
|
||||||
|
</header>
|
||||||
|
<div className="search-box">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email to invite"
|
||||||
|
value={newEmail}
|
||||||
|
onChange={(e) => setNewEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={inviteMember}>Invite Member</button>
|
||||||
|
</div>
|
||||||
|
{members.map(member => (
|
||||||
|
<div key={member.id} className="detail-card">
|
||||||
|
<h3>{member.name}</h3>
|
||||||
|
<p><strong>Email:</strong> {member.email}</p>
|
||||||
|
<p><strong>Role:</strong> {member.role}</p>
|
||||||
|
<button className="btn-cancel" style={{marginTop: '0.5rem'}}>Remove</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/org-members/index.html
Normal file
19
servers/calendly/src/ui/react-app/org-members/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/org-members/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/org-members/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
14
servers/calendly/src/ui/react-app/org-members/vite.config.ts
Normal file
14
servers/calendly/src/ui/react-app/org-members/vite.config.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
40
servers/calendly/src/ui/react-app/scheduling-links/App.tsx
Normal file
40
servers/calendly/src/ui/react-app/scheduling-links/App.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function SchedulingLinks() {
|
||||||
|
const [links, setLinks] = useState<any[]>([]);
|
||||||
|
const [eventTypeUri, setEventTypeUri] = useState('');
|
||||||
|
|
||||||
|
const createLink = async () => {
|
||||||
|
// Call MCP tool calendly_create_scheduling_link
|
||||||
|
const newLink = {
|
||||||
|
url: 'https://calendly.com/link/abc123',
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
setLinks([...links, newLink]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>🔗 Scheduling Links</h1>
|
||||||
|
<p>Create single-use scheduling links</p>
|
||||||
|
</header>
|
||||||
|
<div className="search-box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Event Type URI"
|
||||||
|
value={eventTypeUri}
|
||||||
|
onChange={(e) => setEventTypeUri(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={createLink}>Create Link</button>
|
||||||
|
</div>
|
||||||
|
{links.map((link, i) => (
|
||||||
|
<div key={i} className="detail-card">
|
||||||
|
<p><strong>URL:</strong> <a href={link.url} target="_blank">{link.url}</a></p>
|
||||||
|
<p><strong>Created:</strong> {new Date(link.created).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/scheduling-links/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/scheduling-links/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
106
servers/calendly/src/ui/react-app/shared-styles.css
Normal file
106
servers/calendly/src/ui/react-app/shared-styles.css
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
|
background: #0f1419;
|
||||||
|
color: #e4e6eb;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 2px solid #2a3441;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #8b949e;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #238636;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #2ea043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #da3633 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background: #f85149 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #0d1117;
|
||||||
|
border: 1px solid #2a3441;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #e4e6eb;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card, .grid-item {
|
||||||
|
background: #161b22;
|
||||||
|
border: 1px solid #2a3441;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.active {
|
||||||
|
background: #238636;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.canceled {
|
||||||
|
background: #da3633;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
40
servers/calendly/src/ui/react-app/webhook-manager/App.tsx
Normal file
40
servers/calendly/src/ui/react-app/webhook-manager/App.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
export default function WebhookManager() {
|
||||||
|
const [webhooks] = useState([
|
||||||
|
{ id: '1', url: 'https://api.example.com/webhook', events: ['invitee.created'], status: 'active' },
|
||||||
|
]);
|
||||||
|
const [newUrl, setNewUrl] = useState('');
|
||||||
|
|
||||||
|
const createWebhook = async () => {
|
||||||
|
// Call MCP tool calendly_create_webhook_subscription
|
||||||
|
alert(`Creating webhook for ${newUrl}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<header className="header">
|
||||||
|
<h1>🪝 Webhook Manager</h1>
|
||||||
|
<p>Manage webhook subscriptions</p>
|
||||||
|
</header>
|
||||||
|
<div className="search-box">
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
placeholder="Webhook URL"
|
||||||
|
value={newUrl}
|
||||||
|
onChange={(e) => setNewUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={createWebhook}>Create Webhook</button>
|
||||||
|
</div>
|
||||||
|
{webhooks.map(webhook => (
|
||||||
|
<div key={webhook.id} className="detail-card">
|
||||||
|
<p><strong>URL:</strong> {webhook.url}</p>
|
||||||
|
<p><strong>Events:</strong> {webhook.events.join(', ')}</p>
|
||||||
|
<span className={`status-badge ${webhook.status}`}>{webhook.status}</span>
|
||||||
|
<button className="btn-cancel" style={{marginTop: '1rem'}}>Delete</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
servers/calendly/src/ui/react-app/webhook-manager/index.html
Normal file
19
servers/calendly/src/ui/react-app/webhook-manager/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Calendly MCP App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module">
|
||||||
|
import React from 'https://esm.sh/react@18.3.1';
|
||||||
|
import ReactDOM from 'https://esm.sh/react-dom@18.3.1/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(React.createElement(App));
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
servers/calendly/src/ui/react-app/webhook-manager/styles.css
Symbolic link
1
servers/calendly/src/ui/react-app/webhook-manager/styles.css
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../shared-styles.css
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,15 +1,21 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist", "src/ui"]
|
||||||
}
|
}
|
||||||
|
|||||||
236
servers/clickup/README.md
Normal file
236
servers/clickup/README.md
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# ClickUp MCP Server
|
||||||
|
|
||||||
|
Complete Model Context Protocol server for ClickUp - the all-in-one productivity platform.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **70+ Tools** across all ClickUp domains
|
||||||
|
- **25 React Apps** for rich interactive UIs
|
||||||
|
- **Full API Coverage**: Tasks, Spaces, Folders, Lists, Views, Comments, Docs, Goals, Tags, Time Tracking, Teams, Webhooks, Custom Fields, Templates, and Guests
|
||||||
|
- **Production Ready**: Rate limiting, pagination, error handling, comprehensive types
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @mcpengine/clickup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to your MCP settings:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"clickup": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/path/to/@mcpengine/clickup/dist/index.js"],
|
||||||
|
"env": {
|
||||||
|
"CLICKUP_API_TOKEN": "your-api-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
ClickUp MCP supports two authentication methods:
|
||||||
|
|
||||||
|
1. **Personal API Token** (recommended for development):
|
||||||
|
- Get your token from: https://app.clickup.com/settings/apps
|
||||||
|
- Set `CLICKUP_API_TOKEN` environment variable
|
||||||
|
|
||||||
|
2. **OAuth2** (for production apps):
|
||||||
|
- Set `CLICKUP_CLIENT_ID`, `CLICKUP_CLIENT_SECRET`, and `CLICKUP_OAUTH_TOKEN`
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Tasks (17 tools)
|
||||||
|
- `clickup_tasks_list` - List tasks with filtering
|
||||||
|
- `clickup_tasks_get` - Get task details
|
||||||
|
- `clickup_tasks_create` - Create new task
|
||||||
|
- `clickup_tasks_update` - Update task
|
||||||
|
- `clickup_tasks_delete` - Delete task
|
||||||
|
- `clickup_tasks_filter` - Advanced task filtering
|
||||||
|
- `clickup_tasks_bulk_update` - Bulk update tasks
|
||||||
|
- `clickup_tasks_get_time_entries` - Get time entries for task
|
||||||
|
- `clickup_tasks_add_time_entry` - Add time entry
|
||||||
|
- `clickup_tasks_get_custom_fields` - Get custom field values
|
||||||
|
- `clickup_tasks_set_custom_field` - Set custom field value
|
||||||
|
- `clickup_tasks_add_dependency` - Add task dependency
|
||||||
|
- `clickup_tasks_remove_dependency` - Remove dependency
|
||||||
|
- `clickup_tasks_list_members` - List task members
|
||||||
|
- `clickup_tasks_add_comment` - Add comment to task
|
||||||
|
- `clickup_tasks_get_comments` - Get task comments
|
||||||
|
- `clickup_tasks_search` - Search tasks
|
||||||
|
|
||||||
|
### Spaces (5 tools)
|
||||||
|
- `clickup_spaces_list` - List spaces
|
||||||
|
- `clickup_spaces_get` - Get space details
|
||||||
|
- `clickup_spaces_create` - Create space
|
||||||
|
- `clickup_spaces_update` - Update space
|
||||||
|
- `clickup_spaces_delete` - Delete space
|
||||||
|
|
||||||
|
### Folders (5 tools)
|
||||||
|
- `clickup_folders_list` - List folders
|
||||||
|
- `clickup_folders_get` - Get folder details
|
||||||
|
- `clickup_folders_create` - Create folder
|
||||||
|
- `clickup_folders_update` - Update folder
|
||||||
|
- `clickup_folders_delete` - Delete folder
|
||||||
|
|
||||||
|
### Lists (7 tools)
|
||||||
|
- `clickup_lists_list` - List lists
|
||||||
|
- `clickup_lists_get` - Get list details
|
||||||
|
- `clickup_lists_create` - Create list
|
||||||
|
- `clickup_lists_update` - Update list
|
||||||
|
- `clickup_lists_delete` - Delete list
|
||||||
|
- `clickup_lists_add_task` - Add task to list
|
||||||
|
- `clickup_lists_remove_task` - Remove task from list
|
||||||
|
|
||||||
|
### Views (5 tools)
|
||||||
|
- `clickup_views_list` - List views
|
||||||
|
- `clickup_views_get` - Get view details
|
||||||
|
- `clickup_views_create` - Create view
|
||||||
|
- `clickup_views_update` - Update view
|
||||||
|
- `clickup_views_delete` - Delete view
|
||||||
|
|
||||||
|
### Comments (5 tools)
|
||||||
|
- `clickup_comments_list` - List comments
|
||||||
|
- `clickup_comments_get` - Get comment
|
||||||
|
- `clickup_comments_create` - Create comment
|
||||||
|
- `clickup_comments_update` - Update comment
|
||||||
|
- `clickup_comments_delete` - Delete comment
|
||||||
|
|
||||||
|
### Docs (3 tools)
|
||||||
|
- `clickup_docs_list` - List docs
|
||||||
|
- `clickup_docs_get` - Get doc
|
||||||
|
- `clickup_docs_create` - Create doc
|
||||||
|
- `clickup_docs_search` - Search docs
|
||||||
|
|
||||||
|
### Goals (7 tools)
|
||||||
|
- `clickup_goals_list` - List goals
|
||||||
|
- `clickup_goals_get` - Get goal
|
||||||
|
- `clickup_goals_create` - Create goal
|
||||||
|
- `clickup_goals_update` - Update goal
|
||||||
|
- `clickup_goals_delete` - Delete goal
|
||||||
|
- `clickup_goals_add_key_result` - Add key result
|
||||||
|
- `clickup_goals_update_key_result` - Update key result
|
||||||
|
|
||||||
|
### Tags (5 tools)
|
||||||
|
- `clickup_tags_list` - List tags
|
||||||
|
- `clickup_tags_create` - Create tag
|
||||||
|
- `clickup_tags_update` - Update tag
|
||||||
|
- `clickup_tags_delete` - Delete tag
|
||||||
|
- `clickup_tags_add_to_task` - Add tag to task
|
||||||
|
|
||||||
|
### Checklists (6 tools)
|
||||||
|
- `clickup_checklists_create` - Create checklist
|
||||||
|
- `clickup_checklists_update` - Update checklist
|
||||||
|
- `clickup_checklists_delete` - Delete checklist
|
||||||
|
- `clickup_checklists_create_item` - Create checklist item
|
||||||
|
- `clickup_checklists_update_item` - Update item
|
||||||
|
- `clickup_checklists_delete_item` - Delete item
|
||||||
|
|
||||||
|
### Time Tracking (7 tools)
|
||||||
|
- `clickup_time_list_entries` - List time entries
|
||||||
|
- `clickup_time_get_entry` - Get time entry
|
||||||
|
- `clickup_time_create` - Create time entry
|
||||||
|
- `clickup_time_update` - Update time entry
|
||||||
|
- `clickup_time_delete` - Delete time entry
|
||||||
|
- `clickup_time_get_running` - Get running timer
|
||||||
|
- `clickup_time_start` - Start timer
|
||||||
|
- `clickup_time_stop` - Stop timer
|
||||||
|
|
||||||
|
### Teams (6 tools)
|
||||||
|
- `clickup_teams_list_workspaces` - List workspaces
|
||||||
|
- `clickup_teams_get_workspace` - Get workspace
|
||||||
|
- `clickup_teams_list_members` - List members
|
||||||
|
- `clickup_teams_get_member` - Get member
|
||||||
|
- `clickup_teams_list_groups` - List groups
|
||||||
|
- `clickup_teams_create_group` - Create group
|
||||||
|
|
||||||
|
### Webhooks (4 tools)
|
||||||
|
- `clickup_webhooks_list` - List webhooks
|
||||||
|
- `clickup_webhooks_create` - Create webhook
|
||||||
|
- `clickup_webhooks_update` - Update webhook
|
||||||
|
- `clickup_webhooks_delete` - Delete webhook
|
||||||
|
|
||||||
|
### Custom Fields (4 tools)
|
||||||
|
- `clickup_custom_fields_list` - List custom fields
|
||||||
|
- `clickup_custom_fields_get` - Get custom field
|
||||||
|
- `clickup_custom_fields_set_value` - Set field value
|
||||||
|
- `clickup_custom_fields_remove_value` - Remove value
|
||||||
|
|
||||||
|
### Templates (2 tools)
|
||||||
|
- `clickup_templates_list` - List templates
|
||||||
|
- `clickup_templates_apply` - Apply template
|
||||||
|
|
||||||
|
### Guests (6 tools)
|
||||||
|
- `clickup_guests_invite` - Invite guest
|
||||||
|
- `clickup_guests_get` - Get guest
|
||||||
|
- `clickup_guests_edit` - Edit guest
|
||||||
|
- `clickup_guests_remove` - Remove guest
|
||||||
|
- `clickup_guests_add_to_task` - Add guest to task
|
||||||
|
- `clickup_guests_add_to_list` - Add guest to list
|
||||||
|
|
||||||
|
## Available Apps
|
||||||
|
|
||||||
|
### Task Management
|
||||||
|
- **task-dashboard** - Overview with status counts, overdue, priority breakdown
|
||||||
|
- **task-detail** - Full task view with subtasks, comments, custom fields, time entries, dependencies
|
||||||
|
- **task-grid** - Sortable/filterable task list
|
||||||
|
- **task-board** - Kanban board by status (drag-drop)
|
||||||
|
|
||||||
|
### Workspace & Organization
|
||||||
|
- **space-overview** - Space with folders, lists, members
|
||||||
|
- **folder-overview** - Folder with lists and task summaries
|
||||||
|
- **list-view** - List detail with task table
|
||||||
|
- **workspace-overview** - High-level workspace stats
|
||||||
|
|
||||||
|
### Views & Visualization
|
||||||
|
- **calendar-view** - Tasks on calendar by due date
|
||||||
|
- **gantt-view** - Timeline/gantt of tasks with dependencies
|
||||||
|
- **sprint-board** - Sprint-style task board with velocity
|
||||||
|
|
||||||
|
### Goals & Tracking
|
||||||
|
- **goal-tracker** - Goals with key results progress bars
|
||||||
|
- **time-dashboard** - Time tracking overview, entries by date/member
|
||||||
|
- **time-entries** - Time entry list with task associations
|
||||||
|
- **member-workload** - Per-member task counts, time logged, overdue
|
||||||
|
|
||||||
|
### Content & Collaboration
|
||||||
|
- **doc-browser** - Document list with search
|
||||||
|
- **comment-thread** - Threaded comments for a task
|
||||||
|
- **checklist-manager** - Checklists with item completion
|
||||||
|
- **tag-manager** - Tag list with task counts
|
||||||
|
- **custom-fields-editor** - Custom field values on a task
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
- **template-gallery** - Available templates with preview
|
||||||
|
- **search-results** - Universal search across tasks/docs
|
||||||
|
- **activity-feed** - Recent changes across workspace
|
||||||
|
|
||||||
|
## API Coverage
|
||||||
|
|
||||||
|
This server implements the complete ClickUp API v2:
|
||||||
|
- https://clickup.com/api/clickupapiref/operation/GetTasks/
|
||||||
|
- Rate limiting and pagination handled automatically
|
||||||
|
- Comprehensive error handling and retries
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
npm run watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@ -1,20 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-server-clickup",
|
"name": "@mcpengine/clickup",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"description": "ClickUp MCP Server - Complete task management, collaboration, and productivity platform integration",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"type": "module",
|
||||||
"build": "tsc",
|
"bin": {
|
||||||
"start": "node dist/index.js",
|
"clickup-mcp": "./dist/index.js"
|
||||||
"dev": "tsx src/index.ts"
|
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && npm run chmod",
|
||||||
|
"chmod": "chmod +x dist/index.js",
|
||||||
|
"watch": "tsc --watch",
|
||||||
|
"prepare": "npm run build"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"clickup",
|
||||||
|
"tasks",
|
||||||
|
"project-management",
|
||||||
|
"productivity",
|
||||||
|
"collaboration"
|
||||||
|
],
|
||||||
|
"author": "MCPEngine",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
"@modelcontextprotocol/sdk": "^1.0.6",
|
||||||
"zod": "^3.22.4"
|
"axios": "^1.7.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^22.0.0",
|
||||||
"tsx": "^4.7.0",
|
"typescript": "^5.5.4"
|
||||||
"typescript": "^5.3.0"
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
570
servers/clickup/src/clients/clickup.ts
Normal file
570
servers/clickup/src/clients/clickup.ts
Normal file
@ -0,0 +1,570 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp API Client
|
||||||
|
* API v2: https://clickup.com/api
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
|
||||||
|
import type { ClickUpConfig, ClickUpError } from '../types.js';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://api.clickup.com/api/v2';
|
||||||
|
const RATE_LIMIT_DELAY = 100; // ms between requests
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const RETRY_DELAY = 1000; // ms
|
||||||
|
|
||||||
|
export class ClickUpClient {
|
||||||
|
private client: AxiosInstance;
|
||||||
|
private lastRequestTime = 0;
|
||||||
|
private apiToken: string;
|
||||||
|
|
||||||
|
constructor(config: ClickUpConfig) {
|
||||||
|
this.apiToken = config.apiToken || config.oauthToken || '';
|
||||||
|
|
||||||
|
if (!this.apiToken) {
|
||||||
|
throw new Error('ClickUp API token is required. Set CLICKUP_API_TOKEN environment variable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = axios.create({
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Authorization': this.apiToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response interceptor for error handling
|
||||||
|
this.client.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => this.handleError(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleError(error: AxiosError): Promise<never> {
|
||||||
|
if (error.response) {
|
||||||
|
const clickupError = error.response.data as ClickUpError;
|
||||||
|
const status = error.response.status;
|
||||||
|
|
||||||
|
if (status === 429) {
|
||||||
|
throw new Error(`Rate limit exceeded. Please try again later. ${clickupError.err || ''}`);
|
||||||
|
} else if (status === 401) {
|
||||||
|
throw new Error('Unauthorized. Check your API token.');
|
||||||
|
} else if (status === 403) {
|
||||||
|
throw new Error(`Forbidden: ${clickupError.err || 'Access denied'}`);
|
||||||
|
} else if (status === 404) {
|
||||||
|
throw new Error(`Not found: ${clickupError.err || 'Resource does not exist'}`);
|
||||||
|
} else if (status === 400) {
|
||||||
|
throw new Error(`Bad request: ${clickupError.err || 'Invalid parameters'}`);
|
||||||
|
} else {
|
||||||
|
throw new Error(`ClickUp API error (${status}): ${clickupError.err || error.message}`);
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
throw new Error('No response from ClickUp API. Check your network connection.');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Request error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async rateLimit(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastRequest = now - this.lastRequestTime;
|
||||||
|
|
||||||
|
if (timeSinceLastRequest < RATE_LIMIT_DELAY) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, RATE_LIMIT_DELAY - timeSinceLastRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastRequestTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async retryRequest<T>(
|
||||||
|
fn: () => Promise<T>,
|
||||||
|
retries = MAX_RETRIES
|
||||||
|
): Promise<T> {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (retries > 0 && error instanceof Error && error.message.includes('Rate limit')) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||||
|
return this.retryRequest(fn, retries - 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||||
|
await this.rateLimit();
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.get<T>(endpoint, { params });
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||||
|
await this.rateLimit();
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.post<T>(endpoint, data, config);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async put<T>(endpoint: string, data?: any): Promise<T> {
|
||||||
|
await this.rateLimit();
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.put<T>(endpoint, data);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
|
await this.rateLimit();
|
||||||
|
return this.retryRequest(async () => {
|
||||||
|
const response = await this.client.delete<T>(endpoint);
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination helper
|
||||||
|
async *paginate<T>(
|
||||||
|
endpoint: string,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
dataKey: string
|
||||||
|
): AsyncGenerator<T[], void, unknown> {
|
||||||
|
let page = 0;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const response: any = await this.get(endpoint, { ...params, page });
|
||||||
|
const items = response[dataKey] || [];
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
yield items;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMore = !response.last_page && items.length > 0;
|
||||||
|
page++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Team / Workspace =====
|
||||||
|
|
||||||
|
async getAuthorizedTeams() {
|
||||||
|
return this.get('/team');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTeam(teamId: string) {
|
||||||
|
return this.get(`/team/${teamId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Spaces =====
|
||||||
|
|
||||||
|
async getSpaces(teamId: string, archived = false) {
|
||||||
|
return this.get(`/team/${teamId}/space`, { archived });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpace(spaceId: string) {
|
||||||
|
return this.get(`/space/${spaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSpace(teamId: string, data: any) {
|
||||||
|
return this.post(`/team/${teamId}/space`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSpace(spaceId: string, data: any) {
|
||||||
|
return this.put(`/space/${spaceId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSpace(spaceId: string) {
|
||||||
|
return this.delete(`/space/${spaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Folders =====
|
||||||
|
|
||||||
|
async getFolders(spaceId: string, archived = false) {
|
||||||
|
return this.get(`/space/${spaceId}/folder`, { archived });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFolder(folderId: string) {
|
||||||
|
return this.get(`/folder/${folderId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFolder(spaceId: string, data: any) {
|
||||||
|
return this.post(`/space/${spaceId}/folder`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFolder(folderId: string, data: any) {
|
||||||
|
return this.put(`/folder/${folderId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFolder(folderId: string) {
|
||||||
|
return this.delete(`/folder/${folderId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Lists =====
|
||||||
|
|
||||||
|
async getFolderLists(folderId: string, archived = false) {
|
||||||
|
return this.get(`/folder/${folderId}/list`, { archived });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpaceLists(spaceId: string, archived = false) {
|
||||||
|
return this.get(`/space/${spaceId}/list`, { archived });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getList(listId: string) {
|
||||||
|
return this.get(`/list/${listId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createList(folderId: string, data: any) {
|
||||||
|
return this.post(`/folder/${folderId}/list`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createFolderlessList(spaceId: string, data: any) {
|
||||||
|
return this.post(`/space/${spaceId}/list`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateList(listId: string, data: any) {
|
||||||
|
return this.put(`/list/${listId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteList(listId: string) {
|
||||||
|
return this.delete(`/list/${listId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTaskToList(listId: string, taskId: string) {
|
||||||
|
return this.post(`/list/${listId}/task/${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTaskFromList(listId: string, taskId: string) {
|
||||||
|
return this.delete(`/list/${listId}/task/${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Views =====
|
||||||
|
|
||||||
|
async getViews(teamId: string, spaceId: string, listId?: string, folderId?: string) {
|
||||||
|
const params: any = { space_id: spaceId };
|
||||||
|
if (listId) params.list_id = listId;
|
||||||
|
if (folderId) params.folder_id = folderId;
|
||||||
|
return this.get(`/team/${teamId}/view`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getView(viewId: string) {
|
||||||
|
return this.get(`/view/${viewId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getViewTasks(viewId: string, page = 0) {
|
||||||
|
return this.get(`/view/${viewId}/task`, { page });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createView(teamId: string, spaceId: string, data: any) {
|
||||||
|
return this.post(`/team/${teamId}/view`, { ...data, space_id: spaceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateView(viewId: string, data: any) {
|
||||||
|
return this.put(`/view/${viewId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteView(viewId: string) {
|
||||||
|
return this.delete(`/view/${viewId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tasks =====
|
||||||
|
|
||||||
|
async getTasks(listId: string, params: any = {}) {
|
||||||
|
return this.get(`/list/${listId}/task`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTask(taskId: string, params: any = {}) {
|
||||||
|
return this.get(`/task/${taskId}`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTask(listId: string, data: any) {
|
||||||
|
return this.post(`/list/${listId}/task`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTask(taskId: string, data: any) {
|
||||||
|
return this.put(`/task/${taskId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTask(taskId: string) {
|
||||||
|
return this.delete(`/task/${taskId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFilteredTasks(teamId: string, params: any = {}) {
|
||||||
|
return this.get(`/team/${teamId}/task`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkUpdateTasks(taskIds: string[], data: any) {
|
||||||
|
return this.post('/task/bulk', { task_ids: taskIds, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Task Dependencies =====
|
||||||
|
|
||||||
|
async addDependency(taskId: string, dependsOn: string, dependencyOf?: string) {
|
||||||
|
return this.post(`/task/${taskId}/dependency`, {
|
||||||
|
depends_on: dependsOn,
|
||||||
|
dependency_of: dependencyOf
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDependency(taskId: string, dependsOn: string, dependencyOf?: string) {
|
||||||
|
return this.delete(`/task/${taskId}/dependency?depends_on=${dependsOn}${dependencyOf ? `&dependency_of=${dependencyOf}` : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Task Members =====
|
||||||
|
|
||||||
|
async getTaskMembers(taskId: string) {
|
||||||
|
return this.get(`/task/${taskId}/member`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Comments =====
|
||||||
|
|
||||||
|
async getTaskComments(taskId: string) {
|
||||||
|
return this.get(`/task/${taskId}/comment`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getListComments(listId: string) {
|
||||||
|
return this.get(`/list/${listId}/comment`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getViewComments(viewId: string) {
|
||||||
|
return this.get(`/view/${viewId}/comment`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createComment(taskId: string, data: any) {
|
||||||
|
return this.post(`/task/${taskId}/comment`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateComment(commentId: string, data: any) {
|
||||||
|
return this.put(`/comment/${commentId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteComment(commentId: string) {
|
||||||
|
return this.delete(`/comment/${commentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Checklists =====
|
||||||
|
|
||||||
|
async createChecklist(taskId: string, data: any) {
|
||||||
|
return this.post(`/task/${taskId}/checklist`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateChecklist(checklistId: string, data: any) {
|
||||||
|
return this.put(`/checklist/${checklistId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChecklist(checklistId: string) {
|
||||||
|
return this.delete(`/checklist/${checklistId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createChecklistItem(checklistId: string, data: any) {
|
||||||
|
return this.post(`/checklist/${checklistId}/checklist_item`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateChecklistItem(checklistId: string, checklistItemId: string, data: any) {
|
||||||
|
return this.put(`/checklist/${checklistId}/checklist_item/${checklistItemId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChecklistItem(checklistId: string, checklistItemId: string) {
|
||||||
|
return this.delete(`/checklist/${checklistId}/checklist_item/${checklistItemId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Goals =====
|
||||||
|
|
||||||
|
async getGoals(teamId: string) {
|
||||||
|
return this.get(`/team/${teamId}/goal`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGoal(goalId: string) {
|
||||||
|
return this.get(`/goal/${goalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createGoal(teamId: string, data: any) {
|
||||||
|
return this.post(`/team/${teamId}/goal`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateGoal(goalId: string, data: any) {
|
||||||
|
return this.put(`/goal/${goalId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteGoal(goalId: string) {
|
||||||
|
return this.delete(`/goal/${goalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createKeyResult(goalId: string, data: any) {
|
||||||
|
return this.post(`/goal/${goalId}/key_result`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateKeyResult(keyResultId: string, data: any) {
|
||||||
|
return this.put(`/key_result/${keyResultId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteKeyResult(keyResultId: string) {
|
||||||
|
return this.delete(`/key_result/${keyResultId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tags =====
|
||||||
|
|
||||||
|
async getSpaceTags(spaceId: string) {
|
||||||
|
return this.get(`/space/${spaceId}/tag`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSpaceTag(spaceId: string, data: any) {
|
||||||
|
return this.post(`/space/${spaceId}/tag`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTag(spaceId: string, tagName: string, data: any) {
|
||||||
|
return this.put(`/space/${spaceId}/tag/${tagName}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTag(spaceId: string, tagName: string) {
|
||||||
|
return this.delete(`/space/${spaceId}/tag/${tagName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addTagToTask(taskId: string, tagName: string) {
|
||||||
|
return this.post(`/task/${taskId}/tag/${tagName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTagFromTask(taskId: string, tagName: string) {
|
||||||
|
return this.delete(`/task/${taskId}/tag/${tagName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Time Tracking =====
|
||||||
|
|
||||||
|
async getTimeEntries(teamId: string, params: any = {}) {
|
||||||
|
return this.get(`/team/${teamId}/time_entries`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeEntry(teamId: string, timerId: string) {
|
||||||
|
return this.get(`/team/${teamId}/time_entries/${timerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTimeEntry(teamId: string, data: any) {
|
||||||
|
return this.post(`/team/${teamId}/time_entries`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTimeEntry(teamId: string, timerId: string, data: any) {
|
||||||
|
return this.put(`/team/${teamId}/time_entries/${timerId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTimeEntry(teamId: string, timerId: string) {
|
||||||
|
return this.delete(`/team/${teamId}/time_entries/${timerId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRunningTimeEntry(teamId: string, assignee?: string) {
|
||||||
|
return this.get(`/team/${teamId}/time_entries/current`, assignee ? { assignee } : {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTimer(teamId: string, taskId: string, data: any = {}) {
|
||||||
|
return this.post(`/team/${teamId}/time_entries/start/${taskId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopTimer(teamId: string) {
|
||||||
|
return this.post(`/team/${teamId}/time_entries/stop`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTaskTimeEntries(taskId: string) {
|
||||||
|
return this.get(`/task/${taskId}/time`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Custom Fields =====
|
||||||
|
|
||||||
|
async getAccessibleCustomFields(listId: string) {
|
||||||
|
return this.get(`/list/${listId}/field`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCustomFieldValue(taskId: string, fieldId: string, value: any) {
|
||||||
|
return this.post(`/task/${taskId}/field/${fieldId}`, { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeCustomFieldValue(taskId: string, fieldId: string) {
|
||||||
|
return this.delete(`/task/${taskId}/field/${fieldId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Webhooks =====
|
||||||
|
|
||||||
|
async getWebhooks(teamId: string) {
|
||||||
|
return this.get(`/team/${teamId}/webhook`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWebhook(teamId: string, data: any) {
|
||||||
|
return this.post(`/team/${teamId}/webhook`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWebhook(webhookId: string, data: any) {
|
||||||
|
return this.put(`/webhook/${webhookId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWebhook(webhookId: string) {
|
||||||
|
return this.delete(`/webhook/${webhookId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Templates =====
|
||||||
|
|
||||||
|
async getTemplates(teamId: string, page = 0) {
|
||||||
|
return this.get(`/team/${teamId}/taskTemplate`, { page });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTaskFromTemplate(listId: string, templateId: string, name: string) {
|
||||||
|
return this.post(`/list/${listId}/taskTemplate/${templateId}`, { name });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Members / Guests =====
|
||||||
|
|
||||||
|
async getListMembers(listId: string) {
|
||||||
|
return this.get(`/list/${listId}/member`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async inviteGuestToWorkspace(teamId: string, email: string, canEditTags: boolean) {
|
||||||
|
return this.post(`/team/${teamId}/guest`, { email, can_edit_tags: canEditTags });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGuest(teamId: string, guestId: string) {
|
||||||
|
return this.get(`/team/${teamId}/guest/${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async editGuestOnWorkspace(teamId: string, guestId: string, data: any) {
|
||||||
|
return this.put(`/team/${teamId}/guest/${guestId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGuestFromWorkspace(teamId: string, guestId: string) {
|
||||||
|
return this.delete(`/team/${teamId}/guest/${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGuestToTask(taskId: string, guestId: string, permissionLevel?: string) {
|
||||||
|
return this.post(`/task/${taskId}/guest/${guestId}`,
|
||||||
|
permissionLevel ? { permission_level: permissionLevel } : {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGuestFromTask(taskId: string, guestId: string) {
|
||||||
|
return this.delete(`/task/${taskId}/guest/${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGuestToList(listId: string, guestId: string, permissionLevel?: string) {
|
||||||
|
return this.post(`/list/${listId}/guest/${guestId}`,
|
||||||
|
permissionLevel ? { permission_level: permissionLevel } : {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGuestFromList(listId: string, guestId: string) {
|
||||||
|
return this.delete(`/list/${listId}/guest/${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addGuestToFolder(folderId: string, guestId: string, permissionLevel?: string) {
|
||||||
|
return this.post(`/folder/${folderId}/guest/${guestId}`,
|
||||||
|
permissionLevel ? { permission_level: permissionLevel } : {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeGuestFromFolder(folderId: string, guestId: string) {
|
||||||
|
return this.delete(`/folder/${folderId}/guest/${guestId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Docs =====
|
||||||
|
|
||||||
|
async getDocs(workspaceId: string) {
|
||||||
|
return this.get(`/team/${workspaceId}/docs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchDocs(workspaceId: string, search: string) {
|
||||||
|
return this.get(`/team/${workspaceId}/docs`, { search });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,504 +1,164 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
/**
|
||||||
|
* ClickUp MCP Server
|
||||||
|
* Complete integration with ClickUp API v2
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
Tool,
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
// ============================================
|
import { ClickUpClient } from './clients/clickup.js';
|
||||||
// CONFIGURATION
|
import type { ClickUpConfig } from './types.js';
|
||||||
// ============================================
|
|
||||||
const MCP_NAME = "clickup";
|
|
||||||
const MCP_VERSION = "1.0.0";
|
|
||||||
const API_BASE_URL = "https://api.clickup.com/api/v2";
|
|
||||||
|
|
||||||
// ============================================
|
// Import tool creators
|
||||||
// API CLIENT
|
import { createTasksTools } from './tools/tasks-tools.js';
|
||||||
// ============================================
|
import { createSpacesTools } from './tools/spaces-tools.js';
|
||||||
class ClickUpClient {
|
import { createFoldersTools } from './tools/folders-tools.js';
|
||||||
private apiKey: string;
|
import { createListsTools } from './tools/lists-tools.js';
|
||||||
private baseUrl: string;
|
import { createViewsTools } from './tools/views-tools.js';
|
||||||
|
import { createCommentsTools } from './tools/comments-tools.js';
|
||||||
|
import { createDocsTools } from './tools/docs-tools.js';
|
||||||
|
import { createGoalsTools } from './tools/goals-tools.js';
|
||||||
|
import { createTagsTools } from './tools/tags-tools.js';
|
||||||
|
import { createChecklistsTools } from './tools/checklists-tools.js';
|
||||||
|
import { createTimeTrackingTools } from './tools/time-tracking-tools.js';
|
||||||
|
import { createTeamsTools } from './tools/teams-tools.js';
|
||||||
|
import { createWebhooksTools } from './tools/webhooks-tools.js';
|
||||||
|
import { createCustomFieldsTools } from './tools/custom-fields-tools.js';
|
||||||
|
import { createTemplatesTools } from './tools/templates-tools.js';
|
||||||
|
import { createGuestsTools } from './tools/guests-tools.js';
|
||||||
|
|
||||||
constructor(apiKey: string) {
|
class ClickUpServer {
|
||||||
this.apiKey = apiKey;
|
private server: Server;
|
||||||
this.baseUrl = API_BASE_URL;
|
private client: ClickUpClient;
|
||||||
}
|
private tools: Map<string, any> = new Map();
|
||||||
|
|
||||||
async request(endpoint: string, options: RequestInit = {}) {
|
constructor() {
|
||||||
const url = `${this.baseUrl}${endpoint}`;
|
this.server = new Server(
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
"Authorization": this.apiKey,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text();
|
|
||||||
throw new Error(`ClickUp API error: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle empty responses (like 204 No Content)
|
|
||||||
const text = await response.text();
|
|
||||||
return text ? JSON.parse(text) : { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(endpoint: string) {
|
|
||||||
return this.request(endpoint, { method: "GET" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async post(endpoint: string, data: any) {
|
|
||||||
return this.request(endpoint, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async put(endpoint: string, data: any) {
|
|
||||||
return this.request(endpoint, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Space endpoints
|
|
||||||
async listSpaces(teamId: string, archived?: boolean) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (archived !== undefined) params.append("archived", archived.toString());
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/team/${teamId}/space${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// List endpoints
|
|
||||||
async listLists(folderId: string, archived?: boolean) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (archived !== undefined) params.append("archived", archived.toString());
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/folder/${folderId}/list${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listFolderlessLists(spaceId: string, archived?: boolean) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (archived !== undefined) params.append("archived", archived.toString());
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/space/${spaceId}/list${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task endpoints
|
|
||||||
async listTasks(listId: string, options?: {
|
|
||||||
archived?: boolean;
|
|
||||||
page?: number;
|
|
||||||
order_by?: string;
|
|
||||||
reverse?: boolean;
|
|
||||||
subtasks?: boolean;
|
|
||||||
statuses?: string[];
|
|
||||||
include_closed?: boolean;
|
|
||||||
assignees?: string[];
|
|
||||||
due_date_gt?: number;
|
|
||||||
due_date_lt?: number;
|
|
||||||
}) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (options?.archived !== undefined) params.append("archived", options.archived.toString());
|
|
||||||
if (options?.page !== undefined) params.append("page", options.page.toString());
|
|
||||||
if (options?.order_by) params.append("order_by", options.order_by);
|
|
||||||
if (options?.reverse !== undefined) params.append("reverse", options.reverse.toString());
|
|
||||||
if (options?.subtasks !== undefined) params.append("subtasks", options.subtasks.toString());
|
|
||||||
if (options?.include_closed !== undefined) params.append("include_closed", options.include_closed.toString());
|
|
||||||
if (options?.statuses) options.statuses.forEach(s => params.append("statuses[]", s));
|
|
||||||
if (options?.assignees) options.assignees.forEach(a => params.append("assignees[]", a));
|
|
||||||
if (options?.due_date_gt) params.append("due_date_gt", options.due_date_gt.toString());
|
|
||||||
if (options?.due_date_lt) params.append("due_date_lt", options.due_date_lt.toString());
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/list/${listId}/task${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTask(taskId: string, includeSubtasks?: boolean) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (includeSubtasks !== undefined) params.append("include_subtasks", includeSubtasks.toString());
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/task/${taskId}${query}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTask(listId: string, data: {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
assignees?: string[];
|
|
||||||
tags?: string[];
|
|
||||||
status?: string;
|
|
||||||
priority?: number;
|
|
||||||
due_date?: number;
|
|
||||||
due_date_time?: boolean;
|
|
||||||
time_estimate?: number;
|
|
||||||
start_date?: number;
|
|
||||||
start_date_time?: boolean;
|
|
||||||
notify_all?: boolean;
|
|
||||||
parent?: string;
|
|
||||||
links_to?: string;
|
|
||||||
custom_fields?: any[];
|
|
||||||
}) {
|
|
||||||
return this.post(`/list/${listId}/task`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTask(taskId: string, data: {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
assignees?: { add?: string[]; rem?: string[] };
|
|
||||||
status?: string;
|
|
||||||
priority?: number;
|
|
||||||
due_date?: number;
|
|
||||||
due_date_time?: boolean;
|
|
||||||
time_estimate?: number;
|
|
||||||
start_date?: number;
|
|
||||||
start_date_time?: boolean;
|
|
||||||
parent?: string;
|
|
||||||
archived?: boolean;
|
|
||||||
}) {
|
|
||||||
return this.put(`/task/${taskId}`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comment endpoints
|
|
||||||
async addComment(taskId: string, commentText: string, assignee?: string, notifyAll?: boolean) {
|
|
||||||
const payload: any = { comment_text: commentText };
|
|
||||||
if (assignee) payload.assignee = assignee;
|
|
||||||
if (notifyAll !== undefined) payload.notify_all = notifyAll;
|
|
||||||
return this.post(`/task/${taskId}/comment`, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time tracking endpoints
|
|
||||||
async getTimeEntries(teamId: string, options?: {
|
|
||||||
start_date?: number;
|
|
||||||
end_date?: number;
|
|
||||||
assignee?: string;
|
|
||||||
include_task_tags?: boolean;
|
|
||||||
include_location_names?: boolean;
|
|
||||||
space_id?: string;
|
|
||||||
folder_id?: string;
|
|
||||||
list_id?: string;
|
|
||||||
task_id?: string;
|
|
||||||
}) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (options?.start_date) params.append("start_date", options.start_date.toString());
|
|
||||||
if (options?.end_date) params.append("end_date", options.end_date.toString());
|
|
||||||
if (options?.assignee) params.append("assignee", options.assignee);
|
|
||||||
if (options?.include_task_tags !== undefined) params.append("include_task_tags", options.include_task_tags.toString());
|
|
||||||
if (options?.include_location_names !== undefined) params.append("include_location_names", options.include_location_names.toString());
|
|
||||||
if (options?.space_id) params.append("space_id", options.space_id);
|
|
||||||
if (options?.folder_id) params.append("folder_id", options.folder_id);
|
|
||||||
if (options?.list_id) params.append("list_id", options.list_id);
|
|
||||||
if (options?.task_id) params.append("task_id", options.task_id);
|
|
||||||
const query = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
return this.get(`/team/${teamId}/time_entries${query}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL DEFINITIONS
|
|
||||||
// ============================================
|
|
||||||
const tools = [
|
|
||||||
{
|
{
|
||||||
name: "list_spaces",
|
name: 'clickup',
|
||||||
description: "List all spaces in a ClickUp workspace/team",
|
version: '1.0.0',
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
team_id: { type: "string", description: "The workspace/team ID" },
|
|
||||||
archived: { type: "boolean", description: "Include archived spaces" },
|
|
||||||
},
|
|
||||||
required: ["team_id"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "list_lists",
|
capabilities: {
|
||||||
description: "List all lists in a folder or space (folderless lists)",
|
tools: {},
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
folder_id: { type: "string", description: "The folder ID (for lists in a folder)" },
|
|
||||||
space_id: { type: "string", description: "The space ID (for folderless lists)" },
|
|
||||||
archived: { type: "boolean", description: "Include archived lists" },
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "list_tasks",
|
|
||||||
description: "List tasks in a list with optional filters",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
list_id: { type: "string", description: "The list ID" },
|
|
||||||
archived: { type: "boolean", description: "Filter by archived status" },
|
|
||||||
page: { type: "number", description: "Page number (0-indexed)" },
|
|
||||||
order_by: {
|
|
||||||
type: "string",
|
|
||||||
description: "Order by field: id, created, updated, due_date",
|
|
||||||
enum: ["id", "created", "updated", "due_date"]
|
|
||||||
},
|
|
||||||
reverse: { type: "boolean", description: "Reverse order" },
|
|
||||||
subtasks: { type: "boolean", description: "Include subtasks" },
|
|
||||||
include_closed: { type: "boolean", description: "Include closed tasks" },
|
|
||||||
statuses: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Filter by status names"
|
|
||||||
},
|
|
||||||
assignees: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Filter by assignee user IDs"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["list_id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_task",
|
|
||||||
description: "Get detailed information about a specific task",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
task_id: { type: "string", description: "The task ID" },
|
|
||||||
include_subtasks: { type: "boolean", description: "Include subtask details" },
|
|
||||||
},
|
|
||||||
required: ["task_id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "create_task",
|
|
||||||
description: "Create a new task in a list",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
list_id: { type: "string", description: "The list ID to create the task in" },
|
|
||||||
name: { type: "string", description: "Task name" },
|
|
||||||
description: { type: "string", description: "Task description (supports markdown)" },
|
|
||||||
assignees: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Array of user IDs to assign"
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "Array of tag names"
|
|
||||||
},
|
|
||||||
status: { type: "string", description: "Status name" },
|
|
||||||
priority: {
|
|
||||||
type: "number",
|
|
||||||
description: "Priority: 1=urgent, 2=high, 3=normal, 4=low",
|
|
||||||
enum: [1, 2, 3, 4]
|
|
||||||
},
|
|
||||||
due_date: { type: "number", description: "Due date as Unix timestamp in milliseconds" },
|
|
||||||
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
|
|
||||||
time_estimate: { type: "number", description: "Time estimate in milliseconds" },
|
|
||||||
parent: { type: "string", description: "Parent task ID (to create as subtask)" },
|
|
||||||
},
|
|
||||||
required: ["list_id", "name"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "update_task",
|
|
||||||
description: "Update an existing task",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
task_id: { type: "string", description: "The task ID to update" },
|
|
||||||
name: { type: "string", description: "New task name" },
|
|
||||||
description: { type: "string", description: "New task description" },
|
|
||||||
status: { type: "string", description: "New status name" },
|
|
||||||
priority: {
|
|
||||||
type: "number",
|
|
||||||
description: "Priority: 1=urgent, 2=high, 3=normal, 4=low, null=none",
|
|
||||||
enum: [1, 2, 3, 4]
|
|
||||||
},
|
|
||||||
due_date: { type: "number", description: "Due date as Unix timestamp in milliseconds" },
|
|
||||||
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
|
|
||||||
time_estimate: { type: "number", description: "Time estimate in milliseconds" },
|
|
||||||
assignees_add: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "User IDs to add as assignees"
|
|
||||||
},
|
|
||||||
assignees_remove: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string" },
|
|
||||||
description: "User IDs to remove from assignees"
|
|
||||||
},
|
|
||||||
archived: { type: "boolean", description: "Archive or unarchive the task" },
|
|
||||||
},
|
|
||||||
required: ["task_id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add_comment",
|
|
||||||
description: "Add a comment to a task",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
task_id: { type: "string", description: "The task ID" },
|
|
||||||
comment_text: { type: "string", description: "Comment text (supports markdown)" },
|
|
||||||
assignee: { type: "string", description: "User ID to assign the comment to" },
|
|
||||||
notify_all: { type: "boolean", description: "Notify all assignees" },
|
|
||||||
},
|
|
||||||
required: ["task_id", "comment_text"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "get_time_entries",
|
|
||||||
description: "Get time tracking entries for a workspace",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object" as const,
|
|
||||||
properties: {
|
|
||||||
team_id: { type: "string", description: "The workspace/team ID" },
|
|
||||||
start_date: { type: "number", description: "Start date as Unix timestamp in milliseconds" },
|
|
||||||
end_date: { type: "number", description: "End date as Unix timestamp in milliseconds" },
|
|
||||||
assignee: { type: "string", description: "Filter by user ID" },
|
|
||||||
task_id: { type: "string", description: "Filter by task ID" },
|
|
||||||
list_id: { type: "string", description: "Filter by list ID" },
|
|
||||||
space_id: { type: "string", description: "Filter by space ID" },
|
|
||||||
include_task_tags: { type: "boolean", description: "Include task tags in response" },
|
|
||||||
include_location_names: { type: "boolean", description: "Include location names in response" },
|
|
||||||
},
|
|
||||||
required: ["team_id"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL HANDLERS
|
|
||||||
// ============================================
|
|
||||||
async function handleTool(client: ClickUpClient, name: string, args: any) {
|
|
||||||
switch (name) {
|
|
||||||
case "list_spaces": {
|
|
||||||
const { team_id, archived } = args;
|
|
||||||
return await client.listSpaces(team_id, archived);
|
|
||||||
}
|
}
|
||||||
case "list_lists": {
|
|
||||||
const { folder_id, space_id, archived } = args;
|
|
||||||
if (folder_id) {
|
|
||||||
return await client.listLists(folder_id, archived);
|
|
||||||
} else if (space_id) {
|
|
||||||
return await client.listFolderlessLists(space_id, archived);
|
|
||||||
} else {
|
|
||||||
throw new Error("Either folder_id or space_id is required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "list_tasks": {
|
|
||||||
const { list_id, archived, page, order_by, reverse, subtasks, include_closed, statuses, assignees } = args;
|
|
||||||
return await client.listTasks(list_id, {
|
|
||||||
archived,
|
|
||||||
page,
|
|
||||||
order_by,
|
|
||||||
reverse,
|
|
||||||
subtasks,
|
|
||||||
include_closed,
|
|
||||||
statuses,
|
|
||||||
assignees,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case "get_task": {
|
|
||||||
const { task_id, include_subtasks } = args;
|
|
||||||
return await client.getTask(task_id, include_subtasks);
|
|
||||||
}
|
|
||||||
case "create_task": {
|
|
||||||
const { list_id, name, description, assignees, tags, status, priority, due_date, start_date, time_estimate, parent } = args;
|
|
||||||
return await client.createTask(list_id, {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
assignees,
|
|
||||||
tags,
|
|
||||||
status,
|
|
||||||
priority,
|
|
||||||
due_date,
|
|
||||||
start_date,
|
|
||||||
time_estimate,
|
|
||||||
parent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case "update_task": {
|
|
||||||
const { task_id, name, description, status, priority, due_date, start_date, time_estimate, assignees_add, assignees_remove, archived } = args;
|
|
||||||
const updateData: any = {};
|
|
||||||
if (name !== undefined) updateData.name = name;
|
|
||||||
if (description !== undefined) updateData.description = description;
|
|
||||||
if (status !== undefined) updateData.status = status;
|
|
||||||
if (priority !== undefined) updateData.priority = priority;
|
|
||||||
if (due_date !== undefined) updateData.due_date = due_date;
|
|
||||||
if (start_date !== undefined) updateData.start_date = start_date;
|
|
||||||
if (time_estimate !== undefined) updateData.time_estimate = time_estimate;
|
|
||||||
if (archived !== undefined) updateData.archived = archived;
|
|
||||||
if (assignees_add || assignees_remove) {
|
|
||||||
updateData.assignees = {};
|
|
||||||
if (assignees_add) updateData.assignees.add = assignees_add;
|
|
||||||
if (assignees_remove) updateData.assignees.rem = assignees_remove;
|
|
||||||
}
|
|
||||||
return await client.updateTask(task_id, updateData);
|
|
||||||
}
|
|
||||||
case "add_comment": {
|
|
||||||
const { task_id, comment_text, assignee, notify_all } = args;
|
|
||||||
return await client.addComment(task_id, comment_text, assignee, notify_all);
|
|
||||||
}
|
|
||||||
case "get_time_entries": {
|
|
||||||
const { team_id, start_date, end_date, assignee, task_id, list_id, space_id, include_task_tags, include_location_names } = args;
|
|
||||||
return await client.getTimeEntries(team_id, {
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
assignee,
|
|
||||||
task_id,
|
|
||||||
list_id,
|
|
||||||
space_id,
|
|
||||||
include_task_tags,
|
|
||||||
include_location_names,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown tool: ${name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// SERVER SETUP
|
|
||||||
// ============================================
|
|
||||||
async function main() {
|
|
||||||
const apiKey = process.env.CLICKUP_API_KEY;
|
|
||||||
if (!apiKey) {
|
|
||||||
console.error("Error: CLICKUP_API_KEY environment variable required");
|
|
||||||
console.error("Get your API key from ClickUp Settings > Apps > API Token");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new ClickUpClient(apiKey);
|
|
||||||
|
|
||||||
const server = new Server(
|
|
||||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
|
||||||
{ capabilities: { tools: {} } }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
// Initialize ClickUp client
|
||||||
tools,
|
const config: ClickUpConfig = {
|
||||||
|
apiToken: process.env.CLICKUP_API_TOKEN,
|
||||||
|
oauthToken: process.env.CLICKUP_OAUTH_TOKEN,
|
||||||
|
clientId: process.env.CLICKUP_CLIENT_ID,
|
||||||
|
clientSecret: process.env.CLICKUP_CLIENT_SECRET,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = new ClickUpClient(config);
|
||||||
|
|
||||||
|
// Register all tools
|
||||||
|
this.registerTools();
|
||||||
|
|
||||||
|
// Setup request handlers
|
||||||
|
this.setupHandlers();
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
this.server.onerror = (error) => {
|
||||||
|
console.error('[MCP Error]', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
await this.server.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerTools() {
|
||||||
|
const allTools = [
|
||||||
|
...createTasksTools(this.client),
|
||||||
|
...createSpacesTools(this.client),
|
||||||
|
...createFoldersTools(this.client),
|
||||||
|
...createListsTools(this.client),
|
||||||
|
...createViewsTools(this.client),
|
||||||
|
...createCommentsTools(this.client),
|
||||||
|
...createDocsTools(this.client),
|
||||||
|
...createGoalsTools(this.client),
|
||||||
|
...createTagsTools(this.client),
|
||||||
|
...createChecklistsTools(this.client),
|
||||||
|
...createTimeTrackingTools(this.client),
|
||||||
|
...createTeamsTools(this.client),
|
||||||
|
...createWebhooksTools(this.client),
|
||||||
|
...createCustomFieldsTools(this.client),
|
||||||
|
...createTemplatesTools(this.client),
|
||||||
|
...createGuestsTools(this.client),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tool of allTools) {
|
||||||
|
this.tools.set(tool.name, tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupHandlers() {
|
||||||
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
const tools: Tool[] = Array.from(this.tools.values()).map(tool => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: tool.inputSchema.shape,
|
||||||
|
required: Object.keys(tool.inputSchema.shape).filter(
|
||||||
|
key => !tool.inputSchema.shape[key].isOptional()
|
||||||
|
),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
return { tools };
|
||||||
const { name, arguments: args } = request.params;
|
});
|
||||||
|
|
||||||
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const tool = this.tools.get(request.params.name);
|
||||||
|
|
||||||
|
if (!tool) {
|
||||||
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await handleTool(client, name, args || {});
|
// Validate input
|
||||||
return {
|
const args = tool.inputSchema.parse(request.params.arguments);
|
||||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
||||||
};
|
// Execute tool
|
||||||
|
const result = await tool.handler(args);
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
if (error instanceof Error) {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Error: ${message}` }],
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: `Error: ${error.message}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
isError: true,
|
isError: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(console.error);
|
async run() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await this.server.connect(transport);
|
||||||
|
console.error('ClickUp MCP server running on stdio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new ClickUpServer();
|
||||||
|
server.run().catch(console.error);
|
||||||
|
|||||||
97
servers/clickup/src/tools/checklists-tools.ts
Normal file
97
servers/clickup/src/tools/checklists-tools.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Checklists Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createChecklistsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_create',
|
||||||
|
description: 'Create a checklist on a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
name: z.string().describe('Checklist name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { task_id, ...data } = args;
|
||||||
|
const response = await client.createChecklist(task_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_update',
|
||||||
|
description: 'Update a checklist',
|
||||||
|
inputSchema: z.object({
|
||||||
|
checklist_id: z.string().describe('Checklist ID'),
|
||||||
|
name: z.string().optional().describe('Checklist name'),
|
||||||
|
position: z.number().optional().describe('Position/order index'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { checklist_id, ...data } = args;
|
||||||
|
const response = await client.updateChecklist(checklist_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_delete',
|
||||||
|
description: 'Delete a checklist',
|
||||||
|
inputSchema: z.object({
|
||||||
|
checklist_id: z.string().describe('Checklist ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteChecklist(args.checklist_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Checklist deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_create_item',
|
||||||
|
description: 'Create a checklist item',
|
||||||
|
inputSchema: z.object({
|
||||||
|
checklist_id: z.string().describe('Checklist ID'),
|
||||||
|
name: z.string().describe('Item name'),
|
||||||
|
assignee: z.number().optional().describe('Assignee user ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { checklist_id, ...data } = args;
|
||||||
|
const response = await client.createChecklistItem(checklist_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_update_item',
|
||||||
|
description: 'Update a checklist item',
|
||||||
|
inputSchema: z.object({
|
||||||
|
checklist_id: z.string().describe('Checklist ID'),
|
||||||
|
checklist_item_id: z.string().describe('Checklist Item ID'),
|
||||||
|
name: z.string().optional().describe('Item name'),
|
||||||
|
assignee: z.number().optional().describe('Assignee user ID'),
|
||||||
|
resolved: z.boolean().optional().describe('Mark as resolved/completed'),
|
||||||
|
parent: z.string().optional().describe('Parent item ID (for nesting)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { checklist_id, checklist_item_id, ...data } = args;
|
||||||
|
const response = await client.updateChecklistItem(checklist_id, checklist_item_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_checklists_delete_item',
|
||||||
|
description: 'Delete a checklist item',
|
||||||
|
inputSchema: z.object({
|
||||||
|
checklist_id: z.string().describe('Checklist ID'),
|
||||||
|
checklist_item_id: z.string().describe('Checklist Item ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteChecklistItem(args.checklist_id, args.checklist_item_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Checklist item deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
77
servers/clickup/src/tools/comments-tools.ts
Normal file
77
servers/clickup/src/tools/comments-tools.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Comments Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createCommentsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_comments_list',
|
||||||
|
description: 'List comments on a task, list, or view',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().optional().describe('Task ID'),
|
||||||
|
list_id: z.string().optional().describe('List ID'),
|
||||||
|
view_id: z.string().optional().describe('View ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
let response;
|
||||||
|
if (args.task_id) {
|
||||||
|
response = await client.getTaskComments(args.task_id);
|
||||||
|
} else if (args.list_id) {
|
||||||
|
response = await client.getListComments(args.list_id);
|
||||||
|
} else if (args.view_id) {
|
||||||
|
response = await client.getViewComments(args.view_id);
|
||||||
|
} else {
|
||||||
|
throw new Error('One of task_id, list_id, or view_id must be provided');
|
||||||
|
}
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_comments_create',
|
||||||
|
description: 'Create a comment on a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
comment_text: z.string().describe('Comment text'),
|
||||||
|
assignee: z.number().optional().describe('Assign comment to user ID'),
|
||||||
|
notify_all: z.boolean().optional().describe('Notify all task watchers'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { task_id, ...data } = args;
|
||||||
|
const response = await client.createComment(task_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_comments_update',
|
||||||
|
description: 'Update a comment',
|
||||||
|
inputSchema: z.object({
|
||||||
|
comment_id: z.string().describe('Comment ID'),
|
||||||
|
comment_text: z.string().optional().describe('Comment text'),
|
||||||
|
assignee: z.number().optional().describe('Assign comment to user ID'),
|
||||||
|
resolved: z.boolean().optional().describe('Mark as resolved'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { comment_id, ...data } = args;
|
||||||
|
const response = await client.updateComment(comment_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_comments_delete',
|
||||||
|
description: 'Delete a comment',
|
||||||
|
inputSchema: z.object({
|
||||||
|
comment_id: z.string().describe('Comment ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteComment(args.comment_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Comment deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
62
servers/clickup/src/tools/custom-fields-tools.ts
Normal file
62
servers/clickup/src/tools/custom-fields-tools.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Custom Fields Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createCustomFieldsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_custom_fields_list',
|
||||||
|
description: 'List accessible custom fields for a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getAccessibleCustomFields(args.list_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_custom_fields_get',
|
||||||
|
description: 'Get custom field values for a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const task = await client.getTask(args.task_id);
|
||||||
|
const customFields = (task as any).custom_fields || [];
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(customFields, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_custom_fields_set_value',
|
||||||
|
description: 'Set a custom field value on a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
field_id: z.string().describe('Custom field ID'),
|
||||||
|
value: z.any().describe('Field value (type depends on field type)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.setCustomFieldValue(args.task_id, args.field_id, args.value);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_custom_fields_remove_value',
|
||||||
|
description: 'Remove a custom field value from a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
field_id: z.string().describe('Custom field ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.removeCustomFieldValue(args.task_id, args.field_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Custom field value removed successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
35
servers/clickup/src/tools/docs-tools.ts
Normal file
35
servers/clickup/src/tools/docs-tools.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Docs Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createDocsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_docs_list',
|
||||||
|
description: 'List all docs in a workspace',
|
||||||
|
inputSchema: z.object({
|
||||||
|
workspace_id: z.string().describe('Workspace/Team ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getDocs(args.workspace_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_docs_search',
|
||||||
|
description: 'Search docs in a workspace',
|
||||||
|
inputSchema: z.object({
|
||||||
|
workspace_id: z.string().describe('Workspace/Team ID'),
|
||||||
|
search: z.string().describe('Search query'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.searchDocs(args.workspace_id, args.search);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
75
servers/clickup/src/tools/folders-tools.ts
Normal file
75
servers/clickup/src/tools/folders-tools.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Folders Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createFoldersTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_folders_list',
|
||||||
|
description: 'List all folders in a space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
archived: z.boolean().optional().describe('Include archived folders'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getFolders(args.space_id, args.archived);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_folders_get',
|
||||||
|
description: 'Get a specific folder by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().describe('Folder ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getFolder(args.folder_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_folders_create',
|
||||||
|
description: 'Create a new folder',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
name: z.string().describe('Folder name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { space_id, ...data } = args;
|
||||||
|
const response = await client.createFolder(space_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_folders_update',
|
||||||
|
description: 'Update a folder',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().describe('Folder ID'),
|
||||||
|
name: z.string().optional().describe('Folder name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { folder_id, ...data } = args;
|
||||||
|
const response = await client.updateFolder(folder_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_folders_delete',
|
||||||
|
description: 'Delete a folder',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().describe('Folder ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteFolder(args.folder_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Folder deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
123
servers/clickup/src/tools/goals-tools.ts
Normal file
123
servers/clickup/src/tools/goals-tools.ts
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Goals Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createGoalsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_list',
|
||||||
|
description: 'List all goals in a team',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getGoals(args.team_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_get',
|
||||||
|
description: 'Get a specific goal by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
goal_id: z.string().describe('Goal ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getGoal(args.goal_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_create',
|
||||||
|
description: 'Create a new goal',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
name: z.string().describe('Goal name'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
|
||||||
|
description: z.string().optional().describe('Goal description'),
|
||||||
|
multiple_owners: z.boolean().optional().describe('Allow multiple owners'),
|
||||||
|
owners: z.array(z.number()).optional().describe('Owner user IDs'),
|
||||||
|
color: z.string().optional().describe('Color hex code'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, ...data } = args;
|
||||||
|
const response = await client.createGoal(team_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_update',
|
||||||
|
description: 'Update a goal',
|
||||||
|
inputSchema: z.object({
|
||||||
|
goal_id: z.string().describe('Goal ID'),
|
||||||
|
name: z.string().optional().describe('Goal name'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
|
||||||
|
description: z.string().optional().describe('Goal description'),
|
||||||
|
rem_owners: z.array(z.number()).optional().describe('Remove owner user IDs'),
|
||||||
|
add_owners: z.array(z.number()).optional().describe('Add owner user IDs'),
|
||||||
|
color: z.string().optional().describe('Color hex code'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { goal_id, ...data } = args;
|
||||||
|
const response = await client.updateGoal(goal_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_delete',
|
||||||
|
description: 'Delete a goal',
|
||||||
|
inputSchema: z.object({
|
||||||
|
goal_id: z.string().describe('Goal ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteGoal(args.goal_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Goal deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_add_key_result',
|
||||||
|
description: 'Add a key result to a goal',
|
||||||
|
inputSchema: z.object({
|
||||||
|
goal_id: z.string().describe('Goal ID'),
|
||||||
|
name: z.string().describe('Key result name'),
|
||||||
|
owners: z.array(z.number()).optional().describe('Owner user IDs'),
|
||||||
|
type: z.string().describe('Type: number, currency, boolean, percentage, automatic'),
|
||||||
|
steps_start: z.number().optional().describe('Starting value (for number/currency/percentage)'),
|
||||||
|
steps_end: z.number().optional().describe('Target value'),
|
||||||
|
unit: z.string().optional().describe('Unit (for currency)'),
|
||||||
|
task_ids: z.array(z.string()).optional().describe('Task IDs (for automatic)'),
|
||||||
|
list_ids: z.array(z.string()).optional().describe('List IDs (for automatic)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { goal_id, ...data } = args;
|
||||||
|
const response = await client.createKeyResult(goal_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_goals_update_key_result',
|
||||||
|
description: 'Update a key result',
|
||||||
|
inputSchema: z.object({
|
||||||
|
key_result_id: z.string().describe('Key Result ID'),
|
||||||
|
name: z.string().optional().describe('Key result name'),
|
||||||
|
note: z.string().optional().describe('Note'),
|
||||||
|
steps_current: z.number().optional().describe('Current value'),
|
||||||
|
steps_start: z.number().optional().describe('Starting value'),
|
||||||
|
steps_end: z.number().optional().describe('Target value'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { key_result_id, ...data } = args;
|
||||||
|
const response = await client.updateKeyResult(key_result_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
114
servers/clickup/src/tools/guests-tools.ts
Normal file
114
servers/clickup/src/tools/guests-tools.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Guests Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createGuestsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_invite',
|
||||||
|
description: 'Invite a guest to a workspace',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
email: z.string().describe('Guest email address'),
|
||||||
|
can_edit_tags: z.boolean().optional().describe('Allow guest to edit tags'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.inviteGuestToWorkspace(
|
||||||
|
args.team_id,
|
||||||
|
args.email,
|
||||||
|
args.can_edit_tags || false
|
||||||
|
);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_get',
|
||||||
|
description: 'Get guest details',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getGuest(args.team_id, args.guest_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_edit',
|
||||||
|
description: 'Edit guest permissions',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
username: z.string().optional().describe('Username'),
|
||||||
|
can_edit_tags: z.boolean().optional().describe('Allow guest to edit tags'),
|
||||||
|
can_see_time_spent: z.boolean().optional().describe('Allow guest to see time spent'),
|
||||||
|
can_see_time_estimated: z.boolean().optional().describe('Allow guest to see time estimated'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, guest_id, ...data } = args;
|
||||||
|
const response = await client.editGuestOnWorkspace(team_id, guest_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_remove',
|
||||||
|
description: 'Remove a guest from a workspace',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.removeGuestFromWorkspace(args.team_id, args.guest_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Guest removed successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_add_to_task',
|
||||||
|
description: 'Add a guest to a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addGuestToTask(args.task_id, args.guest_id, args.permission_level);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_add_to_list',
|
||||||
|
description: 'Add a guest to a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addGuestToList(args.list_id, args.guest_id, args.permission_level);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_guests_add_to_folder',
|
||||||
|
description: 'Add a guest to a folder',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().describe('Folder ID'),
|
||||||
|
guest_id: z.string().describe('Guest ID'),
|
||||||
|
permission_level: z.string().optional().describe('Permission level (read, comment, edit, create)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addGuestToFolder(args.folder_id, args.guest_id, args.permission_level);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
129
servers/clickup/src/tools/lists-tools.ts
Normal file
129
servers/clickup/src/tools/lists-tools.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Lists Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createListsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_list',
|
||||||
|
description: 'List all lists in a folder or space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().optional().describe('Folder ID'),
|
||||||
|
space_id: z.string().optional().describe('Space ID (for folderless lists)'),
|
||||||
|
archived: z.boolean().optional().describe('Include archived lists'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
let response;
|
||||||
|
if (args.folder_id) {
|
||||||
|
response = await client.getFolderLists(args.folder_id, args.archived);
|
||||||
|
} else if (args.space_id) {
|
||||||
|
response = await client.getSpaceLists(args.space_id, args.archived);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either folder_id or space_id must be provided');
|
||||||
|
}
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_get',
|
||||||
|
description: 'Get a specific list by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getList(args.list_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_create',
|
||||||
|
description: 'Create a new list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
folder_id: z.string().optional().describe('Folder ID'),
|
||||||
|
space_id: z.string().optional().describe('Space ID (for folderless list)'),
|
||||||
|
name: z.string().describe('List name'),
|
||||||
|
content: z.string().optional().describe('List description'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp)'),
|
||||||
|
due_date_time: z.boolean().optional().describe('Include time in due date'),
|
||||||
|
priority: z.number().optional().describe('Priority (1-4)'),
|
||||||
|
assignee: z.number().optional().describe('Default assignee user ID'),
|
||||||
|
status: z.string().optional().describe('Default status'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { folder_id, space_id, ...data } = args;
|
||||||
|
let response;
|
||||||
|
if (folder_id) {
|
||||||
|
response = await client.createList(folder_id, data);
|
||||||
|
} else if (space_id) {
|
||||||
|
response = await client.createFolderlessList(space_id, data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either folder_id or space_id must be provided');
|
||||||
|
}
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_update',
|
||||||
|
description: 'Update a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
name: z.string().optional().describe('List name'),
|
||||||
|
content: z.string().optional().describe('List description'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp)'),
|
||||||
|
due_date_time: z.boolean().optional().describe('Include time in due date'),
|
||||||
|
priority: z.number().optional().describe('Priority (1-4)'),
|
||||||
|
assignee: z.number().optional().describe('Default assignee user ID'),
|
||||||
|
unset_status: z.boolean().optional().describe('Remove default status'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { list_id, ...data } = args;
|
||||||
|
const response = await client.updateList(list_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_delete',
|
||||||
|
description: 'Delete a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteList(args.list_id);
|
||||||
|
return { content: [{ type: 'text', text: 'List deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_add_task',
|
||||||
|
description: 'Add an existing task to a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addTaskToList(args.list_id, args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_lists_remove_task',
|
||||||
|
description: 'Remove a task from a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.removeTaskFromList(args.list_id, args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Task removed from list successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
112
servers/clickup/src/tools/spaces-tools.ts
Normal file
112
servers/clickup/src/tools/spaces-tools.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Spaces Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createSpacesTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_spaces_list',
|
||||||
|
description: 'List all spaces in a workspace',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team/Workspace ID'),
|
||||||
|
archived: z.boolean().optional().describe('Include archived spaces'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getSpaces(args.team_id, args.archived);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_spaces_get',
|
||||||
|
description: 'Get a specific space by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getSpace(args.space_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_spaces_create',
|
||||||
|
description: 'Create a new space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team/Workspace ID'),
|
||||||
|
name: z.string().describe('Space name'),
|
||||||
|
multiple_assignees: z.boolean().optional().describe('Allow multiple assignees'),
|
||||||
|
features: z.object({
|
||||||
|
due_dates: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
start_date: z.boolean().optional(),
|
||||||
|
remap_due_dates: z.boolean().optional(),
|
||||||
|
remap_closed_due_date: z.boolean().optional(),
|
||||||
|
}).optional(),
|
||||||
|
time_tracking: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
tags: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
time_estimates: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
checklists: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
custom_fields: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
remap_dependencies: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
dependency_warning: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
portfolios: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
}).optional().describe('Space features configuration'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, ...data } = args;
|
||||||
|
const response = await client.createSpace(team_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_spaces_update',
|
||||||
|
description: 'Update a space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
name: z.string().optional().describe('Space name'),
|
||||||
|
color: z.string().optional().describe('Space color (hex)'),
|
||||||
|
private: z.boolean().optional().describe('Make space private'),
|
||||||
|
admin_can_manage: z.boolean().optional().describe('Allow admins to manage'),
|
||||||
|
multiple_assignees: z.boolean().optional().describe('Allow multiple assignees'),
|
||||||
|
features: z.object({
|
||||||
|
due_dates: z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
start_date: z.boolean().optional(),
|
||||||
|
remap_due_dates: z.boolean().optional(),
|
||||||
|
remap_closed_due_date: z.boolean().optional(),
|
||||||
|
}).optional(),
|
||||||
|
time_tracking: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
tags: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
time_estimates: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
checklists: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
custom_fields: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
remap_dependencies: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
dependency_warning: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
portfolios: z.object({ enabled: z.boolean().optional() }).optional(),
|
||||||
|
}).optional().describe('Space features configuration'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { space_id, ...data } = args;
|
||||||
|
const response = await client.updateSpace(space_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_spaces_delete',
|
||||||
|
description: 'Delete a space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteSpace(args.space_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Space deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
96
servers/clickup/src/tools/tags-tools.ts
Normal file
96
servers/clickup/src/tools/tags-tools.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Tags Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createTagsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_list',
|
||||||
|
description: 'List all tags in a space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getSpaceTags(args.space_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_create',
|
||||||
|
description: 'Create a new tag',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
name: z.string().describe('Tag name'),
|
||||||
|
tag_fg: z.string().optional().describe('Foreground color (hex)'),
|
||||||
|
tag_bg: z.string().optional().describe('Background color (hex)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { space_id, name, ...data } = args;
|
||||||
|
const response = await client.createSpaceTag(space_id, { tag: { name, ...data } });
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_update',
|
||||||
|
description: 'Update a tag',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
tag_name: z.string().describe('Current tag name'),
|
||||||
|
new_name: z.string().optional().describe('New tag name'),
|
||||||
|
tag_fg: z.string().optional().describe('Foreground color (hex)'),
|
||||||
|
tag_bg: z.string().optional().describe('Background color (hex)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { space_id, tag_name, new_name, ...data } = args;
|
||||||
|
const updateData: any = { ...data };
|
||||||
|
if (new_name) updateData.name = new_name;
|
||||||
|
const response = await client.updateTag(space_id, tag_name, { tag: updateData });
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_delete',
|
||||||
|
description: 'Delete a tag',
|
||||||
|
inputSchema: z.object({
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
tag_name: z.string().describe('Tag name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteTag(args.space_id, args.tag_name);
|
||||||
|
return { content: [{ type: 'text', text: 'Tag deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_add_to_task',
|
||||||
|
description: 'Add a tag to a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
tag_name: z.string().describe('Tag name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addTagToTask(args.task_id, args.tag_name);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tags_remove_from_task',
|
||||||
|
description: 'Remove a tag from a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
tag_name: z.string().describe('Tag name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.removeTagFromTask(args.task_id, args.tag_name);
|
||||||
|
return { content: [{ type: 'text', text: 'Tag removed from task successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
325
servers/clickup/src/tools/tasks-tools.ts
Normal file
325
servers/clickup/src/tools/tasks-tools.ts
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Tasks Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createTasksTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_list',
|
||||||
|
description: 'List tasks in a list with optional filtering',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
archived: z.boolean().optional().describe('Include archived tasks'),
|
||||||
|
page: z.number().optional().describe('Page number for pagination'),
|
||||||
|
order_by: z.string().optional().describe('Order by field'),
|
||||||
|
reverse: z.boolean().optional().describe('Reverse order'),
|
||||||
|
subtasks: z.boolean().optional().describe('Include subtasks'),
|
||||||
|
statuses: z.array(z.string()).optional().describe('Filter by statuses'),
|
||||||
|
include_closed: z.boolean().optional().describe('Include closed tasks'),
|
||||||
|
assignees: z.array(z.string()).optional().describe('Filter by assignees'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Filter by tags'),
|
||||||
|
due_date_gt: z.number().optional().describe('Due date greater than (Unix timestamp)'),
|
||||||
|
due_date_lt: z.number().optional().describe('Due date less than (Unix timestamp)'),
|
||||||
|
date_created_gt: z.number().optional().describe('Created after (Unix timestamp)'),
|
||||||
|
date_created_lt: z.number().optional().describe('Created before (Unix timestamp)'),
|
||||||
|
date_updated_gt: z.number().optional().describe('Updated after (Unix timestamp)'),
|
||||||
|
date_updated_lt: z.number().optional().describe('Updated before (Unix timestamp)'),
|
||||||
|
custom_fields: z.array(z.object({
|
||||||
|
field_id: z.string(),
|
||||||
|
operator: z.string(),
|
||||||
|
value: z.any()
|
||||||
|
})).optional().describe('Custom field filters'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTasks(args.list_id, args);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_get',
|
||||||
|
description: 'Get a specific task by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
custom_task_ids: z.boolean().optional().describe('Use custom task IDs'),
|
||||||
|
team_id: z.string().optional().describe('Team ID (required if using custom task IDs)'),
|
||||||
|
include_subtasks: z.boolean().optional().describe('Include subtasks'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTask(args.task_id, {
|
||||||
|
custom_task_ids: args.custom_task_ids,
|
||||||
|
team_id: args.team_id,
|
||||||
|
include_subtasks: args.include_subtasks
|
||||||
|
});
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_create',
|
||||||
|
description: 'Create a new task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
name: z.string().describe('Task name'),
|
||||||
|
description: z.string().optional().describe('Task description'),
|
||||||
|
assignees: z.array(z.number()).optional().describe('Assignee user IDs'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Tag names'),
|
||||||
|
status: z.string().optional().describe('Status name'),
|
||||||
|
priority: z.number().optional().describe('Priority (1=urgent, 2=high, 3=normal, 4=low)'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
|
||||||
|
due_date_time: z.boolean().optional().describe('Include time in due date'),
|
||||||
|
time_estimate: z.number().optional().describe('Time estimate in milliseconds'),
|
||||||
|
start_date: z.number().optional().describe('Start date (Unix timestamp in milliseconds)'),
|
||||||
|
start_date_time: z.boolean().optional().describe('Include time in start date'),
|
||||||
|
notify_all: z.boolean().optional().describe('Notify all task watchers'),
|
||||||
|
parent: z.string().optional().describe('Parent task ID for subtasks'),
|
||||||
|
links_to: z.string().optional().describe('Link to another task ID'),
|
||||||
|
check_required_custom_fields: z.boolean().optional().describe('Validate required custom fields'),
|
||||||
|
custom_fields: z.array(z.object({
|
||||||
|
id: z.string(),
|
||||||
|
value: z.any()
|
||||||
|
})).optional().describe('Custom field values'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { list_id, ...data } = args;
|
||||||
|
const response = await client.createTask(list_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_update',
|
||||||
|
description: 'Update an existing task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
name: z.string().optional().describe('Task name'),
|
||||||
|
description: z.string().optional().describe('Task description'),
|
||||||
|
status: z.string().optional().describe('Status name'),
|
||||||
|
priority: z.number().optional().describe('Priority (1=urgent, 2=high, 3=normal, 4=low)'),
|
||||||
|
due_date: z.number().optional().describe('Due date (Unix timestamp in milliseconds)'),
|
||||||
|
due_date_time: z.boolean().optional().describe('Include time in due date'),
|
||||||
|
parent: z.string().optional().describe('Parent task ID'),
|
||||||
|
time_estimate: z.number().optional().describe('Time estimate in milliseconds'),
|
||||||
|
start_date: z.number().optional().describe('Start date (Unix timestamp in milliseconds)'),
|
||||||
|
start_date_time: z.boolean().optional().describe('Include time in start date'),
|
||||||
|
assignees_add: z.array(z.number()).optional().describe('Add assignees (user IDs)'),
|
||||||
|
assignees_rem: z.array(z.number()).optional().describe('Remove assignees (user IDs)'),
|
||||||
|
archived: z.boolean().optional().describe('Archive/unarchive task'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { task_id, assignees_add, assignees_rem, ...data } = args;
|
||||||
|
|
||||||
|
if (assignees_add || assignees_rem) {
|
||||||
|
data.assignees = {
|
||||||
|
add: assignees_add,
|
||||||
|
rem: assignees_rem
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.updateTask(task_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_delete',
|
||||||
|
description: 'Delete a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
custom_task_ids: z.boolean().optional().describe('Use custom task IDs'),
|
||||||
|
team_id: z.string().optional().describe('Team ID (required if using custom task IDs)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.deleteTask(args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Task deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_search',
|
||||||
|
description: 'Search/filter tasks across a team',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
page: z.number().optional().describe('Page number'),
|
||||||
|
order_by: z.string().optional().describe('Order by field'),
|
||||||
|
reverse: z.boolean().optional().describe('Reverse order'),
|
||||||
|
subtasks: z.boolean().optional().describe('Include subtasks'),
|
||||||
|
space_ids: z.array(z.string()).optional().describe('Filter by space IDs'),
|
||||||
|
project_ids: z.array(z.string()).optional().describe('Filter by project/folder IDs'),
|
||||||
|
list_ids: z.array(z.string()).optional().describe('Filter by list IDs'),
|
||||||
|
statuses: z.array(z.string()).optional().describe('Filter by statuses'),
|
||||||
|
include_closed: z.boolean().optional().describe('Include closed tasks'),
|
||||||
|
assignees: z.array(z.string()).optional().describe('Filter by assignee IDs'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Filter by tags'),
|
||||||
|
due_date_gt: z.number().optional().describe('Due date greater than'),
|
||||||
|
due_date_lt: z.number().optional().describe('Due date less than'),
|
||||||
|
date_created_gt: z.number().optional().describe('Created after'),
|
||||||
|
date_created_lt: z.number().optional().describe('Created before'),
|
||||||
|
date_updated_gt: z.number().optional().describe('Updated after'),
|
||||||
|
date_updated_lt: z.number().optional().describe('Updated before'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, ...params } = args;
|
||||||
|
const response = await client.getFilteredTasks(team_id, params);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_bulk_update',
|
||||||
|
description: 'Bulk update multiple tasks',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_ids: z.array(z.string()).describe('Array of task IDs'),
|
||||||
|
status: z.string().optional().describe('Status to set'),
|
||||||
|
priority: z.number().optional().describe('Priority to set'),
|
||||||
|
assignees_add: z.array(z.number()).optional().describe('Add assignees'),
|
||||||
|
assignees_rem: z.array(z.number()).optional().describe('Remove assignees'),
|
||||||
|
archived: z.boolean().optional().describe('Archive/unarchive'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { task_ids, assignees_add, assignees_rem, ...data } = args;
|
||||||
|
|
||||||
|
if (assignees_add || assignees_rem) {
|
||||||
|
data.assignees = {
|
||||||
|
add: assignees_add,
|
||||||
|
rem: assignees_rem
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.bulkUpdateTasks(task_ids, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_get_time_entries',
|
||||||
|
description: 'Get time entries for a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTaskTimeEntries(args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_add_time_entry',
|
||||||
|
description: 'Add a time entry to a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
duration: z.number().describe('Duration in milliseconds'),
|
||||||
|
start: z.number().describe('Start time (Unix timestamp in milliseconds)'),
|
||||||
|
description: z.string().optional().describe('Time entry description'),
|
||||||
|
billable: z.boolean().optional().describe('Is billable'),
|
||||||
|
assignee: z.number().optional().describe('Assignee user ID'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Tag names'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, task_id, ...data } = args;
|
||||||
|
data.tid = task_id;
|
||||||
|
const response = await client.createTimeEntry(team_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_get_custom_fields',
|
||||||
|
description: 'Get custom field values for a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const task = await client.getTask(args.task_id);
|
||||||
|
const customFields = (task as any).custom_fields || [];
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(customFields, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_set_custom_field',
|
||||||
|
description: 'Set a custom field value on a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
field_id: z.string().describe('Custom field ID'),
|
||||||
|
value: z.any().describe('Field value'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.setCustomFieldValue(args.task_id, args.field_id, args.value);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_add_dependency',
|
||||||
|
description: 'Add a task dependency',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
depends_on: z.string().optional().describe('Task this depends on'),
|
||||||
|
dependency_of: z.string().optional().describe('Task that depends on this'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.addDependency(args.task_id, args.depends_on, args.dependency_of);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_remove_dependency',
|
||||||
|
description: 'Remove a task dependency',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
depends_on: z.string().optional().describe('Task this depends on'),
|
||||||
|
dependency_of: z.string().optional().describe('Task that depends on this'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.deleteDependency(args.task_id, args.depends_on, args.dependency_of);
|
||||||
|
return { content: [{ type: 'text', text: 'Dependency removed' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_list_members',
|
||||||
|
description: 'List members assigned to a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTaskMembers(args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_add_comment',
|
||||||
|
description: 'Add a comment to a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
comment_text: z.string().describe('Comment text'),
|
||||||
|
assignee: z.number().optional().describe('Assign comment to user ID'),
|
||||||
|
notify_all: z.boolean().optional().describe('Notify all task watchers'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { task_id, ...data } = args;
|
||||||
|
const response = await client.createComment(task_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_tasks_get_comments',
|
||||||
|
description: 'Get comments for a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTaskComments(args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
56
servers/clickup/src/tools/teams-tools.ts
Normal file
56
servers/clickup/src/tools/teams-tools.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Teams/Workspaces Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createTeamsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_teams_list_workspaces',
|
||||||
|
description: 'List all authorized workspaces/teams',
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getAuthorizedTeams();
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_teams_get_workspace',
|
||||||
|
description: 'Get workspace/team details',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team/Workspace ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTeam(args.team_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_teams_list_members',
|
||||||
|
description: 'List members of a list',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getListMembers(args.list_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_teams_get_member',
|
||||||
|
description: 'Get task member details',
|
||||||
|
inputSchema: z.object({
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTaskMembers(args.task_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
37
servers/clickup/src/tools/templates-tools.ts
Normal file
37
servers/clickup/src/tools/templates-tools.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Templates Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createTemplatesTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_templates_list',
|
||||||
|
description: 'List available task templates',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
page: z.number().optional().describe('Page number'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTemplates(args.team_id, args.page);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_templates_apply',
|
||||||
|
description: 'Create a task from a template',
|
||||||
|
inputSchema: z.object({
|
||||||
|
list_id: z.string().describe('List ID'),
|
||||||
|
template_id: z.string().describe('Template ID'),
|
||||||
|
name: z.string().describe('Task name'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.createTaskFromTemplate(args.list_id, args.template_id, args.name);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
141
servers/clickup/src/tools/time-tracking-tools.ts
Normal file
141
servers/clickup/src/tools/time-tracking-tools.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Time Tracking Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createTimeTrackingTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_time_list_entries',
|
||||||
|
description: 'List time entries in a team',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
start_date: z.number().optional().describe('Start date filter (Unix timestamp in milliseconds)'),
|
||||||
|
end_date: z.number().optional().describe('End date filter (Unix timestamp in milliseconds)'),
|
||||||
|
assignee: z.number().optional().describe('Filter by assignee user ID'),
|
||||||
|
include_task_tags: z.boolean().optional().describe('Include task tags'),
|
||||||
|
include_location_names: z.boolean().optional().describe('Include location names'),
|
||||||
|
space_id: z.string().optional().describe('Filter by space ID'),
|
||||||
|
folder_id: z.string().optional().describe('Filter by folder ID'),
|
||||||
|
list_id: z.string().optional().describe('Filter by list ID'),
|
||||||
|
task_id: z.string().optional().describe('Filter by task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, ...params } = args;
|
||||||
|
const response = await client.getTimeEntries(team_id, params);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_get_entry',
|
||||||
|
description: 'Get a specific time entry',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
timer_id: z.string().describe('Timer/Time Entry ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getTimeEntry(args.team_id, args.timer_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_create',
|
||||||
|
description: 'Create a time entry',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
duration: z.number().describe('Duration in milliseconds'),
|
||||||
|
start: z.number().describe('Start time (Unix timestamp in milliseconds)'),
|
||||||
|
description: z.string().optional().describe('Time entry description'),
|
||||||
|
billable: z.boolean().optional().describe('Is billable'),
|
||||||
|
assignee: z.number().optional().describe('Assignee user ID'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Tag names'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, task_id, ...data } = args;
|
||||||
|
data.tid = task_id;
|
||||||
|
const response = await client.createTimeEntry(team_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_update',
|
||||||
|
description: 'Update a time entry',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
timer_id: z.string().describe('Timer/Time Entry ID'),
|
||||||
|
description: z.string().optional().describe('Time entry description'),
|
||||||
|
billable: z.boolean().optional().describe('Is billable'),
|
||||||
|
start: z.number().optional().describe('Start time (Unix timestamp in milliseconds)'),
|
||||||
|
end: z.number().optional().describe('End time (Unix timestamp in milliseconds)'),
|
||||||
|
duration: z.number().optional().describe('Duration in milliseconds'),
|
||||||
|
assignee: z.number().optional().describe('Assignee user ID'),
|
||||||
|
tags: z.array(z.string()).optional().describe('Tag names'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, timer_id, ...data } = args;
|
||||||
|
const response = await client.updateTimeEntry(team_id, timer_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_delete',
|
||||||
|
description: 'Delete a time entry',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
timer_id: z.string().describe('Timer/Time Entry ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteTimeEntry(args.team_id, args.timer_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Time entry deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_get_running',
|
||||||
|
description: 'Get running timer for a user',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
assignee: z.string().optional().describe('Assignee user ID (defaults to current user)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getRunningTimeEntry(args.team_id, args.assignee);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_start',
|
||||||
|
description: 'Start a timer for a task',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
task_id: z.string().describe('Task ID'),
|
||||||
|
description: z.string().optional().describe('Timer description'),
|
||||||
|
billable: z.boolean().optional().describe('Is billable'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, task_id, ...data } = args;
|
||||||
|
const response = await client.startTimer(team_id, task_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_time_stop',
|
||||||
|
description: 'Stop the running timer',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.stopTimer(args.team_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
116
servers/clickup/src/tools/views-tools.ts
Normal file
116
servers/clickup/src/tools/views-tools.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Views Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createViewsTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_views_list',
|
||||||
|
description: 'List all views in a workspace/space',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team/Workspace ID'),
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
list_id: z.string().optional().describe('List ID'),
|
||||||
|
folder_id: z.string().optional().describe('Folder ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getViews(args.team_id, args.space_id, args.list_id, args.folder_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_views_get',
|
||||||
|
description: 'Get a specific view by ID',
|
||||||
|
inputSchema: z.object({
|
||||||
|
view_id: z.string().describe('View ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getView(args.view_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_views_get_tasks',
|
||||||
|
description: 'Get tasks from a view',
|
||||||
|
inputSchema: z.object({
|
||||||
|
view_id: z.string().describe('View ID'),
|
||||||
|
page: z.number().optional().describe('Page number'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getViewTasks(args.view_id, args.page);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_views_create',
|
||||||
|
description: 'Create a new view',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team/Workspace ID'),
|
||||||
|
space_id: z.string().describe('Space ID'),
|
||||||
|
name: z.string().describe('View name'),
|
||||||
|
type: z.string().describe('View type (list, board, calendar, gantt, etc.)'),
|
||||||
|
parent: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.number()
|
||||||
|
}).optional().describe('Parent object (list, folder, space)'),
|
||||||
|
grouping: z.object({
|
||||||
|
field: z.string(),
|
||||||
|
dir: z.number()
|
||||||
|
}).optional().describe('Grouping configuration'),
|
||||||
|
sorting: z.object({
|
||||||
|
fields: z.array(z.object({
|
||||||
|
field: z.string(),
|
||||||
|
dir: z.number()
|
||||||
|
}))
|
||||||
|
}).optional().describe('Sorting configuration'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, space_id, ...data } = args;
|
||||||
|
const response = await client.createView(team_id, space_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_views_update',
|
||||||
|
description: 'Update a view',
|
||||||
|
inputSchema: z.object({
|
||||||
|
view_id: z.string().describe('View ID'),
|
||||||
|
name: z.string().optional().describe('View name'),
|
||||||
|
grouping: z.object({
|
||||||
|
field: z.string(),
|
||||||
|
dir: z.number()
|
||||||
|
}).optional().describe('Grouping configuration'),
|
||||||
|
sorting: z.object({
|
||||||
|
fields: z.array(z.object({
|
||||||
|
field: z.string(),
|
||||||
|
dir: z.number()
|
||||||
|
}))
|
||||||
|
}).optional().describe('Sorting configuration'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { view_id, ...data } = args;
|
||||||
|
const response = await client.updateView(view_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_views_delete',
|
||||||
|
description: 'Delete a view',
|
||||||
|
inputSchema: z.object({
|
||||||
|
view_id: z.string().describe('View ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteView(args.view_id);
|
||||||
|
return { content: [{ type: 'text', text: 'View deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
69
servers/clickup/src/tools/webhooks-tools.ts
Normal file
69
servers/clickup/src/tools/webhooks-tools.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp Webhooks Tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { ClickUpClient } from '../clients/clickup.js';
|
||||||
|
|
||||||
|
export function createWebhooksTools(client: ClickUpClient) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'clickup_webhooks_list',
|
||||||
|
description: 'List all webhooks in a team',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const response = await client.getWebhooks(args.team_id);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_webhooks_create',
|
||||||
|
description: 'Create a webhook',
|
||||||
|
inputSchema: z.object({
|
||||||
|
team_id: z.string().describe('Team ID'),
|
||||||
|
endpoint: z.string().describe('Webhook endpoint URL'),
|
||||||
|
events: z.array(z.string()).describe('Event types (e.g., taskCreated, taskUpdated, taskDeleted)'),
|
||||||
|
space_id: z.string().optional().describe('Filter to space ID'),
|
||||||
|
folder_id: z.string().optional().describe('Filter to folder ID'),
|
||||||
|
list_id: z.string().optional().describe('Filter to list ID'),
|
||||||
|
task_id: z.string().optional().describe('Filter to task ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { team_id, ...data } = args;
|
||||||
|
const response = await client.createWebhook(team_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_webhooks_update',
|
||||||
|
description: 'Update a webhook',
|
||||||
|
inputSchema: z.object({
|
||||||
|
webhook_id: z.string().describe('Webhook ID'),
|
||||||
|
endpoint: z.string().optional().describe('Webhook endpoint URL'),
|
||||||
|
events: z.array(z.string()).optional().describe('Event types'),
|
||||||
|
status: z.string().optional().describe('Status (active, disabled)'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
const { webhook_id, ...data } = args;
|
||||||
|
const response = await client.updateWebhook(webhook_id, data);
|
||||||
|
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'clickup_webhooks_delete',
|
||||||
|
description: 'Delete a webhook',
|
||||||
|
inputSchema: z.object({
|
||||||
|
webhook_id: z.string().describe('Webhook ID'),
|
||||||
|
}),
|
||||||
|
handler: async (args: any) => {
|
||||||
|
await client.deleteWebhook(args.webhook_id);
|
||||||
|
return { content: [{ type: 'text', text: 'Webhook deleted successfully' }] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
596
servers/clickup/src/types.ts
Normal file
596
servers/clickup/src/types.ts
Normal file
@ -0,0 +1,596 @@
|
|||||||
|
/**
|
||||||
|
* ClickUp API Types
|
||||||
|
* Based on ClickUp API v2: https://clickup.com/api
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ===== Core Types =====
|
||||||
|
|
||||||
|
export interface ClickUpConfig {
|
||||||
|
apiToken?: string;
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
oauthToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpUser {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
color: string;
|
||||||
|
profilePicture: string | null;
|
||||||
|
initials: string;
|
||||||
|
role: number;
|
||||||
|
custom_role: number | null;
|
||||||
|
last_active: string;
|
||||||
|
date_joined: string;
|
||||||
|
date_invited: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpTeam {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
avatar: string | null;
|
||||||
|
members: ClickUpUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Task Types =====
|
||||||
|
|
||||||
|
export interface ClickUpTask {
|
||||||
|
id: string;
|
||||||
|
custom_id: string | null;
|
||||||
|
name: string;
|
||||||
|
text_content: string;
|
||||||
|
description: string;
|
||||||
|
status: ClickUpStatus;
|
||||||
|
orderindex: string;
|
||||||
|
date_created: string;
|
||||||
|
date_updated: string;
|
||||||
|
date_closed: string | null;
|
||||||
|
date_done: string | null;
|
||||||
|
archived: boolean;
|
||||||
|
creator: ClickUpUser;
|
||||||
|
assignees: ClickUpUser[];
|
||||||
|
watchers: ClickUpUser[];
|
||||||
|
checklists: ClickUpChecklist[];
|
||||||
|
tags: ClickUpTag[];
|
||||||
|
parent: string | null;
|
||||||
|
priority: ClickUpPriority | null;
|
||||||
|
due_date: string | null;
|
||||||
|
start_date: string | null;
|
||||||
|
points: number | null;
|
||||||
|
time_estimate: number | null;
|
||||||
|
time_spent: number | null;
|
||||||
|
custom_fields: ClickUpCustomFieldValue[];
|
||||||
|
dependencies: ClickUpTaskDependency[];
|
||||||
|
linked_tasks: ClickUpLinkedTask[];
|
||||||
|
team_id: string;
|
||||||
|
url: string;
|
||||||
|
permission_level: string;
|
||||||
|
list: ClickUpListReference;
|
||||||
|
project: ClickUpFolderReference;
|
||||||
|
folder: ClickUpFolderReference;
|
||||||
|
space: ClickUpSpaceReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpStatus {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
color: string;
|
||||||
|
orderindex: number;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpPriority {
|
||||||
|
id: string;
|
||||||
|
priority: string;
|
||||||
|
color: string;
|
||||||
|
orderindex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpTaskDependency {
|
||||||
|
task_id: string;
|
||||||
|
depends_on: string;
|
||||||
|
type: number;
|
||||||
|
date_created: string;
|
||||||
|
user: ClickUpUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpLinkedTask {
|
||||||
|
task_id: string;
|
||||||
|
link_id: string;
|
||||||
|
date_created: string;
|
||||||
|
user: ClickUpUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Space Types =====
|
||||||
|
|
||||||
|
export interface ClickUpSpace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
private: boolean;
|
||||||
|
statuses: ClickUpStatus[];
|
||||||
|
multiple_assignees: boolean;
|
||||||
|
features: ClickUpSpaceFeatures;
|
||||||
|
archived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpSpaceFeatures {
|
||||||
|
due_dates: { enabled: boolean; start_date: boolean; remap_due_dates: boolean; remap_closed_due_date: boolean };
|
||||||
|
time_tracking: { enabled: boolean };
|
||||||
|
tags: { enabled: boolean };
|
||||||
|
time_estimates: { enabled: boolean };
|
||||||
|
checklists: { enabled: boolean };
|
||||||
|
custom_fields: { enabled: boolean };
|
||||||
|
remap_dependencies: { enabled: boolean };
|
||||||
|
dependency_warning: { enabled: boolean };
|
||||||
|
portfolios: { enabled: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpSpaceReference {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
access: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Folder Types =====
|
||||||
|
|
||||||
|
export interface ClickUpFolder {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
orderindex: number;
|
||||||
|
override_statuses: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
space: ClickUpSpaceReference;
|
||||||
|
task_count: string;
|
||||||
|
archived: boolean;
|
||||||
|
statuses: ClickUpStatus[];
|
||||||
|
lists: ClickUpList[];
|
||||||
|
permission_level: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpFolderReference {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
hidden: boolean;
|
||||||
|
access: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== List Types =====
|
||||||
|
|
||||||
|
export interface ClickUpList {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
orderindex: number;
|
||||||
|
status: ClickUpStatus | null;
|
||||||
|
priority: ClickUpPriority | null;
|
||||||
|
assignee: ClickUpUser | null;
|
||||||
|
task_count: number;
|
||||||
|
due_date: string | null;
|
||||||
|
start_date: string | null;
|
||||||
|
folder: ClickUpFolderReference;
|
||||||
|
space: ClickUpSpaceReference;
|
||||||
|
archived: boolean;
|
||||||
|
override_statuses: boolean;
|
||||||
|
statuses: ClickUpStatus[];
|
||||||
|
permission_level: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpListReference {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
access: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== View Types =====
|
||||||
|
|
||||||
|
export interface ClickUpView {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
parent: {
|
||||||
|
id: string;
|
||||||
|
type: number;
|
||||||
|
};
|
||||||
|
grouping: {
|
||||||
|
field: string;
|
||||||
|
dir: number;
|
||||||
|
};
|
||||||
|
divide: {
|
||||||
|
field: string | null;
|
||||||
|
dir: number | null;
|
||||||
|
};
|
||||||
|
sorting: {
|
||||||
|
fields: Array<{
|
||||||
|
field: string;
|
||||||
|
dir: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
filters: {
|
||||||
|
op: string;
|
||||||
|
fields: any[];
|
||||||
|
};
|
||||||
|
columns: {
|
||||||
|
fields: any[];
|
||||||
|
};
|
||||||
|
team_sidebar: {
|
||||||
|
assignees: any[];
|
||||||
|
assigned_comments: boolean;
|
||||||
|
unassigned_tasks: boolean;
|
||||||
|
};
|
||||||
|
settings: {
|
||||||
|
show_task_locations: boolean;
|
||||||
|
show_subtasks: number;
|
||||||
|
show_subtask_parent_names: boolean;
|
||||||
|
show_closed_subtasks: boolean;
|
||||||
|
show_assignees: boolean;
|
||||||
|
show_images: boolean;
|
||||||
|
collapse_empty_columns: boolean | null;
|
||||||
|
me_comments: boolean;
|
||||||
|
me_subtasks: boolean;
|
||||||
|
me_checklists: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Comment Types =====
|
||||||
|
|
||||||
|
export interface ClickUpComment {
|
||||||
|
id: string;
|
||||||
|
comment: Array<{
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
comment_text: string;
|
||||||
|
user: ClickUpUser;
|
||||||
|
resolved: boolean;
|
||||||
|
assignee: ClickUpUser | null;
|
||||||
|
assigned_by: ClickUpUser | null;
|
||||||
|
reactions: ClickUpReaction[];
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpReaction {
|
||||||
|
reaction: string;
|
||||||
|
users: ClickUpUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Doc Types =====
|
||||||
|
|
||||||
|
export interface ClickUpDoc {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
parent: {
|
||||||
|
id: string;
|
||||||
|
type: number;
|
||||||
|
};
|
||||||
|
date_created: string;
|
||||||
|
date_updated: string;
|
||||||
|
creator: ClickUpUser;
|
||||||
|
deleted: boolean;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Goal Types =====
|
||||||
|
|
||||||
|
export interface ClickUpGoal {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
team_id: string;
|
||||||
|
date_created: string;
|
||||||
|
start_date: string | null;
|
||||||
|
due_date: string | null;
|
||||||
|
description: string;
|
||||||
|
private: boolean;
|
||||||
|
archived: boolean;
|
||||||
|
creator: ClickUpUser;
|
||||||
|
color: string;
|
||||||
|
pretty_id: string;
|
||||||
|
multiple_owners: boolean;
|
||||||
|
folder_id: string | null;
|
||||||
|
members: ClickUpUser[];
|
||||||
|
owners: ClickUpUser[];
|
||||||
|
key_results: ClickUpKeyResult[];
|
||||||
|
percent_completed: number;
|
||||||
|
history: any[];
|
||||||
|
pretty_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpKeyResult {
|
||||||
|
id: string;
|
||||||
|
goal_id: string;
|
||||||
|
name: string;
|
||||||
|
creator: ClickUpUser;
|
||||||
|
type: string;
|
||||||
|
unit: string | null;
|
||||||
|
steps_start: number;
|
||||||
|
steps_end: number;
|
||||||
|
steps_current: number;
|
||||||
|
percent_completed: number;
|
||||||
|
task_ids: string[];
|
||||||
|
list_ids: string[];
|
||||||
|
subcategory_ids: string[];
|
||||||
|
owners: ClickUpUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Tag Types =====
|
||||||
|
|
||||||
|
export interface ClickUpTag {
|
||||||
|
name: string;
|
||||||
|
tag_fg: string;
|
||||||
|
tag_bg: string;
|
||||||
|
creator: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Checklist Types =====
|
||||||
|
|
||||||
|
export interface ClickUpChecklist {
|
||||||
|
id: string;
|
||||||
|
task_id: string;
|
||||||
|
name: string;
|
||||||
|
orderindex: number;
|
||||||
|
resolved: number;
|
||||||
|
unresolved: number;
|
||||||
|
items: ClickUpChecklistItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpChecklistItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
orderindex: number;
|
||||||
|
assignee: ClickUpUser | null;
|
||||||
|
resolved: boolean;
|
||||||
|
parent: string | null;
|
||||||
|
date_created: string;
|
||||||
|
children: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Time Tracking Types =====
|
||||||
|
|
||||||
|
export interface ClickUpTimeEntry {
|
||||||
|
id: string;
|
||||||
|
task: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: ClickUpStatus;
|
||||||
|
custom_type: any;
|
||||||
|
};
|
||||||
|
wid: string;
|
||||||
|
user: ClickUpUser;
|
||||||
|
billable: boolean;
|
||||||
|
start: string;
|
||||||
|
end: string | null;
|
||||||
|
duration: string;
|
||||||
|
description: string;
|
||||||
|
tags: ClickUpTag[];
|
||||||
|
source: string;
|
||||||
|
at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Custom Field Types =====
|
||||||
|
|
||||||
|
export interface ClickUpCustomField {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
type_config: any;
|
||||||
|
date_created: string;
|
||||||
|
hide_from_guests: boolean;
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpCustomFieldValue {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
type_config: any;
|
||||||
|
date_created: string;
|
||||||
|
hide_from_guests: boolean;
|
||||||
|
value: any;
|
||||||
|
required: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Webhook Types =====
|
||||||
|
|
||||||
|
export interface ClickUpWebhook {
|
||||||
|
id: string;
|
||||||
|
userid: number;
|
||||||
|
team_id: number;
|
||||||
|
endpoint: string;
|
||||||
|
client_id: string;
|
||||||
|
events: string[];
|
||||||
|
task_id: string | null;
|
||||||
|
list_id: string | null;
|
||||||
|
folder_id: string | null;
|
||||||
|
space_id: string | null;
|
||||||
|
health: {
|
||||||
|
status: string;
|
||||||
|
fail_count: number;
|
||||||
|
};
|
||||||
|
secret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Template Types =====
|
||||||
|
|
||||||
|
export interface ClickUpTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Guest Types =====
|
||||||
|
|
||||||
|
export interface ClickUpGuest {
|
||||||
|
user: ClickUpUser;
|
||||||
|
invited_by: ClickUpUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Workspace Types =====
|
||||||
|
|
||||||
|
export interface ClickUpWorkspace {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
avatar: string | null;
|
||||||
|
members: ClickUpUser[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== API Response Types =====
|
||||||
|
|
||||||
|
export interface ClickUpListResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
last_page?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpTasksResponse {
|
||||||
|
tasks: ClickUpTask[];
|
||||||
|
last_page?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpSpacesResponse {
|
||||||
|
spaces: ClickUpSpace[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpFoldersResponse {
|
||||||
|
folders: ClickUpFolder[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpListsResponse {
|
||||||
|
lists: ClickUpList[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpViewsResponse {
|
||||||
|
views: ClickUpView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpCommentsResponse {
|
||||||
|
comments: ClickUpComment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpGoalsResponse {
|
||||||
|
goals: ClickUpGoal[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpTimeEntriesResponse {
|
||||||
|
data: ClickUpTimeEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClickUpWebhooksResponse {
|
||||||
|
webhooks: ClickUpWebhook[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Error Types =====
|
||||||
|
|
||||||
|
export interface ClickUpError {
|
||||||
|
err: string;
|
||||||
|
ECODE: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== API Request Types =====
|
||||||
|
|
||||||
|
export interface CreateTaskRequest {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
assignees?: number[];
|
||||||
|
tags?: string[];
|
||||||
|
status?: string;
|
||||||
|
priority?: number;
|
||||||
|
due_date?: number;
|
||||||
|
due_date_time?: boolean;
|
||||||
|
time_estimate?: number;
|
||||||
|
start_date?: number;
|
||||||
|
start_date_time?: boolean;
|
||||||
|
notify_all?: boolean;
|
||||||
|
parent?: string;
|
||||||
|
links_to?: string;
|
||||||
|
check_required_custom_fields?: boolean;
|
||||||
|
custom_fields?: Array<{
|
||||||
|
id: string;
|
||||||
|
value: any;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTaskRequest {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
status?: string;
|
||||||
|
priority?: number;
|
||||||
|
due_date?: number;
|
||||||
|
due_date_time?: boolean;
|
||||||
|
time_estimate?: number;
|
||||||
|
start_date?: number;
|
||||||
|
start_date_time?: boolean;
|
||||||
|
assignees?: {
|
||||||
|
add?: number[];
|
||||||
|
rem?: number[];
|
||||||
|
};
|
||||||
|
archived?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSpaceRequest {
|
||||||
|
name: string;
|
||||||
|
multiple_assignees?: boolean;
|
||||||
|
features?: Partial<ClickUpSpaceFeatures>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateFolderRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateListRequest {
|
||||||
|
name: string;
|
||||||
|
content?: string;
|
||||||
|
due_date?: number;
|
||||||
|
due_date_time?: boolean;
|
||||||
|
priority?: number;
|
||||||
|
assignee?: number;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCommentRequest {
|
||||||
|
comment_text: string;
|
||||||
|
assignee?: number;
|
||||||
|
notify_all?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateGoalRequest {
|
||||||
|
name: string;
|
||||||
|
due_date?: number;
|
||||||
|
description?: string;
|
||||||
|
multiple_owners?: boolean;
|
||||||
|
owners?: number[];
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateKeyResultRequest {
|
||||||
|
name: string;
|
||||||
|
owners?: number[];
|
||||||
|
type: string;
|
||||||
|
steps_start?: number;
|
||||||
|
steps_end?: number;
|
||||||
|
unit?: string;
|
||||||
|
task_ids?: string[];
|
||||||
|
list_ids?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTimeEntryRequest {
|
||||||
|
description?: string;
|
||||||
|
tags?: string[];
|
||||||
|
start: number;
|
||||||
|
billable?: boolean;
|
||||||
|
duration: number;
|
||||||
|
assignee?: number;
|
||||||
|
tid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateWebhookRequest {
|
||||||
|
endpoint: string;
|
||||||
|
events: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChecklistRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChecklistItemRequest {
|
||||||
|
name: string;
|
||||||
|
assignee?: number;
|
||||||
|
}
|
||||||
@ -1,14 +1,20 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user