@alanse/clickup-multi-mcp-server 1.0.0

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.
Files changed (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -0,0 +1,833 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Tag Tools
6
+ *
7
+ * Provides tools for managing tags in ClickUp:
8
+ * - Get space tags
9
+ * - Create, update, delete space tags
10
+ * - Add/remove tags to/from tasks
11
+ */
12
+ import { clickUpServices } from '../services/shared.js';
13
+ import { Logger } from '../logger.js';
14
+ import { sponsorService } from '../utils/sponsor-service.js';
15
+ import { processColorCommand } from '../utils/color-processor.js';
16
+ import { validateTaskIdentification } from './task/utilities.js';
17
+ // Create a logger specific to tag tools
18
+ const logger = new Logger('TagTools');
19
+ // Use shared services instance
20
+ const { task: taskService } = clickUpServices;
21
+ //=============================================================================
22
+ // TOOL DEFINITIONS
23
+ //=============================================================================
24
+ /**
25
+ * Tool definition for getting space tags
26
+ */
27
+ export const getSpaceTagsTool = {
28
+ name: "get_space_tags",
29
+ description: `Gets all tags in a ClickUp space. Use spaceId (preferred) or spaceName. Tags are defined at space level - check available tags before adding to tasks.`,
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ spaceId: {
34
+ type: "string",
35
+ description: "ID of the space to get tags from. Use this instead of spaceName if you have the ID."
36
+ },
37
+ spaceName: {
38
+ type: "string",
39
+ description: "Name of the space to get tags from. Only use if you don't have spaceId."
40
+ }
41
+ }
42
+ }
43
+ };
44
+ /**
45
+ * Tool definition for creating a tag in a space
46
+ */
47
+ export const createSpaceTagTool = {
48
+ name: "create_space_tag",
49
+ description: `Purpose: Create a new tag in a ClickUp space.
50
+
51
+ Valid Usage:
52
+ 1. Provide spaceId (preferred if available)
53
+ 2. Provide spaceName (will be resolved to a space ID)
54
+
55
+ Requirements:
56
+ - tagName: REQUIRED
57
+ - EITHER spaceId OR spaceName: REQUIRED
58
+
59
+ Notes:
60
+ - New tag will be available for all tasks in the space
61
+ - You can specify background and foreground colors in HEX format (e.g., #FF0000)
62
+ - You can also provide a color command (e.g., "blue tag") to automatically generate colors
63
+ - After creating a tag, you can add it to tasks using add_tag_to_task`,
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ spaceId: {
68
+ type: "string",
69
+ description: "ID of the space to create tag in. Use this instead of spaceName if you have the ID."
70
+ },
71
+ spaceName: {
72
+ type: "string",
73
+ description: "Name of the space to create tag in. Only use if you don't have spaceId."
74
+ },
75
+ tagName: {
76
+ type: "string",
77
+ description: "Name of the tag to create."
78
+ },
79
+ tagBg: {
80
+ type: "string",
81
+ description: "Background color for the tag in HEX format (e.g., #FF0000). Defaults to #000000 (black)."
82
+ },
83
+ tagFg: {
84
+ type: "string",
85
+ description: "Foreground (text) color for the tag in HEX format (e.g., #FFFFFF). Defaults to #FFFFFF (white)."
86
+ },
87
+ colorCommand: {
88
+ type: "string",
89
+ description: "Natural language color command (e.g., 'blue tag', 'dark red background'). When provided, this will override tagBg and tagFg with automatically generated values."
90
+ }
91
+ },
92
+ required: ["tagName"]
93
+ }
94
+ };
95
+ /**
96
+ * Tool definition for updating a tag in a space
97
+ */
98
+ export const updateSpaceTagTool = {
99
+ name: "update_space_tag",
100
+ description: `Purpose: Update an existing tag in a ClickUp space.
101
+
102
+ Valid Usage:
103
+ 1. Provide spaceId (preferred if available)
104
+ 2. Provide spaceName (will be resolved to a space ID)
105
+
106
+ Requirements:
107
+ - tagName: REQUIRED
108
+ - EITHER spaceId OR spaceName: REQUIRED
109
+ - At least one of newTagName, tagBg, tagFg, or colorCommand must be provided
110
+
111
+ Notes:
112
+ - Changes to the tag will apply to all tasks in the space that use this tag
113
+ - You can provide a color command (e.g., "blue tag") to automatically generate colors
114
+ - You cannot partially update a tag - provide all properties you want to keep`,
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ spaceId: {
119
+ type: "string",
120
+ description: "ID of the space containing the tag. Use this instead of spaceName if you have the ID."
121
+ },
122
+ spaceName: {
123
+ type: "string",
124
+ description: "Name of the space containing the tag. Only use if you don't have spaceId."
125
+ },
126
+ tagName: {
127
+ type: "string",
128
+ description: "Current name of the tag to update."
129
+ },
130
+ newTagName: {
131
+ type: "string",
132
+ description: "New name for the tag."
133
+ },
134
+ tagBg: {
135
+ type: "string",
136
+ description: "New background color for the tag in HEX format (e.g., #FF0000)."
137
+ },
138
+ tagFg: {
139
+ type: "string",
140
+ description: "New foreground (text) color for the tag in HEX format (e.g., #FFFFFF)."
141
+ },
142
+ colorCommand: {
143
+ type: "string",
144
+ description: "Natural language color command (e.g., 'blue tag', 'dark red background'). When provided, this will override tagBg and tagFg with automatically generated values."
145
+ }
146
+ },
147
+ required: ["tagName"]
148
+ }
149
+ };
150
+ /**
151
+ * Tool definition for deleting a tag in a space
152
+ */
153
+ export const deleteSpaceTagTool = {
154
+ name: "delete_space_tag",
155
+ description: `Purpose: Delete a tag from a ClickUp space.
156
+
157
+ Valid Usage:
158
+ 1. Provide spaceId (preferred if available)
159
+ 2. Provide spaceName (will be resolved to a space ID)
160
+
161
+ Requirements:
162
+ - tagName: REQUIRED
163
+ - EITHER spaceId OR spaceName: REQUIRED
164
+
165
+ Warning:
166
+ - This will remove the tag from all tasks in the space
167
+ - This action cannot be undone`,
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ spaceId: {
172
+ type: "string",
173
+ description: "ID of the space containing the tag. Use this instead of spaceName if you have the ID."
174
+ },
175
+ spaceName: {
176
+ type: "string",
177
+ description: "Name of the space containing the tag. Only use if you don't have spaceId."
178
+ },
179
+ tagName: {
180
+ type: "string",
181
+ description: "Name of the tag to delete."
182
+ }
183
+ },
184
+ required: ["tagName"]
185
+ }
186
+ };
187
+ /**
188
+ * Tool definition for adding a tag to a task
189
+ */
190
+ export const addTagToTaskTool = {
191
+ name: "add_tag_to_task",
192
+ description: `Adds existing tag to task. Use taskId (preferred) or taskName + optional listName. Tag must exist in space (use get_space_tags to verify, create_space_tag if needed). WARNING: Will fail if tag doesn't exist.`,
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ taskId: {
197
+ type: "string",
198
+ description: "ID of the task to add tag to. Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
199
+ },
200
+ customTaskId: {
201
+ type: "string",
202
+ 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."
203
+ },
204
+ taskName: {
205
+ type: "string",
206
+ description: "Name of the task to add tag to. Will search across all lists unless listName is provided."
207
+ },
208
+ listName: {
209
+ type: "string",
210
+ description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
211
+ },
212
+ tagName: {
213
+ type: "string",
214
+ description: "Name of the tag to add to the task. The tag must already exist in the space."
215
+ }
216
+ },
217
+ required: ["tagName"]
218
+ }
219
+ };
220
+ /**
221
+ * Tool definition for removing a tag from a task
222
+ */
223
+ export const removeTagFromTaskTool = {
224
+ name: "remove_tag_from_task",
225
+ description: `Removes tag from task. Use taskId (preferred) or taskName + optional listName. Only removes tag-task association, tag remains in space. For multiple tasks, provide listName to disambiguate.`,
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ taskId: {
230
+ type: "string",
231
+ description: "ID of the task to remove tag from. Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
232
+ },
233
+ customTaskId: {
234
+ type: "string",
235
+ 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."
236
+ },
237
+ taskName: {
238
+ type: "string",
239
+ description: "Name of the task to remove tag from. Will search across all lists unless listName is provided."
240
+ },
241
+ listName: {
242
+ type: "string",
243
+ description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
244
+ },
245
+ tagName: {
246
+ type: "string",
247
+ description: "Name of the tag to remove from the task."
248
+ }
249
+ },
250
+ required: ["tagName"]
251
+ }
252
+ };
253
+ //=============================================================================
254
+ // HANDLER WRAPPER UTILITY
255
+ //=============================================================================
256
+ /**
257
+ * Creates a wrapped handler function with standard error handling and response formatting
258
+ */
259
+ function createHandlerWrapper(handler, formatResponse = (result) => result) {
260
+ return async (params) => {
261
+ try {
262
+ logger.debug('Handler called with params', { params });
263
+ // Call the handler
264
+ const result = await handler(params);
265
+ // Format the result for response
266
+ const formattedResult = formatResponse(result);
267
+ // Use the sponsor service to create the formatted response
268
+ return sponsorService.createResponse(formattedResult, true);
269
+ }
270
+ catch (error) {
271
+ // Log the error
272
+ logger.error('Error in handler', { error: error.message, code: error.code });
273
+ // Format and return the error using sponsor service
274
+ return sponsorService.createErrorResponse(error, params);
275
+ }
276
+ };
277
+ }
278
+ //=============================================================================
279
+ // TAG TOOL HANDLERS
280
+ //=============================================================================
281
+ /**
282
+ * Wrapper for getSpaceTags handler
283
+ */
284
+ export const handleGetSpaceTags = createHandlerWrapper(getSpaceTags, (tags) => ({
285
+ tags: tags || [],
286
+ count: Array.isArray(tags) ? tags.length : 0
287
+ }));
288
+ /**
289
+ * Wrapper for createSpaceTag handler
290
+ */
291
+ export const handleCreateSpaceTag = createHandlerWrapper(createSpaceTag);
292
+ /**
293
+ * Wrapper for updateSpaceTag handler
294
+ */
295
+ export const handleUpdateSpaceTag = createHandlerWrapper(updateSpaceTag);
296
+ /**
297
+ * Wrapper for deleteSpaceTag handler
298
+ */
299
+ export const handleDeleteSpaceTag = createHandlerWrapper(deleteSpaceTag, () => ({
300
+ success: true,
301
+ message: "Tag deleted successfully"
302
+ }));
303
+ /**
304
+ * Wrapper for addTagToTask handler
305
+ */
306
+ export const handleAddTagToTask = createHandlerWrapper(addTagToTask, (result) => {
307
+ if (!result.success) {
308
+ return {
309
+ success: false,
310
+ error: result.error
311
+ };
312
+ }
313
+ return {
314
+ success: true,
315
+ message: "Tag added to task successfully"
316
+ };
317
+ });
318
+ /**
319
+ * Wrapper for removeTagFromTask handler
320
+ */
321
+ export const handleRemoveTagFromTask = createHandlerWrapper(removeTagFromTask, () => ({
322
+ success: true,
323
+ message: "Tag removed from task successfully"
324
+ }));
325
+ //=============================================================================
326
+ // TOOL DEFINITIONS AND HANDLERS EXPORT
327
+ //=============================================================================
328
+ // Tool definitions with their handler mappings
329
+ export const tagTools = [
330
+ { definition: getSpaceTagsTool, handler: handleGetSpaceTags },
331
+ { definition: createSpaceTagTool, handler: handleCreateSpaceTag },
332
+ { definition: updateSpaceTagTool, handler: handleUpdateSpaceTag },
333
+ { definition: deleteSpaceTagTool, handler: handleDeleteSpaceTag },
334
+ { definition: addTagToTaskTool, handler: handleAddTagToTask },
335
+ { definition: removeTagFromTaskTool, handler: handleRemoveTagFromTask }
336
+ ];
337
+ /**
338
+ * Get all tags in a space
339
+ * @param params - Space identifier (id or name)
340
+ * @returns Tags in the space
341
+ */
342
+ export async function getSpaceTags(params) {
343
+ const { spaceId, spaceName } = params;
344
+ if (!spaceId && !spaceName) {
345
+ logger.error('getSpaceTags called without required parameters');
346
+ throw new Error('Either spaceId or spaceName is required');
347
+ }
348
+ logger.info('Getting tags for space', { spaceId, spaceName });
349
+ try {
350
+ // If spaceName is provided, we need to resolve it to an ID
351
+ let resolvedSpaceId = spaceId;
352
+ if (!resolvedSpaceId && spaceName) {
353
+ logger.debug(`Resolving space name: ${spaceName}`);
354
+ const spaces = await clickUpServices.workspace.getSpaces();
355
+ const space = spaces.find(s => s.name.toLowerCase() === spaceName.toLowerCase());
356
+ if (!space) {
357
+ logger.error(`Space not found: ${spaceName}`);
358
+ throw new Error(`Space not found: ${spaceName}`);
359
+ }
360
+ resolvedSpaceId = space.id;
361
+ }
362
+ // Get tags from the space
363
+ const tagsResponse = await clickUpServices.tag.getSpaceTags(resolvedSpaceId);
364
+ if (!tagsResponse.success) {
365
+ logger.error('Failed to get space tags', tagsResponse.error);
366
+ throw new Error(tagsResponse.error?.message || 'Failed to get space tags');
367
+ }
368
+ logger.info(`Successfully retrieved ${tagsResponse.data?.length || 0} tags`);
369
+ return tagsResponse.data || [];
370
+ }
371
+ catch (error) {
372
+ logger.error('Error in getSpaceTags', error);
373
+ throw error;
374
+ }
375
+ }
376
+ /**
377
+ * Create a new tag in a space
378
+ * @param params - Space identifier and tag details
379
+ * @returns Created tag
380
+ */
381
+ export async function createSpaceTag(params) {
382
+ let { spaceId, spaceName, tagName, tagBg = '#000000', tagFg = '#ffffff', colorCommand } = params;
383
+ // Process color command if provided
384
+ if (colorCommand) {
385
+ const colors = processColorCommand(colorCommand);
386
+ if (colors) {
387
+ tagBg = colors.background;
388
+ tagFg = colors.foreground;
389
+ logger.info(`Processed color command: "${colorCommand}" → BG: ${tagBg}, FG: ${tagFg}`);
390
+ }
391
+ else {
392
+ logger.warn(`Could not process color command: "${colorCommand}". Using default colors.`);
393
+ }
394
+ }
395
+ if (!tagName) {
396
+ logger.error('createSpaceTag called without tagName');
397
+ return {
398
+ success: false,
399
+ error: {
400
+ message: 'tagName is required'
401
+ }
402
+ };
403
+ }
404
+ if (!spaceId && !spaceName) {
405
+ logger.error('createSpaceTag called without space identifier');
406
+ return {
407
+ success: false,
408
+ error: {
409
+ message: 'Either spaceId or spaceName is required'
410
+ }
411
+ };
412
+ }
413
+ logger.info('Creating tag in space', { spaceId, spaceName, tagName, tagBg, tagFg });
414
+ try {
415
+ // If spaceName is provided, we need to resolve it to an ID
416
+ let resolvedSpaceId = spaceId;
417
+ if (!resolvedSpaceId && spaceName) {
418
+ logger.debug(`Resolving space name: ${spaceName}`);
419
+ const spaces = await clickUpServices.workspace.getSpaces();
420
+ const space = spaces.find(s => s.name.toLowerCase() === spaceName.toLowerCase());
421
+ if (!space) {
422
+ logger.error(`Space not found: ${spaceName}`);
423
+ return {
424
+ success: false,
425
+ error: {
426
+ message: `Space not found: ${spaceName}`
427
+ }
428
+ };
429
+ }
430
+ resolvedSpaceId = space.id;
431
+ }
432
+ // Create tag in the space
433
+ const tagResponse = await clickUpServices.tag.createSpaceTag(resolvedSpaceId, {
434
+ tag_name: tagName,
435
+ tag_bg: tagBg,
436
+ tag_fg: tagFg
437
+ });
438
+ if (!tagResponse.success) {
439
+ logger.error('Failed to create space tag', tagResponse.error);
440
+ return {
441
+ success: false,
442
+ error: tagResponse.error || {
443
+ message: 'Failed to create space tag'
444
+ }
445
+ };
446
+ }
447
+ logger.info(`Successfully created tag: ${tagName}`);
448
+ return {
449
+ success: true,
450
+ data: tagResponse.data
451
+ };
452
+ }
453
+ catch (error) {
454
+ logger.error('Error in createSpaceTag', error);
455
+ return {
456
+ success: false,
457
+ error: {
458
+ message: error.message || 'Failed to create space tag',
459
+ code: error.code,
460
+ details: error.data
461
+ }
462
+ };
463
+ }
464
+ }
465
+ /**
466
+ * Update an existing tag in a space
467
+ * @param params - Space identifier, tag name, and updated properties
468
+ * @returns Updated tag
469
+ */
470
+ export async function updateSpaceTag(params) {
471
+ const { spaceId, spaceName, tagName, newTagName, colorCommand } = params;
472
+ let { tagBg, tagFg } = params;
473
+ // Process color command if provided
474
+ if (colorCommand) {
475
+ const colors = processColorCommand(colorCommand);
476
+ if (colors) {
477
+ tagBg = colors.background;
478
+ tagFg = colors.foreground;
479
+ logger.info(`Processed color command: "${colorCommand}" → BG: ${tagBg}, FG: ${tagFg}`);
480
+ }
481
+ else {
482
+ logger.warn(`Could not process color command: "${colorCommand}". Using default colors.`);
483
+ }
484
+ }
485
+ if (!tagName) {
486
+ logger.error('updateSpaceTag called without tagName');
487
+ return {
488
+ success: false,
489
+ error: {
490
+ message: 'tagName is required'
491
+ }
492
+ };
493
+ }
494
+ if (!spaceId && !spaceName) {
495
+ logger.error('updateSpaceTag called without space identifier');
496
+ return {
497
+ success: false,
498
+ error: {
499
+ message: 'Either spaceId or spaceName is required'
500
+ }
501
+ };
502
+ }
503
+ // Make sure there's at least one property to update
504
+ if (!newTagName && !tagBg && !tagFg && !colorCommand) {
505
+ logger.error('updateSpaceTag called without properties to update');
506
+ return {
507
+ success: false,
508
+ error: {
509
+ message: 'At least one property (newTagName, tagBg, tagFg, or colorCommand) must be provided'
510
+ }
511
+ };
512
+ }
513
+ logger.info('Updating tag in space', { spaceId, spaceName, tagName, newTagName, tagBg, tagFg });
514
+ try {
515
+ // If spaceName is provided, we need to resolve it to an ID
516
+ let resolvedSpaceId = spaceId;
517
+ if (!resolvedSpaceId && spaceName) {
518
+ logger.debug(`Resolving space name: ${spaceName}`);
519
+ const spaces = await clickUpServices.workspace.getSpaces();
520
+ const space = spaces.find(s => s.name.toLowerCase() === spaceName.toLowerCase());
521
+ if (!space) {
522
+ logger.error(`Space not found: ${spaceName}`);
523
+ return {
524
+ success: false,
525
+ error: {
526
+ message: `Space not found: ${spaceName}`
527
+ }
528
+ };
529
+ }
530
+ resolvedSpaceId = space.id;
531
+ }
532
+ // Prepare update data
533
+ const updateData = {};
534
+ if (newTagName)
535
+ updateData.tag_name = newTagName;
536
+ if (tagBg)
537
+ updateData.tag_bg = tagBg;
538
+ if (tagFg)
539
+ updateData.tag_fg = tagFg;
540
+ // Update tag in the space
541
+ const tagResponse = await clickUpServices.tag.updateSpaceTag(resolvedSpaceId, tagName, updateData);
542
+ if (!tagResponse.success) {
543
+ logger.error('Failed to update space tag', tagResponse.error);
544
+ return {
545
+ success: false,
546
+ error: tagResponse.error || {
547
+ message: 'Failed to update space tag'
548
+ }
549
+ };
550
+ }
551
+ logger.info(`Successfully updated tag: ${tagName}`);
552
+ return {
553
+ success: true,
554
+ data: tagResponse.data
555
+ };
556
+ }
557
+ catch (error) {
558
+ logger.error('Error in updateSpaceTag', error);
559
+ return {
560
+ success: false,
561
+ error: {
562
+ message: error.message || 'Failed to update space tag',
563
+ code: error.code,
564
+ details: error.data
565
+ }
566
+ };
567
+ }
568
+ }
569
+ /**
570
+ * Delete a tag from a space
571
+ * @param params - Space identifier and tag name
572
+ * @returns Success status
573
+ */
574
+ export async function deleteSpaceTag(params) {
575
+ const { spaceId, spaceName, tagName } = params;
576
+ if (!tagName) {
577
+ logger.error('deleteSpaceTag called without tagName');
578
+ return {
579
+ success: false,
580
+ error: {
581
+ message: 'tagName is required'
582
+ }
583
+ };
584
+ }
585
+ if (!spaceId && !spaceName) {
586
+ logger.error('deleteSpaceTag called without space identifier');
587
+ return {
588
+ success: false,
589
+ error: {
590
+ message: 'Either spaceId or spaceName is required'
591
+ }
592
+ };
593
+ }
594
+ logger.info('Deleting tag from space', { spaceId, spaceName, tagName });
595
+ try {
596
+ // If spaceName is provided, we need to resolve it to an ID
597
+ let resolvedSpaceId = spaceId;
598
+ if (!resolvedSpaceId && spaceName) {
599
+ logger.debug(`Resolving space name: ${spaceName}`);
600
+ const spaces = await clickUpServices.workspace.getSpaces();
601
+ const space = spaces.find(s => s.name.toLowerCase() === spaceName.toLowerCase());
602
+ if (!space) {
603
+ logger.error(`Space not found: ${spaceName}`);
604
+ return {
605
+ success: false,
606
+ error: {
607
+ message: `Space not found: ${spaceName}`
608
+ }
609
+ };
610
+ }
611
+ resolvedSpaceId = space.id;
612
+ }
613
+ // Delete tag from the space
614
+ const tagResponse = await clickUpServices.tag.deleteSpaceTag(resolvedSpaceId, tagName);
615
+ if (!tagResponse.success) {
616
+ logger.error('Failed to delete space tag', tagResponse.error);
617
+ return {
618
+ success: false,
619
+ error: tagResponse.error || {
620
+ message: 'Failed to delete space tag'
621
+ }
622
+ };
623
+ }
624
+ logger.info(`Successfully deleted tag: ${tagName}`);
625
+ return {
626
+ success: true
627
+ };
628
+ }
629
+ catch (error) {
630
+ logger.error('Error in deleteSpaceTag', error);
631
+ return {
632
+ success: false,
633
+ error: {
634
+ message: error.message || 'Failed to delete space tag',
635
+ code: error.code,
636
+ details: error.data
637
+ }
638
+ };
639
+ }
640
+ }
641
+ /**
642
+ * Simple task ID resolver
643
+ */
644
+ async function resolveTaskId(params) {
645
+ const { taskId, customTaskId, taskName, listName } = params;
646
+ try {
647
+ // First validate task identification with global lookup enabled
648
+ const validationResult = validateTaskIdentification({ taskId, customTaskId, taskName, listName }, { useGlobalLookup: true });
649
+ if (!validationResult.isValid) {
650
+ return {
651
+ success: false,
652
+ error: { message: validationResult.errorMessage }
653
+ };
654
+ }
655
+ const result = await taskService.findTasks({
656
+ taskId,
657
+ customTaskId,
658
+ taskName,
659
+ listName,
660
+ allowMultipleMatches: false,
661
+ useSmartDisambiguation: true,
662
+ includeFullDetails: false
663
+ });
664
+ if (!result || Array.isArray(result)) {
665
+ return {
666
+ success: false,
667
+ error: { message: 'Task not found with the provided identification' }
668
+ };
669
+ }
670
+ return { success: true, taskId: result.id };
671
+ }
672
+ catch (error) {
673
+ return {
674
+ success: false,
675
+ error: {
676
+ message: error.message || 'Failed to resolve task ID',
677
+ code: error.code,
678
+ details: error.data
679
+ }
680
+ };
681
+ }
682
+ }
683
+ /**
684
+ * Add a tag to a task
685
+ * @param params - Task identifier and tag name
686
+ * @returns Success status
687
+ */
688
+ export async function addTagToTask(params) {
689
+ const { taskId, customTaskId, taskName, listName, tagName } = params;
690
+ if (!tagName) {
691
+ logger.error('addTagToTask called without tagName');
692
+ return {
693
+ success: false,
694
+ error: {
695
+ message: 'tagName is required'
696
+ }
697
+ };
698
+ }
699
+ if (!taskId && !customTaskId && !taskName) {
700
+ logger.error('addTagToTask called without task identifier');
701
+ return {
702
+ success: false,
703
+ error: {
704
+ message: 'Either taskId, customTaskId, or taskName is required'
705
+ }
706
+ };
707
+ }
708
+ logger.info('Adding tag to task', { taskId, customTaskId, taskName, listName, tagName });
709
+ try {
710
+ // Resolve the task ID
711
+ const taskIdResult = await resolveTaskId({ taskId, customTaskId, taskName, listName });
712
+ if (!taskIdResult.success) {
713
+ return {
714
+ success: false,
715
+ error: taskIdResult.error
716
+ };
717
+ }
718
+ // Add tag to the task
719
+ const result = await clickUpServices.tag.addTagToTask(taskIdResult.taskId, tagName);
720
+ if (!result.success) {
721
+ logger.error('Failed to add tag to task', result.error);
722
+ // Provide more specific error messages based on error code
723
+ if (result.error?.code === 'TAG_NOT_FOUND') {
724
+ return {
725
+ success: false,
726
+ error: {
727
+ message: `The tag "${tagName}" does not exist in the space. Please create it first using create_space_tag.`
728
+ }
729
+ };
730
+ }
731
+ else if (result.error?.code === 'SPACE_NOT_FOUND') {
732
+ return {
733
+ success: false,
734
+ error: {
735
+ message: 'Could not determine which space the task belongs to.'
736
+ }
737
+ };
738
+ }
739
+ else if (result.error?.code === 'TAG_VERIFICATION_FAILED') {
740
+ return {
741
+ success: false,
742
+ error: {
743
+ message: 'The tag addition could not be verified. Please check if the tag was added manually.'
744
+ }
745
+ };
746
+ }
747
+ return {
748
+ success: false,
749
+ error: result.error || {
750
+ message: 'Failed to add tag to task'
751
+ }
752
+ };
753
+ }
754
+ logger.info(`Successfully added tag "${tagName}" to task ${taskIdResult.taskId}`);
755
+ return {
756
+ success: true
757
+ };
758
+ }
759
+ catch (error) {
760
+ logger.error('Error in addTagToTask', error);
761
+ return {
762
+ success: false,
763
+ error: {
764
+ message: error.message || 'Failed to add tag to task',
765
+ code: error.code,
766
+ details: error.data
767
+ }
768
+ };
769
+ }
770
+ }
771
+ /**
772
+ * Remove a tag from a task
773
+ * @param params - Task identifier and tag name
774
+ * @returns Success status
775
+ */
776
+ export async function removeTagFromTask(params) {
777
+ const { taskId, customTaskId, taskName, listName, tagName } = params;
778
+ if (!tagName) {
779
+ logger.error('removeTagFromTask called without tagName');
780
+ return {
781
+ success: false,
782
+ error: {
783
+ message: 'tagName is required'
784
+ }
785
+ };
786
+ }
787
+ if (!taskId && !customTaskId && !taskName) {
788
+ logger.error('removeTagFromTask called without task identifier');
789
+ return {
790
+ success: false,
791
+ error: {
792
+ message: 'Either taskId, customTaskId, or taskName is required'
793
+ }
794
+ };
795
+ }
796
+ logger.info('Removing tag from task', { taskId, customTaskId, taskName, listName, tagName });
797
+ try {
798
+ // Resolve the task ID
799
+ const taskIdResult = await resolveTaskId({ taskId, customTaskId, taskName, listName });
800
+ if (!taskIdResult.success) {
801
+ return {
802
+ success: false,
803
+ error: taskIdResult.error
804
+ };
805
+ }
806
+ // Remove tag from the task
807
+ const result = await clickUpServices.tag.removeTagFromTask(taskIdResult.taskId, tagName);
808
+ if (!result.success) {
809
+ logger.error('Failed to remove tag from task', result.error);
810
+ return {
811
+ success: false,
812
+ error: result.error || {
813
+ message: 'Failed to remove tag from task'
814
+ }
815
+ };
816
+ }
817
+ logger.info(`Successfully removed tag "${tagName}" from task ${taskIdResult.taskId}`);
818
+ return {
819
+ success: true
820
+ };
821
+ }
822
+ catch (error) {
823
+ logger.error('Error in removeTagFromTask', error);
824
+ return {
825
+ success: false,
826
+ error: {
827
+ message: error.message || 'Failed to remove tag from task',
828
+ code: error.code,
829
+ details: error.data
830
+ }
831
+ };
832
+ }
833
+ }