@taazkareem/clickup-mcp-server 0.8.4 → 0.8.5

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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI applications. This server allows AI agents to interact with ClickUp tasks, spaces, lists, and folders through a standardized protocol.
8
8
 
9
- > 🚀 **Status Update:** v0.8.4 is released with security features and compatibility improvements! Added comprehensive opt-in enhanced security features, fixed Gemini compatibility (Issue #79), and resolved priority handling and subtask retrieval issues. See [Release Notes](release-notes.md) for full details.
9
+ > 🚀 **Status Update:** v0.8.5 is released with comprehensive natural language date parsing and critical bug fixes! Added 47+ natural language patterns (100% accuracy), extended time units (months/years), dynamic number support, fixed task assignment functionality, and resolved time tracking issues. See [Release Notes](release-notes.md) for full details.
10
10
 
11
11
  ## Setup
12
12
 
package/build/server.js CHANGED
@@ -45,7 +45,7 @@ const isToolEnabled = (toolName) => {
45
45
  };
46
46
  export const server = new Server({
47
47
  name: "clickup-mcp-server",
48
- version: "0.8.4",
48
+ version: "0.8.5",
49
49
  }, {
50
50
  capabilities: {
51
51
  tools: {},
@@ -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') {
@@ -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",