@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.
@@ -52,16 +52,17 @@ export const createTaskTool = {
52
52
 
53
53
  Valid Usage:
54
54
  1. Provide listId (preferred if available)
55
- 2. Provide listName (will look up the list ID)
55
+ 2. Provide listName (system will look up the list ID)
56
56
 
57
57
  Requirements:
58
58
  - name: REQUIRED
59
- - List identification: EITHER listId OR listName REQUIRED
59
+ - EITHER listId OR listName: REQUIRED
60
60
 
61
61
  Notes:
62
- - Use create_bulk_tasks for multiple tasks
63
- - Set parent parameter to create a subtask
64
- - Custom fields can be set via custom_fields parameter`,
62
+ - For multiple tasks, use create_bulk_tasks instead
63
+ - Reuse list IDs from previous responses when possible to avoid redundant lookups
64
+ - To create a subtask, set the parent parameter to the ID of the parent task
65
+ - Custom fields can be set using the custom_fields parameter (array of {id, value} objects)`,
65
66
  inputSchema: {
66
67
  type: "object",
67
68
  properties: {
@@ -144,17 +145,25 @@ export const updateTaskTool = {
144
145
  description: `Purpose: Modify properties of an existing task.
145
146
 
146
147
  Valid Usage:
147
- 1. Use taskId (preferred) - works with both regular and custom IDs
148
- 2. Use taskName + listName for targeted search
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)
149
151
 
150
152
  Requirements:
151
- - Task identification: EITHER taskId OR taskName REQUIRED
152
- - Updates: At least one update field (name, description, status, priority, dueDate) REQUIRED
153
+ - At least one update field (name, description, status, priority, dueDate) must be provided
154
+ - EITHER taskId OR taskName: REQUIRED
155
+ - listName: Optional, but recommended when using taskName
153
156
 
154
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
155
161
  - Only specified fields will be updated
156
- - Custom fields can be set using the custom_fields parameter
157
- - Using taskName without listName may match multiple tasks`,
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`,
158
167
  inputSchema: {
159
168
  type: "object",
160
169
  properties: {
@@ -227,15 +236,16 @@ export const moveTaskTool = {
227
236
  description: `Purpose: Move a task to a different list.
228
237
 
229
238
  Valid Usage:
230
- 1. Use taskId + destination list (preferred)
231
- 2. Use taskName + sourceListName + destination list
239
+ 1. Use taskId + (listId OR listName) - preferred
240
+ 2. Use taskName + sourceListName + (listId OR listName)
232
241
 
233
242
  Requirements:
234
- - Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
235
- - Destination: EITHER listId OR listName REQUIRED
243
+ - Destination list: EITHER listId OR listName REQUIRED
244
+ - When using taskName, sourceListName is REQUIRED
236
245
 
237
246
  Warning:
238
- - Task statuses may reset if destination list has different status options`,
247
+ - Task statuses may reset if destination list has different status options
248
+ - System cannot find a task by name without knowing which list to search in`,
239
249
  inputSchema: {
240
250
  type: "object",
241
251
  properties: {
@@ -271,15 +281,18 @@ export const duplicateTaskTool = {
271
281
  description: `Purpose: Create a copy of a task in the same or different list.
272
282
 
273
283
  Valid Usage:
274
- 1. Use taskId (preferred)
275
- 2. Use taskName + sourceListName
284
+ 1. Use taskId + optional (listId OR listName) - preferred
285
+ 2. Use taskName + sourceListName + optional (listId OR listName)
276
286
 
277
287
  Requirements:
278
- - Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
279
- - Destination: OPTIONAL - defaults to original list
288
+ - When using taskName, sourceListName is REQUIRED
280
289
 
281
290
  Notes:
282
- - The duplicate preserves the original task's properties`,
291
+ - The duplicate preserves the original task's properties
292
+ - If no destination list specified, uses same list as original task
293
+
294
+ Warning:
295
+ - System cannot find a task by name without knowing which list to search in`,
283
296
  inputSchema: {
284
297
  type: "object",
285
298
  properties: {
@@ -315,14 +328,20 @@ export const getTaskTool = {
315
328
  description: `Purpose: Retrieve detailed information about a specific task.
316
329
 
317
330
  Valid Usage:
318
- 1. Use taskId (preferred) - works with both regular and custom IDs
319
- 2. Use taskName + listName for targeted search
331
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs (like "DEV-1234")
332
+ 2. Use taskName alone (will search across all lists in the workspace)
333
+ 3. Use taskName + listName (for faster, targeted search)
334
+ 4. Use customTaskId for explicit custom ID lookup
320
335
 
321
336
  Requirements:
322
- - Task identification: EITHER taskId OR (taskName + listName) REQUIRED
337
+ - EITHER taskId OR taskName OR customTaskId: REQUIRED
338
+ - listName: Optional, but recommended when using taskName for faster and more precise lookup
323
339
 
324
- Notes:
325
- - Task names are only unique within a list
340
+ Note:
341
+ - When using just taskName, the system performs a global search across all lists
342
+ - Task names are most unique within a specific list, so providing listName increases reliability
343
+ - Regular task IDs are always 9 characters long (e.g., "86b394eqa")
344
+ - Custom IDs have an uppercase prefix followed by a hyphen and number (e.g., "DEV-1234")
326
345
  - Set subtasks=true to include all subtasks in the response`,
327
346
  inputSchema: {
328
347
  type: "object",
@@ -333,11 +352,11 @@ Notes:
333
352
  },
334
353
  taskName: {
335
354
  type: "string",
336
- description: "Name of task to retrieve. When using this parameter, you MUST also provide listName."
355
+ description: "Name of task to retrieve. Can be used alone for a global search, or with listName for faster lookup."
337
356
  },
338
357
  listName: {
339
358
  type: "string",
340
- description: "Name of list containing the task. REQUIRED when using taskName."
359
+ description: "Name of list containing the task. Optional but recommended when using taskName."
341
360
  },
342
361
  customTaskId: {
343
362
  type: "string",
@@ -362,11 +381,12 @@ Valid Usage:
362
381
  2. Use listName
363
382
 
364
383
  Requirements:
365
- - List identification: EITHER listId OR listName REQUIRED
384
+ - EITHER listId OR listName is REQUIRED
366
385
 
367
386
  Notes:
368
- - Use filters (archived, statuses) to narrow down results
369
- - Pagination and sorting available`,
387
+ - Use filters (archived, statuses, etc.) to narrow down results
388
+ - Pagination available through page parameter
389
+ - Sorting available through order_by and reverse parameters`,
370
390
  inputSchema: {
371
391
  type: "object",
372
392
  properties: {
@@ -416,14 +436,12 @@ export const getTaskCommentsTool = {
416
436
 
417
437
  Valid Usage:
418
438
  1. Use taskId (preferred)
419
- 2. Use taskName + listName for targeted search
420
-
421
- Requirements:
422
- - Task identification: EITHER taskId OR taskName REQUIRED
439
+ 2. Use taskName + optional listName
423
440
 
424
441
  Notes:
442
+ - If using taskName, providing listName helps locate the correct task
425
443
  - Task names may not be unique across different lists
426
- - Use start/startId parameters for pagination`,
444
+ - Use start and startId parameters for pagination through comments`,
427
445
  inputSchema: {
428
446
  type: "object",
429
447
  properties: {
@@ -462,11 +480,13 @@ Valid Usage:
462
480
  2. Use taskName + listName
463
481
 
464
482
  Requirements:
465
- - Task identification: EITHER taskId OR (taskName + listName) REQUIRED
466
- - commentText: REQUIRED
483
+ - EITHER taskId OR (taskName + listName) is REQUIRED
484
+ - commentText is REQUIRED
467
485
 
468
486
  Notes:
469
- - Set notifyAll=true to notify all task assignees`,
487
+ - When using taskName, providing listName helps locate the correct task
488
+ - Set notifyAll to true to send notifications to all task assignees
489
+ - Use assignee to assign the comment to a specific user (optional)`,
470
490
  inputSchema: {
471
491
  type: "object",
472
492
  properties: {
@@ -506,15 +526,24 @@ export const deleteTaskTool = {
506
526
  description: `Purpose: PERMANENTLY DELETE a task.
507
527
 
508
528
  Valid Usage:
509
- 1. Use taskId (preferred and safest)
510
- 2. Use taskName + listName for targeted search
529
+ 1. Use taskId alone (preferred and safest)
530
+ 2. Use taskName alone (will search across all lists)
531
+ 3. Use taskName + listName (for faster, targeted search)
511
532
 
512
533
  Requirements:
513
- - Task identification: EITHER taskId OR taskName REQUIRED
534
+ - EITHER taskId OR taskName: REQUIRED
535
+ - listName: Optional, but recommended when using taskName
536
+
537
+ Notes:
538
+ - The tool automatically searches for tasks using smart name matching
539
+ - When only taskName is provided, it searches across all lists
540
+ - Adding listName narrows the search to a specific list for better performance
541
+ - Supports both regular task IDs and custom IDs (like 'DEV-1234')
514
542
 
515
543
  Warning:
516
544
  - This action CANNOT be undone
517
- - Using taskName without listName may match multiple tasks`,
545
+ - Using taskName without listName may match multiple tasks
546
+ - If multiple matches are found, the operation will fail with a disambiguation error`,
518
547
  inputSchema: {
519
548
  type: "object",
520
549
  properties: {
@@ -11,7 +11,7 @@ import { formatDueDate } from '../utils.js';
11
11
  import { clickUpServices } from '../../services/shared.js';
12
12
  import { findListIDByName } from '../../tools/list.js';
13
13
  // Use shared services instance for ID resolution
14
- const { workspace: workspaceService } = clickUpServices;
14
+ const { workspace: workspaceService, task: taskService } = clickUpServices;
15
15
  //=============================================================================
16
16
  // DATA FORMATTING UTILITIES
17
17
  //=============================================================================
@@ -65,22 +65,38 @@ export function formatTaskData(task, additional = {}) {
65
65
  ...additional
66
66
  };
67
67
  }
68
- //=============================================================================
69
- // VALIDATION UTILITIES
70
- //=============================================================================
71
68
  /**
72
69
  * Validates task identification parameters
73
- * Ensures either taskId, customTaskId, or both taskName and listName are provided
74
- * When useGlobalLookup is true, allows taskName without listName
70
+ *
71
+ * @param params - Task identification parameters
72
+ * @param options - Validation options
73
+ * @returns Validation result with error message if any
75
74
  */
76
- export function validateTaskIdentification(taskId, taskName, listName, customTaskId, useGlobalLookup = true) {
75
+ export function validateTaskIdentification(params, options = {}) {
76
+ const { taskId, taskName, customTaskId, listName } = params;
77
+ const { requireTaskId = false, useGlobalLookup = true } = options;
78
+ // If taskId is required, it must be provided
79
+ if (requireTaskId && !taskId) {
80
+ return {
81
+ isValid: false,
82
+ errorMessage: 'Task ID is required for this operation'
83
+ };
84
+ }
85
+ // At least one identification method must be provided
77
86
  if (!taskId && !taskName && !customTaskId) {
78
- throw new Error("Either taskId, customTaskId, or taskName must be provided");
87
+ return {
88
+ isValid: false,
89
+ errorMessage: 'Either taskId, taskName, or customTaskId must be provided to identify the task'
90
+ };
79
91
  }
80
- // When global lookup is not enabled, we need list context for task name lookup
81
- if (!useGlobalLookup && !taskId && !customTaskId && taskName && !listName) {
82
- throw new Error("listName is required when using taskName and global lookup is disabled");
92
+ // When using taskName without global lookup, listName is required
93
+ if (taskName && !taskId && !customTaskId && !useGlobalLookup && !listName) {
94
+ return {
95
+ isValid: false,
96
+ errorMessage: 'When identifying a task by name, you must also provide the listName parameter'
97
+ };
83
98
  }
99
+ return { isValid: true };
84
100
  }
85
101
  /**
86
102
  * Validates list identification parameters
@@ -99,7 +115,7 @@ export function validateTaskUpdateData(updateData) {
99
115
  // Check if there are any valid update fields present
100
116
  const hasUpdates = Object.keys(updateData).some(key => {
101
117
  return ['name', 'description', 'markdown_description', 'status', 'priority',
102
- 'dueDate', 'startDate', 'taskId', 'taskName', 'custom_fields'].includes(key);
118
+ 'dueDate', 'startDate', 'custom_fields'].includes(key);
103
119
  });
104
120
  if (!hasUpdates) {
105
121
  throw new Error("At least one field to update must be provided");
@@ -231,3 +247,34 @@ export function extractTreePath(root, targetId) {
231
247
  // Not found in this branch
232
248
  return [];
233
249
  }
250
+ /**
251
+ * Get task ID from various identification methods
252
+ */
253
+ export async function getTaskId(taskId, taskName, listName, customTaskId, requireId = false) {
254
+ // Validate task identification
255
+ const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true });
256
+ if (!validationResult.isValid) {
257
+ throw new Error(validationResult.errorMessage);
258
+ }
259
+ try {
260
+ const result = await taskService.findTasks({
261
+ taskId,
262
+ customTaskId,
263
+ taskName,
264
+ listName,
265
+ allowMultipleMatches: false,
266
+ useSmartDisambiguation: true,
267
+ includeFullDetails: false
268
+ });
269
+ if (!result || Array.isArray(result)) {
270
+ throw new Error(`Task not found with the provided identification`);
271
+ }
272
+ return result.id;
273
+ }
274
+ catch (error) {
275
+ if (error.message.includes('Multiple tasks found')) {
276
+ throw new Error(`Multiple tasks found with name "${taskName}". Please provide list name to disambiguate.`);
277
+ }
278
+ throw error;
279
+ }
280
+ }
@@ -12,20 +12,27 @@
12
12
  */
13
13
  export const getWorkspaceTasksTool = {
14
14
  name: "get_workspace_tasks",
15
- description: `Purpose: Retrieve tasks from across the entire workspace with filtering.
15
+ description: `Purpose: Retrieve tasks from across the entire workspace with powerful filtering options, including tag-based filtering.
16
16
 
17
17
  Valid Usage:
18
- 1. Apply any combination of filters (tags, lists, statuses, etc.)
19
- 2. Use pagination for large result sets
18
+ 1. Apply any combination of filters (tags, lists, folders, spaces, statuses, etc.)
19
+ 2. Use pagination to manage large result sets
20
20
 
21
21
  Requirements:
22
- - At least one filter parameter REQUIRED (tags, list_ids, statuses, etc.)
22
+ - At least one filter parameter is REQUIRED (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)
23
+ - Pagination parameters (page, order_by, reverse) alone are not considered filters
23
24
 
24
25
  Notes:
25
- - Searches across all lists in the workspace
26
- - Tag filtering allows cross-list organization
27
- - Set detail_level=summary for lightweight responses
28
- - detail_level=detailed (default) returns complete data`,
26
+ - Provides workspace-wide task access (unlike get_tasks which only searches in one list)
27
+ - Returns complete task details including descriptions, assignees, custom fields, and all metadata
28
+ - Tag filtering is especially useful for cross-list organization (e.g., "project-x", "blocker", "needs-review")
29
+ - Combine multiple filters to narrow down your search scope
30
+ - Use pagination for large result sets
31
+ - Use the detail_level parameter to control the amount of data returned:
32
+ - "summary": Returns lightweight task data (name, status, list, tags)
33
+ - "detailed": Returns complete task data with all fields (DEFAULT if not specified)
34
+ - Responses exceeding 50,000 tokens automatically switch to summary format to avoid hitting LLM token limits
35
+ `,
29
36
  parameters: {
30
37
  type: 'object',
31
38
  properties: {
@@ -6,6 +6,9 @@
6
6
  *
7
7
  * This module provides utilities for handling dates, timestamps, and due date parsing.
8
8
  */
9
+ import { Logger } from '../logger.js';
10
+ // Create a logger instance for date utilities
11
+ const logger = new Logger('DateUtils');
9
12
  /**
10
13
  * Get a timestamp for a relative time
11
14
  *
@@ -197,8 +200,8 @@ export function parseDueDate(dateString) {
197
200
  return undefined;
198
201
  }
199
202
  catch (error) {
200
- console.warn(`Failed to parse due date: ${dateString}`, error);
201
- return undefined;
203
+ logger.warn(`Failed to parse due date: ${dateString}`, error);
204
+ throw new Error(`Invalid date format: ${dateString}`);
202
205
  }
203
206
  }
204
207
  /**
@@ -225,8 +228,8 @@ export function formatDueDate(timestamp) {
225
228
  }).replace(' at', ',');
226
229
  }
227
230
  catch (error) {
228
- console.warn(`Failed to format due date: ${timestamp}`, error);
229
- return undefined;
231
+ logger.warn(`Failed to format due date: ${timestamp}`, error);
232
+ throw new Error(`Invalid timestamp: ${timestamp}`);
230
233
  }
231
234
  }
232
235
  /**
@@ -9,37 +9,110 @@
9
9
  import { clickUpServices } from '../services/shared.js';
10
10
  import { findListIDByName } from '../tools/list.js';
11
11
  /**
12
- * Check if a task name matches search criteria
12
+ * Check if a name matches another name using a variety of matching strategies
13
+ * Returns a structured result with match quality information rather than just a boolean
13
14
  *
14
- * Performs flexible case-insensitive and emoji-aware text matching
15
- * Used by multiple components for consistent name matching behavior
15
+ * @param actualName The actual name to check
16
+ * @param searchName The name being searched for
17
+ * @returns A structured result with match details
16
18
  */
17
- export function isNameMatch(taskName, searchTerm) {
18
- const normalizedTask = taskName.toLowerCase().trim();
19
- const normalizedSearch = searchTerm.toLowerCase().trim();
20
- // Handle empty strings - don't match empty task names
21
- if (normalizedTask === '')
22
- return false;
23
- if (normalizedSearch === '')
24
- return false;
25
- // Exact match check
26
- if (normalizedTask === normalizedSearch)
27
- return true;
28
- // Substring match check
29
- if (normalizedTask.includes(normalizedSearch) || normalizedSearch.includes(normalizedTask))
30
- return true;
31
- // Handle emoji characters in names
32
- if (/[\p{Emoji}]/u.test(normalizedSearch) || /[\p{Emoji}]/u.test(normalizedTask)) {
33
- const taskWithoutEmoji = normalizedTask.replace(/[\p{Emoji}]/gu, '').trim();
34
- const searchWithoutEmoji = normalizedSearch.replace(/[\p{Emoji}]/gu, '').trim();
35
- // Don't match if either becomes empty after emoji removal
36
- if (taskWithoutEmoji === '' || searchWithoutEmoji === '')
37
- return false;
38
- return taskWithoutEmoji === searchWithoutEmoji ||
39
- taskWithoutEmoji.includes(searchWithoutEmoji) ||
40
- searchWithoutEmoji.includes(taskWithoutEmoji);
41
- }
42
- return false;
19
+ export function isNameMatch(actualName, searchName) {
20
+ if (!actualName || !searchName) {
21
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'One of the names is empty' };
22
+ }
23
+ // Remove any extra whitespace
24
+ const normalizedActualName = actualName.trim();
25
+ const normalizedSearchName = searchName.trim();
26
+ // Handle empty names after normalization
27
+ if (normalizedActualName === '') {
28
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'Actual name is empty' };
29
+ }
30
+ if (normalizedSearchName === '') {
31
+ return { isMatch: false, score: 0, exactMatch: false, reason: 'Search name is empty' };
32
+ }
33
+ // 1. Exact match (highest quality)
34
+ if (normalizedActualName === normalizedSearchName) {
35
+ return {
36
+ isMatch: true,
37
+ score: 100,
38
+ exactMatch: true,
39
+ reason: 'Exact match'
40
+ };
41
+ }
42
+ // 2. Case-insensitive exact match (high quality)
43
+ if (normalizedActualName.toLowerCase() === normalizedSearchName.toLowerCase()) {
44
+ return {
45
+ isMatch: true,
46
+ score: 90,
47
+ exactMatch: true,
48
+ reason: 'Case-insensitive exact match'
49
+ };
50
+ }
51
+ // 3. Match after removing emojis (moderate quality)
52
+ const actualNameWithoutEmoji = normalizedActualName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
53
+ const searchNameWithoutEmoji = normalizedSearchName.replace(/[\p{Emoji}\u{FE00}-\u{FE0F}\u200d]+/gu, '').trim();
54
+ if (actualNameWithoutEmoji === searchNameWithoutEmoji) {
55
+ return {
56
+ isMatch: true,
57
+ score: 80,
58
+ exactMatch: false,
59
+ reason: 'Exact match after removing emojis'
60
+ };
61
+ }
62
+ if (actualNameWithoutEmoji.toLowerCase() === searchNameWithoutEmoji.toLowerCase()) {
63
+ return {
64
+ isMatch: true,
65
+ score: 70,
66
+ exactMatch: false,
67
+ reason: 'Case-insensitive match after removing emojis'
68
+ };
69
+ }
70
+ // 4. Substring matches (lower quality)
71
+ const lowerActual = normalizedActualName.toLowerCase();
72
+ const lowerSearch = normalizedSearchName.toLowerCase();
73
+ // Full substring (term completely contained)
74
+ if (lowerActual.includes(lowerSearch)) {
75
+ return {
76
+ isMatch: true,
77
+ score: 60,
78
+ exactMatch: false,
79
+ reason: 'Search term found as substring in actual name'
80
+ };
81
+ }
82
+ if (lowerSearch.includes(lowerActual)) {
83
+ return {
84
+ isMatch: true,
85
+ score: 50,
86
+ exactMatch: false,
87
+ reason: 'Actual name found as substring in search term'
88
+ };
89
+ }
90
+ // 5. Fuzzy emoji-less matches (lowest quality)
91
+ const lowerActualNoEmoji = actualNameWithoutEmoji.toLowerCase();
92
+ const lowerSearchNoEmoji = searchNameWithoutEmoji.toLowerCase();
93
+ if (lowerActualNoEmoji.includes(lowerSearchNoEmoji)) {
94
+ return {
95
+ isMatch: true,
96
+ score: 40,
97
+ exactMatch: false,
98
+ reason: 'Search term (without emoji) found as substring in actual name'
99
+ };
100
+ }
101
+ if (lowerSearchNoEmoji.includes(lowerActualNoEmoji)) {
102
+ return {
103
+ isMatch: true,
104
+ score: 30,
105
+ exactMatch: false,
106
+ reason: 'Actual name (without emoji) found as substring in search term'
107
+ };
108
+ }
109
+ // No match found
110
+ return {
111
+ isMatch: false,
112
+ score: 0,
113
+ exactMatch: false,
114
+ reason: 'No match found with any matching strategy'
115
+ };
43
116
  }
44
117
  /**
45
118
  * Resolve a list ID from either a direct ID or list name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
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",