@taazkareem/clickup-mcp-server 0.6.5 → 0.6.7

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.
@@ -48,20 +48,7 @@ const handleOperationError = (operation, error) => {
48
48
  */
49
49
  export const createTaskTool = {
50
50
  name: "create_task",
51
- description: `Purpose: Create a single task in a ClickUp list.
52
-
53
- Valid Usage:
54
- 1. Provide listId (preferred if available)
55
- 2. Provide listName (will look up the list ID)
56
-
57
- Requirements:
58
- - name: REQUIRED
59
- - List identification: EITHER listId OR listName REQUIRED
60
-
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`,
51
+ description: `Creates a single task in a ClickUp list. Use listId (preferred) or listName. Required: name + list info. For multiple tasks use create_bulk_tasks. Can create subtasks via parent param. Supports custom fields as array of {id, value}.`,
65
52
  inputSchema: {
66
53
  type: "object",
67
54
  properties: {
@@ -141,20 +128,7 @@ Notes:
141
128
  */
142
129
  export const updateTaskTool = {
143
130
  name: "update_task",
144
- description: `Purpose: Modify properties of an existing task.
145
-
146
- Valid Usage:
147
- 1. Use taskId (preferred) - works with both regular and custom IDs
148
- 2. Use taskName + listName for targeted search
149
-
150
- Requirements:
151
- - Task identification: EITHER taskId OR taskName REQUIRED
152
- - Updates: At least one update field (name, description, status, priority, dueDate) REQUIRED
153
-
154
- Notes:
155
- - 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`,
131
+ description: `Updates task properties. Use taskId (preferred) or taskName + optional listName. At least one update field required. Custom fields supported as array of {id, value}. WARNING: Using taskName without listName may match multiple tasks.`,
158
132
  inputSchema: {
159
133
  type: "object",
160
134
  properties: {
@@ -224,18 +198,7 @@ Notes:
224
198
  */
225
199
  export const moveTaskTool = {
226
200
  name: "move_task",
227
- description: `Purpose: Move a task to a different list.
228
-
229
- Valid Usage:
230
- 1. Use taskId + destination list (preferred)
231
- 2. Use taskName + sourceListName + destination list
232
-
233
- Requirements:
234
- - Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
235
- - Destination: EITHER listId OR listName REQUIRED
236
-
237
- Warning:
238
- - Task statuses may reset if destination list has different status options`,
201
+ description: `Moves task to different list. Use taskId + (listId/listName) preferred, or taskName + sourceListName + (listId/listName). WARNING: Task statuses may reset if destination list has different status options.`,
239
202
  inputSchema: {
240
203
  type: "object",
241
204
  properties: {
@@ -268,18 +231,7 @@ Warning:
268
231
  */
269
232
  export const duplicateTaskTool = {
270
233
  name: "duplicate_task",
271
- description: `Purpose: Create a copy of a task in the same or different list.
272
-
273
- Valid Usage:
274
- 1. Use taskId (preferred)
275
- 2. Use taskName + sourceListName
276
-
277
- Requirements:
278
- - Task identification: EITHER taskId OR (taskName + sourceListName) REQUIRED
279
- - Destination: OPTIONAL - defaults to original list
280
-
281
- Notes:
282
- - The duplicate preserves the original task's properties`,
234
+ description: `Creates copy of task in same/different list. Use taskId + optional (listId/listName), or taskName + sourceListName + optional (listId/listName). Preserves original properties. Default: same list as original.`,
283
235
  inputSchema: {
284
236
  type: "object",
285
237
  properties: {
@@ -308,22 +260,11 @@ Notes:
308
260
  }
309
261
  };
310
262
  /**
311
- * Tool definition for retrieving a task
263
+ * Tool definition for retrieving task details
312
264
  */
313
265
  export const getTaskTool = {
314
266
  name: "get_task",
315
- description: `Purpose: Retrieve detailed information about a specific task.
316
-
317
- Valid Usage:
318
- 1. Use taskId (preferred) - works with both regular and custom IDs
319
- 2. Use taskName + listName for targeted search
320
-
321
- Requirements:
322
- - Task identification: EITHER taskId OR (taskName + listName) REQUIRED
323
-
324
- Notes:
325
- - Task names are only unique within a list
326
- - Set subtasks=true to include all subtasks in the response`,
267
+ description: `Gets task details by taskId (works with regular/custom IDs) or taskName. For taskName search, provide listName for faster lookup. Set subtasks=true to include all subtask details.`,
327
268
  inputSchema: {
328
269
  type: "object",
329
270
  properties: {
@@ -333,11 +274,11 @@ Notes:
333
274
  },
334
275
  taskName: {
335
276
  type: "string",
336
- description: "Name of task to retrieve. When using this parameter, you MUST also provide listName."
277
+ description: "Name of task to retrieve. Can be used alone for a global search, or with listName for faster lookup."
337
278
  },
338
279
  listName: {
339
280
  type: "string",
340
- description: "Name of list containing the task. REQUIRED when using taskName."
281
+ description: "Name of list containing the task. Optional but recommended when using taskName."
341
282
  },
342
283
  customTaskId: {
343
284
  type: "string",
@@ -362,11 +303,12 @@ Valid Usage:
362
303
  2. Use listName
363
304
 
364
305
  Requirements:
365
- - List identification: EITHER listId OR listName REQUIRED
306
+ - EITHER listId OR listName is REQUIRED
366
307
 
367
308
  Notes:
368
- - Use filters (archived, statuses) to narrow down results
369
- - Pagination and sorting available`,
309
+ - Use filters (archived, statuses, etc.) to narrow down results
310
+ - Pagination available through page parameter
311
+ - Sorting available through order_by and reverse parameters`,
370
312
  inputSchema: {
371
313
  type: "object",
372
314
  properties: {
@@ -412,18 +354,7 @@ Notes:
412
354
  */
413
355
  export const getTaskCommentsTool = {
414
356
  name: "get_task_comments",
415
- description: `Purpose: Retrieve comments for a ClickUp task.
416
-
417
- Valid Usage:
418
- 1. Use taskId (preferred)
419
- 2. Use taskName + listName for targeted search
420
-
421
- Requirements:
422
- - Task identification: EITHER taskId OR taskName REQUIRED
423
-
424
- Notes:
425
- - Task names may not be unique across different lists
426
- - Use start/startId parameters for pagination`,
357
+ description: `Gets task comments. Use taskId (preferred) or taskName + optional listName. Use start/startId params for pagination. Task names may not be unique across lists.`,
427
358
  inputSchema: {
428
359
  type: "object",
429
360
  properties: {
@@ -455,18 +386,7 @@ Notes:
455
386
  */
456
387
  export const createTaskCommentTool = {
457
388
  name: "create_task_comment",
458
- description: `Purpose: Create a comment on a ClickUp task.
459
-
460
- Valid Usage:
461
- 1. Use taskId (preferred)
462
- 2. Use taskName + listName
463
-
464
- Requirements:
465
- - Task identification: EITHER taskId OR (taskName + listName) REQUIRED
466
- - commentText: REQUIRED
467
-
468
- Notes:
469
- - Set notifyAll=true to notify all task assignees`,
389
+ description: `Creates task comment. Use taskId (preferred) or taskName + listName. Required: commentText. Optional: notifyAll to notify assignees, assignee to assign comment.`,
470
390
  inputSchema: {
471
391
  type: "object",
472
392
  properties: {
@@ -503,18 +423,7 @@ Notes:
503
423
  */
504
424
  export const deleteTaskTool = {
505
425
  name: "delete_task",
506
- description: `Purpose: PERMANENTLY DELETE a task.
507
-
508
- Valid Usage:
509
- 1. Use taskId (preferred and safest)
510
- 2. Use taskName + listName for targeted search
511
-
512
- Requirements:
513
- - Task identification: EITHER taskId OR taskName REQUIRED
514
-
515
- Warning:
516
- - This action CANNOT be undone
517
- - Using taskName without listName may match multiple tasks`,
426
+ description: `PERMANENTLY deletes task. Use taskId (preferred/safest) or taskName + optional listName. WARNING: Cannot be undone. Using taskName without listName may match multiple tasks.`,
518
427
  inputSchema: {
519
428
  type: "object",
520
429
  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: {
@@ -19,18 +19,7 @@ const { workspace: workspaceService } = clickUpServices;
19
19
  */
20
20
  export const workspaceHierarchyTool = {
21
21
  name: 'get_workspace_hierarchy',
22
- description: `Purpose: Retrieve the complete workspace hierarchy including spaces, folders, and lists.
23
-
24
- Valid Usage:
25
- 1. Call without parameters to get the full hierarchy
26
-
27
- Requirements:
28
- - No parameters required
29
-
30
- Notes:
31
- - Returns a tree structure showing all spaces, folders, and lists
32
- - Each item includes its name and ID
33
- - Use this to navigate the workspace and understand its organization`,
22
+ description: `Gets complete workspace hierarchy (spaces, folders, lists). No parameters needed. Returns tree structure with names and IDs for navigation.`,
34
23
  inputSchema: {
35
24
  type: 'object',
36
25
  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.7",
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",