@taazkareem/clickup-mcp-server 0.6.5 → 0.6.6

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.
@@ -13,8 +13,11 @@ import { clickUpServices } from '../services/shared.js';
13
13
  import { Logger } from '../logger.js';
14
14
  import { sponsorService } from '../utils/sponsor-service.js';
15
15
  import { processColorCommand } from '../utils/color-processor.js';
16
+ import { validateTaskIdentification } from './task/utilities.js';
16
17
  // Create a logger specific to tag tools
17
18
  const logger = new Logger('TagTools');
19
+ // Use shared services instance
20
+ const { task: taskService } = clickUpServices;
18
21
  //=============================================================================
19
22
  // TOOL DEFINITIONS
20
23
  //=============================================================================
@@ -26,11 +29,15 @@ export const getSpaceTagsTool = {
26
29
  description: `Purpose: Get all tags available in a ClickUp space.
27
30
 
28
31
  Valid Usage:
29
- 1. Provide spaceId (preferred)
30
- 2. Provide spaceName
32
+ 1. Provide spaceId (preferred if available)
33
+ 2. Provide spaceName (will be resolved to a space ID)
31
34
 
32
35
  Requirements:
33
- - Space identification: EITHER spaceId OR spaceName REQUIRED`,
36
+ - EITHER spaceId OR spaceName is REQUIRED
37
+
38
+ Notes:
39
+ - Tags are defined at the space level in ClickUp
40
+ - You need to know the available tags before adding them to tasks`,
34
41
  inputSchema: {
35
42
  type: "object",
36
43
  properties: {
@@ -53,15 +60,18 @@ export const createSpaceTagTool = {
53
60
  description: `Purpose: Create a new tag in a ClickUp space.
54
61
 
55
62
  Valid Usage:
56
- 1. Provide spaceId + tagName (preferred)
57
- 2. Provide spaceName + tagName
63
+ 1. Provide spaceId (preferred if available)
64
+ 2. Provide spaceName (will be resolved to a space ID)
58
65
 
59
66
  Requirements:
60
67
  - tagName: REQUIRED
61
- - Space identification: EITHER spaceId OR spaceName REQUIRED
68
+ - EITHER spaceId OR spaceName: REQUIRED
62
69
 
63
70
  Notes:
64
- - Specify colors with HEX values or natural language command`,
71
+ - New tag will be available for all tasks in the space
72
+ - You can specify background and foreground colors in HEX format (e.g., #FF0000)
73
+ - You can also provide a color command (e.g., "blue tag") to automatically generate colors
74
+ - After creating a tag, you can add it to tasks using add_tag_to_task`,
65
75
  inputSchema: {
66
76
  type: "object",
67
77
  properties: {
@@ -194,17 +204,18 @@ export const addTagToTaskTool = {
194
204
 
195
205
  Valid Usage:
196
206
  1. Provide taskId (preferred if available)
197
- 2. Provide taskName + listName
207
+ 2. Provide taskName (optionally with listName for disambiguation)
198
208
 
199
209
  Requirements:
200
210
  - tagName: REQUIRED
201
- - EITHER taskId OR (taskName + listName): REQUIRED
211
+ - EITHER taskId OR customTaskId OR taskName: REQUIRED
202
212
  - The tag MUST exist in the space containing the task before calling this tool
203
213
 
204
214
  Warning:
205
215
  - The operation will fail if the tag does not exist in the space
206
216
  - Always use get_space_tags first to verify the tag exists
207
217
  - If the tag doesn't exist, create it using create_space_tag before adding it to the task
218
+ - If multiple tasks have the same name, provide listName to disambiguate
208
219
 
209
220
  Notes:
210
221
  - Use get_space_tags to see available tags
@@ -222,11 +233,11 @@ Notes:
222
233
  },
223
234
  taskName: {
224
235
  type: "string",
225
- description: "Name of the task to add tag to. When using this parameter, you MUST also provide listName."
236
+ description: "Name of the task to add tag to. Will search across all lists unless listName is provided."
226
237
  },
227
238
  listName: {
228
239
  type: "string",
229
- description: "Name of the list containing the task. REQUIRED when using taskName."
240
+ description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
230
241
  },
231
242
  tagName: {
232
243
  type: "string",
@@ -245,15 +256,16 @@ export const removeTagFromTaskTool = {
245
256
 
246
257
  Valid Usage:
247
258
  1. Provide taskId (preferred if available)
248
- 2. Provide taskName + listName
259
+ 2. Provide taskName (optionally with listName for disambiguation)
249
260
 
250
261
  Requirements:
251
262
  - tagName: REQUIRED
252
- - EITHER taskId OR (taskName + listName): REQUIRED
263
+ - EITHER taskId OR customTaskId OR taskName: REQUIRED
253
264
 
254
265
  Notes:
255
266
  - This only removes the association between the tag and task
256
- - The tag will still exist in the space`,
267
+ - The tag will still exist in the space
268
+ - If multiple tasks have the same name, provide listName to disambiguate`,
257
269
  inputSchema: {
258
270
  type: "object",
259
271
  properties: {
@@ -267,11 +279,11 @@ Notes:
267
279
  },
268
280
  taskName: {
269
281
  type: "string",
270
- description: "Name of the task to remove tag from. When using this parameter, you MUST also provide listName."
282
+ description: "Name of the task to remove tag from. Will search across all lists unless listName is provided."
271
283
  },
272
284
  listName: {
273
285
  type: "string",
274
- description: "Name of the list containing the task. REQUIRED when using taskName."
286
+ description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
275
287
  },
276
288
  tagName: {
277
289
  type: "string",
@@ -334,10 +346,18 @@ export const handleDeleteSpaceTag = createHandlerWrapper(deleteSpaceTag, () => (
334
346
  /**
335
347
  * Wrapper for addTagToTask handler
336
348
  */
337
- export const handleAddTagToTask = createHandlerWrapper(addTagToTask, () => ({
338
- success: true,
339
- message: "Tag added to task successfully"
340
- }));
349
+ export const handleAddTagToTask = createHandlerWrapper(addTagToTask, (result) => {
350
+ if (!result.success) {
351
+ return {
352
+ success: false,
353
+ error: result.error
354
+ };
355
+ }
356
+ return {
357
+ success: true,
358
+ message: "Tag added to task successfully"
359
+ };
360
+ });
341
361
  /**
342
362
  * Wrapper for removeTagFromTask handler
343
363
  */
@@ -666,28 +686,42 @@ export async function deleteSpaceTag(params) {
666
686
  */
667
687
  async function resolveTaskId(params) {
668
688
  const { taskId, customTaskId, taskName, listName } = params;
669
- // If we have a direct taskId, use it
670
- if (taskId) {
671
- return { success: true, taskId };
672
- }
673
- // Custom task ID handling
674
- if (customTaskId) {
675
- return { success: true, taskId: customTaskId };
689
+ try {
690
+ // First validate task identification with global lookup enabled
691
+ const validationResult = validateTaskIdentification({ taskId, customTaskId, taskName, listName }, { useGlobalLookup: true });
692
+ if (!validationResult.isValid) {
693
+ return {
694
+ success: false,
695
+ error: { message: validationResult.errorMessage }
696
+ };
697
+ }
698
+ const result = await taskService.findTasks({
699
+ taskId,
700
+ customTaskId,
701
+ taskName,
702
+ listName,
703
+ allowMultipleMatches: false,
704
+ useSmartDisambiguation: true,
705
+ includeFullDetails: false
706
+ });
707
+ if (!result || Array.isArray(result)) {
708
+ return {
709
+ success: false,
710
+ error: { message: 'Task not found with the provided identification' }
711
+ };
712
+ }
713
+ return { success: true, taskId: result.id };
676
714
  }
677
- // Task name lookup (requires list name)
678
- if (taskName && listName) {
679
- // Implementation would go here
715
+ catch (error) {
680
716
  return {
681
717
  success: false,
682
- error: { message: 'Task name resolution not implemented yet' }
718
+ error: {
719
+ message: error.message || 'Failed to resolve task ID',
720
+ code: error.code,
721
+ details: error.data
722
+ }
683
723
  };
684
724
  }
685
- return {
686
- success: false,
687
- error: {
688
- message: 'Task identifier is required (taskId, customTaskId, or taskName+listName)'
689
- }
690
- };
691
725
  }
692
726
  /**
693
727
  * Add a tag to a task
@@ -705,12 +739,12 @@ export async function addTagToTask(params) {
705
739
  }
706
740
  };
707
741
  }
708
- if (!taskId && !customTaskId && !(taskName && listName)) {
709
- logger.error('addTagToTask called without proper task identifier');
742
+ if (!taskId && !customTaskId && !taskName) {
743
+ logger.error('addTagToTask called without task identifier');
710
744
  return {
711
745
  success: false,
712
746
  error: {
713
- message: 'Either taskId, customTaskId, or both taskName and listName are required'
747
+ message: 'Either taskId, customTaskId, or taskName is required'
714
748
  }
715
749
  };
716
750
  }
@@ -728,6 +762,31 @@ export async function addTagToTask(params) {
728
762
  const result = await clickUpServices.tag.addTagToTask(taskIdResult.taskId, tagName);
729
763
  if (!result.success) {
730
764
  logger.error('Failed to add tag to task', result.error);
765
+ // Provide more specific error messages based on error code
766
+ if (result.error?.code === 'TAG_NOT_FOUND') {
767
+ return {
768
+ success: false,
769
+ error: {
770
+ message: `The tag "${tagName}" does not exist in the space. Please create it first using create_space_tag.`
771
+ }
772
+ };
773
+ }
774
+ else if (result.error?.code === 'SPACE_NOT_FOUND') {
775
+ return {
776
+ success: false,
777
+ error: {
778
+ message: 'Could not determine which space the task belongs to.'
779
+ }
780
+ };
781
+ }
782
+ else if (result.error?.code === 'TAG_VERIFICATION_FAILED') {
783
+ return {
784
+ success: false,
785
+ error: {
786
+ message: 'The tag addition could not be verified. Please check if the tag was added manually.'
787
+ }
788
+ };
789
+ }
731
790
  return {
732
791
  success: false,
733
792
  error: result.error || {
@@ -768,12 +827,12 @@ export async function removeTagFromTask(params) {
768
827
  }
769
828
  };
770
829
  }
771
- if (!taskId && !customTaskId && !(taskName && listName)) {
772
- logger.error('removeTagFromTask called without proper task identifier');
830
+ if (!taskId && !customTaskId && !taskName) {
831
+ logger.error('removeTagFromTask called without task identifier');
773
832
  return {
774
833
  success: false,
775
834
  error: {
776
- message: 'Either taskId, customTaskId, or both taskName and listName are required'
835
+ message: 'Either taskId, customTaskId, or taskName is required'
777
836
  }
778
837
  };
779
838
  }
@@ -10,8 +10,11 @@
10
10
  import { clickUpServices } from '../../services/shared.js';
11
11
  import { validateTaskIdentification } from './utilities.js';
12
12
  import { sponsorService } from '../../utils/sponsor-service.js';
13
+ import { Logger } from '../../logger.js';
13
14
  // Use shared services instance
14
15
  const { task: taskService } = clickUpServices;
16
+ // Create a logger instance for attachments
17
+ const logger = new Logger('TaskAttachments');
15
18
  // Session storage for chunked uploads (in-memory for demonstration)
16
19
  const chunkSessions = new Map();
17
20
  // Clean up expired sessions periodically
@@ -21,7 +24,7 @@ setInterval(() => {
21
24
  for (const [token, session] of chunkSessions.entries()) {
22
25
  if (now - session.timestamp > expired) {
23
26
  chunkSessions.delete(token);
24
- console.log(`Cleaned up expired upload session: ${token}`);
27
+ logger.debug(`Cleaned up expired upload session: ${token}`);
25
28
  }
26
29
  }
27
30
  }, 3600 * 1000); // Check every hour
@@ -33,23 +36,39 @@ export const attachTaskFileTool = {
33
36
  description: `Purpose: Attaches a file to a ClickUp task.
34
37
 
35
38
  Valid Usage:
36
- 1. Use taskId (preferred) - works with both regular and custom IDs
37
- 2. Use taskName + listName for targeted search
39
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs
40
+ 2. Use taskName alone (will search across all lists)
41
+ 3. Use taskName + listName (for faster, targeted search)
38
42
 
39
- File Sources:
40
- - Base64: Provide file_data + file_name
41
- - URL: Provide file_url starting with http:// or https://
42
- - Local file: Provide file_url as absolute path
43
- - Chunked upload: Use chunk_* parameters for large files
43
+ File Source Options:
44
+ 1. Upload from base64: Provide file_data + file_name
45
+ 2. Upload from URL: Provide file_url starting with http:// or https://
46
+ 3. Upload from local file: Provide file_url as absolute path (starting with / or drive letter)
47
+ 4. For large files: Use chunk_* parameters for advanced chunked uploading
44
48
 
45
49
  Requirements:
46
- - Task identification: EITHER taskId OR taskName REQUIRED
47
- - File source: EITHER file_data+file_name OR file_url OR chunk_session REQUIRED
50
+ - EITHER taskId OR taskName: REQUIRED
51
+ - listName: Optional, but recommended when using taskName
52
+ - File Source: ONE of the following is REQUIRED:
53
+ - file_data + file_name
54
+ - file_url (web URL or local path)
55
+ - chunk_session (for continuing chunked upload)
48
56
 
49
57
  Notes:
50
- - System automatically selects best upload method based on file size
51
- - Base64 uploads limited to 10MB
52
- - Using taskName without listName may match multiple tasks`,
58
+ - The tool automatically searches for tasks using smart name matching
59
+ - When only taskName is provided, it searches across all lists
60
+ - Adding listName narrows the search to a specific list for better performance
61
+ - The system automatically selects the best upload method based on file size and source:
62
+ - Base64 method: Limited to 10MB due to encoding overhead
63
+ - URL method: Works for files hosted online
64
+ - Local file method: Works with absolute paths only
65
+ - Large files: Automatically uses chunked uploading
66
+
67
+ Warning:
68
+ - Using taskName without listName may match multiple tasks
69
+ - If multiple matches are found, the operation will fail with a disambiguation error
70
+ - For local files, relative paths are not supported
71
+ - Base64 uploads over 10MB will automatically switch to chunked upload mode`,
53
72
  inputSchema: {
54
73
  type: "object",
55
74
  properties: {
@@ -106,9 +125,12 @@ Notes:
106
125
  */
107
126
  async function attachTaskFileHandler(params) {
108
127
  // Extract common parameters
109
- const { taskId, taskName, listName, file_name, file_data, file_url, auth_header, chunk_total, chunk_size, chunk_index, session_id } = params;
128
+ const { taskId, taskName, listName, customTaskId, file_name, file_data, file_url, auth_header, chunk_total, chunk_size, chunk_index, session_id } = params;
110
129
  // Validate task identification
111
- validateTaskIdentification(params);
130
+ const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { useGlobalLookup: true });
131
+ if (!validationResult.isValid) {
132
+ throw new Error(validationResult.errorMessage);
133
+ }
112
134
  // Validate file source - either file_data or file_url must be provided
113
135
  if (!file_data && !file_url && !session_id) {
114
136
  throw new Error("Either file_data, file_url, or session_id must be provided");
@@ -134,13 +156,13 @@ async function attachTaskFileHandler(params) {
134
156
  // CASE 2: URL-based upload or local file path
135
157
  if (file_url) {
136
158
  // Check if it's a local file path
137
- console.log(`Checking if path is local: ${file_url}`);
159
+ logger.debug(`Checking if path is local: ${file_url}`);
138
160
  if (file_url.startsWith('/') || /^[A-Za-z]:\\/.test(file_url)) {
139
- console.log(`Detected as local path, proceeding to handle: ${file_url}`);
161
+ logger.debug(`Detected as local path, proceeding to handle: ${file_url}`);
140
162
  return await handleLocalFileUpload(resolvedTaskId, file_url, file_name);
141
163
  }
142
164
  else if (file_url.startsWith('http://') || file_url.startsWith('https://')) {
143
- console.log(`Detected as URL, proceeding with URL upload: ${file_url}`);
165
+ logger.debug(`Detected as URL, proceeding with URL upload: ${file_url}`);
144
166
  return await handleUrlUpload(resolvedTaskId, file_url, file_name, auth_header);
145
167
  }
146
168
  else {
@@ -167,7 +189,7 @@ async function attachTaskFileHandler(params) {
167
189
  throw new Error("Invalid parameters: Unable to determine upload method");
168
190
  }
169
191
  catch (error) {
170
- console.error(`Error attaching file to task:`, error);
192
+ logger.error(`Error attaching file to task:`, error);
171
193
  throw error;
172
194
  }
173
195
  }
@@ -315,7 +337,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
315
337
  // Import fs and path modules
316
338
  const fs = await import('fs');
317
339
  const path = await import('path');
318
- console.log(`Processing absolute file path: ${filePath}`);
340
+ logger.debug(`Processing absolute file path: ${filePath}`);
319
341
  // Normalize the path to prevent directory traversal attacks
320
342
  const normalizedPath = path.normalize(filePath);
321
343
  // Check if file exists
@@ -332,7 +354,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
332
354
  // Read file
333
355
  const fileBuffer = fs.readFileSync(normalizedPath);
334
356
  const fileSize = fileBuffer.length;
335
- console.log(`Successfully read file: ${extractedFileName} (${fileSize} bytes)`);
357
+ logger.debug(`Successfully read file: ${extractedFileName} (${fileSize} bytes)`);
336
358
  // Choose upload method based on file size
337
359
  if (fileSize > 10 * 1024 * 1024) {
338
360
  // For large files, start chunked upload process
@@ -81,11 +81,13 @@ Valid Usage:
81
81
 
82
82
  Requirements:
83
83
  - tasks: REQUIRED (array of tasks, each with at least a name)
84
- - List identification: EITHER listId OR listName REQUIRED
84
+ - EITHER listId OR listName: REQUIRED
85
+ - All tasks will be created in the specified list
85
86
 
86
87
  Notes:
87
- - Configure batch processing via options parameter
88
- - Custom fields supported for each task`,
88
+ - Configure batch size and concurrency via options for performance
89
+ - Each task should have a name with emoji prefix
90
+ - Custom fields can be set for each task using the custom_fields property (array of {id, value} objects)`,
89
91
  inputSchema: {
90
92
  type: "object",
91
93
  properties: {
@@ -168,16 +170,22 @@ export const updateBulkTasksTool = {
168
170
  description: `Purpose: Update multiple tasks efficiently in a single operation.
169
171
 
170
172
  Valid Usage:
171
- 1. Provide array of tasks with taskId (preferred)
172
- 2. Provide array of tasks with taskName + listName
173
+ 1. For each task, provide taskId (preferred)
174
+ 2. For each task, provide taskName + listName
173
175
 
174
176
  Requirements:
175
177
  - tasks: REQUIRED (array of tasks to update)
176
- - Each task needs identification and update fields
178
+ - For each task entry, EITHER taskId OR (taskName + listName) is REQUIRED
179
+ - At least one update field per task (name, description, status, priority, dueDate)
177
180
 
178
181
  Notes:
179
182
  - Only specified fields will be updated for each task
180
- - Configure batch processing via options parameter`,
183
+ - Configure batch size and concurrency via options for performance
184
+ - Each task can have different fields to update
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`,
181
189
  inputSchema: {
182
190
  type: "object",
183
191
  properties: {
@@ -261,14 +269,21 @@ export const moveBulkTasksTool = {
261
269
  description: `Purpose: Move multiple tasks to a different list efficiently.
262
270
 
263
271
  Valid Usage:
264
- 1. Provide tasks array + target list
272
+ 1. For each task, provide taskId + target list (preferred)
273
+ 2. For each task, provide taskName + listName + target list
265
274
 
266
275
  Requirements:
267
- - tasks: REQUIRED (array of task identifiers)
268
- - Target list: EITHER targetListId OR targetListName REQUIRED
276
+ - tasks: REQUIRED (array of tasks to move)
277
+ - EITHER targetListId OR targetListName: REQUIRED
278
+ - For each task entry, EITHER taskId OR (taskName + listName) is REQUIRED
279
+
280
+ Notes:
281
+ - Configure batch size and concurrency via options for performance
282
+ - All tasks will be moved to the same destination list
269
283
 
270
284
  Warning:
271
- - Task statuses may reset if destination list has different status options`,
285
+ - Task statuses may reset if destination list has different status options
286
+ - Using taskName without listName will fail as tasks may have identical names across lists`,
272
287
  inputSchema: {
273
288
  type: "object",
274
289
  properties: {
@@ -318,15 +333,20 @@ export const deleteBulkTasksTool = {
318
333
  description: `Purpose: PERMANENTLY DELETE multiple tasks at once.
319
334
 
320
335
  Valid Usage:
321
- 1. Provide array of tasks with taskId (preferred)
322
- 2. Provide array of tasks with taskName + listName
336
+ 1. For each task, provide taskId (preferred and safest)
337
+ 2. For each task, provide taskName + listName
323
338
 
324
339
  Requirements:
325
- - tasks: REQUIRED (array of task identifiers)
340
+ - tasks: REQUIRED (array of tasks to delete)
341
+ - For each task entry, EITHER taskId OR (taskName + listName) is REQUIRED
342
+
343
+ Notes:
344
+ - Configure batch size and concurrency via options for performance
326
345
 
327
346
  Warning:
328
- - This action CANNOT be undone
329
- - Always provide listName when using taskName`,
347
+ - This action CANNOT be undone for any of the tasks
348
+ - Using taskName without listName is dangerous as names may not be unique
349
+ - Always provide listName when using taskName for safer targeting`,
330
350
  inputSchema: {
331
351
  type: "object",
332
352
  properties: {