@taazkareem/clickup-mcp-server 0.6.4 → 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.
@@ -7,6 +7,9 @@
7
7
  * Handles workspace hierarchy and space-related operations
8
8
  */
9
9
  import { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
10
+ import { Logger } from '../../logger.js';
11
+ // Create a logger instance for workspace service
12
+ const logger = new Logger('WorkspaceService');
10
13
  /**
11
14
  * Service for workspace-related operations
12
15
  */
@@ -29,8 +32,7 @@ export class WorkspaceService extends BaseClickUpService {
29
32
  * @returns - A standardized ClickUpServiceError
30
33
  */
31
34
  handleError(error, message) {
32
- // Log the error for debugging
33
- console.error('WorkspaceService error:', error);
35
+ logger.error('WorkspaceService error:', error);
34
36
  // If the error is already a ClickUpServiceError, return it
35
37
  if (error instanceof ClickUpServiceError) {
36
38
  return error;
@@ -104,11 +106,11 @@ export class WorkspaceService extends BaseClickUpService {
104
106
  try {
105
107
  // If we have the hierarchy in memory and not forcing refresh, return it
106
108
  if (this.workspaceHierarchy && !forceRefresh) {
107
- this.logger.debug('Returning cached workspace hierarchy');
109
+ logger.debug('Returning cached workspace hierarchy');
108
110
  return this.workspaceHierarchy;
109
111
  }
110
112
  const startTime = Date.now();
111
- this.logger.info('Starting workspace hierarchy fetch');
113
+ logger.info('Starting workspace hierarchy fetch');
112
114
  // Start building the workspace tree
113
115
  const workspaceTree = {
114
116
  root: {
@@ -121,7 +123,7 @@ export class WorkspaceService extends BaseClickUpService {
121
123
  const spacesStartTime = Date.now();
122
124
  const spaces = await this.getSpaces();
123
125
  const spacesTime = Date.now() - spacesStartTime;
124
- this.logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
126
+ logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
125
127
  // Process spaces in batches to respect rate limits
126
128
  const batchSize = 3; // Process 3 spaces at a time
127
129
  const spaceNodes = [];
@@ -130,7 +132,7 @@ export class WorkspaceService extends BaseClickUpService {
130
132
  for (let i = 0; i < spaces.length; i += batchSize) {
131
133
  const batchStartTime = Date.now();
132
134
  const spaceBatch = spaces.slice(i, i + batchSize);
133
- this.logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
135
+ logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
134
136
  const batchNodes = await Promise.all(spaceBatch.map(async (space) => {
135
137
  const spaceStartTime = Date.now();
136
138
  const spaceNode = {
@@ -173,12 +175,12 @@ export class WorkspaceService extends BaseClickUpService {
173
175
  }));
174
176
  folderNodes.push(...batchFolderNodes);
175
177
  const folderBatchTime = Date.now() - folderBatchStartTime;
176
- this.logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
178
+ logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
177
179
  }
178
180
  // Add folder nodes to space
179
181
  spaceNode.children?.push(...folderNodes);
180
182
  // Add folderless lists to space
181
- this.logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
183
+ logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
182
184
  const listNodes = listsInSpace.map(list => ({
183
185
  id: list.id,
184
186
  name: list.name,
@@ -187,17 +189,17 @@ export class WorkspaceService extends BaseClickUpService {
187
189
  }));
188
190
  spaceNode.children?.push(...listNodes);
189
191
  const spaceTime = Date.now() - spaceStartTime;
190
- this.logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
192
+ logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
191
193
  return spaceNode;
192
194
  }));
193
195
  spaceNodes.push(...batchNodes);
194
196
  const batchTime = Date.now() - batchStartTime;
195
- this.logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
197
+ logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
196
198
  }
197
199
  // Add all space nodes to the workspace tree
198
200
  workspaceTree.root.children.push(...spaceNodes);
199
201
  const totalTime = Date.now() - startTime;
200
- this.logger.info('Workspace hierarchy fetch completed', {
202
+ logger.info('Workspace hierarchy fetch completed', {
201
203
  duration: totalTime,
202
204
  spaces: spaces.length,
203
205
  folders: totalFolders,
@@ -293,7 +295,7 @@ export class WorkspaceService extends BaseClickUpService {
293
295
  try {
294
296
  // The /space/{space_id}/list endpoint already returns folderless lists only
295
297
  const lists = await this.getFolderlessLists(spaceId);
296
- this.logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
298
+ logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
297
299
  // Return all lists without filtering since the API already returns folderless lists
298
300
  return lists;
299
301
  }
@@ -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
  //=============================================================================
@@ -201,17 +204,18 @@ export const addTagToTaskTool = {
201
204
 
202
205
  Valid Usage:
203
206
  1. Provide taskId (preferred if available)
204
- 2. Provide taskName + listName
207
+ 2. Provide taskName (optionally with listName for disambiguation)
205
208
 
206
209
  Requirements:
207
210
  - tagName: REQUIRED
208
- - EITHER taskId OR (taskName + listName): REQUIRED
211
+ - EITHER taskId OR customTaskId OR taskName: REQUIRED
209
212
  - The tag MUST exist in the space containing the task before calling this tool
210
213
 
211
214
  Warning:
212
215
  - The operation will fail if the tag does not exist in the space
213
216
  - Always use get_space_tags first to verify the tag exists
214
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
215
219
 
216
220
  Notes:
217
221
  - Use get_space_tags to see available tags
@@ -229,11 +233,11 @@ Notes:
229
233
  },
230
234
  taskName: {
231
235
  type: "string",
232
- 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."
233
237
  },
234
238
  listName: {
235
239
  type: "string",
236
- 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."
237
241
  },
238
242
  tagName: {
239
243
  type: "string",
@@ -252,15 +256,16 @@ export const removeTagFromTaskTool = {
252
256
 
253
257
  Valid Usage:
254
258
  1. Provide taskId (preferred if available)
255
- 2. Provide taskName + listName
259
+ 2. Provide taskName (optionally with listName for disambiguation)
256
260
 
257
261
  Requirements:
258
262
  - tagName: REQUIRED
259
- - EITHER taskId OR (taskName + listName): REQUIRED
263
+ - EITHER taskId OR customTaskId OR taskName: REQUIRED
260
264
 
261
265
  Notes:
262
266
  - This only removes the association between the tag and task
263
- - 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`,
264
269
  inputSchema: {
265
270
  type: "object",
266
271
  properties: {
@@ -274,11 +279,11 @@ Notes:
274
279
  },
275
280
  taskName: {
276
281
  type: "string",
277
- 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."
278
283
  },
279
284
  listName: {
280
285
  type: "string",
281
- 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."
282
287
  },
283
288
  tagName: {
284
289
  type: "string",
@@ -341,10 +346,18 @@ export const handleDeleteSpaceTag = createHandlerWrapper(deleteSpaceTag, () => (
341
346
  /**
342
347
  * Wrapper for addTagToTask handler
343
348
  */
344
- export const handleAddTagToTask = createHandlerWrapper(addTagToTask, () => ({
345
- success: true,
346
- message: "Tag added to task successfully"
347
- }));
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
+ });
348
361
  /**
349
362
  * Wrapper for removeTagFromTask handler
350
363
  */
@@ -673,28 +686,42 @@ export async function deleteSpaceTag(params) {
673
686
  */
674
687
  async function resolveTaskId(params) {
675
688
  const { taskId, customTaskId, taskName, listName } = params;
676
- // If we have a direct taskId, use it
677
- if (taskId) {
678
- return { success: true, taskId };
679
- }
680
- // Custom task ID handling
681
- if (customTaskId) {
682
- 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 };
683
714
  }
684
- // Task name lookup (requires list name)
685
- if (taskName && listName) {
686
- // Implementation would go here
715
+ catch (error) {
687
716
  return {
688
717
  success: false,
689
- 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
+ }
690
723
  };
691
724
  }
692
- return {
693
- success: false,
694
- error: {
695
- message: 'Task identifier is required (taskId, customTaskId, or taskName+listName)'
696
- }
697
- };
698
725
  }
699
726
  /**
700
727
  * Add a tag to a task
@@ -712,12 +739,12 @@ export async function addTagToTask(params) {
712
739
  }
713
740
  };
714
741
  }
715
- if (!taskId && !customTaskId && !(taskName && listName)) {
716
- logger.error('addTagToTask called without proper task identifier');
742
+ if (!taskId && !customTaskId && !taskName) {
743
+ logger.error('addTagToTask called without task identifier');
717
744
  return {
718
745
  success: false,
719
746
  error: {
720
- message: 'Either taskId, customTaskId, or both taskName and listName are required'
747
+ message: 'Either taskId, customTaskId, or taskName is required'
721
748
  }
722
749
  };
723
750
  }
@@ -735,6 +762,31 @@ export async function addTagToTask(params) {
735
762
  const result = await clickUpServices.tag.addTagToTask(taskIdResult.taskId, tagName);
736
763
  if (!result.success) {
737
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
+ }
738
790
  return {
739
791
  success: false,
740
792
  error: result.error || {
@@ -775,12 +827,12 @@ export async function removeTagFromTask(params) {
775
827
  }
776
828
  };
777
829
  }
778
- if (!taskId && !customTaskId && !(taskName && listName)) {
779
- logger.error('removeTagFromTask called without proper task identifier');
830
+ if (!taskId && !customTaskId && !taskName) {
831
+ logger.error('removeTagFromTask called without task identifier');
780
832
  return {
781
833
  success: false,
782
834
  error: {
783
- message: 'Either taskId, customTaskId, or both taskName and listName are required'
835
+ message: 'Either taskId, customTaskId, or taskName is required'
784
836
  }
785
837
  };
786
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
@@ -122,9 +125,12 @@ Warning:
122
125
  */
123
126
  async function attachTaskFileHandler(params) {
124
127
  // Extract common parameters
125
- 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;
126
129
  // Validate task identification
127
- validateTaskIdentification(params);
130
+ const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { useGlobalLookup: true });
131
+ if (!validationResult.isValid) {
132
+ throw new Error(validationResult.errorMessage);
133
+ }
128
134
  // Validate file source - either file_data or file_url must be provided
129
135
  if (!file_data && !file_url && !session_id) {
130
136
  throw new Error("Either file_data, file_url, or session_id must be provided");
@@ -150,13 +156,13 @@ async function attachTaskFileHandler(params) {
150
156
  // CASE 2: URL-based upload or local file path
151
157
  if (file_url) {
152
158
  // Check if it's a local file path
153
- console.log(`Checking if path is local: ${file_url}`);
159
+ logger.debug(`Checking if path is local: ${file_url}`);
154
160
  if (file_url.startsWith('/') || /^[A-Za-z]:\\/.test(file_url)) {
155
- console.log(`Detected as local path, proceeding to handle: ${file_url}`);
161
+ logger.debug(`Detected as local path, proceeding to handle: ${file_url}`);
156
162
  return await handleLocalFileUpload(resolvedTaskId, file_url, file_name);
157
163
  }
158
164
  else if (file_url.startsWith('http://') || file_url.startsWith('https://')) {
159
- console.log(`Detected as URL, proceeding with URL upload: ${file_url}`);
165
+ logger.debug(`Detected as URL, proceeding with URL upload: ${file_url}`);
160
166
  return await handleUrlUpload(resolvedTaskId, file_url, file_name, auth_header);
161
167
  }
162
168
  else {
@@ -183,7 +189,7 @@ async function attachTaskFileHandler(params) {
183
189
  throw new Error("Invalid parameters: Unable to determine upload method");
184
190
  }
185
191
  catch (error) {
186
- console.error(`Error attaching file to task:`, error);
192
+ logger.error(`Error attaching file to task:`, error);
187
193
  throw error;
188
194
  }
189
195
  }
@@ -331,7 +337,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
331
337
  // Import fs and path modules
332
338
  const fs = await import('fs');
333
339
  const path = await import('path');
334
- console.log(`Processing absolute file path: ${filePath}`);
340
+ logger.debug(`Processing absolute file path: ${filePath}`);
335
341
  // Normalize the path to prevent directory traversal attacks
336
342
  const normalizedPath = path.normalize(filePath);
337
343
  // Check if file exists
@@ -348,7 +354,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
348
354
  // Read file
349
355
  const fileBuffer = fs.readFileSync(normalizedPath);
350
356
  const fileSize = fileBuffer.length;
351
- console.log(`Successfully read file: ${extractedFileName} (${fileSize} bytes)`);
357
+ logger.debug(`Successfully read file: ${extractedFileName} (${fileSize} bytes)`);
352
358
  // Choose upload method based on file size
353
359
  if (fileSize > 10 * 1024 * 1024) {
354
360
  // For large files, start chunked upload process