@taazkareem/clickup-mcp-server 0.8.4 → 0.9.0

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 (36) hide show
  1. package/LICENSE +9 -17
  2. package/README.md +33 -38
  3. package/build/enhanced_server.js +262 -0
  4. package/build/index.js +9 -3
  5. package/build/license.js +172 -0
  6. package/build/middleware/auth.js +211 -0
  7. package/build/routes/auth.js +306 -0
  8. package/build/schemas/member.js +13 -0
  9. package/build/server.js +15 -1
  10. package/build/server.log +15 -0
  11. package/build/services/auth/oauth2.js +236 -0
  12. package/build/services/auth/session.js +337 -0
  13. package/build/services/clickup/adapter.js +281 -0
  14. package/build/services/clickup/factory.js +339 -0
  15. package/build/services/clickup/task/task-attachments.js +20 -12
  16. package/build/services/clickup/task/task-comments.js +19 -9
  17. package/build/services/clickup/task/task-core.js +68 -4
  18. package/build/services/clickup/task/task-custom-fields.js +23 -13
  19. package/build/services/clickup/task/task-search.js +79 -71
  20. package/build/services/clickup/task/task-service.js +88 -9
  21. package/build/services/clickup/task/task-tags.js +25 -13
  22. package/build/sse_server.js +4 -4
  23. package/build/tools/documents.js +11 -4
  24. package/build/tools/health.js +23 -0
  25. package/build/tools/member.js +2 -4
  26. package/build/tools/task/bulk-operations.js +5 -5
  27. package/build/tools/task/handlers.js +62 -12
  28. package/build/tools/task/single-operations.js +9 -9
  29. package/build/tools/task/time-tracking.js +61 -170
  30. package/build/tools/task/utilities.js +56 -22
  31. package/build/utils/date-utils.js +341 -141
  32. package/build/utils/schema-compatibility.js +222 -0
  33. package/build/utils/universal-schema-compatibility.js +171 -0
  34. package/build/virtual-sdk/generator.js +53 -0
  35. package/build/virtual-sdk/registry.js +45 -0
  36. package/package.json +2 -2
@@ -7,12 +7,20 @@
7
7
  * Handles comment operations for ClickUp tasks, including:
8
8
  * - Retrieving comments for a task
9
9
  * - Creating comments on a task
10
+ *
11
+ * REFACTORED: Now uses composition instead of inheritance.
12
+ * Only depends on TaskServiceCore for base functionality.
10
13
  */
11
- import { TaskServiceAttachments } from './task-attachments.js';
12
14
  /**
13
15
  * Comments functionality for the TaskService
16
+ *
17
+ * This service handles all comment-related operations for ClickUp tasks.
18
+ * It uses composition to access core functionality instead of inheritance.
14
19
  */
