@taazkareem/clickup-mcp-server 0.4.72 → 0.4.74

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.
@@ -0,0 +1,421 @@
1
+ /**
2
+ * ClickUp MCP Single Task Operations
3
+ *
4
+ * This module defines tools for single task operations including creating,
5
+ * updating, moving, duplicating, and deleting tasks, as well as retrieving
6
+ * task details and comments.
7
+ */
8
+ import { clickUpServices } from '../../services/shared.js';
9
+ // Use shared services instance
10
+ const { task: taskService } = clickUpServices;
11
+ //=============================================================================
12
+ // COMMON VALIDATION UTILITIES
13
+ //=============================================================================
14
+ // Common validation functions
15
+ const validateTaskName = (name) => {
16
+ if (!name || typeof name !== 'string') {
17
+ throw new Error("A task name is required");
18
+ }
19
+ const trimmedName = name.trim();
20
+ if (trimmedName.length === 0) {
21
+ throw new Error("Task name cannot be empty or only whitespace");
22
+ }
23
+ return trimmedName;
24
+ };
25
+ const validatePriority = (priority) => {
26
+ if (priority !== undefined && (typeof priority !== 'number' || priority < 1 || priority > 4)) {
27
+ throw new Error("Priority must be a number between 1 and 4");
28
+ }
29
+ };
30
+ const validateDueDate = (dueDate) => {
31
+ if (dueDate && typeof dueDate !== 'string') {
32
+ throw new Error("Due date must be a string in timestamp format or natural language");
33
+ }
34
+ };
35
+ // Common error handler
36
+ const handleOperationError = (operation, error) => {
37
+ console.error(`Error ${operation}:`, error);
38
+ throw error;
39
+ };
40
+ //=============================================================================
41
+ // SINGLE TASK OPERATION TOOLS
42
+ //=============================================================================
43
+ /**
44
+ * Tool definition for creating a single task
45
+ */
46
+ export const createTaskTool = {
47
+ name: "create_task",
48
+ description: `Purpose: Create a single task in a ClickUp list.
49
+
50
+ Valid Usage:
51
+ 1. Provide listId (preferred if available)
52
+ 2. Provide listName (system will look up the list ID)
53
+
54
+ Requirements:
55
+ - name: REQUIRED
56
+ - EITHER listId OR listName: REQUIRED
57
+
58
+ Notes:
59
+ - For multiple tasks, use create_bulk_tasks instead
60
+ - Reuse list IDs from previous responses when possible to avoid redundant lookups`,
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ name: {
65
+ type: "string",
66
+ description: "REQUIRED: Name of the task. Put a relevant emoji followed by a blank space before the name."
67
+ },
68
+ listId: {
69
+ type: "string",
70
+ description: "REQUIRED (unless listName provided): ID of the list to create the task in. If you have this ID from a previous response, use it directly rather than looking up by name."
71
+ },
72
+ listName: {
73
+ type: "string",
74
+ description: "REQUIRED (unless listId provided): Name of the list to create the task in - will automatically find the list by name."
75
+ },
76
+ description: {
77
+ type: "string",
78
+ description: "Optional plain text description for the task"
79
+ },
80
+ markdown_description: {
81
+ type: "string",
82
+ description: "Optional markdown formatted description for the task. If provided, this takes precedence over description"
83
+ },
84
+ status: {
85
+ type: "string",
86
+ description: "Optional: Override the default ClickUp status. In most cases, you should omit this to use ClickUp defaults"
87
+ },
88
+ priority: {
89
+ type: "number",
90
+ description: "Optional priority of the task (1-4), where 1 is urgent/highest priority and 4 is lowest priority. Only set this when explicitly requested."
91
+ },
92
+ dueDate: {
93
+ type: "string",
94
+ description: "Optional due date. Supports Unix timestamps (ms) or natural language like '1 hour from now', 'tomorrow', 'next week', etc."
95
+ }
96
+ }
97
+ }
98
+ };
99
+ /**
100
+ * Tool definition for updating a task
101
+ */
102
+ export const updateTaskTool = {
103
+ name: "update_task",
104
+ description: `Purpose: Modify properties of an existing task.
105
+
106
+ Valid Usage:
107
+ 1. Use taskId alone (preferred if available)
108
+ 2. Use taskName + listName
109
+
110
+ Requirements:
111
+ - At least one update field (name, description, status, priority, dueDate) must be provided
112
+ - When using taskName, listName is REQUIRED
113
+
114
+ Notes:
115
+ - Only specified fields will be updated
116
+ - Using taskId is more reliable than taskName`,
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ taskId: {
121
+ type: "string",
122
+ description: "ID of the task to update (preferred). Use this instead of taskName if you have it from a previous response."
123
+ },
124
+ taskName: {
125
+ type: "string",
126
+ description: "Name of the task to update. Only use this if you don't have taskId. When using this parameter, you MUST also provide listName."
127
+ },
128
+ listName: {
129
+ type: "string",
130
+ description: "Name of the list containing the task. REQUIRED when using taskName."
131
+ },
132
+ name: {
133
+ type: "string",
134
+ description: "New name for the task. Include emoji prefix if appropriate."
135
+ },
136
+ description: {
137
+ type: "string",
138
+ description: "New plain text description. Will be ignored if markdown_description is provided."
139
+ },
140
+ markdown_description: {
141
+ type: "string",
142
+ description: "New markdown description. Takes precedence over plain text description."
143
+ },
144
+ status: {
145
+ type: "string",
146
+ description: "New status. Must be valid for the task's current list."
147
+ },
148
+ priority: {
149
+ type: ["number", "null"],
150
+ enum: [1, 2, 3, 4, null],
151
+ description: "New priority: 1 (urgent) to 4 (low). Set null to clear priority."
152
+ },
153
+ dueDate: {
154
+ type: "string",
155
+ description: "New due date. Supports both Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', 'next week', or '3 days from now'."
156
+ }
157
+ },
158
+ required: []
159
+ }
160
+ };
161
+ /**
162
+ * Tool definition for moving a task
163
+ */
164
+ export const moveTaskTool = {
165
+ name: "move_task",
166
+ description: `Purpose: Move a task to a different list.
167
+
168
+ Valid Usage:
169
+ 1. Use taskId + (listId OR listName) - preferred
170
+ 2. Use taskName + sourceListName + (listId OR listName)
171
+
172
+ Requirements:
173
+ - Destination list: EITHER listId OR listName REQUIRED
174
+ - When using taskName, sourceListName is REQUIRED
175
+
176
+ Warning:
177
+ - Task statuses may reset if destination list has different status options
178
+ - System cannot find a task by name without knowing which list to search in`,
179
+ inputSchema: {
180
+ type: "object",
181
+ properties: {
182
+ taskId: {
183
+ type: "string",
184
+ description: "ID of the task to move (preferred). Use this instead of taskName if you have it."
185
+ },
186
+ taskName: {
187
+ type: "string",
188
+ description: "Name of the task to move. When using this, you MUST also provide sourceListName."
189
+ },
190
+ sourceListName: {
191
+ type: "string",
192
+ description: "REQUIRED with taskName: Current list containing the task."
193
+ },
194
+ listId: {
195
+ type: "string",
196
+ description: "ID of destination list (preferred). Use this instead of listName if you have it."
197
+ },
198
+ listName: {
199
+ type: "string",
200
+ description: "Name of destination list. Only use if you don't have listId."
201
+ }
202
+ },
203
+ required: []
204
+ }
205
+ };
206
+ /**
207
+ * Tool definition for duplicating a task
208
+ */
209
+ export const duplicateTaskTool = {
210
+ name: "duplicate_task",
211
+ description: `Purpose: Create a copy of a task in the same or different list.
212
+
213
+ Valid Usage:
214
+ 1. Use taskId + optional (listId OR listName) - preferred
215
+ 2. Use taskName + sourceListName + optional (listId OR listName)
216
+
217
+ Requirements:
218
+ - When using taskName, sourceListName is REQUIRED
219
+
220
+ Notes:
221
+ - The duplicate preserves the original task's properties
222
+ - If no destination list specified, uses same list as original task
223
+
224
+ Warning:
225
+ - System cannot find a task by name without knowing which list to search in`,
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ taskId: {
230
+ type: "string",
231
+ description: "ID of task to duplicate (preferred). Use this instead of taskName if you have it."
232
+ },
233
+ taskName: {
234
+ type: "string",
235
+ description: "Name of task to duplicate. When using this, you MUST provide sourceListName."
236
+ },
237
+ sourceListName: {
238
+ type: "string",
239
+ description: "REQUIRED with taskName: List containing the original task."
240
+ },
241
+ listId: {
242
+ type: "string",
243
+ description: "ID of list for the duplicate (optional). Defaults to same list as original."
244
+ },
245
+ listName: {
246
+ type: "string",
247
+ description: "Name of list for the duplicate. Only use if you don't have listId."
248
+ }
249
+ },
250
+ required: []
251
+ }
252
+ };
253
+ /**
254
+ * Tool definition for retrieving a task
255
+ */
256
+ export const getTaskTool = {
257
+ name: "get_task",
258
+ description: `Purpose: Retrieve detailed information about a specific task.
259
+
260
+ Valid Usage:
261
+ 1. Use taskId alone (preferred)
262
+ 2. Use taskName + listName
263
+
264
+ Requirements:
265
+ - When using taskName, listName is REQUIRED
266
+
267
+ Note:
268
+ - Task names are only unique within a list, so the system needs to know which list to search in`,
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ taskId: {
273
+ type: "string",
274
+ description: "ID of task to retrieve (preferred). Use this instead of taskName if you have it."
275
+ },
276
+ taskName: {
277
+ type: "string",
278
+ description: "Name of task to retrieve. When using this parameter, you MUST also provide listName."
279
+ },
280
+ listName: {
281
+ type: "string",
282
+ description: "Name of list containing the task. REQUIRED when using taskName."
283
+ }
284
+ },
285
+ required: []
286
+ }
287
+ };
288
+ /**
289
+ * Tool definition for retrieving tasks from a list
290
+ */
291
+ export const getTasksTool = {
292
+ name: "get_tasks",
293
+ description: `Purpose: Retrieve tasks from a list with optional filtering.
294
+
295
+ Valid Usage:
296
+ 1. Use listId (preferred)
297
+ 2. Use listName
298
+
299
+ Requirements:
300
+ - EITHER listId OR listName is REQUIRED
301
+
302
+ Notes:
303
+ - Use filters (archived, statuses, etc.) to narrow down results
304
+ - Pagination available through page parameter
305
+ - Sorting available through order_by and reverse parameters`,
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ listId: {
310
+ type: "string",
311
+ description: "ID of list to get tasks from (preferred). Use this instead of listName if you have it."
312
+ },
313
+ listName: {
314
+ type: "string",
315
+ description: "Name of list to get tasks from. Only use if you don't have listId."
316
+ },
317
+ archived: {
318
+ type: "boolean",
319
+ description: "Include archived tasks"
320
+ },
321
+ page: {
322
+ type: "number",
323
+ description: "Page number for pagination (starts at 0)"
324
+ },
325
+ order_by: {
326
+ type: "string",
327
+ description: "Sort field: due_date, created, updated"
328
+ },
329
+ reverse: {
330
+ type: "boolean",
331
+ description: "Reverse sort order (descending)"
332
+ },
333
+ subtasks: {
334
+ type: "boolean",
335
+ description: "Include subtasks"
336
+ },
337
+ statuses: {
338
+ type: "array",
339
+ items: {
340
+ type: "string"
341
+ },
342
+ description: "Filter by status names (e.g. ['To Do', 'In Progress'])"
343
+ }
344
+ },
345
+ required: []
346
+ }
347
+ };
348
+ /**
349
+ * Tool definition for deleting a task
350
+ */
351
+ export const deleteTaskTool = {
352
+ name: "delete_task",
353
+ description: `Purpose: PERMANENTLY DELETE a task.
354
+
355
+ Valid Usage:
356
+ 1. Use taskId alone (preferred and safest)
357
+ 2. Use taskName + optional listName
358
+
359
+ Warning:
360
+ - This action CANNOT be undone
361
+ - Using taskName is risky as names may not be unique
362
+ - Provide listName when using taskName for more precise targeting`,
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ taskId: {
367
+ type: "string",
368
+ description: "ID of task to delete (preferred). Use this instead of taskName for safety."
369
+ },
370
+ taskName: {
371
+ type: "string",
372
+ description: "Name of task to delete. Use with extreme caution as names may not be unique."
373
+ },
374
+ listName: {
375
+ type: "string",
376
+ description: "Name of list containing the task. Helps ensure correct task deletion when using taskName."
377
+ }
378
+ }
379
+ }
380
+ };
381
+ /**
382
+ * Tool definition for retrieving task comments
383
+ */
384
+ export const getTaskCommentsTool = {
385
+ name: "get_task_comments",
386
+ description: `Purpose: Retrieve comments for a ClickUp task.
387
+
388
+ Valid Usage:
389
+ 1. Use taskId (preferred)
390
+ 2. Use taskName + optional listName
391
+
392
+ Notes:
393
+ - If using taskName, providing listName helps locate the correct task
394
+ - Task names may not be unique across different lists
395
+ - Use start and startId parameters for pagination through comments`,
396
+ inputSchema: {
397
+ type: "object",
398
+ properties: {
399
+ taskId: {
400
+ type: "string",
401
+ description: "ID of task to retrieve comments for (preferred). Use this instead of taskName if you have it."
402
+ },
403
+ taskName: {
404
+ type: "string",
405
+ description: "Name of task to retrieve comments for. Warning: Task names may not be unique."
406
+ },
407
+ listName: {
408
+ type: "string",
409
+ description: "Name of list containing the task. Helps find the right task when using taskName."
410
+ },
411
+ start: {
412
+ type: "number",
413
+ description: "Timestamp (in milliseconds) to start retrieving comments from. Used for pagination."
414
+ },
415
+ startId: {
416
+ type: "string",
417
+ description: "Comment ID to start from. Used together with start for pagination."
418
+ }
419
+ }
420
+ }
421
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ClickUp MCP Task Utilities
3
+ *
4
+ * This module provides utility functions for task-related operations including
5
+ * data formatting, validation, and resolution of IDs from names.
6
+ */
7
+ import { formatDueDate } from '../utils.js';
8
+ import { clickUpServices } from '../../services/shared.js';
9
+ // Use shared services instance for ID resolution
10
+ const { workspace: workspaceService } = clickUpServices;
11
+ //=============================================================================
12
+ // DATA FORMATTING UTILITIES
13
+ //=============================================================================
14
+ /**
15
+ * Formats task data for response
16
+ */
17
+ export function formatTaskData(task, additional = {}) {
18
+ return {
19
+ id: task.id,
20
+ name: task.name,
21
+ url: task.url,
22
+ status: task.status?.status || "Unknown",
23
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
24
+ list: task.list.name,
25
+ space: task.space.name,
26
+ folder: task.folder?.name,
27
+ ...additional
28
+ };
29
+ }
30
+ //=============================================================================
31
+ // VALIDATION UTILITIES
32
+ //=============================================================================
33
+ /**
34
+ * Validates task identification parameters
35
+ * Ensures either taskId is provided or both taskName and listName are provided
36
+ */
37
+ export function validateTaskIdentification(taskId, taskName, listName) {
38
+ if (!taskId && !taskName) {
39
+ throw new Error("Either taskId or taskName must be provided");
40
+ }
41
+ if (!taskId && taskName && !listName) {
42
+ throw new Error("When using taskName, listName is required to locate the task");
43
+ }
44
+ }
45
+ /**
46
+ * Validates list identification parameters
47
+ * Ensures either listId or listName is provided
48
+ */
49
+ export function validateListIdentification(listId, listName) {
50
+ if (!listId && !listName) {
51
+ throw new Error("Either listId or listName must be provided");
52
+ }
53
+ }
54
+ /**
55
+ * Validates task update data
56
+ * Ensures at least one update field is provided
57
+ */
58
+ export function validateTaskUpdateData(updateData) {
59
+ const hasUpdates = Object.keys(updateData).length > 0;
60
+ if (!hasUpdates) {
61
+ throw new Error("At least one field to update must be provided");
62
+ }
63
+ }
64
+ /**
65
+ * Validate bulk operation tasks array
66
+ */
67
+ export function validateBulkTasks(tasks) {
68
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
69
+ throw new Error('You must provide a non-empty array of tasks');
70
+ }
71
+ }
72
+ /**
73
+ * Parse options for bulk operations
74
+ */
75
+ export function parseBulkOptions(rawOptions) {
76
+ if (typeof rawOptions === 'string') {
77
+ try {
78
+ return JSON.parse(rawOptions);
79
+ }
80
+ catch (error) {
81
+ return undefined;
82
+ }
83
+ }
84
+ return rawOptions;
85
+ }
86
+ //=============================================================================
87
+ // ID RESOLUTION UTILITIES
88
+ //=============================================================================
89
+ /**
90
+ * Resolves a task ID from either direct ID or name+list combination
91
+ * Handles validation and throws appropriate errors
92
+ */
93
+ export async function resolveTaskIdWithValidation(taskId, taskName, listName) {
94
+ // Validate parameters
95
+ validateTaskIdentification(taskId, taskName, listName);
96
+ // If taskId is provided, use it directly
97
+ if (taskId)
98
+ return taskId;
99
+ // At this point we know we have taskName and listName (validation ensures this)
100
+ // Find the list ID from its name
101
+ const listId = await resolveListIdWithValidation(undefined, listName);
102
+ // Find the task in the list
103
+ const { task: taskService } = clickUpServices;
104
+ const foundTask = await taskService.findTaskByName(listId, taskName);
105
+ if (!foundTask) {
106
+ throw new Error(`Task "${taskName}" not found in list "${listName}"`);
107
+ }
108
+ return foundTask.id;
109
+ }
110
+ /**
111
+ * Resolves a list ID from either direct ID or name
112
+ * Handles validation and throws appropriate errors
113
+ */
114
+ export async function resolveListIdWithValidation(listId, listName) {
115
+ // Validate parameters
116
+ validateListIdentification(listId, listName);
117
+ // If listId is provided, use it directly
118
+ if (listId)
119
+ return listId;
120
+ // At this point we know we have listName (validation ensures this)
121
+ // Find the list ID from its name
122
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
123
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
124
+ if (!listInfo) {
125
+ throw new Error(`List "${listName}" not found`);
126
+ }
127
+ return listInfo.id;
128
+ }
129
+ //=============================================================================
130
+ // PATH EXTRACTION HELPER FUNCTIONS
131
+ //=============================================================================
132
+ /**
133
+ * Extract path from node to root
134
+ */
135
+ export function extractPath(node) {
136
+ if (!node)
137
+ return '';
138
+ if (!node.parent)
139
+ return node.name;
140
+ return `${extractPath(node.parent)} > ${node.name}`;
141
+ }
142
+ /**
143
+ * Extract path from root to a specific node
144
+ */
145
+ export function extractTreePath(root, targetId) {
146
+ if (!root)
147
+ return [];
148
+ // If this node is the target, return it in an array
149
+ if (root.id === targetId) {
150
+ return [root];
151
+ }
152
+ // Check children if they exist
153
+ if (root.children) {
154
+ for (const child of root.children) {
155
+ const path = extractTreePath(child, targetId);
156
+ if (path.length > 0) {
157
+ return [root, ...path];
158
+ }
159
+ }
160
+ }
161
+ // Not found in this branch
162
+ return [];
163
+ }