@taazkareem/clickup-mcp-server 0.6.3 → 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.
package/build/server.js CHANGED
@@ -19,7 +19,7 @@ const logger = new Logger('Server');
19
19
  const { workspace } = clickUpServices;
20
20
  export const server = new Server({
21
21
  name: "clickup-mcp-server",
22
- version: "0.6.3",
22
+ version: "0.6.4",
23
23
  }, {
24
24
  capabilities: {
25
25
  tools: {},
@@ -7,15 +7,21 @@
7
7
  * Enhanced implementation for bulk operations that leverages the existing single-operation methods.
8
8
  * This approach reduces code duplication while offering powerful concurrency management.
9
9
  */
10
- import { ClickUpServiceError, ErrorCode } from './base.js';
11
- import { processBatch } from '../../utils/concurrency-utils.js';
12
10
  import { Logger } from '../../logger.js';
11
+ import { processBatch } from '../../utils/concurrency-utils.js';
12
+ import { ClickUpServiceError, ErrorCode } from './base.js';
13
+ import { clickUpServices } from '../shared.js';
14
+ import { findListIDByName } from '../../tools/list.js';
13
15
  // Create logger instance
14
16
  const logger = new Logger('BulkService');
15
17
  /**
16
- * Service for handling bulk operations in ClickUp
18
+ * Service for performing bulk operations in ClickUp
17
19
  */
18
20
  export class BulkService {
21
+ /**
22
+ * Create a new bulk service
23
+ * @param taskService ClickUp Task Service instance
24
+ */
19
25
  constructor(taskService) {
20
26
  this.taskService = taskService;
21
27
  logger.info('BulkService initialized');
@@ -54,6 +60,61 @@ export class BulkService {
54
60
  throw new ClickUpServiceError(`Failed to create tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { listId, taskCount: tasks.length });
55
61
  }
56
62
  }
63
+ /**
64
+ * Find task by name within a specific list
65
+ */
66
+ async findTaskInList(taskName, listName) {
67
+ try {
68
+ // First get the list ID using the global lookup utility
69
+ const listInfo = await findListIDByName(clickUpServices.workspace, listName);
70
+ if (!listInfo) {
71
+ throw new ClickUpServiceError(`List "${listName}" not found`, ErrorCode.NOT_FOUND);
72
+ }
73
+ logger.info(`List "${listName}" resolved to ID: ${listInfo.id}`);
74
+ // Get tasks from the list using the resolved ID
75
+ const taskList = await this.taskService.getTasks(listInfo.id);
76
+ // Find the task by name - first try exact match
77
+ let matchingTask = taskList.find(t => t.name === taskName);
78
+ // If no exact match, try case-insensitive match
79
+ if (!matchingTask) {
80
+ matchingTask = taskList.find(t => t.name.toLowerCase() === taskName.toLowerCase());
81
+ // If still no match, try substring match as a fallback
82
+ if (!matchingTask) {
83
+ matchingTask = taskList.find(t => t.name.toLowerCase().includes(taskName.toLowerCase()) ||
84
+ taskName.toLowerCase().includes(t.name.toLowerCase()));
85
+ }
86
+ }
87
+ if (!matchingTask) {
88
+ throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
89
+ }
90
+ logger.info(`Task "${taskName}" found with ID: ${matchingTask.id}`);
91
+ return matchingTask.id;
92
+ }
93
+ catch (error) {
94
+ // Enhance the error message
95
+ if (error instanceof ClickUpServiceError) {
96
+ throw error;
97
+ }
98
+ throw new ClickUpServiceError(`Error finding task "${taskName}" in list "${listName}": ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN);
99
+ }
100
+ }
101
+ /**
102
+ * Resolve task ID using provided identifiers
103
+ */
104
+ async resolveTaskId(task) {
105
+ const { taskId, taskName, listName, customTaskId } = task;
106
+ if (taskId) {
107
+ return taskId;
108
+ }
109
+ if (customTaskId) {
110
+ const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
111
+ return resolvedTask.id;
112
+ }
113
+ if (taskName && listName) {
114
+ return await this.findTaskInList(taskName, listName);
115
+ }
116
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
117
+ }
57
118
  /**
58
119
  * Update multiple tasks
59
120
  * @param tasks Array of tasks to update with their new data
@@ -63,36 +124,10 @@ export class BulkService {
63
124
  async updateTasks(tasks, options) {
64
125
  logger.info('Starting bulk update operation', { taskCount: tasks.length });
65
126
  try {
66
- // Extract all task IDs that need validation
67
- const taskIds = tasks
68
- .map(task => task.taskId)
69
- .filter((id) => !!id);
70
- // Validate all tasks exist in parallel
71
- if (taskIds.length > 0) {
72
- await this.taskService.validateTasksExist(taskIds);
73
- }
74
- // Process updates in batches
75
127
  return await processBatch(tasks, async (task) => {
76
128
  const { taskId, taskName, listName, customTaskId, ...updateData } = task;
77
- if (taskId) {
78
- return await this.taskService.updateTask(taskId, updateData);
79
- }
80
- else if (customTaskId) {
81
- const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
82
- return await this.taskService.updateTask(resolvedTask.id, updateData);
83
- }
84
- else if (taskName && listName) {
85
- // For tasks identified by name, we need to resolve the ID first
86
- const taskList = await this.taskService.getTasks(listName);
87
- const matchingTask = taskList.find(t => t.name === taskName);
88
- if (!matchingTask) {
89
- throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
90
- }
91
- return await this.taskService.updateTask(matchingTask.id, updateData);
92
- }
93
- else {
94
- throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
95
- }
129
+ const resolvedTaskId = await this.resolveTaskId({ taskId, taskName, listName, customTaskId });
130
+ return await this.taskService.updateTask(resolvedTaskId, updateData);
96
131
  }, options);
97
132
  }
98
133
  catch (error) {
@@ -103,44 +138,31 @@ export class BulkService {
103
138
  /**
104
139
  * Move multiple tasks to a different list
105
140
  * @param tasks Array of tasks to move (each with taskId or taskName + listName)
106
- * @param targetListId ID of the destination list
141
+ * @param targetListId ID of the destination list or list name
107
142
  * @param options Optional batch processing settings
108
143
  * @returns Array of moved tasks
109
144
  */
110
145
  async moveTasks(tasks, targetListId, options) {
111
146
  logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
112
147
  try {
113
- // First validate the destination list exists
114
- await this.taskService.validateListExists(targetListId);
115
- // Extract all task IDs that need validation
116
- const taskIds = tasks
117
- .map(task => task.taskId)
118
- .filter((id) => !!id);
119
- // Validate all tasks exist in parallel
120
- if (taskIds.length > 0) {
121
- await this.taskService.validateTasksExist(taskIds);
148
+ // Determine if targetListId is actually an ID or a name
149
+ let resolvedTargetListId = targetListId;
150
+ // If the targetListId doesn't match the pattern of a list ID (usually just numbers),
151
+ // assume it's a list name and try to resolve it
152
+ if (!/^\d+$/.test(targetListId)) {
153
+ logger.info(`Target list appears to be a name: "${targetListId}", attempting to resolve`);
154
+ const listInfo = await findListIDByName(clickUpServices.workspace, targetListId);
155
+ if (!listInfo) {
156
+ throw new ClickUpServiceError(`Target list "${targetListId}" not found`, ErrorCode.NOT_FOUND);
157
+ }
158
+ resolvedTargetListId = listInfo.id;
159
+ logger.info(`Resolved target list to ID: ${resolvedTargetListId}`);
122
160
  }
123
- // Process moves in batches
161
+ // Validate the destination list exists
162
+ await this.taskService.validateListExists(resolvedTargetListId);
124
163
  return await processBatch(tasks, async (task) => {
125
- if (task.taskId) {
126
- return await this.taskService.moveTask(task.taskId, targetListId);
127
- }
128
- else if (task.customTaskId) {
129
- const resolvedTask = await this.taskService.getTaskByCustomId(task.customTaskId);
130
- return await this.taskService.moveTask(resolvedTask.id, targetListId);
131
- }
132
- else if (task.taskName && task.listName) {
133
- // For tasks identified by name, we need to resolve the ID first
134
- const taskList = await this.taskService.getTasks(task.listName);
135
- const matchingTask = taskList.find(t => t.name === task.taskName);
136
- if (!matchingTask) {
137
- throw new ClickUpServiceError(`Task "${task.taskName}" not found in list "${task.listName}"`, ErrorCode.NOT_FOUND);
138
- }
139
- return await this.taskService.moveTask(matchingTask.id, targetListId);
140
- }
141
- else {
142
- throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
143
- }
164
+ const resolvedTaskId = await this.resolveTaskId(task);
165
+ return await this.taskService.moveTask(resolvedTaskId, resolvedTargetListId);
144
166
  }, options);
145
167
  }
146
168
  catch (error) {
@@ -149,33 +171,22 @@ export class BulkService {
149
171
  }
150
172
  }
151
173
  /**
152
- * Delete multiple tasks efficiently
153
- *
154
- * @param taskIds Array of task IDs to delete
174
+ * Delete multiple tasks
175
+ * @param tasks Array of tasks to delete (each with taskId or taskName + listName)
155
176
  * @param options Batch processing options
156
177
  * @returns Results containing successful and failed deletions
157
178
  */
158
- async deleteTasks(taskIds, options) {
159
- logger.info(`Deleting ${taskIds.length} tasks`, {
160
- batchSize: options?.batchSize,
161
- concurrency: options?.concurrency
162
- });
179
+ async deleteTasks(tasks, options) {
180
+ logger.info('Starting bulk delete operation', { taskCount: tasks.length });
163
181
  try {
164
- return await processBatch(taskIds, async (taskId, index) => {
165
- logger.debug(`Deleting task ${index + 1}/${taskIds.length}`, {
166
- taskId
167
- });
168
- // Reuse the single-task delete method
169
- await this.taskService.deleteTask(taskId);
170
- return taskId; // Return the ID for successful deletions
182
+ return await processBatch(tasks, async (task) => {
183
+ const resolvedTaskId = await this.resolveTaskId(task);
184
+ await this.taskService.deleteTask(resolvedTaskId);
171
185
  }, options);
172
186
  }
173
187
  catch (error) {
174
- logger.error(`Failed to delete tasks in bulk`, {
175
- taskCount: taskIds.length,
176
- error: error instanceof Error ? error.message : String(error)
177
- });
178
- throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { taskCount: taskIds.length });
188
+ logger.error('Bulk delete operation failed', error);
189
+ throw error;
179
190
  }
180
191
  }
181
192
  }
@@ -276,7 +276,7 @@ export class TaskServiceCore extends BaseClickUpService {
276
276
  }
277
277
  }
278
278
  /**
279
- * Move a task to a different list
279
+ * Move a task to another list
280
280
  * @param taskId The ID of the task to move
281
281
  * @param destinationListId The ID of the list to move the task to
282
282
  * @returns The updated task
@@ -285,70 +285,32 @@ export class TaskServiceCore extends BaseClickUpService {
285
285
  const startTime = Date.now();
286
286
  this.logOperation('moveTask', { taskId, destinationListId, operation: 'start' });
287
287
  try {
288
- // First, get both the task and list info in parallel
289
- const [task, listResult] = await Promise.all([
288
+ // First, get task and validate destination list
289
+ const [sourceTask, _] = await Promise.all([
290
290
  this.validateTaskExists(taskId),
291
- this.validateListExists(destinationListId).then(() => this.listService.getList(destinationListId))
291
+ this.validateListExists(destinationListId)
292
292
  ]);
293
- const originalTask = task;
294
- const destinationList = listResult;
295
- // Log parallel request timing
296
- const parallelRequestTime = Date.now() - startTime;
297
- this.logOperation('moveTask', {
298
- taskId,
299
- destinationListId,
300
- operation: 'parallel_requests_complete',
301
- timing: { parallelRequestTime }
302
- });
303
- const currentStatus = originalTask.status?.status;
304
- const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
305
- // Enhanced status mapping logic
306
- let newStatus = currentStatus;
307
- if (currentStatus && availableStatuses.length > 0) {
308
- // Only map status if current status isn't available in destination list
309
- if (!availableStatuses.includes(currentStatus)) {
310
- // Try to find a similar status
311
- const similarStatus = availableStatuses.find(s => s.toLowerCase().includes(currentStatus.toLowerCase()) ||
312
- currentStatus.toLowerCase().includes(s.toLowerCase()));
313
- // If no similar status found, use the first available status
314
- newStatus = similarStatus || availableStatuses[0];
315
- this.logger.debug('Status mapping', {
316
- original: currentStatus,
317
- mapped: newStatus,
318
- available: availableStatuses
319
- });
320
- }
321
- }
322
- // Make the move request
323
- const movedTask = await this.makeRequest(async () => {
324
- const response = await this.client.post(`/task/${taskId}`, {
325
- list: destinationListId,
326
- status: newStatus
327
- });
328
- return response.data;
329
- });
330
- // Cache the moved task
331
- this.validationCache.tasks.set(taskId, {
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, {
332
302
  validatedAt: Date.now(),
333
- task: movedTask
303
+ task: newTask
334
304
  });
335
305
  const totalTime = Date.now() - startTime;
336
306
  this.logOperation('moveTask', {
337
307
  taskId,
338
308
  destinationListId,
339
309
  operation: 'complete',
340
- timing: {
341
- totalTime,
342
- parallelRequestTime,
343
- moveOperationTime: totalTime - parallelRequestTime
344
- },
345
- statusMapping: {
346
- original: currentStatus,
347
- new: newStatus,
348
- wasMapped: currentStatus !== newStatus
349
- }
310
+ timing: { totalTime },
311
+ newTaskId: newTask.id
350
312
  });
351
- return movedTask;
313
+ return newTask;
352
314
  }
353
315
  catch (error) {
354
316
  // Log failure
@@ -356,7 +318,7 @@ export class TaskServiceCore extends BaseClickUpService {
356
318
  taskId,
357
319
  destinationListId,
358
320
  operation: 'failed',
359
- error: error.message,
321
+ error: error instanceof Error ? error.message : String(error),
360
322
  timing: { totalTime: Date.now() - startTime }
361
323
  });
362
324
  throw this.handleError(error, 'Failed to move task');
@@ -33,22 +33,39 @@ export const attachTaskFileTool = {
33
33
  description: `Purpose: Attaches a file to a ClickUp task.
34
34
 
35
35
  Valid Usage:
36
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs
37
+ 2. Use taskName alone (will search across all lists)
38
+ 3. Use taskName + listName (for faster, targeted search)
39
+
40
+ File Source Options:
36
41
  1. Upload from base64: Provide file_data + file_name
37
- 2. Upload from URL or local file: Provide file_url + optional file_name
38
- - For web URLs: Use http:// or https:// URLs
39
- - For local files: Use absolute file paths (starting with / or drive letter)
40
- 3. For large files, advanced options are available via chunk_* parameters
42
+ 2. Upload from URL: Provide file_url starting with http:// or https://
43
+ 3. Upload from local file: Provide file_url as absolute path (starting with / or drive letter)
44
+ 4. For large files: Use chunk_* parameters for advanced chunked uploading
41
45
 
42
46
  Requirements:
43
- - EITHER taskId OR (taskName + listName) is REQUIRED
44
- - EITHER file_data OR file_url is REQUIRED
47
+ - EITHER taskId OR taskName: REQUIRED
48
+ - listName: Optional, but recommended when using taskName
49
+ - File Source: ONE of the following is REQUIRED:
50
+ - file_data + file_name
51
+ - file_url (web URL or local path)
52
+ - chunk_session (for continuing chunked upload)
45
53
 
46
54
  Notes:
47
- - The system automatically selects the best upload method based on file size and source
48
- - Base64 method has a 10MB size limit due to encoding overhead (file_data parameter)
49
- - URL method works for files hosted online (file_url parameter with http/https)
50
- - Local file method works with absolute paths only (file_url parameter with / or drive letter)
51
- - For large files, the system may use chunked uploading automatically`,
55
+ - The tool automatically searches for tasks using smart name matching
56
+ - When only taskName is provided, it searches across all lists
57
+ - Adding listName narrows the search to a specific list for better performance
58
+ - The system automatically selects the best upload method based on file size and source:
59
+ - Base64 method: Limited to 10MB due to encoding overhead
60
+ - URL method: Works for files hosted online
61
+ - Local file method: Works with absolute paths only
62
+ - Large files: Automatically uses chunked uploading
63
+
64
+ Warning:
65
+ - Using taskName without listName may match multiple tasks
66
+ - If multiple matches are found, the operation will fail with a disambiguation error
67
+ - For local files, relative paths are not supported
68
+ - Base64 uploads over 10MB will automatically switch to chunked upload mode`,
52
69
  inputSchema: {
53
70
  type: "object",
54
71
  properties: {
@@ -58,11 +75,11 @@ Notes:
58
75
  },
59
76
  taskName: {
60
77
  type: "string",
61
- description: "Name of the task to attach the file to. When using this parameter, you MUST also provide listName."
78
+ description: "Name of the task to attach the file to. The tool will search for tasks with this name across all lists unless listName is specified."
62
79
  },
63
80
  listName: {
64
81
  type: "string",
65
- description: "Name of the list containing the task. REQUIRED when using taskName."
82
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
66
83
  },
67
84
  file_name: {
68
85
  type: "string",
@@ -97,8 +114,7 @@ Notes:
97
114
  type: "boolean",
98
115
  description: "Optional: For advanced usage with large file chunking. Whether this is the final chunk."
99
116
  }
100
- },
101
- required: [] // Will validate based on context in the handler
117
+ }
102
118
  }
103
119
  };
104
120
  /**
@@ -76,17 +76,17 @@ export const createBulkTasksTool = {
76
76
  description: `Purpose: Create multiple tasks in a list efficiently.
77
77
 
78
78
  Valid Usage:
79
- 1. An array of tasks with required properties + listId (preferred)
80
- 2. An array of tasks with required properties + listName
79
+ 1. Provide listId + array of tasks (preferred)
80
+ 2. Provide listName + array of tasks
81
81
 
82
82
  Requirements:
83
83
  - tasks: REQUIRED (array of tasks, each with at least a name)
84
84
  - EITHER listId OR listName: REQUIRED
85
+ - All tasks will be created in the specified list
85
86
 
86
87
  Notes:
87
88
  - Configure batch size and concurrency via options for performance
88
89
  - Each task should have a name with emoji prefix
89
- - All tasks will be created in the same list
90
90
  - Custom fields can be set for each task using the custom_fields property (array of {id, value} objects)`,
91
91
  inputSchema: {
92
92
  type: "object",
@@ -157,37 +157,7 @@ Notes:
157
157
  type: "string",
158
158
  description: "Name of list for new tasks. Only use if you don't have listId."
159
159
  },
160
- options: {
161
- description: "Processing options (or JSON string representing options)",
162
- oneOf: [
163
- {
164
- type: "object",
165
- description: "Optional processing settings",
166
- properties: {
167
- batchSize: {
168
- type: "number",
169
- description: "Tasks per batch (default: 10)"
170
- },
171
- concurrency: {
172
- type: "number",
173
- description: "Parallel operations (default: 3)"
174
- },
175
- continueOnError: {
176
- type: "boolean",
177
- description: "Continue if some tasks fail"
178
- },
179
- retryCount: {
180
- type: "number",
181
- description: "Retry attempts for failures"
182
- }
183
- }
184
- },
185
- {
186
- type: "string",
187
- description: "JSON string representing options. Will be parsed automatically."
188
- }
189
- ]
190
- }
160
+ options: bulkOptionsSchema
191
161
  },
192
162
  required: ["tasks"]
193
163
  }
@@ -212,7 +182,10 @@ Notes:
212
182
  - Only specified fields will be updated for each task
213
183
  - Configure batch size and concurrency via options for performance
214
184
  - Each task can have different fields to update
215
- - Custom fields can be updated using the custom_fields property (array of {id, value} objects)`,
185
+ - Custom fields can be updated using the custom_fields property (array of {id, value} objects)
186
+
187
+ Warning:
188
+ - Using taskName without listName will fail as tasks may have identical names across lists`,
216
189
  inputSchema: {
217
190
  type: "object",
218
191
  properties: {
@@ -222,7 +195,22 @@ Notes:
222
195
  items: {
223
196
  type: "object",
224
197
  properties: {
225
- ...taskIdentifierSchema,
198
+ taskId: {
199
+ type: "string",
200
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
201
+ },
202
+ taskName: {
203
+ type: "string",
204
+ description: "Task name. Requires listName when used."
205
+ },
206
+ listName: {
207
+ type: "string",
208
+ description: "REQUIRED with taskName: List containing the task."
209
+ },
210
+ customTaskId: {
211
+ type: "string",
212
+ 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."
213
+ },
226
214
  name: {
227
215
  type: "string",
228
216
  description: "New name with emoji prefix"
@@ -268,37 +256,7 @@ Notes:
268
256
  }
269
257
  }
270
258
  },
271
- options: {
272
- description: "Processing options (or JSON string representing options)",
273
- oneOf: [
274
- {
275
- type: "object",
276
- description: "Optional processing settings",
277
- properties: {
278
- batchSize: {
279
- type: "number",
280
- description: "Tasks per batch (default: 10)"
281
- },
282
- concurrency: {
283
- type: "number",
284
- description: "Parallel operations (default: 3)"
285
- },
286
- continueOnError: {
287
- type: "boolean",
288
- description: "Continue if some tasks fail"
289
- },
290
- retryCount: {
291
- type: "number",
292
- description: "Retry attempts for failures"
293
- }
294
- }
295
- },
296
- {
297
- type: "string",
298
- description: "JSON string representing options. Will be parsed automatically."
299
- }
300
- ]
301
- }
259
+ options: bulkOptionsSchema
302
260
  },
303
261
  required: ["tasks"]
304
262
  }
@@ -335,7 +293,22 @@ Warning:
335
293
  items: {
336
294
  type: "object",
337
295
  properties: {
338
- ...taskIdentifierSchema
296
+ taskId: {
297
+ type: "string",
298
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
299
+ },
300
+ taskName: {
301
+ type: "string",
302
+ description: "Task name. Requires listName when used."
303
+ },
304
+ listName: {
305
+ type: "string",
306
+ description: "REQUIRED with taskName: List containing the task."
307
+ },
308
+ customTaskId: {
309
+ type: "string",
310
+ 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."
311
+ }
339
312
  }
340
313
  }
341
314
  },
@@ -383,7 +356,22 @@ Warning:
383
356
  items: {
384
357
  type: "object",
385
358
  properties: {
386
- ...taskIdentifierSchema
359
+ taskId: {
360
+ type: "string",
361
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
362
+ },
363
+ taskName: {
364
+ type: "string",
365
+ description: "Task name. Requires listName when used."
366
+ },
367
+ listName: {
368
+ type: "string",
369
+ description: "REQUIRED with taskName: List containing the task."
370
+ },
371
+ customTaskId: {
372
+ type: "string",
373
+ 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."
374
+ }
387
375
  }
388
376
  }
389
377
  },
@@ -392,58 +392,76 @@ export async function getWorkspaceTasksHandler(taskService, params) {
392
392
  * Handler for creating multiple tasks
393
393
  */
394
394
  export async function createBulkTasksHandler(params) {
395
- validateBulkTasks(params.tasks);
396
- const listId = await getListId(params.listId, params.listName);
397
- // Process tasks - prepare data for each task
398
- const tasks = params.tasks.map(task => {
399
- const processedTask = { ...task };
395
+ const { tasks, listId, listName, options } = params;
396
+ // Validate tasks array
397
+ validateBulkTasks(tasks, 'create');
398
+ // Validate and resolve list ID
399
+ const targetListId = await resolveListIdWithValidation(listId, listName);
400
+ // Format tasks for creation
401
+ const formattedTasks = tasks.map(task => {
402
+ const taskData = {
403
+ name: task.name,
404
+ description: task.description,
405
+ markdown_description: task.markdown_description,
406
+ status: task.status,
407
+ priority: toTaskPriority(task.priority),
408
+ tags: task.tags,
409
+ custom_fields: task.custom_fields
410
+ };
411
+ // Add due date if specified
400
412
  if (task.dueDate) {
401
- processedTask.due_date = parseDueDate(task.dueDate);
402
- delete processedTask.dueDate;
413
+ taskData.due_date = parseDueDate(task.dueDate);
414
+ taskData.due_date_time = true;
403
415
  }
404
- // Make sure custom_fields is preserved in the processed task
405
- if (task.custom_fields) {
406
- processedTask.custom_fields = task.custom_fields;
416
+ // Add start date if specified
417
+ if (task.startDate) {
418
+ taskData.start_date = parseDueDate(task.startDate);
419
+ taskData.start_date_time = true;
407
420
  }
408
- return processedTask;
421
+ return taskData;
409
422
  });
410
- const result = await bulkService.createTasks(listId, tasks, parseBulkOptions(params.options));
411
- return result.successful;
423
+ // Parse bulk options
424
+ const bulkOptions = parseBulkOptions(options);
425
+ // Create tasks - pass arguments in correct order: listId, tasks, options
426
+ return await bulkService.createTasks(targetListId, formattedTasks, bulkOptions);
412
427
  }
413
428
  /**
414
429
  * Handler for updating multiple tasks
415
430
  */
416
431
  export async function updateBulkTasksHandler(params) {
417
- validateBulkTasks(params.tasks);
418
- const updates = await Promise.all(params.tasks.map(async (task) => {
419
- validateTaskUpdateData(task);
420
- const taskId = await getTaskId(task.taskId, task.taskName, task.listName);
421
- return { id: taskId, data: buildUpdateData(task) };
422
- }));
423
- const result = await bulkService.updateTasks(updates, parseBulkOptions(params.options));
424
- return result.successful;
432
+ const { tasks, options } = params;
433
+ // Validate tasks array
434
+ validateBulkTasks(tasks, 'update');
435
+ // Parse bulk options
436
+ const bulkOptions = parseBulkOptions(options);
437
+ // Update tasks
438
+ return await bulkService.updateTasks(tasks, bulkOptions);
425
439
  }
426
440
  /**
427
441
  * Handler for moving multiple tasks
428
442
  */
429
443
  export async function moveBulkTasksHandler(params) {
430
- validateBulkTasks(params.tasks);
431
- if (!params.targetListId && !params.targetListName) {
432
- throw new Error("Either targetListId or targetListName must be provided");
433
- }
434
- const targetListId = await getListId(params.targetListId, params.targetListName);
435
- const taskIds = await mapTaskIds(params.tasks);
436
- const result = await bulkService.moveTasks(taskIds.map(taskId => ({ taskId })), targetListId, parseBulkOptions(params.options));
437
- return result.successful;
444
+ const { tasks, targetListId, targetListName, options } = params;
445
+ // Validate tasks array
446
+ validateBulkTasks(tasks, 'move');
447
+ // Validate and resolve target list ID
448
+ const resolvedTargetListId = await resolveListIdWithValidation(targetListId, targetListName);
449
+ // Parse bulk options
450
+ const bulkOptions = parseBulkOptions(options);
451
+ // Move tasks
452
+ return await bulkService.moveTasks(tasks, resolvedTargetListId, bulkOptions);
438
453
  }
439
454
  /**
440
455
  * Handler for deleting multiple tasks
441
456
  */
442
457
  export async function deleteBulkTasksHandler(params) {
443
- validateBulkTasks(params.tasks);
444
- const taskIds = await mapTaskIds(params.tasks);
445
- await bulkService.deleteTasks(taskIds, parseBulkOptions(params.options));
446
- return taskIds.map(() => true);
458
+ const { tasks, options } = params;
459
+ // Validate tasks array
460
+ validateBulkTasks(tasks, 'delete');
461
+ // Parse bulk options
462
+ const bulkOptions = parseBulkOptions(options);
463
+ // Delete tasks
464
+ return await bulkService.deleteTasks(tasks, bulkOptions);
447
465
  }
448
466
  /**
449
467
  * Handler for deleting a task
@@ -100,22 +100,37 @@ export const handleCreateTaskComment = createHandlerWrapper(createTaskCommentHan
100
100
  //=============================================================================
101
101
  // BULK TASK OPERATIONS - HANDLER IMPLEMENTATIONS
102
102
  //=============================================================================
103
- export const handleCreateBulkTasks = createHandlerWrapper(createBulkTasksHandler, (tasks) => ({
104
- tasks,
105
- count: tasks.length
103
+ export const handleCreateBulkTasks = createHandlerWrapper(createBulkTasksHandler, (result) => ({
104
+ successful: result.successful,
105
+ failed: result.failed,
106
+ count: result.totals.total,
107
+ success_count: result.totals.success,
108
+ failure_count: result.totals.failure,
109
+ errors: result.failed.map(f => f.error)
106
110
  }));
107
- export const handleUpdateBulkTasks = createHandlerWrapper(updateBulkTasksHandler, (tasks) => ({
108
- tasks,
109
- count: tasks.length
111
+ export const handleUpdateBulkTasks = createHandlerWrapper(updateBulkTasksHandler, (result) => ({
112
+ successful: result.successful,
113
+ failed: result.failed,
114
+ count: result.totals.total,
115
+ success_count: result.totals.success,
116
+ failure_count: result.totals.failure,
117
+ errors: result.failed.map(f => f.error)
110
118
  }));
111
- export const handleMoveBulkTasks = createHandlerWrapper(moveBulkTasksHandler, (tasks) => ({
112
- tasks,
113
- count: tasks.length
119
+ export const handleMoveBulkTasks = createHandlerWrapper(moveBulkTasksHandler, (result) => ({
120
+ successful: result.successful,
121
+ failed: result.failed,
122
+ count: result.totals.total,
123
+ success_count: result.totals.success,
124
+ failure_count: result.totals.failure,
125
+ errors: result.failed.map(f => f.error)
114
126
  }));
115
- export const handleDeleteBulkTasks = createHandlerWrapper(deleteBulkTasksHandler, (results) => ({
116
- success: true,
117
- count: results.length,
118
- results
127
+ export const handleDeleteBulkTasks = createHandlerWrapper(deleteBulkTasksHandler, (result) => ({
128
+ successful: result.successful,
129
+ failed: result.failed,
130
+ count: result.totals.total,
131
+ success_count: result.totals.success,
132
+ failure_count: result.totals.failure,
133
+ errors: result.failed.map(f => f.error)
119
134
  }));
120
135
  //=============================================================================
121
136
  // WORKSPACE TASK OPERATIONS - HANDLER IMPLEMENTATIONS
@@ -128,22 +143,101 @@ export const handleGetWorkspaceTasks = createHandlerWrapper(
128
143
  // TOOL DEFINITIONS AND HANDLERS EXPORT
129
144
  //=============================================================================
130
145
  // Tool definitions with their handler mappings
131
- export const taskTools = [
132
- // Single task operations
133
- { definition: createTaskTool, handler: handleCreateTask },
134
- { definition: getTaskTool, handler: handleGetTask },
135
- { definition: getTasksTool, handler: handleGetTasks },
136
- { definition: updateTaskTool, handler: handleUpdateTask },
137
- { definition: moveTaskTool, handler: handleMoveTask },
138
- { definition: duplicateTaskTool, handler: handleDuplicateTask },
139
- { definition: deleteTaskTool, handler: handleDeleteTask },
140
- { definition: getTaskCommentsTool, handler: handleGetTaskComments },
141
- { definition: createTaskCommentTool, handler: handleCreateTaskComment },
142
- // Bulk task operations
143
- { definition: createBulkTasksTool, handler: handleCreateBulkTasks },
144
- { definition: updateBulkTasksTool, handler: handleUpdateBulkTasks },
145
- { definition: moveBulkTasksTool, handler: handleMoveBulkTasks },
146
- { definition: deleteBulkTasksTool, handler: handleDeleteBulkTasks },
147
- // Team task operations
148
- { definition: getWorkspaceTasksTool, handler: handleGetWorkspaceTasks }
146
+ export const tools = [
147
+ {
148
+ definition: createTaskTool,
149
+ handler: createTaskHandler
150
+ },
151
+ {
152
+ definition: updateTaskTool,
153
+ handler: updateTaskHandler
154
+ },
155
+ {
156
+ definition: moveTaskTool,
157
+ handler: moveTaskHandler
158
+ },
159
+ {
160
+ definition: duplicateTaskTool,
161
+ handler: duplicateTaskHandler
162
+ },
163
+ {
164
+ definition: getTaskTool,
165
+ handler: getTaskHandler
166
+ },
167
+ {
168
+ definition: getTasksTool,
169
+ handler: getTasksHandler
170
+ },
171
+ {
172
+ definition: getTaskCommentsTool,
173
+ handler: getTaskCommentsHandler
174
+ },
175
+ {
176
+ definition: createTaskCommentTool,
177
+ handler: createTaskCommentHandler
178
+ },
179
+ {
180
+ definition: deleteTaskTool,
181
+ handler: deleteTaskHandler
182
+ },
183
+ {
184
+ definition: getWorkspaceTasksTool,
185
+ handler: getWorkspaceTasksHandler
186
+ },
187
+ {
188
+ definition: createBulkTasksTool,
189
+ handler: async (params) => {
190
+ const result = await createBulkTasksHandler(params);
191
+ return {
192
+ successful: result.successful,
193
+ failed: result.failed,
194
+ count: result.totals.total,
195
+ success_count: result.totals.success,
196
+ failure_count: result.totals.failure,
197
+ errors: result.failed.map(f => f.error)
198
+ };
199
+ }
200
+ },
201
+ {
202
+ definition: updateBulkTasksTool,
203
+ handler: async (params) => {
204
+ const result = await updateBulkTasksHandler(params);
205
+ return {
206
+ successful: result.successful,
207
+ failed: result.failed,
208
+ count: result.totals.total,
209
+ success_count: result.totals.success,
210
+ failure_count: result.totals.failure,
211
+ errors: result.failed.map(f => f.error)
212
+ };
213
+ }
214
+ },
215
+ {
216
+ definition: moveBulkTasksTool,
217
+ handler: async (params) => {
218
+ const result = await moveBulkTasksHandler(params);
219
+ return {
220
+ successful: result.successful,
221
+ failed: result.failed,
222
+ count: result.totals.total,
223
+ success_count: result.totals.success,
224
+ failure_count: result.totals.failure,
225
+ errors: result.failed.map(f => f.error)
226
+ };
227
+ }
228
+ },
229
+ {
230
+ definition: deleteBulkTasksTool,
231
+ handler: async (params) => {
232
+ const result = await deleteBulkTasksHandler(params);
233
+ return {
234
+ successful: result.successful,
235
+ failed: result.failed,
236
+ count: result.totals.total,
237
+ success_count: result.totals.success,
238
+ failure_count: result.totals.failure,
239
+ errors: result.failed.map(f => f.error)
240
+ };
241
+ }
242
+ }
149
243
  ];
@@ -145,31 +145,39 @@ export const updateTaskTool = {
145
145
  description: `Purpose: Modify properties of an existing task.
146
146
 
147
147
  Valid Usage:
148
- 1. Use taskId alone (preferred if available)
149
- 2. Use taskName + listName
148
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs
149
+ 2. Use taskName alone (will search across all lists)
150
+ 3. Use taskName + listName (for faster, targeted search)
150
151
 
151
152
  Requirements:
152
153
  - At least one update field (name, description, status, priority, dueDate) must be provided
153
- - When using taskName, listName is REQUIRED
154
+ - EITHER taskId OR taskName: REQUIRED
155
+ - listName: Optional, but recommended when using taskName
154
156
 
155
157
  Notes:
158
+ - The tool automatically searches for tasks using smart name matching
159
+ - When only taskName is provided, it searches across all lists
160
+ - Adding listName narrows the search to a specific list for better performance
156
161
  - Only specified fields will be updated
157
- - Using taskId is more reliable than taskName
158
- - You can set custom field values using the custom_fields parameter (array of {id, value} objects)`,
162
+ - Custom fields can be set using the custom_fields parameter (array of {id, value} objects)
163
+
164
+ Warning:
165
+ - Using taskName without listName may match multiple tasks
166
+ - If multiple matches are found, the operation will fail with a disambiguation error`,
159
167
  inputSchema: {
160
168
  type: "object",
161
169
  properties: {
162
170
  taskId: {
163
171
  type: "string",
164
- description: "ID of the task to update (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
172
+ description: "ID of task to update (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
165
173
  },
166
174
  taskName: {
167
175
  type: "string",
168
- description: "Name of the task to update. Only use this if you don't have taskId. When using this parameter, you MUST also provide listName."
176
+ description: "Name of task to update. The tool will search for tasks with this name across all lists unless listName is specified."
169
177
  },
170
178
  listName: {
171
179
  type: "string",
172
- description: "Name of the list containing the task. REQUIRED when using taskName."
180
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
173
181
  },
174
182
  name: {
175
183
  type: "string",
@@ -356,8 +364,7 @@ Note:
356
364
  type: "boolean",
357
365
  description: "Whether to include subtasks in the response. Set to true to retrieve full details of all subtasks."
358
366
  }
359
- },
360
- required: []
367
+ }
361
368
  }
362
369
  };
363
370
  /**
@@ -518,12 +525,23 @@ export const deleteTaskTool = {
518
525
 
519
526
  Valid Usage:
520
527
  1. Use taskId alone (preferred and safest)
521
- 2. Use taskName + optional listName
528
+ 2. Use taskName alone (will search across all lists)
529
+ 3. Use taskName + listName (for faster, targeted search)
530
+
531
+ Requirements:
532
+ - EITHER taskId OR taskName: REQUIRED
533
+ - listName: Optional, but recommended when using taskName
534
+
535
+ Notes:
536
+ - The tool automatically searches for tasks using smart name matching
537
+ - When only taskName is provided, it searches across all lists
538
+ - Adding listName narrows the search to a specific list for better performance
539
+ - Supports both regular task IDs and custom IDs (like 'DEV-1234')
522
540
 
523
541
  Warning:
524
542
  - This action CANNOT be undone
525
- - Using taskName is risky as names may not be unique
526
- - Provide listName when using taskName for more precise targeting`,
543
+ - Using taskName without listName may match multiple tasks
544
+ - If multiple matches are found, the operation will fail with a disambiguation error`,
527
545
  inputSchema: {
528
546
  type: "object",
529
547
  properties: {
@@ -533,11 +551,11 @@ Warning:
533
551
  },
534
552
  taskName: {
535
553
  type: "string",
536
- description: "Name of task to delete. Use with extreme caution as names may not be unique."
554
+ description: "Name of task to delete. The tool will search for tasks with this name across all lists unless listName is specified."
537
555
  },
538
556
  listName: {
539
557
  type: "string",
540
- description: "Name of list containing the task. Helps ensure correct task deletion when using taskName."
558
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
541
559
  }
542
560
  }
543
561
  }
@@ -117,12 +117,31 @@ export function validateTaskUpdateData(updateData) {
117
117
  }
118
118
  }
119
119
  /**
120
- * Validate bulk operation tasks array
120
+ * Validate bulk task array and task identification
121
+ * @param tasks Array of tasks to validate
122
+ * @param operation The bulk operation type ('create', 'update', 'move', 'delete')
121
123
  */
122
- export function validateBulkTasks(tasks) {
123
- if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
124
- throw new Error('You must provide a non-empty array of tasks');
124
+ export function validateBulkTasks(tasks, operation = 'update') {
125
+ if (!Array.isArray(tasks) || tasks.length === 0) {
126
+ throw new Error("tasks must be a non-empty array");
125
127
  }
128
+ tasks.forEach((task, index) => {
129
+ if (!task || typeof task !== 'object') {
130
+ throw new Error(`Task at index ${index} must be an object`);
131
+ }
132
+ // Skip task identification validation for create operations
133
+ if (operation === 'create') {
134
+ return;
135
+ }
136
+ // For bulk operations, require listName when using taskName
137
+ if (task.taskName && !task.listName) {
138
+ throw new Error(`Task at index ${index} using taskName must also provide listName`);
139
+ }
140
+ // At least one identifier is required for non-create operations
141
+ if (!task.taskId && !task.taskName && !task.customTaskId) {
142
+ throw new Error(`Task at index ${index} must provide either taskId, taskName + listName, or customTaskId`);
143
+ }
144
+ });
126
145
  }
127
146
  /**
128
147
  * Parse options for bulk operations
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",