@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 +1 -1
- package/build/server.js +1 -1
- package/build/services/clickup/task/task-core.js +68 -4
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +5 -5
- package/build/tools/task/handlers.js +62 -12
- package/build/tools/task/single-operations.js +9 -9
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/package.json +1 -1
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.
|
|
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
|
@@ -179,11 +179,29 @@ export class TaskServiceCore extends BaseClickUpService {
|
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
181
|
* Get a task by its ID
|
|
182
|
-
*
|
|
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}`,
|
|
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') {
|
package/build/tools/member.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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).
|
|
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').
|
|
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).
|
|
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).
|
|
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).
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
-
|
|
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).
|
|
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).
|
|
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).
|
|
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 (
|
|
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).
|
|
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').
|
|
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).
|
|
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).
|
|
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).
|
|
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",
|