@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 tag operations for ClickUp tasks, including:
8
8
  * - Adding tags to a task
9
9
  * - Removing tags from a task
10
+ *
11
+ * REFACTORED: Now uses composition instead of inheritance.
12
+ * Only depends on TaskServiceCore for getTask() and base functionality.
10
13
  */
11
- import { TaskServiceComments } from './task-comments.js';
12
14
  /**
13
15
  * Tags functionality for the TaskService
16
+ *
17
+ * This service handles all tag-related operations for ClickUp tasks.
18
+ * It uses composition to access core functionality instead of inheritance.
14
19
  */
15
- export class TaskServiceTags extends TaskServiceComments {
20
+ export class TaskServiceTags {
21
+ constructor(core) {
22
+ this.core = core;
23
+ }
16
24
  /**
17
25
  * Add a tag to a task
18
26
  *
@@ -21,16 +29,18 @@ export class TaskServiceTags extends TaskServiceComments {
21
29
  * @returns Success response
22
30
  */
23
31
  async addTagToTask(taskId, tagName) {
24
- this.logOperation('addTagToTask', { taskId, tagName });
32
+ this.core.logOperation('addTagToTask', { taskId, tagName });
25
33
  try {
26
34
  const payload = {
27
35
  tag_name: tagName,
28
36
  };
29
- await this.client.post(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, payload);
37
+ await this.core.makeRequest(async () => {
38
+ return await this.core.client.post(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`, payload);
39
+ });
30
40
  return true;
31
41
  }
32
42
  catch (error) {
33
- throw this.handleError(error, `Failed to add tag "${tagName}" to task`);
43
+ throw this.core.handleError(error, `Failed to add tag "${tagName}" to task`);
34
44
  }
35
45
  }
36
46
  /**
@@ -41,13 +51,15 @@ export class TaskServiceTags extends TaskServiceComments {
41
51
  * @returns Success response
42
52
  */
43
53
  async removeTagFromTask(taskId, tagName) {
44
- this.logOperation('removeTagFromTask', { taskId, tagName });
54
+ this.core.logOperation('removeTagFromTask', { taskId, tagName });
45
55
  try {
46
- await this.client.delete(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`);
56
+ await this.core.makeRequest(async () => {
57
+ return await this.core.client.delete(`/task/${taskId}/tag/${encodeURIComponent(tagName)}`);
58
+ });
47
59
  return true;
48
60
  }
49
61
  catch (error) {
50
- throw this.handleError(error, `Failed to remove tag "${tagName}" from task`);
62
+ throw this.core.handleError(error, `Failed to remove tag "${tagName}" from task`);
51
63
  }
52
64
  }
53
65
  /**
@@ -57,14 +69,14 @@ export class TaskServiceTags extends TaskServiceComments {
57
69
  * @returns Array of task tags
58
70
  */
59
71
  async getTaskTags(taskId) {
60
- this.logOperation('getTaskTags', { taskId });
72
+ this.core.logOperation('getTaskTags', { taskId });
61
73
  try {
62
74
  // We need to fetch the full task to get its tags
63
- const task = await this.getTask(taskId);
75
+ const task = await this.core.getTask(taskId);
64
76
  return task.tags || [];
65
77
  }
66
78
  catch (error) {
67
- throw this.handleError(error, 'Failed to get task tags');
79
+ throw this.core.handleError(error, 'Failed to get task tags');
68
80
  }
69
81
  }
70
82
  /**
@@ -75,7 +87,7 @@ export class TaskServiceTags extends TaskServiceComments {
75
87
  * @returns Success response
76
88
  */
77
89
  async updateTaskTags(taskId, tagNames) {
78
- this.logOperation('updateTaskTags', { taskId, tagNames });
90
+ this.core.logOperation('updateTaskTags', { taskId, tagNames });
79
91
  try {
80
92
  // First get existing tags
81
93
  const existingTags = await this.getTaskTags(taskId);
@@ -95,7 +107,7 @@ export class TaskServiceTags extends TaskServiceComments {
95
107
  return true;
96
108
  }
97
109
  catch (error) {
98
- throw this.handleError(error, 'Failed to update task tags');
110
+ throw this.core.handleError(error, 'Failed to update task tags');
99
111
  }
100
112
  }
101
113
  }
@@ -244,11 +244,11 @@ export function startSSEServer() {
244
244
  }
245
245
  catch (error) {
246
246
  logger.error('Failed to start HTTPS server', {
247
- error: error.message,
248
- sslKeyPath: configuration.sslKeyPath,
249
- sslCertPath: configuration.sslCertPath
247
+ error: 'An error occurred while starting HTTPS server.',
248
+ sslKeyPath: 'REDACTED',
249
+ sslCertPath: 'REDACTED'
250
250
  });
251
- console.log(`❌ Failed to start HTTPS server: ${error.message}`);
251
+ console.log(`❌ Failed to start HTTPS server. Please check the server configuration and logs for details.`);
252
252
  return null;
253
253
  }
254
254
  }
@@ -19,7 +19,14 @@ const { document: documentService } = clickUpServices;
19
19
  */
20
20
  export const createDocumentTool = {
21
21
  name: "create_document",
22
- description: `Creates a document in a ClickUp space, folder, or list. Requires name, parent info, visibility and create_page flag.`,
22
+ description: `Creates a document in a ClickUp space, folder, or list. Requires name, parent info, visibility and create_page flag.
23
+
24
+ Example usage:
25
+ - For list: parent: {"id": "901407953112", "type": 6}
26
+ - For space: parent: {"id": "90141392755", "type": 4}
27
+ - For folder: parent: {"id": "90144231850", "type": 5}
28
+
29
+ Note: Document creation permissions may vary by ClickUp plan and parent container type.`,
23
30
  inputSchema: {
24
31
  type: "object",
25
32
  properties: {
@@ -32,16 +39,16 @@ export const createDocumentTool = {
32
39
  properties: {
33
40
  id: {
34
41
  type: "string",
35
- description: "ID of the parent container (space, folder, or list)"
42
+ description: "ID of the parent container (space, folder, or list). Use actual ID from workspace hierarchy."
36
43
  },
37
44
  type: {
38
45
  type: "number",
39
46
  enum: [4, 5, 6, 7, 12],
40
- description: "Type of the parent container (4=space, 5=folder, 6=list, 7=everything, 12=workspace)"
47
+ description: "Type of the parent container: 4=space, 5=folder, 6=list, 7=everything, 12=workspace. Most commonly use 6 for lists."
41
48
  }
42
49
  },
43
50
  required: ["id", "type"],
44
- description: "Parent container information"
51
+ description: "Parent container object with id and type properties. Example: {\"id\": \"901407953112\", \"type\": 6}"
45
52
  },
46
53
  visibility: {
47
54
  type: "string",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Simple health‑check tool – returns a tiny JSON payload confirming the server is alive.
3
+ */
4
+ export const healthCheckTool = {
5
+ name: "health_check",
6
+ description: "Returns a simple health status for the MCP server.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {},
10
+ additionalProperties: false,
11
+ },
12
+ outputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ status: { type: "string" },
16
+ },
17
+ required: ["status"],
18
+ },
19
+ };
20
+ /** Handler invoked by CallTool when the client requests "health_check" */
21
+ export async function handleHealthCheck() {
22
+ return { status: "ok" };
23
+ }
@@ -97,12 +97,10 @@ export async function handleResolveAssignees(parameters) {
97
97
  m.name?.toLowerCase() === input.toLowerCase());
98
98
  return found ? found.id : null;
99
99
  });
100
- // Return a plain object, not wrapped in sponsorService.createResponse
101
- return { userIds: resolved };
100
+ return sponsorService.createResponse({ userIds: resolved }, true);
102
101
  }
103
102
  catch (error) {
104
103
  const errorMessage = error instanceof Error ? error.message : String(error);
105
- // Return a plain error object
106
- return { error: `Failed to resolve assignees: ${errorMessage}` };
104
+ return sponsorService.createErrorResponse(`Failed to resolve assignees: ${errorMessage}`);
107
105
  }
108
106
  }