15
- export class TaskServiceComments extends TaskServiceAttachments {
20
+ export class TaskServiceComments {
21
+ constructor(core) {
22
+ this.core = core;
23
+ }
16
24
  /**
17
25
  * Get all comments for a task
18
26
  *
@@ -22,7 +30,7 @@ export class TaskServiceComments extends TaskServiceAttachments {
22
30
  * @returns Array of task comments
23
31
  */
24
32
  async getTaskComments(taskId, start, startId) {
25
- this.logOperation('getTaskComments', { taskId, start, startId });
33
+ this.core.logOperation('getTaskComments', { taskId, start, startId });
26
34
  try {
27
35
  // Build query parameters for pagination
28
36
  const queryParams = new URLSearchParams();
@@ -33,11 +41,13 @@ export class TaskServiceComments extends TaskServiceAttachments {
33
41
  queryParams.append('start_id', startId);
34
42
  }
35
43
  const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
36
- const response = await this.client.get(`/task/${taskId}/comment${queryString}`);
37
- return response.data.comments || [];
44
+ return await this.core.makeRequest(async () => {
45
+ const response = await this.core.client.get(`/task/${taskId}/comment${queryString}`);
46
+ return response.data.comments || [];
47
+ });
38
48
  }
39
49
  catch (error) {
40
- throw this.handleError(error, 'Failed to get task comments');
50
+ throw this.core.handleError(error, 'Failed to get task comments');
41
51
  }
42
52
  }
43
53
  /**
@@ -50,7 +60,7 @@ export class TaskServiceComments extends TaskServiceAttachments {
50
60
  * @returns The created comment
51
61
  */
52
62
  async createTaskComment(taskId, commentText, notifyAll = false, assignee) {
53
- this.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
63
+ this.core.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
54
64
  try {
55
65
  const payload = {
56
66
  comment_text: commentText,
@@ -60,7 +70,7 @@ export class TaskServiceComments extends TaskServiceAttachments {
60
70
  payload.assignee = assignee;
61
71
  }
62
72
  // Make the request directly without using makeRequest for better error handling
63
- const response = await this.client.post(`/task/${taskId}/comment`, payload);
73
+ const response = await this.core.client.post(`/task/${taskId}/comment`, payload);
64
74
  // Handle different response formats from ClickUp API
65
75
  if (response.data) {
66
76
  if (response.data.comment) {
@@ -98,7 +108,7 @@ export class TaskServiceComments extends TaskServiceAttachments {
98
108
  resolved: false
99
109
  };
100
110
  }
101
- throw this.handleError(error, 'Failed to create task comment');
111
+ throw this.core.handleError(error, 'Failed to create task comment');
102
112
  }
103
113
  }
104
114
  }
@@ -179,11 +179,29 @@ export class TaskServiceCore extends BaseClickUpService {
179
179
  }
180
180
  /**
181
181
  * Get a task by its ID
182
- * @param taskId The ID of the task to retrieve
182
+ * Automatically detects custom task IDs and routes them appropriately
183
+ * @param taskId The ID of the task to retrieve (regular or custom)
183
184
  * @returns The task
184
185
  */
185
186
  async getTask(taskId) {
186
187
  this.logOperation('getTask', { taskId });
188
+ // Import the detection function here to avoid circular dependencies
189
+ const { isCustomTaskId } = await import('../../../tools/task/utilities.js');
190
+ // Test the detection function
191
+ const isCustom = isCustomTaskId(taskId);
192
+ this.logger.debug('Custom task ID detection result', {
193
+ taskId,
194
+ isCustom,
195
+ taskIdLength: taskId.length,
196
+ containsHyphen: taskId.includes('-'),
197
+ containsUnderscore: taskId.includes('_')
198
+ });
199
+ // Automatically detect custom task IDs and route to appropriate method
200
+ if (isCustom) {
201
+ this.logger.debug('Detected custom task ID, routing to getTaskByCustomId', { taskId });
202
+ return this.getTaskByCustomId(taskId);
203
+ }
204
+ this.logger.debug('Detected regular task ID, using standard getTask flow', { taskId });
187
205
  try {
188
206
  return await this.makeRequest(async () => {
189
207
  const response = await this.client.get(`/task/${taskId}`);
@@ -196,6 +214,14 @@ export class TaskServiceCore extends BaseClickUpService {
196
214
  });
197
215
  }
198
216
  catch (error) {
217
+ // If this was detected as a regular task ID but failed, provide helpful error message
218
+ // suggesting it might be a custom ID that wasn't properly detected
219
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.NOT_FOUND) {
220
+ const { isCustomTaskId } = await import('../../../tools/task/utilities.js');
221
+ if (!isCustomTaskId(taskId) && (taskId.includes('-') || taskId.includes('_'))) {
222
+ throw new ClickUpServiceError(`Task ${taskId} not found. If this is a custom task ID, ensure your workspace has custom task IDs enabled and you have access to the task.`, ErrorCode.NOT_FOUND, error.data);
223
+ }
224
+ }
199
225
  throw this.handleError(error, `Failed to get task ${taskId}`);
200
226
  }
201
227
  }
@@ -259,6 +285,14 @@ export class TaskServiceCore extends BaseClickUpService {
259
285
  custom_task_ids: 'true',
260
286
  team_id: this.teamId // team_id is required when custom_task_ids is true
261
287
  });
288
+ // Debug logging for troubleshooting
289
+ this.logger.debug('Making custom task ID API request', {
290
+ customTaskId,
291
+ url,
292
+ teamId: this.teamId,
293
+ params: params.toString(),
294
+ fullUrl: `${url}?${params.toString()}`
295
+ });
262
296
  // Note: The ClickUp API documentation for GET /task/{task_id} doesn't explicitly mention
263
297
  // filtering by list_id when custom_task_ids=true. This parameter might be ignored.
264
298
  if (listId) {
@@ -276,6 +310,13 @@ export class TaskServiceCore extends BaseClickUpService {
276
310
  });
277
311
  }
278
312
  catch (error) {
313
+ // Enhanced error logging for debugging
314
+ this.logger.error('Custom task ID request failed', {
315
+ customTaskId,
316
+ teamId: this.teamId,
317
+ error: error instanceof Error ? error.message : String(error),
318
+ errorDetails: error
319
+ });
279
320
  // Provide more specific error context if possible
280
321
  if (error instanceof ClickUpServiceError && error.code === ErrorCode.NOT_FOUND) {
281
322
  throw new ClickUpServiceError(`Task with custom ID ${customTaskId} not found or not accessible for team ${this.teamId}.`, ErrorCode.NOT_FOUND, error.data);
@@ -292,11 +333,34 @@ export class TaskServiceCore extends BaseClickUpService {
292
333
  async updateTask(taskId, updateData) {
293
334
  this.logOperation('updateTask', { taskId, ...updateData });
294
335
  try {
295
- // Extract custom fields from updateData
296
- const { custom_fields, ...standardFields } = updateData;
336
+ // Extract custom fields and assignees from updateData
337
+ const { custom_fields, assignees, ...standardFields } = updateData;
338
+ // Prepare the fields to send to API
339
+ let fieldsToSend = { ...standardFields };
340
+ // Handle assignees separately if provided
341
+ if (assignees !== undefined) {
342
+ // Get current task to compare assignees
343
+ const currentTask = await this.getTask(taskId);
344
+ const currentAssigneeIds = currentTask.assignees.map(a => a.id);
345
+ let assigneesToProcess;
346
+ if (Array.isArray(assignees)) {
347
+ // If assignees is an array, calculate add/rem based on current vs new
348
+ const newAssigneeIds = assignees;
349
+ assigneesToProcess = {
350
+ add: newAssigneeIds.filter(id => !currentAssigneeIds.includes(id)),
351
+ rem: currentAssigneeIds.filter(id => !newAssigneeIds.includes(id))
352
+ };
353
+ }
354
+ else {
355
+ // If assignees is already in add/rem format, use it directly
356
+ assigneesToProcess = assignees;
357
+ }
358
+ // Add assignees to the fields in the correct format
359
+ fieldsToSend.assignees = assigneesToProcess;
360
+ }
297
361
  // First update the standard fields
298
362
  const updatedTask = await this.makeRequest(async () => {
299
- const response = await this.client.put(`/task/${taskId}`, standardFields);
363
+ const response = await this.client.put(`/task/${taskId}`, fieldsToSend);
300
364
  // Handle both JSON and text responses
301
365
  const data = response.data;
302
366
  if (typeof data === 'string') {
@@ -7,12 +7,20 @@
7
7
  * Handles custom fields operations for ClickUp tasks, including:
8
8
  * - Setting custom field values
9
9
  * - Retrieving custom field values
10
+ *
11
+ * REFACTORED: Now uses composition instead of inheritance.
12
+ * Only depends on TaskServiceCore for getTask() and base functionality.
10
13
  */
11
- import { TaskServiceTags } from './task-tags.js';
12
14
  /**
13
15
  * Custom fields functionality for the TaskService
16
+ *
17
+ * This service handles all custom field operations for ClickUp tasks.
18
+ * It uses composition to access core functionality instead of inheritance.
14
19
  */
15
- export class TaskServiceCustomFields extends TaskServiceTags {
20
+ export class TaskServiceCustomFields {
21
+ constructor(core) {
22
+ this.core = core;
23
+ }
16
24
  /**
17
25
  * Set a single custom field value on a task
18
26
  *
@@ -22,16 +30,18 @@ export class TaskServiceCustomFields extends TaskServiceTags {
22
30
  * @returns Success response
23
31
  */
24
32
  async setCustomFieldValue(taskId, fieldId, value) {
25
- this.logOperation('setCustomFieldValue', { taskId, fieldId, value });
33
+ this.core.logOperation('setCustomFieldValue', { taskId, fieldId, value });
26
34
  try {
27
35
  const payload = {
28
36
  value
29
37
  };
30
- await this.client.post(`/task/${taskId}/field/${fieldId}`, payload);
38
+ await this.core.makeRequest(async () => {
39
+ return await this.core.client.post(`/task/${taskId}/field/${fieldId}`, payload);
40
+ });
31
41
  return true;
32
42
  }
33
43
  catch (error) {
34
- throw this.handleError(error, `Failed to set custom field "${fieldId}" value`);
44
+ throw this.core.handleError(error, `Failed to set custom field "${fieldId}" value`);
35
45
  }
36
46
  }
37
47
  /**
@@ -42,7 +52,7 @@ export class TaskServiceCustomFields extends TaskServiceTags {
42
52
  * @returns Success response
43
53
  */
44
54
  async setCustomFieldValues(taskId, customFields) {
45
- this.logOperation('setCustomFieldValues', { taskId, customFields });
55
+ this.core.logOperation('setCustomFieldValues', { taskId, customFields });
46
56
  try {
47
57
  // Execute each update sequentially
48
58
  for (const field of customFields) {
@@ -51,7 +61,7 @@ export class TaskServiceCustomFields extends TaskServiceTags {
51
61
  return true;
52
62
  }
53
63
  catch (error) {
54
- throw this.handleError(error, 'Failed to set custom field values');
64
+ throw this.core.handleError(error, 'Failed to set custom field values');
55
65
  }
56
66
  }
57
67
  /**
@@ -61,14 +71,14 @@ export class TaskServiceCustomFields extends TaskServiceTags {
61
71
  * @returns Record mapping field IDs to their values
62
72
  */
63
73
  async getCustomFieldValues(taskId) {
64
- this.logOperation('getCustomFieldValues', { taskId });
74
+ this.core.logOperation('getCustomFieldValues', { taskId });
65
75
  try {
66
76
  // We need to fetch the full task to get its custom fields
67
- const task = await this.getTask(taskId);
77
+ const task = await this.core.getTask(taskId);
68
78
  return task.custom_fields || {};
69
79
  }
70
80
  catch (error) {
71
- throw this.handleError(error, 'Failed to get custom field values');
81
+ throw this.core.handleError(error, 'Failed to get custom field values');
72
82
  }
73
83
  }
74
84
  /**
@@ -80,18 +90,18 @@ export class TaskServiceCustomFields extends TaskServiceTags {
80
90
  * @throws ClickUpServiceError if the field doesn't exist
81
91
  */
82
92
  async getCustomFieldValue(taskId, fieldId) {
83
- this.logOperation('getCustomFieldValue', { taskId, fieldId });
93
+ this.core.logOperation('getCustomFieldValue', { taskId, fieldId });
84
94
  try {
85
95
  const customFields = await this.getCustomFieldValues(taskId);
86
96
  if (fieldId in customFields) {
87
97
  return customFields[fieldId];
88
98
  }
89
99
  else {
90
- throw this.handleError(new Error(`Custom field "${fieldId}" not found on task`), `Custom field "${fieldId}" not found on task`);
100
+ throw this.core.handleError(new Error(`Custom field "${fieldId}" not found on task`), `Custom field "${fieldId}" not found on task`);
91
101
  }
92
102
  }
93
103
  catch (error) {
94
- throw this.handleError(error, `Failed to get custom field "${fieldId}" value`);
104
+ throw this.core.handleError(error, `Failed to get custom field "${fieldId}" value`);
95
105
  }
96
106
  }
97
107
  }