@taazkareem/clickup-mcp-server 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +111 -69
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +439 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +49 -20
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +102 -18
  21. package/build/tools/task/handlers.js +216 -53
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +161 -32
  24. package/build/tools/task/single-operations.js +82 -17
  25. package/build/tools/task/utilities.js +47 -75
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
@@ -0,0 +1,439 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Core Module
6
+ *
7
+ * Handles core operations related to tasks in ClickUp, including:
8
+ * - Base service initialization
9
+ * - Core utility methods
10
+ * - Basic CRUD operations
11
+ */
12
+ import { BaseClickUpService, ErrorCode, ClickUpServiceError } from '../base.js';
13
+ import { ListService } from '../list.js';
14
+ /**
15
+ * Core TaskService class providing basic task operations
16
+ */
17
+ export class TaskServiceCore extends BaseClickUpService {
18
+ constructor(apiKey, teamId, baseUrl, workspaceService) {
19
+ super(apiKey, teamId, baseUrl);
20
+ this.workspaceService = null;
21
+ // Cache for validated tasks and lists
22
+ this.validationCache = {
23
+ tasks: new Map(),
24
+ lists: new Map()
25
+ };
26
+ // Cache TTL in milliseconds (5 minutes)
27
+ this.CACHE_TTL = 5 * 60 * 1000;
28
+ if (workspaceService) {
29
+ this.workspaceService = workspaceService;
30
+ this.logOperation('constructor', { usingSharedWorkspaceService: true });
31
+ }
32
+ // Initialize list service for list lookups
33
+ this.listService = new ListService(apiKey, teamId, baseUrl, this.workspaceService);
34
+ this.logOperation('constructor', { initialized: true });
35
+ }
36
+ /**
37
+ * Helper method to handle errors consistently
38
+ * @param error The error that occurred
39
+ * @param message Optional custom error message
40
+ * @returns A ClickUpServiceError
41
+ */
42
+ handleError(error, message) {
43
+ if (error instanceof ClickUpServiceError) {
44
+ return error;
45
+ }
46
+ return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
47
+ }
48
+ /**
49
+ * Build URL parameters from task filters
50
+ * @param filters Task filters to convert to URL parameters
51
+ * @returns URLSearchParams object
52
+ */
53
+ buildTaskFilterParams(filters) {
54
+ const params = new URLSearchParams();
55
+ // Add all filters to the query parameters
56
+ if (filters.include_closed)
57
+ params.append('include_closed', String(filters.include_closed));
58
+ if (filters.subtasks)
59
+ params.append('subtasks', String(filters.subtasks));
60
+ if (filters.page)
61
+ params.append('page', String(filters.page));
62
+ if (filters.order_by)
63
+ params.append('order_by', filters.order_by);
64
+ if (filters.reverse)
65
+ params.append('reverse', String(filters.reverse));
66
+ // Array parameters
67
+ if (filters.statuses && filters.statuses.length > 0) {
68
+ filters.statuses.forEach(status => params.append('statuses[]', status));
69
+ }
70
+ if (filters.assignees && filters.assignees.length > 0) {
71
+ filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
72
+ }
73
+ // Team tasks endpoint specific parameters
74
+ if (filters.tags && filters.tags.length > 0) {
75
+ filters.tags.forEach(tag => params.append('tags[]', tag));
76
+ }
77
+ if (filters.list_ids && filters.list_ids.length > 0) {
78
+ filters.list_ids.forEach(id => params.append('list_ids[]', id));
79
+ }
80
+ if (filters.folder_ids && filters.folder_ids.length > 0) {
81
+ filters.folder_ids.forEach(id => params.append('folder_ids[]', id));
82
+ }
83
+ if (filters.space_ids && filters.space_ids.length > 0) {
84
+ filters.space_ids.forEach(id => params.append('space_ids[]', id));
85
+ }
86
+ if (filters.archived !== undefined)
87
+ params.append('archived', String(filters.archived));
88
+ if (filters.include_closed_lists !== undefined)
89
+ params.append('include_closed_lists', String(filters.include_closed_lists));
90
+ if (filters.include_archived_lists !== undefined)
91
+ params.append('include_archived_lists', String(filters.include_archived_lists));
92
+ if (filters.include_compact_time_entries !== undefined)
93
+ params.append('include_compact_time_entries', String(filters.include_compact_time_entries));
94
+ // Date filters
95
+ if (filters.due_date_gt)
96
+ params.append('due_date_gt', String(filters.due_date_gt));
97
+ if (filters.due_date_lt)
98
+ params.append('due_date_lt', String(filters.due_date_lt));
99
+ if (filters.date_created_gt)
100
+ params.append('date_created_gt', String(filters.date_created_gt));
101
+ if (filters.date_created_lt)
102
+ params.append('date_created_lt', String(filters.date_created_lt));
103
+ if (filters.date_updated_gt)
104
+ params.append('date_updated_gt', String(filters.date_updated_gt));
105
+ if (filters.date_updated_lt)
106
+ params.append('date_updated_lt', String(filters.date_updated_lt));
107
+ // Handle custom fields if present
108
+ if (filters.custom_fields) {
109
+ Object.entries(filters.custom_fields).forEach(([key, value]) => {
110
+ params.append(`custom_fields[${key}]`, String(value));
111
+ });
112
+ }
113
+ return params;
114
+ }
115
+ /**
116
+ * Extract priority value from a task
117
+ * @param task The task to extract priority from
118
+ * @returns TaskPriority or null
119
+ */
120
+ extractPriorityValue(task) {
121
+ if (!task.priority || !task.priority.id) {
122
+ return null;
123
+ }
124
+ const priorityValue = parseInt(task.priority.id);
125
+ // Ensure it's in the valid range 1-4
126
+ if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
127
+ return null;
128
+ }
129
+ return priorityValue;
130
+ }
131
+ /**
132
+ * Extract task data for creation/duplication
133
+ * @param task The source task
134
+ * @param nameOverride Optional override for the task name
135
+ * @returns CreateTaskData object
136
+ */
137
+ extractTaskData(task, nameOverride) {
138
+ return {
139
+ name: nameOverride || task.name,
140
+ description: task.description || '',
141
+ status: task.status?.status,
142
+ priority: this.extractPriorityValue(task),
143
+ due_date: task.due_date ? Number(task.due_date) : undefined,
144
+ assignees: task.assignees?.map(a => a.id) || []
145
+ };
146
+ }
147
+ /**
148
+ * Create a new task in the specified list
149
+ * @param listId The ID of the list to create the task in
150
+ * @param taskData The data for the new task
151
+ * @returns The created task
152
+ */
153
+ async createTask(listId, taskData) {
154
+ this.logOperation('createTask', { listId, ...taskData });
155
+ try {
156
+ return await this.makeRequest(async () => {
157
+ const response = await this.client.post(`/list/${listId}/task`, taskData);
158
+ return response.data;
159
+ });
160
+ }
161
+ catch (error) {
162
+ throw this.handleError(error, `Failed to create task in list ${listId}`);
163
+ }
164
+ }
165
+ /**
166
+ * Get a specific task by ID
167
+ * @param taskId The ID of the task to retrieve
168
+ * @returns The task details
169
+ */
170
+ async getTask(taskId) {
171
+ this.logOperation('getTask', { taskId });
172
+ try {
173
+ return await this.makeRequest(async () => {
174
+ const response = await this.client.get(`/task/${taskId}`);
175
+ return response.data;
176
+ });
177
+ }
178
+ catch (error) {
179
+ throw this.handleError(error, `Failed to get task ${taskId}`);
180
+ }
181
+ }
182
+ /**
183
+ * Get all tasks in a list with optional filtering
184
+ * @param listId The ID of the list to get tasks from
185
+ * @param filters Optional filters to apply
186
+ * @returns List of tasks matching the filters
187
+ */
188
+ async getTasks(listId, filters = {}) {
189
+ this.logOperation('getTasks', { listId, filters });
190
+ try {
191
+ return await this.makeRequest(async () => {
192
+ const params = this.buildTaskFilterParams(filters);
193
+ const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
194
+ return response.data.tasks;
195
+ });
196
+ }
197
+ catch (error) {
198
+ throw this.handleError(error, `Failed to get tasks from list ${listId}`);
199
+ }
200
+ }
201
+ /**
202
+ * Get subtasks of a specific task
203
+ * @param taskId The ID of the parent task
204
+ * @returns Array of subtask details
205
+ */
206
+ async getSubtasks(taskId) {
207
+ this.logOperation('getSubtasks', { taskId });
208
+ try {
209
+ return await this.makeRequest(async () => {
210
+ const response = await this.client.get(`/task/${taskId}`);
211
+ // Return subtasks if present, otherwise empty array
212
+ return response.data.subtasks || [];
213
+ });
214
+ }
215
+ catch (error) {
216
+ throw this.handleError(error, `Failed to get subtasks for task ${taskId}`);
217
+ }
218
+ }
219
+ /**
220
+ * Get a task by its custom ID
221
+ * @param customTaskId The custom ID of the task (e.g., "ABC-123")
222
+ * @param listId Optional list ID to limit the search
223
+ * @returns The task details
224
+ */
225
+ async getTaskByCustomId(customTaskId, listId) {
226
+ this.logOperation('getTaskByCustomId', { customTaskId, listId });
227
+ try {
228
+ return await this.makeRequest(async () => {
229
+ // Construct the URL with optional list ID
230
+ const url = `/task/custom_task_ids?custom_task_id=${encodeURIComponent(customTaskId)}${listId ? `&list_id=${listId}` : ''}`;
231
+ const response = await this.client.get(url);
232
+ return response.data;
233
+ });
234
+ }
235
+ catch (error) {
236
+ throw this.handleError(error, `Failed to get task with custom ID ${customTaskId}`);
237
+ }
238
+ }
239
+ /**
240
+ * Update an existing task
241
+ * @param taskId The ID of the task to update
242
+ * @param updateData The data to update
243
+ * @returns The updated task
244
+ */
245
+ async updateTask(taskId, updateData) {
246
+ this.logOperation('updateTask', { taskId, ...updateData });
247
+ try {
248
+ return await this.makeRequest(async () => {
249
+ const response = await this.client.put(`/task/${taskId}`, updateData);
250
+ return response.data;
251
+ });
252
+ }
253
+ catch (error) {
254
+ throw this.handleError(error, `Failed to update task ${taskId}`);
255
+ }
256
+ }
257
+ /**
258
+ * Delete a task
259
+ * @param taskId The ID of the task to delete
260
+ * @returns A ServiceResponse indicating success
261
+ */
262
+ async deleteTask(taskId) {
263
+ this.logOperation('deleteTask', { taskId });
264
+ try {
265
+ await this.makeRequest(async () => {
266
+ await this.client.delete(`/task/${taskId}`);
267
+ });
268
+ return {
269
+ success: true,
270
+ data: undefined,
271
+ error: undefined
272
+ };
273
+ }
274
+ catch (error) {
275
+ throw this.handleError(error, `Failed to delete task ${taskId}`);
276
+ }
277
+ }
278
+ /**
279
+ * Move a task to another list
280
+ * @param taskId The ID of the task to move
281
+ * @param destinationListId The ID of the list to move the task to
282
+ * @returns The updated task
283
+ */
284
+ async moveTask(taskId, destinationListId) {
285
+ const startTime = Date.now();
286
+ this.logOperation('moveTask', { taskId, destinationListId, operation: 'start' });
287
+ try {
288
+ // First, get task and validate destination list
289
+ const [sourceTask, _] = await Promise.all([
290
+ this.validateTaskExists(taskId),
291
+ this.validateListExists(destinationListId)
292
+ ]);
293
+ // Extract task data for creating the new task
294
+ const taskData = this.extractTaskData(sourceTask);
295
+ // Create the task in the new list
296
+ const newTask = await this.createTask(destinationListId, taskData);
297
+ // Delete the original task
298
+ await this.deleteTask(taskId);
299
+ // Update the cache
300
+ this.validationCache.tasks.delete(taskId);
301
+ this.validationCache.tasks.set(newTask.id, {
302
+ validatedAt: Date.now(),
303
+ task: newTask
304
+ });
305
+ const totalTime = Date.now() - startTime;
306
+ this.logOperation('moveTask', {
307
+ taskId,
308
+ destinationListId,
309
+ operation: 'complete',
310
+ timing: { totalTime },
311
+ newTaskId: newTask.id
312
+ });
313
+ return newTask;
314
+ }
315
+ catch (error) {
316
+ // Log failure
317
+ this.logOperation('moveTask', {
318
+ taskId,
319
+ destinationListId,
320
+ operation: 'failed',
321
+ error: error instanceof Error ? error.message : String(error),
322
+ timing: { totalTime: Date.now() - startTime }
323
+ });
324
+ throw this.handleError(error, 'Failed to move task');
325
+ }
326
+ }
327
+ /**
328
+ * Duplicate a task, optionally to a different list
329
+ * @param taskId The ID of the task to duplicate
330
+ * @param listId Optional ID of list to create duplicate in (defaults to same list)
331
+ * @returns The duplicated task
332
+ */
333
+ async duplicateTask(taskId, listId) {
334
+ this.logOperation('duplicateTask', { taskId, listId });
335
+ try {
336
+ // Get source task and validate destination list if provided
337
+ const [sourceTask, _] = await Promise.all([
338
+ this.validateTaskExists(taskId),
339
+ listId ? this.validateListExists(listId) : Promise.resolve()
340
+ ]);
341
+ // Create duplicate in specified list or original list
342
+ const targetListId = listId || sourceTask.list.id;
343
+ const taskData = this.extractTaskData(sourceTask);
344
+ return await this.createTask(targetListId, taskData);
345
+ }
346
+ catch (error) {
347
+ throw this.handleError(error, `Failed to duplicate task ${taskId}`);
348
+ }
349
+ }
350
+ /**
351
+ * Validate a task exists and cache the result
352
+ * @param taskId The ID of the task to validate
353
+ * @returns The validated task
354
+ */
355
+ async validateTaskExists(taskId) {
356
+ // Check cache first
357
+ const cached = this.validationCache.tasks.get(taskId);
358
+ if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
359
+ this.logger.debug('Using cached task validation', { taskId });
360
+ return cached.task;
361
+ }
362
+ // Not in cache or expired, fetch task
363
+ const task = await this.getTask(taskId);
364
+ // Cache the validation result
365
+ this.validationCache.tasks.set(taskId, {
366
+ validatedAt: Date.now(),
367
+ task
368
+ });
369
+ return task;
370
+ }
371
+ /**
372
+ * Validate that multiple tasks exist
373
+ * @param taskIds Array of task IDs to validate
374
+ * @returns Map of task IDs to task objects
375
+ */
376
+ async validateTasksExist(taskIds) {
377
+ const results = new Map();
378
+ const toFetch = [];
379
+ // Check cache first
380
+ for (const taskId of taskIds) {
381
+ const cached = this.validationCache.tasks.get(taskId);
382
+ if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
383
+ results.set(taskId, cached.task);
384
+ }
385
+ else {
386
+ toFetch.push(taskId);
387
+ }
388
+ }
389
+ if (toFetch.length > 0) {
390
+ // Fetch uncached tasks in parallel batches
391
+ const batchSize = 5;
392
+ for (let i = 0; i < toFetch.length; i += batchSize) {
393
+ const batch = toFetch.slice(i, i + batchSize);
394
+ const tasks = await Promise.all(batch.map(id => this.getTask(id)));
395
+ // Cache and store results
396
+ tasks.forEach((task, index) => {
397
+ const taskId = batch[index];
398
+ this.validationCache.tasks.set(taskId, {
399
+ validatedAt: Date.now(),
400
+ task
401
+ });
402
+ results.set(taskId, task);
403
+ });
404
+ }
405
+ }
406
+ return results;
407
+ }
408
+ /**
409
+ * Validate a list exists and cache the result
410
+ * @param listId The ID of the list to validate
411
+ */
412
+ async validateListExists(listId) {
413
+ // Check cache first
414
+ const cached = this.validationCache.lists.get(listId);
415
+ if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
416
+ this.logger.debug('Using cached list validation', { listId });
417
+ if (!cached.valid) {
418
+ throw new ClickUpServiceError(`List ${listId} does not exist`, ErrorCode.NOT_FOUND);
419
+ }
420
+ return;
421
+ }
422
+ try {
423
+ await this.listService.getList(listId);
424
+ // Cache the successful validation
425
+ this.validationCache.lists.set(listId, {
426
+ validatedAt: Date.now(),
427
+ valid: true
428
+ });
429
+ }
430
+ catch (error) {
431
+ // Cache the failed validation
432
+ this.validationCache.lists.set(listId, {
433
+ validatedAt: Date.now(),
434
+ valid: false
435
+ });
436
+ throw error;
437
+ }
438
+ }
439
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Custom Fields Module
6
+ *
7
+ * Handles custom fields operations for ClickUp tasks, including:
8
+ * - Setting custom field values
9
+ * - Retrieving custom field values
10
+ */
11
+ import { TaskServiceTags } from './task-tags.js';
12
+ /**
13
+ * Custom fields functionality for the TaskService
14
+ */
15
+ export class TaskServiceCustomFields extends TaskServiceTags {
16
+ /**
17
+ * Set a single custom field value on a task
18
+ *
19
+ * @param taskId ID of the task
20
+ * @param fieldId ID of the custom field
21
+ * @param value Value to set for the custom field
22
+ * @returns Success response
23
+ */
24
+ async setCustomFieldValue(taskId, fieldId, value) {
25
+ this.logOperation('setCustomFieldValue', { taskId, fieldId, value });
26
+ try {
27
+ const payload = {
28
+ value
29
+ };
30
+ await this.client.post(`/task/${taskId}/field/${fieldId}`, payload);
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ throw this.handleError(error, `Failed to set custom field "${fieldId}" value`);
35
+ }
36
+ }
37
+ /**
38
+ * Set multiple custom field values on a task
39
+ *
40
+ * @param taskId ID of the task
41
+ * @param customFields Array of custom field ID and value pairs
42
+ * @returns Success response
43
+ */
44
+ async setCustomFieldValues(taskId, customFields) {
45
+ this.logOperation('setCustomFieldValues', { taskId, customFields });
46
+ try {
47
+ // Execute each update sequentially
48
+ for (const field of customFields) {
49
+ await this.setCustomFieldValue(taskId, field.id, field.value);
50
+ }
51
+ return true;
52
+ }
53
+ catch (error) {
54
+ throw this.handleError(error, 'Failed to set custom field values');
55
+ }
56
+ }
57
+ /**
58
+ * Get all custom field values for a task
59
+ *
60
+ * @param taskId ID of the task
61
+ * @returns Record mapping field IDs to their values
62
+ */
63
+ async getCustomFieldValues(taskId) {
64
+ this.logOperation('getCustomFieldValues', { taskId });
65
+ try {
66
+ // We need to fetch the full task to get its custom fields
67
+ const task = await this.getTask(taskId);
68
+ return task.custom_fields || {};
69
+ }
70
+ catch (error) {
71
+ throw this.handleError(error, 'Failed to get custom field values');
72
+ }
73
+ }
74
+ /**
75
+ * Get a specific custom field value for a task
76
+ *
77
+ * @param taskId ID of the task
78
+ * @param fieldId ID of the custom field
79
+ * @returns The value of the custom field
80
+ * @throws ClickUpServiceError if the field doesn't exist
81
+ */
82
+ async getCustomFieldValue(taskId, fieldId) {
83
+ this.logOperation('getCustomFieldValue', { taskId, fieldId });
84
+ try {
85
+ const customFields = await this.getCustomFieldValues(taskId);
86
+ if (fieldId in customFields) {
87
+ return customFields[fieldId];
88
+ }
89
+ else {
90
+ throw this.handleError(new Error(`Custom field "${fieldId}" not found on task`), `Custom field "${fieldId}" not found on task`);
91
+ }
92
+ }
93
+ catch (error) {
94
+ throw this.handleError(error, `Failed to get custom field "${fieldId}" value`);
95
+ }
96
+ }
97
+ }