diff --git a/servers/calendly/README.md b/servers/calendly/README.md new file mode 100644 index 0000000..3c7b3a4 --- /dev/null +++ b/servers/calendly/README.md @@ -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 diff --git a/servers/calendly/package.json b/servers/calendly/package.json index d24ac11..c0bb978 100644 --- a/servers/calendly/package.json +++ b/servers/calendly/package.json @@ -1,20 +1,35 @@ { - "name": "mcp-server-calendly", + "name": "@mcpengine/calendly-server", "version": "1.0.0", + "description": "Complete Calendly MCP server with 30+ tools and React UI apps", "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "main": "./dist/main.js", + "bin": { + "calendly-mcp": "./dist/main.js" }, + "scripts": { + "build": "tsc && chmod +x dist/main.js", + "dev": "tsc --watch", + "start": "node dist/main.js", + "start:http": "MCP_MODE=http node dist/main.js", + "test": "echo \"No tests yet\" && exit 0" + }, + "keywords": [ + "mcp", + "calendly", + "scheduling", + "model-context-protocol" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/calendly/src/clients/calendly.ts b/servers/calendly/src/clients/calendly.ts new file mode 100644 index 0000000..e776da0 --- /dev/null +++ b/servers/calendly/src/clients/calendly.ts @@ -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( + endpoint: string, + options: RequestInit = {} + ): Promise { + 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 { + 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> { + 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> { + 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> { + 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 { + 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> { + 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> { + const query = this.buildQueryString({ + event_type: eventTypeUri, + ...params, + }); + return this.request(`/event_type_available_times${query}`); + } + + // User Availability & Busy Times + async getUserBusyTimes( + userUri: string, + params: { + start_time: string; + end_time: string; + } + ): Promise> { + const query = this.buildQueryString({ + user: userUri, + ...params, + }); + return this.request(`/user_busy_times${query}`); + } + + // Scheduling Links + async createSchedulingLink(params: { + max_event_count: number; + owner: string; + owner_type: 'EventType' | 'Group'; + }): Promise<{ resource: CalendlySchedulingLink }> { + return this.request('/scheduling_links', { + method: 'POST', + body: JSON.stringify(params), + }); + } + + // Organizations + async getOrganization(uuid: string): Promise<{ resource: CalendlyOrganization }> { + return this.request(`/organizations/${uuid}`); + } + + async listOrganizationMemberships( + organizationUri: string, + params?: { + count?: number; + page_token?: string; + email?: string; + } + ): Promise> { + const query = this.buildQueryString({ + organization: organizationUri, + ...params, + }); + return this.request(`/organization_memberships${query}`); + } + + async listOrganizationInvitations( + organizationUri: string, + params?: { + count?: number; + page_token?: string; + email?: string; + status?: string; + } + ): Promise> { + const query = this.buildQueryString({ + organization: organizationUri, + ...params, + }); + return this.request(`/organization_invitations${query}`); + } + + async inviteUserToOrganization( + organizationUri: string, + email: string + ): Promise<{ resource: CalendlyOrganizationInvitation }> { + return this.request('/organization_invitations', { + method: 'POST', + body: JSON.stringify({ + organization: organizationUri, + email, + }), + }); + } + + async revokeOrganizationInvitation(uuid: string): Promise<{ resource: CalendlyOrganizationInvitation }> { + return this.request(`/organization_invitations/${uuid}`, { + method: 'DELETE', + }); + } + + async removeOrganizationMembership(uuid: string): Promise { + return this.request(`/organization_memberships/${uuid}`, { + method: 'DELETE', + }); + } + + // Routing Forms + async listRoutingForms( + organizationUri: string, + params?: PaginationParams + ): Promise> { + 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> { + 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 { + return this.request(`/webhook_subscriptions/${uuid}`, { + method: 'DELETE', + }); + } +} diff --git a/servers/calendly/src/index.ts b/servers/calendly/src/index.ts deleted file mode 100644 index c9e4f2e..0000000 --- a/servers/calendly/src/index.ts +++ /dev/null @@ -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 { - 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); diff --git a/servers/calendly/src/main.ts b/servers/calendly/src/main.ts new file mode 100644 index 0000000..c9b245d --- /dev/null +++ b/servers/calendly/src/main.ts @@ -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); +}); diff --git a/servers/calendly/src/server.ts b/servers/calendly/src/server.ts new file mode 100644 index 0000000..5ae7b45 --- /dev/null +++ b/servers/calendly/src/server.ts @@ -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'); +} diff --git a/servers/calendly/src/tools/event-types-tools.ts b/servers/calendly/src/tools/event-types-tools.ts new file mode 100644 index 0000000..58b4335 --- /dev/null +++ b/servers/calendly/src/tools/event-types-tools.ts @@ -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), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/tools/events-tools.ts b/servers/calendly/src/tools/events-tools.ts new file mode 100644 index 0000000..019b88f --- /dev/null +++ b/servers/calendly/src/tools/events-tools.ts @@ -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' }), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/tools/organizations-tools.ts b/servers/calendly/src/tools/organizations-tools.ts new file mode 100644 index 0000000..432e223 --- /dev/null +++ b/servers/calendly/src/tools/organizations-tools.ts @@ -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' }), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/tools/scheduling-tools.ts b/servers/calendly/src/tools/scheduling-tools.ts new file mode 100644 index 0000000..8ba12d1 --- /dev/null +++ b/servers/calendly/src/tools/scheduling-tools.ts @@ -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), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/tools/users-tools.ts b/servers/calendly/src/tools/users-tools.ts new file mode 100644 index 0000000..188701a --- /dev/null +++ b/servers/calendly/src/tools/users-tools.ts @@ -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), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/tools/webhooks-tools.ts b/servers/calendly/src/tools/webhooks-tools.ts new file mode 100644 index 0000000..03cf110 --- /dev/null +++ b/servers/calendly/src/tools/webhooks-tools.ts @@ -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' }), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/calendly/src/types/index.ts b/servers/calendly/src/types/index.ts new file mode 100644 index 0000000..c0b2b56 --- /dev/null +++ b/servers/calendly/src/types/index.ts @@ -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 { + 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; + }>; +} diff --git a/servers/calendly/src/ui/react-app/analytics-dashboard/App.tsx b/servers/calendly/src/ui/react-app/analytics-dashboard/App.tsx new file mode 100644 index 0000000..96d86e3 --- /dev/null +++ b/servers/calendly/src/ui/react-app/analytics-dashboard/App.tsx @@ -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 ( +
+
+

📊 Analytics Dashboard

+

Calendly metrics and insights

+
+
+
+

Total Events

+
{stats.totalEvents}
+
+
+

Active Events

+
{stats.activeEvents}
+
+
+

Total Invitees

+
{stats.totalInvitees}
+
+
+

No-Shows

+
{stats.noShows}
+
+
+

Cancel Rate

+
{stats.cancelRate}
+
+
+

Avg Duration

+
{stats.avgDuration}
+
+
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/analytics-dashboard/index.html b/servers/calendly/src/ui/react-app/analytics-dashboard/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/analytics-dashboard/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/analytics-dashboard/styles.css b/servers/calendly/src/ui/react-app/analytics-dashboard/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/analytics-dashboard/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/analytics-dashboard/vite.config.ts b/servers/calendly/src/ui/react-app/analytics-dashboard/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/analytics-dashboard/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/availability-calendar/App.tsx b/servers/calendly/src/ui/react-app/availability-calendar/App.tsx new file mode 100644 index 0000000..d8bd915 --- /dev/null +++ b/servers/calendly/src/ui/react-app/availability-calendar/App.tsx @@ -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 ( +
+
+

📅 Availability Calendar

+

View available time slots

+
+
+ + + +
+
+ {availableTimes.map((slot, i) => ( +
+ {new Date(slot.time).toLocaleString()} + + {slot.available ? 'Available' : 'Busy'} + +
+ ))} +
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/availability-calendar/index.html b/servers/calendly/src/ui/react-app/availability-calendar/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/availability-calendar/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/availability-calendar/styles.css b/servers/calendly/src/ui/react-app/availability-calendar/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/availability-calendar/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/availability-calendar/vite.config.ts b/servers/calendly/src/ui/react-app/availability-calendar/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/availability-calendar/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/booking-flow/App.tsx b/servers/calendly/src/ui/react-app/booking-flow/App.tsx new file mode 100644 index 0000000..b069a9b --- /dev/null +++ b/servers/calendly/src/ui/react-app/booking-flow/App.tsx @@ -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 ( +
+
+

📝 Booking Flow

+

Create a booking experience

+
+
+

Step {step} of 3

+ {step === 1 && ( +
+

Select Event Type

+ + +
+ )} + {step === 2 && ( +
+

Select Time

+ setFormData({...formData, time: e.target.value})} /> +
+ + +
+
+ )} + {step === 3 && ( +
+

Your Information

+ setFormData({...formData, name: e.target.value})} style={{marginBottom: '0.5rem'}} /> + setFormData({...formData, email: e.target.value})} /> +
+ + +
+
+ )} +
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/booking-flow/index.html b/servers/calendly/src/ui/react-app/booking-flow/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/booking-flow/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/booking-flow/styles.css b/servers/calendly/src/ui/react-app/booking-flow/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/booking-flow/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/booking-flow/vite.config.ts b/servers/calendly/src/ui/react-app/booking-flow/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/booking-flow/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/event-dashboard/App.tsx b/servers/calendly/src/ui/react-app/event-dashboard/App.tsx new file mode 100644 index 0000000..33a187b --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-dashboard/App.tsx @@ -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([]); + 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 ( +
+
+

📅 Event Dashboard

+

Manage your Calendly scheduled events

+
+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+

Total Events

+
{events.length}
+
+
+

Active Events

+
{events.filter(e => e.status === 'active').length}
+
+
+

Total Invitees

+
{events.reduce((acc, e) => acc + e.invitees_counter.total, 0)}
+
+
+ + {loading ? ( +
Loading events...
+ ) : ( +
+ {events.map((event) => ( +
+
+

{event.name}

+ {event.status} +
+
+

Start: {formatDate(event.start_time)}

+

End: {formatDate(event.end_time)}

+

Invitees: {event.invitees_counter.active} / {event.invitees_counter.total}

+
+
+ + {event.status === 'active' && ( + + )} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/event-dashboard/index.html b/servers/calendly/src/ui/react-app/event-dashboard/index.html new file mode 100644 index 0000000..54d8223 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-dashboard/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly Event Dashboard - MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/event-dashboard/styles.css b/servers/calendly/src/ui/react-app/event-dashboard/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-dashboard/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/event-dashboard/vite.config.ts b/servers/calendly/src/ui/react-app/event-dashboard/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-dashboard/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/event-detail/App.tsx b/servers/calendly/src/ui/react-app/event-detail/App.tsx new file mode 100644 index 0000000..6be0c52 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-detail/App.tsx @@ -0,0 +1,83 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +export default function EventDetail() { + const [event, setEvent] = useState(null); + const [invitees, setInvitees] = useState([]); + 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 ( +
+
+

📋 Event Details

+
+ +
+ setEventUuid(e.target.value)} + /> + +
+ + {event && ( +
+

{event.name}

+
+
Status: {event.status}
+
Start: {new Date(event.start_time).toLocaleString()}
+
End: {new Date(event.end_time).toLocaleString()}
+
Location: {event.location.type}
+
+ {event.meeting_notes_plain && ( +
+

Meeting Notes

+

{event.meeting_notes_plain}

+
+ )} +
+

Invitees

+ {invitees.map((inv, i) => ( +
+ {inv.name} + {inv.email} + {inv.status} +
+ ))} +
+ {event.status === 'active' && ( + + )} +
+ )} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/event-detail/index.html b/servers/calendly/src/ui/react-app/event-detail/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-detail/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/event-detail/styles.css b/servers/calendly/src/ui/react-app/event-detail/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-detail/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/event-detail/vite.config.ts b/servers/calendly/src/ui/react-app/event-detail/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-detail/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/event-grid/App.tsx b/servers/calendly/src/ui/react-app/event-grid/App.tsx new file mode 100644 index 0000000..b5c6022 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-grid/App.tsx @@ -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 ( +
+
+

📊 Event Grid

+

Calendar grid view of all events

+
+
+ {events.map(event => ( +
+

{event.name}

+

{event.date}

+ {event.status} +
+ ))} +
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/event-grid/index.html b/servers/calendly/src/ui/react-app/event-grid/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-grid/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/event-grid/styles.css b/servers/calendly/src/ui/react-app/event-grid/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-grid/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/event-grid/vite.config.ts b/servers/calendly/src/ui/react-app/event-grid/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-grid/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/event-type-manager/App.tsx b/servers/calendly/src/ui/react-app/event-type-manager/App.tsx new file mode 100644 index 0000000..f8bebe7 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-type-manager/App.tsx @@ -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 ( +
+
+

⚙️ Event Type Manager

+

Manage your Calendly event types

+
+ +
+ {eventTypes.map(type => ( +
+

{type.name}

+

Duration: {type.duration} minutes

+ + {type.active ? 'Active' : 'Inactive'} + +
+ + +
+
+ ))} +
+
+ ); +} diff --git a/servers/calendly/src/ui/react-app/event-type-manager/index.html b/servers/calendly/src/ui/react-app/event-type-manager/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-type-manager/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/event-type-manager/styles.css b/servers/calendly/src/ui/react-app/event-type-manager/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-type-manager/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/event-type-manager/vite.config.ts b/servers/calendly/src/ui/react-app/event-type-manager/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/event-type-manager/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/invitee-list/App.tsx b/servers/calendly/src/ui/react-app/invitee-list/App.tsx new file mode 100644 index 0000000..3ef6015 --- /dev/null +++ b/servers/calendly/src/ui/react-app/invitee-list/App.tsx @@ -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 ( +
+
+

👥 Invitee List

+

Manage event invitees

+
+ {invitees.map(invitee => ( +
+

{invitee.name}

+

Email: {invitee.email}

+

Event: {invitee.event}

+ {invitee.status} +
+ +
+
+ ))} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/invitee-list/index.html b/servers/calendly/src/ui/react-app/invitee-list/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/invitee-list/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/invitee-list/styles.css b/servers/calendly/src/ui/react-app/invitee-list/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/invitee-list/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/invitee-list/vite.config.ts b/servers/calendly/src/ui/react-app/invitee-list/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/invitee-list/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/no-show-tracker/App.tsx b/servers/calendly/src/ui/react-app/no-show-tracker/App.tsx new file mode 100644 index 0000000..1b03794 --- /dev/null +++ b/servers/calendly/src/ui/react-app/no-show-tracker/App.tsx @@ -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 ( +
+
+

🚫 No-Show Tracker

+

Track and manage no-shows

+
+ {noShows.map(noShow => ( +
+

{noShow.invitee}

+

Event: {noShow.event}

+

Date: {noShow.date}

+

Email: {noShow.email}

+ +
+ ))} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/no-show-tracker/index.html b/servers/calendly/src/ui/react-app/no-show-tracker/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/no-show-tracker/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/no-show-tracker/styles.css b/servers/calendly/src/ui/react-app/no-show-tracker/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/no-show-tracker/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/no-show-tracker/vite.config.ts b/servers/calendly/src/ui/react-app/no-show-tracker/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/no-show-tracker/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/org-members/App.tsx b/servers/calendly/src/ui/react-app/org-members/App.tsx new file mode 100644 index 0000000..dea5e34 --- /dev/null +++ b/servers/calendly/src/ui/react-app/org-members/App.tsx @@ -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 ( +
+
+

👨‍💼 Organization Members

+

Manage team members

+
+
+ setNewEmail(e.target.value)} + /> + +
+ {members.map(member => ( +
+

{member.name}

+

Email: {member.email}

+

Role: {member.role}

+ +
+ ))} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/org-members/index.html b/servers/calendly/src/ui/react-app/org-members/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/org-members/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/org-members/styles.css b/servers/calendly/src/ui/react-app/org-members/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/org-members/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/org-members/vite.config.ts b/servers/calendly/src/ui/react-app/org-members/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/org-members/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/scheduling-links/App.tsx b/servers/calendly/src/ui/react-app/scheduling-links/App.tsx new file mode 100644 index 0000000..f401a09 --- /dev/null +++ b/servers/calendly/src/ui/react-app/scheduling-links/App.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import './styles.css'; + +export default function SchedulingLinks() { + const [links, setLinks] = useState([]); + 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 ( +
+
+

🔗 Scheduling Links

+

Create single-use scheduling links

+
+
+ setEventTypeUri(e.target.value)} + /> + +
+ {links.map((link, i) => ( +
+

URL: {link.url}

+

Created: {new Date(link.created).toLocaleString()}

+
+ ))} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/scheduling-links/index.html b/servers/calendly/src/ui/react-app/scheduling-links/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/scheduling-links/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/scheduling-links/styles.css b/servers/calendly/src/ui/react-app/scheduling-links/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/scheduling-links/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/scheduling-links/vite.config.ts b/servers/calendly/src/ui/react-app/scheduling-links/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/scheduling-links/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/src/ui/react-app/shared-styles.css b/servers/calendly/src/ui/react-app/shared-styles.css new file mode 100644 index 0000000..bbd05d3 --- /dev/null +++ b/servers/calendly/src/ui/react-app/shared-styles.css @@ -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; +} diff --git a/servers/calendly/src/ui/react-app/webhook-manager/App.tsx b/servers/calendly/src/ui/react-app/webhook-manager/App.tsx new file mode 100644 index 0000000..a2a9c1f --- /dev/null +++ b/servers/calendly/src/ui/react-app/webhook-manager/App.tsx @@ -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 ( +
+
+

🪝 Webhook Manager

+

Manage webhook subscriptions

+
+
+ setNewUrl(e.target.value)} + /> + +
+ {webhooks.map(webhook => ( +
+

URL: {webhook.url}

+

Events: {webhook.events.join(', ')}

+ {webhook.status} + +
+ ))} +
+ ); +} diff --git a/servers/calendly/src/ui/react-app/webhook-manager/index.html b/servers/calendly/src/ui/react-app/webhook-manager/index.html new file mode 100644 index 0000000..77563fd --- /dev/null +++ b/servers/calendly/src/ui/react-app/webhook-manager/index.html @@ -0,0 +1,19 @@ + + + + + + Calendly MCP App + + +
+ + + diff --git a/servers/calendly/src/ui/react-app/webhook-manager/styles.css b/servers/calendly/src/ui/react-app/webhook-manager/styles.css new file mode 120000 index 0000000..4d1668f --- /dev/null +++ b/servers/calendly/src/ui/react-app/webhook-manager/styles.css @@ -0,0 +1 @@ +../shared-styles.css \ No newline at end of file diff --git a/servers/calendly/src/ui/react-app/webhook-manager/vite.config.ts b/servers/calendly/src/ui/react-app/webhook-manager/vite.config.ts new file mode 100644 index 0000000..ef12ad5 --- /dev/null +++ b/servers/calendly/src/ui/react-app/webhook-manager/vite.config.ts @@ -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, + }, +}); diff --git a/servers/calendly/tsconfig.json b/servers/calendly/tsconfig.json index de6431e..bf41b74 100644 --- a/servers/calendly/tsconfig.json +++ b/servers/calendly/tsconfig.json @@ -1,15 +1,21 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "types": ["node"] }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/ui"] } diff --git a/servers/clickup/README.md b/servers/clickup/README.md new file mode 100644 index 0000000..0ddd36f --- /dev/null +++ b/servers/clickup/README.md @@ -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 diff --git a/servers/clickup/package.json b/servers/clickup/package.json index e3f2809..2a25970 100644 --- a/servers/clickup/package.json +++ b/servers/clickup/package.json @@ -1,20 +1,38 @@ { - "name": "mcp-server-clickup", + "name": "@mcpengine/clickup", "version": "1.0.0", - "type": "module", + "description": "ClickUp MCP Server - Complete task management, collaboration, and productivity platform integration", "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "type": "module", + "bin": { + "clickup-mcp": "./dist/index.js" }, + "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": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.6", + "axios": "^1.7.2", + "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.0.0", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/clickup/src/clients/clickup.ts b/servers/clickup/src/clients/clickup.ts new file mode 100644 index 0000000..7ef0906 --- /dev/null +++ b/servers/clickup/src/clients/clickup.ts @@ -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 { + 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 { + 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( + fn: () => Promise, + retries = MAX_RETRIES + ): Promise { + 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(endpoint: string, params?: Record): Promise { + await this.rateLimit(); + return this.retryRequest(async () => { + const response = await this.client.get(endpoint, { params }); + return response.data; + }); + } + + async post(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise { + await this.rateLimit(); + return this.retryRequest(async () => { + const response = await this.client.post(endpoint, data, config); + return response.data; + }); + } + + async put(endpoint: string, data?: any): Promise { + await this.rateLimit(); + return this.retryRequest(async () => { + const response = await this.client.put(endpoint, data); + return response.data; + }); + } + + async delete(endpoint: string): Promise { + await this.rateLimit(); + return this.retryRequest(async () => { + const response = await this.client.delete(endpoint); + return response.data; + }); + } + + // Pagination helper + async *paginate( + endpoint: string, + params: Record = {}, + dataKey: string + ): AsyncGenerator { + 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 }); + } +} diff --git a/servers/clickup/src/index.ts b/servers/clickup/src/index.ts index 687a635..1e21ddc 100644 --- a/servers/clickup/src/index.ts +++ b/servers/clickup/src/index.ts @@ -1,504 +1,164 @@ #!/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 { CallToolRequestSchema, ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; + Tool, +} from '@modelcontextprotocol/sdk/types.js'; -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "clickup"; -const MCP_VERSION = "1.0.0"; -const API_BASE_URL = "https://api.clickup.com/api/v2"; +import { ClickUpClient } from './clients/clickup.js'; +import type { ClickUpConfig } from './types.js'; -// ============================================ -// API CLIENT -// ============================================ -class ClickUpClient { - private apiKey: string; - private baseUrl: string; +// Import tool creators +import { createTasksTools } from './tools/tasks-tools.js'; +import { createSpacesTools } from './tools/spaces-tools.js'; +import { createFoldersTools } from './tools/folders-tools.js'; +import { createListsTools } from './tools/lists-tools.js'; +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) { - this.apiKey = apiKey; - this.baseUrl = API_BASE_URL; - } +class ClickUpServer { + private server: Server; + private client: ClickUpClient; + private tools: Map = new Map(); - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": this.apiKey, - "Content-Type": "application/json", - ...options.headers, + constructor() { + this.server = new Server( + { + name: 'clickup', + version: '1.0.0', }, - }); - - 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", - description: "List all spaces in a ClickUp workspace/team", - 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", - description: "List all lists in a folder or space (folderless lists)", - 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"] + { + capabilities: { + tools: {}, }, - 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"); } + ); + + // Initialize ClickUp client + 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); } - 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; + } + + 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() + ), + }, + })); + + return { tools }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = this.tools.get(request.params.name); + + if (!tool) { + throw new Error(`Unknown tool: ${request.params.name}`); } - 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}`); + + try { + // Validate input + const args = tool.inputSchema.parse(request.params.arguments); + + // Execute tool + const result = await tool.handler(args); + + return result; + } catch (error) { + if (error instanceof Error) { + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; + } + throw error; + } + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('ClickUp MCP server running on stdio'); } } -// ============================================ -// 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 () => ({ - 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); +const server = new ClickUpServer(); +server.run().catch(console.error); diff --git a/servers/clickup/src/tools/checklists-tools.ts b/servers/clickup/src/tools/checklists-tools.ts new file mode 100644 index 0000000..85b8902 --- /dev/null +++ b/servers/clickup/src/tools/checklists-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/comments-tools.ts b/servers/clickup/src/tools/comments-tools.ts new file mode 100644 index 0000000..418ecb2 --- /dev/null +++ b/servers/clickup/src/tools/comments-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/custom-fields-tools.ts b/servers/clickup/src/tools/custom-fields-tools.ts new file mode 100644 index 0000000..ed958e2 --- /dev/null +++ b/servers/clickup/src/tools/custom-fields-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/docs-tools.ts b/servers/clickup/src/tools/docs-tools.ts new file mode 100644 index 0000000..6f0c1bb --- /dev/null +++ b/servers/clickup/src/tools/docs-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/folders-tools.ts b/servers/clickup/src/tools/folders-tools.ts new file mode 100644 index 0000000..0090247 --- /dev/null +++ b/servers/clickup/src/tools/folders-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/goals-tools.ts b/servers/clickup/src/tools/goals-tools.ts new file mode 100644 index 0000000..040162a --- /dev/null +++ b/servers/clickup/src/tools/goals-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/guests-tools.ts b/servers/clickup/src/tools/guests-tools.ts new file mode 100644 index 0000000..a9e545e --- /dev/null +++ b/servers/clickup/src/tools/guests-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/lists-tools.ts b/servers/clickup/src/tools/lists-tools.ts new file mode 100644 index 0000000..91fc957 --- /dev/null +++ b/servers/clickup/src/tools/lists-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/spaces-tools.ts b/servers/clickup/src/tools/spaces-tools.ts new file mode 100644 index 0000000..298ccfa --- /dev/null +++ b/servers/clickup/src/tools/spaces-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/tags-tools.ts b/servers/clickup/src/tools/tags-tools.ts new file mode 100644 index 0000000..34f924c --- /dev/null +++ b/servers/clickup/src/tools/tags-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/tasks-tools.ts b/servers/clickup/src/tools/tasks-tools.ts new file mode 100644 index 0000000..a5b87d3 --- /dev/null +++ b/servers/clickup/src/tools/tasks-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/teams-tools.ts b/servers/clickup/src/tools/teams-tools.ts new file mode 100644 index 0000000..1dc0577 --- /dev/null +++ b/servers/clickup/src/tools/teams-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/templates-tools.ts b/servers/clickup/src/tools/templates-tools.ts new file mode 100644 index 0000000..71163cf --- /dev/null +++ b/servers/clickup/src/tools/templates-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/time-tracking-tools.ts b/servers/clickup/src/tools/time-tracking-tools.ts new file mode 100644 index 0000000..8d39075 --- /dev/null +++ b/servers/clickup/src/tools/time-tracking-tools.ts @@ -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) }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/views-tools.ts b/servers/clickup/src/tools/views-tools.ts new file mode 100644 index 0000000..8b47bff --- /dev/null +++ b/servers/clickup/src/tools/views-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/tools/webhooks-tools.ts b/servers/clickup/src/tools/webhooks-tools.ts new file mode 100644 index 0000000..655832b --- /dev/null +++ b/servers/clickup/src/tools/webhooks-tools.ts @@ -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' }] }; + } + }, + ]; +} diff --git a/servers/clickup/src/types.ts b/servers/clickup/src/types.ts new file mode 100644 index 0000000..10c2f72 --- /dev/null +++ b/servers/clickup/src/types.ts @@ -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 { + 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; +} + +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; +} diff --git a/servers/clickup/tsconfig.json b/servers/clickup/tsconfig.json index de6431e..38a0f2f 100644 --- a/servers/clickup/tsconfig.json +++ b/servers/clickup/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react-jsx" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]