@@ -50,7 +50,7 @@ const bulkOptionsSchema = {
50
50
  const taskIdentifierSchema = {
51
51
  taskId: {
52
52
  type: "string",
53
- description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
53
+ description: "Task ID (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
54
54
  },
55
55
  taskName: {
56
56
  type: "string",
@@ -62,7 +62,7 @@ const taskIdentifierSchema = {
62
62
  },
63
63
  customTaskId: {
64
64
  type: "string",
65
- description: "Custom task ID (e.g., 'DEV-1234'). Only use if you want to explicitly force custom ID lookup. In most cases, use taskId which auto-detects ID format."
65
+ description: "Custom task ID (e.g., 'DEV-1234'). This parameter is now optional since taskId automatically handles custom IDs. Use only for explicit custom ID lookup or backward compatibility."
66
66
  }
67
67
  };
68
68
  //=============================================================================
@@ -175,7 +175,7 @@ export const updateBulkTasksTool = {
175
175
  properties: {
176
176
  taskId: {
177
177
  type: "string",
178
- description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
178
+ description: "Task ID (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
179
179
  },
180
180
  taskName: {
181
181
  type: "string",
@@ -267,7 +267,7 @@ export const moveBulkTasksTool = {
267
267
  properties: {
268
268
  taskId: {
269
269
  type: "string",
270
- description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
270
+ description: "Task ID (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
271
271
  },
272
272
  taskName: {
273
273
  type: "string",
@@ -314,7 +314,7 @@ export const deleteBulkTasksTool = {
314
314
  properties: {
315
315
  taskId: {
316
316
  type: "string",
317
- description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
317
+ description: "Task ID (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
318
318
  },
319
319
  taskName: {
320
320
  type: "string",
@@ -110,10 +110,15 @@ async function resolveAssignees(assignees) {
110
110
  if (toResolve.length > 0) {
111
111
  try {
112
112
  const result = await handleResolveAssignees({ assignees: toResolve });
113
- if (result.userIds && Array.isArray(result.userIds)) {
114
- for (const userId of result.userIds) {
115
- if (userId !== null && typeof userId === 'number') {
116
- resolved.push(userId);
113
+ // The result is wrapped by sponsorService.createResponse, so we need to parse the JSON
114
+ if (result.content && Array.isArray(result.content) && result.content.length > 0) {
115
+ const dataText = result.content[0].text;
116
+ const parsedData = JSON.parse(dataText);
117
+ if (parsedData.userIds && Array.isArray(parsedData.userIds)) {
118
+ for (const userId of parsedData.userIds) {
119
+ if (userId !== null && typeof userId === 'number') {
120
+ resolved.push(userId);
121
+ }
117
122
  }
118
123
  }
119
124
  }
@@ -143,12 +148,28 @@ async function buildUpdateData(params) {
143
148
  updateData.priority = toTaskPriority(params.priority);
144
149
  }
145
150
  if (params.dueDate !== undefined) {
146
- updateData.due_date = parseDueDate(params.dueDate);
147
- updateData.due_date_time = true;
151
+ const parsedDueDate = parseDueDate(params.dueDate);
152
+ if (parsedDueDate !== undefined) {
153
+ updateData.due_date = parsedDueDate;
154
+ updateData.due_date_time = true;
155
+ }
156
+ else {
157
+ // Clear the due date by setting it to null
158
+ updateData.due_date = null;
159
+ updateData.due_date_time = false;
160
+ }
148
161
  }
149
162
  if (params.startDate !== undefined) {
150
- updateData.start_date = parseDueDate(params.startDate);
151
- updateData.start_date_time = true;
163
+ const parsedStartDate = parseDueDate(params.startDate);
164
+ if (parsedStartDate !== undefined) {
165
+ updateData.start_date = parsedStartDate;
166
+ updateData.start_date_time = true;
167
+ }
168
+ else {
169
+ // Clear the start date by setting it to null
170
+ updateData.start_date = null;
171
+ updateData.start_date_time = false;
172
+ }
152
173
  }
153
174
  // Handle time estimate if provided - convert from string to minutes
154
175
  if (params.time_estimate !== undefined) {
@@ -165,7 +186,21 @@ async function buildUpdateData(params) {
165
186
  }
166
187
  // Handle assignees if provided - resolve emails/usernames to user IDs
167
188
  if (params.assignees !== undefined) {
168
- updateData.assignees = await resolveAssignees(params.assignees);
189
+ // Parse assignees if it's a string (from MCP serialization)
190
+ let assigneesArray = params.assignees;
191
+ if (typeof params.assignees === 'string') {
192
+ try {
193
+ assigneesArray = JSON.parse(params.assignees);
194
+ }
195
+ catch (error) {
196
+ console.warn('Failed to parse assignees string:', params.assignees, error);
197
+ assigneesArray = [];
198
+ }
199
+ }
200
+ const resolvedAssignees = await resolveAssignees(assigneesArray);
201
+ // Store the resolved assignees for processing in the updateTask method
202
+ // The actual add/rem logic will be handled there based on current vs new assignees
203
+ updateData.assignees = resolvedAssignees;
169
204
  }
170
205
  return updateData;
171
206
  }
@@ -181,7 +216,7 @@ async function findTask(params) {
181
216
  throw new Error(validationResult.errorMessage);
182
217
  }
183
218
  try {
184
- // Direct path for taskId - most efficient
219
+ // Direct path for taskId - most efficient (now includes automatic custom ID detection)
185
220
  if (taskId) {
186
221
  const task = await taskService.getTask(taskId);
187
222
  // Add subtasks if requested
@@ -191,7 +226,8 @@ async function findTask(params) {
191
226
  }
192
227
  return { task };
193
228
  }
194
- // Direct path for customTaskId - also efficient
229
+ // Direct path for customTaskId - for explicit custom ID requests
230
+ // Note: This is now mainly for backward compatibility since getTask() handles custom IDs automatically
195
231
  if (customTaskId) {
196
232
  const task = await taskService.getTaskByCustomId(customTaskId);
197
233
  // Add subtasks if requested
@@ -429,7 +465,21 @@ export async function createTaskHandler(params) {
429
465
  const priority = toTaskPriority(params.priority);
430
466
  const listId = await getListId(params.listId, params.listName);
431
467
  // Resolve assignees if provided
432
- const resolvedAssignees = assignees ? await resolveAssignees(assignees) : undefined;
468
+ let resolvedAssignees = undefined;
469
+ if (assignees) {
470
+ // Parse assignees if it's a string (from MCP serialization)
471
+ let assigneesArray = assignees;
472
+ if (typeof assignees === 'string') {
473
+ try {
474
+ assigneesArray = JSON.parse(assignees);
475
+ }
476
+ catch (error) {
477
+ console.warn('Failed to parse assignees string in createTask:', assignees, error);
478
+ assigneesArray = [];
479
+ }
480
+ }
481
+ resolvedAssignees = await resolveAssignees(assigneesArray);
482
+ }
433
483
  const taskData = {
434
484
  name,
435
485
  description,
@@ -144,7 +144,7 @@ export const updateTaskTool = {
144
144
  properties: {
145
145
  taskId: {
146
146
  type: "string",
147
- description: "ID of task to update (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
147
+ description: "ID of task to update (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
148
148
  },
149
149
  taskName: {
150
150
  type: "string",
@@ -229,7 +229,7 @@ export const moveTaskTool = {
229
229
  properties: {
230
230
  taskId: {
231
231
  type: "string",
232
- description: "ID of the task to move (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
232
+ description: "ID of the task to move (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
233
233
  },
234
234
  taskName: {
235
235
  type: "string",
@@ -262,7 +262,7 @@ export const duplicateTaskTool = {
262
262
  properties: {
263
263
  taskId: {
264
264
  type: "string",
265
- description: "ID of task to duplicate (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
265
+ description: "ID of task to duplicate (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
266
266
  },
267
267
  taskName: {
268
268
  type: "string",
@@ -289,13 +289,13 @@ export const duplicateTaskTool = {
289
289
  */
290
290
  export const getTaskTool = {
291
291
  name: "get_task",
292
- description: `Gets task details by taskId (works with regular/custom IDs) or taskName. For taskName search, provide listName for faster lookup. Set subtasks=true to include all subtask details.`,
292
+ description: `Gets task details by taskId (automatically handles both regular and custom IDs) or taskName. For taskName search, provide listName for faster lookup. Set subtasks=true to include all subtask details.`,
293
293
  inputSchema: {
294
294
  type: "object",
295
295
  properties: {
296
296
  taskId: {
297
297
  type: "string",
298
- description: "ID of task to retrieve (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234'). The system automatically detects the ID format."
298
+ description: "ID of task to retrieve (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456'). Simply provide any task ID format here."
299
299
  },
300
300
  taskName: {
301
301
  type: "string",
@@ -307,7 +307,7 @@ export const getTaskTool = {
307
307
  },
308
308
  customTaskId: {
309
309
  type: "string",
310
- description: "Custom task ID (e.g., 'DEV-1234'). Only use this if you want to explicitly force custom ID lookup. In most cases, you can just use taskId which auto-detects ID format."
310
+ description: "Custom task ID (e.g., 'DEV-1234'). This parameter is now optional since taskId automatically handles custom IDs. Use only for explicit custom ID lookup or backward compatibility."
311
311
  },
312
312
  subtasks: {
313
313
  type: "boolean",
@@ -385,7 +385,7 @@ export const getTaskCommentsTool = {
385
385
  properties: {
386
386
  taskId: {
387
387
  type: "string",
388
- description: "ID of task to retrieve comments for (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
388
+ description: "ID of task to retrieve comments for (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
389
389
  },
390
390
  taskName: {
391
391
  type: "string",
@@ -417,7 +417,7 @@ export const createTaskCommentTool = {
417
417
  properties: {
418
418
  taskId: {
419
419
  type: "string",
420
- description: "ID of task to comment on (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
420
+ description: "ID of task to comment on (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
421
421
  },
422
422
  taskName: {
423
423
  type: "string",
@@ -454,7 +454,7 @@ export const deleteTaskTool = {
454
454
  properties: {
455
455
  taskId: {
456
456
  type: "string",
457
- description: "ID of task to delete (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
457
+ description: "ID of task to delete (preferred). Automatically detects and handles both regular task IDs (9 characters) and custom IDs (like 'DEV-1234', 'PROJ-456')."
458
458
  },
459
459
  taskName: {
460
460
  type: "string",