@taazkareem/clickup-mcp-server 0.4.54 → 0.4.57

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.
package/Dockerfile CHANGED
@@ -31,10 +31,6 @@ COPY --from=builder /app/node_modules ./node_modules
31
31
  # Copy the entrypoint script if necessary
32
32
  COPY --from=builder /app/package.json ./
33
33
 
34
- # Set environment variables - don't hardcode values
35
- ENV CLICKUP_API_KEY=""
36
- ENV CLICKUP_TEAM_ID=""
37
-
38
34
  # Expose the desired port (if the server binds to a port)
39
35
  EXPOSE 8080
40
36
 
package/README.md CHANGED
@@ -21,6 +21,9 @@ npx -y @taazkareem/clickup-mcp-server \
21
21
  4. Replace the credentials and click Save
22
22
  5. Use Natural Language to interact with your ClickUp Workspace!
23
23
 
24
+ ## Smithery
25
+ [![smithery badge](https://smithery.ai/badge/@TaazKareem/clickup-mcp-server)](https://smithery.ai/server/@TaazKareem/clickup-mcp-server)
26
+
24
27
  ## Features
25
28
 
26
29
  - 🎯 **Task Management**
@@ -32,9 +35,10 @@ npx -y @taazkareem/clickup-mcp-server \
32
35
  - 📂 **Workspace Organization**
33
36
  - Complete workspace hierarchy (spaces, folders, lists)
34
37
  - Tree structure with clear relationships
35
- - Create and manage lists and folders
38
+ - Full CRUD operations for workspace components
36
39
  - Efficient path-based navigation
37
40
 
41
+
38
42
  - 🔄 **Integration Features**
39
43
  - Name or ID-based item lookup
40
44
  - Case-insensitive name matching
@@ -63,6 +67,12 @@ npx -y @taazkareem/clickup-mcp-server \
63
67
  | [create_list](docs/api-reference.md#list-management) | Create list in space | `name`, `spaceId`/`spaceName` |
64
68
  | [create_folder](docs/api-reference.md#folder-management) | Create folder | `name`, `spaceId`/`spaceName` |
65
69
  | [create_list_in_folder](docs/api-reference.md#list-management) | Create list in folder | `name`, `folderId`/`folderName` |
70
+ | [get_folder](docs/api-reference.md#folder-management) | Get folder details | `folderId`/`folderName` |
71
+ | [update_folder](docs/api-reference.md#folder-management) | Update folder properties | `folderId`/`folderName` |
72
+ | [delete_folder](docs/api-reference.md#folder-management) | Delete folder | `folderId`/`folderName` |
73
+ | [get_list](docs/api-reference.md#list-management) | Get list details | `listId`/`listName` |
74
+ | [update_list](docs/api-reference.md#list-management) | Update list properties | `listId`/`listName` |
75
+ | [delete_list](docs/api-reference.md#list-management) | Delete list | `listId`/`listName` |
66
76
 
67
77
  See [full documentation](docs/api-reference.md) for optional parameters and advanced usage.
68
78
 
package/build/index.js CHANGED
@@ -72,17 +72,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
72
72
  },
73
73
  {
74
74
  name: "create_task",
75
- description: "Create a single task in a ClickUp list. Use this tool for individual task creation only. For multiple tasks, use create_bulk_tasks instead. The tool finds lists by name (case-insensitive) so explicit list IDs aren't required. When creating a task, you must provide either a listId or listName.",
75
+ description: "Create a single task in a ClickUp list. Use this tool for individual task creation only. For multiple tasks, use create_bulk_tasks instead. Before calling this tool, check if you already have the necessary list ID from previous responses in the conversation history, as this avoids redundant lookups. When creating a task, you must provide either a listId or listName.",
76
76
  inputSchema: {
77
77
  type: "object",
78
78
  properties: {
79
79
  listId: {
80
80
  type: "string",
81
- description: "ID of the list to create the task in (optional if using listName instead)"
81
+ description: "ID of the list to create the task in (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
82
82
  },
83
83
  listName: {
84
84
  type: "string",
85
- description: "Name of the list to create the task in - will automatically find the list by name (optional if using listId instead)"
85
+ description: "Name of the list to create the task in - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
86
86
  },
87
87
  name: {
88
88
  type: "string",
@@ -109,22 +109,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
109
109
  description: "Due date of the task (Unix timestamp in milliseconds). Convert dates to this format before submitting."
110
110
  }
111
111
  },
112
- required: ["name"]
112
+ required: []
113
113
  }
114
114
  },
115
115
  {
116
116
  name: "create_bulk_tasks",
117
- description: "Create multiple tasks in a ClickUp list simultaneously. Use this tool when you need to add several related tasks in one operation. The tool finds lists by name (case-insensitive), so explicit list IDs aren't required. More efficient than creating tasks one by one for batch operations.",
117
+ description: "Create multiple tasks in a ClickUp list simultaneously. Use this tool when you need to add several related tasks in one operation. Before calling, check if you already have the necessary list ID from previous responses in the conversation, as this avoids redundant lookups. More efficient than creating tasks one by one for batch operations.",
118
118
  inputSchema: {
119
119
  type: "object",
120
120
  properties: {
121
121
  listId: {
122
122
  type: "string",
123
- description: "ID of the list to create the tasks in (optional if using listName instead)"
123
+ description: "ID of the list to create the tasks in (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
124
124
  },
125
125
  listName: {
126
126
  type: "string",
127
- description: "Name of the list to create the tasks in - will automatically find the list by name (optional if using listId instead)"
127
+ description: "Name of the list to create the tasks in - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
128
128
  },
129
129
  tasks: {
130
130
  type: "array",
@@ -164,26 +164,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
164
164
  description: "Array of user IDs to assign to the task"
165
165
  }
166
166
  },
167
- required: ["name"]
167
+ required: []
168
168
  }
169
169
  }
170
170
  },
171
- required: ["listId", "tasks"]
171
+ required: []
172
172
  }
173
173
  },
174
174
  {
175
175
  name: "create_list",
176
- description: "Create a new list directly in a ClickUp space. Use this tool when you need a top-level list not nested inside a folder. The tool can find spaces by name, so explicit space IDs aren't required. For creating lists inside folders, use create_list_in_folder instead.",
176
+ description: "Create a new list directly in a ClickUp space. Use this tool when you need a top-level list not nested inside a folder. Before calling, check if you already have the necessary space ID from previous responses in the conversation, as this avoids redundant lookups. For creating lists inside folders, use create_list_in_folder instead.",
177
177
  inputSchema: {
178
178
  type: "object",
179
179
  properties: {
180
180
  spaceId: {
181
181
  type: "string",
182
- description: "ID of the space to create the list in (optional if using spaceName instead)"
182
+ description: "ID of the space to create the list in (optional if using spaceName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
183
183
  },
184
184
  spaceName: {
185
185
  type: "string",
186
- description: "Name of the space to create the list in - will automatically find the space by name (optional if using spaceId instead)"
186
+ description: "Name of the space to create the list in - will automatically find the space by name (optional if using spaceId instead). Only use this if you don't already have the space ID from previous responses."
187
187
  },
188
188
  name: {
189
189
  type: "string",
@@ -210,22 +210,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
210
210
  description: "Status of the list"
211
211
  }
212
212
  },
213
- required: ["name"]
213
+ required: []
214
214
  }
215
215
  },
216
216
  {
217
217
  name: "create_folder",
218
- description: "Create a new folder in a ClickUp space for organizing related lists. Use this tool when you need to group multiple lists together. The tool can find spaces by name, so explicit space IDs aren't required. After creating a folder, you can add lists to it using create_list_in_folder.",
218
+ description: "Create a new folder in a ClickUp space for organizing related lists. Use this tool when you need to group multiple lists together. Before calling, check if you already have the necessary space ID from previous responses in the conversation, as this avoids redundant lookups. After creating a folder, you can add lists to it using create_list_in_folder.",
219
219
  inputSchema: {
220
220
  type: "object",
221
221
  properties: {
222
222
  spaceId: {
223
223
  type: "string",
224
- description: "ID of the space to create the folder in (optional if using spaceName instead)"
224
+ description: "ID of the space to create the folder in (optional if using spaceName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
225
225
  },
226
226
  spaceName: {
227
227
  type: "string",
228
- description: "Name of the space to create the folder in - will automatically find the space by name (optional if using spaceId instead)"
228
+ description: "Name of the space to create the folder in - will automatically find the space by name (optional if using spaceId instead). Only use this if you don't already have the space ID from previous responses."
229
229
  },
230
230
  name: {
231
231
  type: "string",
@@ -236,30 +236,30 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
236
236
  description: "Whether to override space statuses with folder-specific statuses"
237
237
  }
238
238
  },
239
- required: ["name"]
239
+ required: []
240
240
  }
241
241
  },
242
242
  {
243
243
  name: "create_list_in_folder",
244
- description: "Create a new list within a ClickUp folder. Use this tool when you need to add a list to an existing folder structure. The tool can find folders and spaces by name, so explicit IDs aren't required. For top-level lists not in folders, use create_list instead.",
244
+ description: "Create a new list within a ClickUp folder. Use this tool when you need to add a list to an existing folder structure. Before calling, check if you already have the necessary folder ID and space ID from previous responses in the conversation, as this avoids redundant lookups. For top-level lists not in folders, use create_list instead.",
245
245
  inputSchema: {
246
246
  type: "object",
247
247
  properties: {
248
248
  folderId: {
249
249
  type: "string",
250
- description: "ID of the folder to create the list in (optional if using folderName instead)"
250
+ description: "ID of the folder to create the list in (optional if using folderName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
251
251
  },
252
252
  folderName: {
253
253
  type: "string",
254
- description: "Name of the folder to create the list in - will automatically find the folder by name (optional if using folderId instead)"
254
+ description: "Name of the folder to create the list in - will automatically find the folder by name (optional if using folderId instead). Only use this if you don't already have the folder ID from previous responses."
255
255
  },
256
256
  spaceId: {
257
257
  type: "string",
258
- description: "ID of the space containing the folder (optional if using spaceName instead)"
258
+ description: "ID of the space containing the folder (optional if using spaceName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
259
259
  },
260
260
  spaceName: {
261
261
  type: "string",
262
- description: "Name of the space containing the folder - will automatically find the space by name (optional if using spaceId instead)"
262
+ description: "Name of the space containing the folder - will automatically find the space by name (optional if using spaceId instead). Only use this if you don't already have the space ID from previous responses."
263
263
  },
264
264
  name: {
265
265
  type: "string",
@@ -274,22 +274,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
274
274
  description: "Status of the list (uses folder default if not specified)"
275
275
  }
276
276
  },
277
- required: ["name"]
277
+ required: []
278
278
  }
279
279
  },
280
280
  {
281
281
  name: "move_task",
282
- description: "Move an existing task from its current list to a different list. Use this tool when you need to relocate a task within your workspace hierarchy. The tool can find tasks and lists by name, so explicit IDs aren't required. Task statuses may be reset if the destination list uses different status options.",
282
+ description: "Move an existing task from its current list to a different list. Use this tool when you need to relocate a task within your workspace hierarchy. Before calling, check if you already have the necessary task ID and list ID from previous responses in the conversation, as this avoids redundant lookups. Task statuses may be reset if the destination list uses different status options.",
283
283
  inputSchema: {
284
284
  type: "object",
285
285
  properties: {
286
286
  taskId: {
287
287
  type: "string",
288
- description: "ID of the task to move (optional if using taskName instead)"
288
+ description: "ID of the task to move (optional if using taskName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
289
289
  },
290
290
  taskName: {
291
291
  type: "string",
292
- description: "Name of the task to move - will automatically find the task by name (optional if using taskId instead)"
292
+ description: "Name of the task to move - will automatically find the task by name (optional if using taskId instead). Only use this if you don't already have the task ID from previous responses."
293
293
  },
294
294
  sourceListName: {
295
295
  type: "string",
@@ -297,29 +297,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
297
297
  },
298
298
  listId: {
299
299
  type: "string",
300
- description: "ID of the destination list (optional if using listName instead)"
300
+ description: "ID of the destination list (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
301
301
  },
302
302
  listName: {
303
303
  type: "string",
304
- description: "Name of the destination list - will automatically find the list by name (optional if using listId instead)"
304
+ description: "Name of the destination list - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
305
305
  }
306
306
  },
307
- required: ["taskName", "listName"]
307
+ required: []
308
308
  }
309
309
  },
310
310
  {
311
311
  name: "duplicate_task",
312
- description: "Create a copy of an existing task in the same or different list. Use this tool when you need to replicate a task's content and properties. The tool can find tasks and lists by name, so explicit IDs aren't required. The duplicate will preserve name, description, priority, and other attributes from the original task.",
312
+ description: "Create a copy of an existing task in the same or different list. Use this tool when you need to replicate a task's content and properties. Before calling, check if you already have the necessary task ID and list ID from previous responses in the conversation, as this avoids redundant lookups. The duplicate will preserve name, description, priority, and other attributes from the original task.",
313
313
  inputSchema: {
314
314
  type: "object",
315
315
  properties: {
316
316
  taskId: {
317
317
  type: "string",
318
- description: "ID of the task to duplicate (optional if using taskName instead)"
318
+ description: "ID of the task to duplicate (optional if using taskName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
319
319
  },
320
320
  taskName: {
321
321
  type: "string",
322
- description: "Name of the task to duplicate - will automatically find the task by name (optional if using taskId instead)"
322
+ description: "Name of the task to duplicate - will automatically find the task by name (optional if using taskId instead). Only use this if you don't already have the task ID from previous responses."
323
323
  },
324
324
  sourceListName: {
325
325
  type: "string",
@@ -327,29 +327,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
327
327
  },
328
328
  listId: {
329
329
  type: "string",
330
- description: "ID of the list to create the duplicate in (optional if using listName instead)"
330
+ description: "ID of the list to create the duplicate in (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
331
331
  },
332
332
  listName: {
333
333
  type: "string",
334
- description: "Name of the list to create the duplicate in - will automatically find the list by name (optional if using listId instead)"
334
+ description: "Name of the list to create the duplicate in - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
335
335
  }
336
336
  },
337
- required: ["taskName", "listName"]
337
+ required: []
338
338
  }
339
339
  },
340
340
  {
341
341
  name: "update_task",
342
- description: "Modify the properties of an existing task. Use this tool when you need to change a task's name, description, status, priority, or due date. The tool can find tasks by name, so explicit task IDs aren't required. Only the fields you specify will be updated; other fields will remain unchanged.",
342
+ description: "Modify the properties of an existing task. Use this tool when you need to change a task's name, description, status, priority, or due date. Before calling, check if you already have the necessary task ID from previous responses in the conversation, as this avoids redundant lookups. Only the fields you specify will be updated; other fields will remain unchanged.",
343
343
  inputSchema: {
344
344
  type: "object",
345
345
  properties: {
346
346
  taskId: {
347
347
  type: "string",
348
- description: "ID of the task to update (optional if using taskName instead)"
348
+ description: "ID of the task to update (optional if using taskName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
349
349
  },
350
350
  taskName: {
351
351
  type: "string",
352
- description: "Name of the task to update - will automatically find the task by name (optional if using taskId instead)"
352
+ description: "Name of the task to update - will automatically find the task by name (optional if using taskId instead). Only use this if you don't already have the task ID from previous responses."
353
353
  },
354
354
  listName: {
355
355
  type: "string",
@@ -363,35 +363,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
363
363
  type: "string",
364
364
  description: "New plain text description for the task"
365
365
  },
366
+ markdown_description: {
367
+ type: "string",
368
+ description: "New markdown formatted description for the task. If provided, this takes precedence over description"
369
+ },
366
370
  status: {
367
371
  type: "string",
368
372
  description: "New status for the task (must be a valid status in the task's list)"
369
373
  },
370
374
  priority: {
371
- type: "number",
372
- description: "New priority for the task (1-4), where 1 is urgent/highest priority and 4 is lowest priority"
373
- },
374
- dueDate: {
375
- type: "string",
376
- description: "New due date for the task (Unix timestamp in milliseconds). Convert dates to this format before submitting."
375
+ type: ["number", "null"],
376
+ enum: [1, 2, 3, 4, null],
377
+ description: "New priority for the task (1-4 or null), where 1 is urgent/highest priority and 4 is lowest priority. Set to null to clear priority.",
378
+ optional: true
377
379
  }
378
380
  },
379
- required: ["taskName"]
381
+ required: []
380
382
  }
381
383
  },
382
384
  {
383
385
  name: "get_tasks",
384
- description: "Retrieve tasks from a ClickUp list with optional filtering capabilities. Use this tool when you need to see existing tasks or analyze your current workload. The tool can find lists by name, eliminating the need for explicit list IDs. Results can be filtered by status, assignees, dates, and more.",
386
+ description: "Retrieve tasks from a ClickUp list with optional filtering capabilities. Use this tool when you need to see existing tasks or analyze your current workload. Before calling, check if you already have the necessary list ID from previous responses in the conversation, as this avoids redundant lookups. Results can be filtered by status, assignees, dates, and more.",
385
387
  inputSchema: {
386
388
  type: "object",
387
389
  properties: {
388
390
  listId: {
389
391
  type: "string",
390
- description: "ID of the list to get tasks from (optional if using listName instead)"
392
+ description: "ID of the list to get tasks from (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
391
393
  },
392
394
  listName: {
393
395
  type: "string",
394
- description: "Name of the list to get tasks from - will automatically find the list by name (optional if using listId instead)"
396
+ description: "Name of the list to get tasks from - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
395
397
  },
396
398
  archived: {
397
399
  type: "boolean",
@@ -456,51 +458,203 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
456
458
  description: "Object with custom field IDs as keys and desired values for filtering"
457
459
  }
458
460
  },
459
- required: ["listName"]
461
+ required: []
460
462
  }
461
463
  },
462
464
  {
463
465
  name: "get_task",
464
- description: "Retrieve comprehensive details about a specific ClickUp task. Use this tool when you need in-depth information about a particular task, including its description, custom fields, attachments, and other metadata. The tool can find tasks by name, eliminating the need for explicit task IDs.",
466
+ description: "Retrieve comprehensive details about a specific ClickUp task. Use this tool when you need in-depth information about a particular task, including its description, custom fields, attachments, and other metadata. Before calling, check if you already have the necessary task ID from previous responses in the conversation, as this avoids redundant lookups.",
465
467
  inputSchema: {
466
468
  type: "object",
467
469
  properties: {
468
470
  taskId: {
469
471
  type: "string",
470
- description: "ID of the task to retrieve (optional if using taskName instead)"
472
+ description: "ID of the task to retrieve (optional if using taskName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
471
473
  },
472
474
  taskName: {
473
475
  type: "string",
474
- description: "Name of the task to retrieve - will automatically find the task by name (optional if using taskId instead)"
476
+ description: "Name of the task to retrieve - will automatically find the task by name (optional if using taskId instead). Only use this if you don't already have the task ID from previous responses."
475
477
  },
476
478
  listName: {
477
479
  type: "string",
478
480
  description: "Optional: Name of the list to narrow down task search when multiple tasks have the same name"
479
481
  }
480
482
  },
481
- required: ["taskName"]
483
+ required: []
482
484
  }
483
485
  },
484
486
  {
485
487
  name: "delete_task",
486
- description: "Permanently remove a task from your ClickUp workspace. Use this tool with caution as deletion cannot be undone. The tool requires an explicit task ID for safety reasons, which you can obtain by first using get_task or get_tasks to find the appropriate task ID.",
488
+ description: "Permanently remove a task from your ClickUp workspace. Use this tool with caution as deletion cannot be undone. Before calling, check if you already have the necessary task ID from previous responses in the conversation, as this avoids redundant lookups. For safety, the task ID is required.",
487
489
  inputSchema: {
488
490
  type: "object",
489
491
  properties: {
490
492
  taskId: {
491
493
  type: "string",
492
- description: "ID of the task to delete - this is required for safety to prevent accidental deletions"
494
+ description: "ID of the task to delete - this is required for safety to prevent accidental deletions. If you have this ID from a previous response, use it directly."
493
495
  },
494
496
  taskName: {
495
497
  type: "string",
496
- description: "Name of the task to delete - will automatically find the task by name (optional if using taskId instead)"
498
+ description: "Name of the task to delete - will automatically find the task by name (optional if using taskId instead). Only use this if you don't already have the task ID from previous responses."
497
499
  },
498
500
  listName: {
499
501
  type: "string",
500
502
  description: "Optional: Name of the list to narrow down task search when multiple tasks have the same name"
501
503
  }
502
504
  },
503
- required: ["taskId"]
505
+ required: []
506
+ }
507
+ },
508
+ {
509
+ name: "get_folder",
510
+ description: "Retrieve details about a specific ClickUp folder including its name, status, and other metadata. Before calling, check if you already have the necessary folder ID from previous responses in the conversation history, as this avoids redundant lookups. Helps you understand folder structure before creating or updating lists.",
511
+ inputSchema: {
512
+ type: "object",
513
+ properties: {
514
+ folderId: {
515
+ type: "string",
516
+ description: "ID of the folder to retrieve (optional if using folderName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
517
+ },
518
+ folderName: {
519
+ type: "string",
520
+ description: "Name of the folder to retrieve - will automatically find the folder by name (optional if using folderId instead). Only use this if you don't already have the folder ID from previous responses."
521
+ },
522
+ spaceId: {
523
+ type: "string",
524
+ description: "ID of the space containing the folder (optional if using spaceName instead, and only needed when using folderName). If you have this ID from a previous response, use it directly rather than looking up by name."
525
+ },
526
+ spaceName: {
527
+ type: "string",
528
+ description: "Name of the space containing the folder (optional if using spaceId instead, and only needed when using folderName). Only use this if you don't already have the space ID from previous responses."
529
+ }
530
+ },
531
+ required: []
532
+ }
533
+ },
534
+ {
535
+ name: "update_folder",
536
+ description: "Modify an existing ClickUp folder's properties, such as name or status settings. Before calling, check if you already have the necessary folder ID from previous responses in the conversation history, as this avoids redundant lookups. Use when reorganizing or renaming workspace elements.",
537
+ inputSchema: {
538
+ type: "object",
539
+ properties: {
540
+ folderId: {
541
+ type: "string",
542
+ description: "ID of the folder to update (optional if using folderName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
543
+ },
544
+ folderName: {
545
+ type: "string",
546
+ description: "Name of the folder to update - will automatically find the folder by name (optional if using folderId instead). Only use this if you don't already have the folder ID from previous responses."
547
+ },
548
+ spaceId: {
549
+ type: "string",
550
+ description: "ID of the space containing the folder (optional if using spaceName instead, and only needed when using folderName). If you have this ID from a previous response, use it directly rather than looking up by name."
551
+ },
552
+ spaceName: {
553
+ type: "string",
554
+ description: "Name of the space containing the folder (optional if using spaceId instead, and only needed when using folderName). Only use this if you don't already have the space ID from previous responses."
555
+ },
556
+ name: {
557
+ type: "string",
558
+ description: "New name for the folder"
559
+ },
560
+ override_statuses: {
561
+ type: "boolean",
562
+ description: "Whether to override space statuses with folder-specific statuses"
563
+ }
564
+ },
565
+ required: []
566
+ }
567
+ },
568
+ {
569
+ name: "delete_folder",
570
+ description: "Permanently remove a folder from your ClickUp workspace. Use with caution as deletion cannot be undone and will remove all lists and tasks within the folder. Before calling, check if you already have the necessary folder ID from previous responses in the conversation history, as this avoids redundant lookups.",
571
+ inputSchema: {
572
+ type: "object",
573
+ properties: {
574
+ folderId: {
575
+ type: "string",
576
+ description: "ID of the folder to delete (optional if using folderName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
577
+ },
578
+ folderName: {
579
+ type: "string",
580
+ description: "Name of the folder to delete - will automatically find the folder by name (optional if using folderId instead). Only use this if you don't already have the folder ID from previous responses."
581
+ },
582
+ spaceId: {
583
+ type: "string",
584
+ description: "ID of the space containing the folder (optional if using spaceName instead, and only needed when using folderName). If you have this ID from a previous response, use it directly rather than looking up by name."
585
+ },
586
+ spaceName: {
587
+ type: "string",
588
+ description: "Name of the space containing the folder (optional if using spaceId instead, and only needed when using folderName). Only use this if you don't already have the space ID from previous responses."
589
+ }
590
+ },
591
+ required: []
592
+ }
593
+ },
594
+ {
595
+ name: "get_list",
596
+ description: "Retrieve details about a specific ClickUp list including its name, content, status options, and other metadata. Before calling, check if you already have the necessary list ID from previous responses in the conversation history, as this avoids redundant lookups. Useful to understand list structure before creating or updating tasks.",
597
+ inputSchema: {
598
+ type: "object",
599
+ properties: {
600
+ listId: {
601
+ type: "string",
602
+ description: "ID of the list to retrieve (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
603
+ },
604
+ listName: {
605
+ type: "string",
606
+ description: "Name of the list to retrieve - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
607
+ }
608
+ },
609
+ required: []
610
+ }
611
+ },
612
+ {
613
+ name: "update_list",
614
+ description: "Modify an existing ClickUp list's properties, such as name, content, or status options. Before calling, check if you already have the necessary list ID from previous responses in the conversation history, as this avoids redundant lookups. Use when reorganizing or renaming workspace elements.",
615
+ inputSchema: {
616
+ type: "object",
617
+ properties: {
618
+ listId: {
619
+ type: "string",
620
+ description: "ID of the list to update (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
621
+ },
622
+ listName: {
623
+ type: "string",
624
+ description: "Name of the list to update - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
625
+ },
626
+ name: {
627
+ type: "string",
628
+ description: "New name for the list"
629
+ },
630
+ content: {
631
+ type: "string",
632
+ description: "New description or content for the list"
633
+ },
634
+ status: {
635
+ type: "string",
636
+ description: "New status for the list"
637
+ }
638
+ },
639
+ required: []
640
+ }
641
+ },
642
+ {
643
+ name: "delete_list",
644
+ description: "Permanently remove a list from your ClickUp workspace. Use with caution as deletion cannot be undone and will remove all tasks within the list. Before calling, check if you already have the necessary list ID from previous responses in the conversation history, as this avoids redundant lookups.",
645
+ inputSchema: {
646
+ type: "object",
647
+ properties: {
648
+ listId: {
649
+ type: "string",
650
+ description: "ID of the list to delete (optional if using listName instead). If you have this ID from a previous response, use it directly rather than looking up by name."
651
+ },
652
+ listName: {
653
+ type: "string",
654
+ description: "Name of the list to delete - will automatically find the list by name (optional if using listId instead). Only use this if you don't already have the list ID from previous responses."
655
+ }
656
+ },
657
+ required: []
504
658
  }
505
659
  }
506
660
  ]
@@ -578,12 +732,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
578
732
  }
579
733
  case "create_bulk_tasks": {
580
734
  const args = request.params.arguments;
581
- if (!args.listId && !args.listName) {
582
- throw new Error("Either listId or listName is required");
583
- }
584
- if (!args.tasks || args.tasks.length === 0) {
735
+ // First validate tasks array
736
+ if (!args.tasks || !Array.isArray(args.tasks) || args.tasks.length === 0) {
585
737
  throw new Error("tasks array is required and must not be empty");
586
738
  }
739
+ // Validate each task has required fields
740
+ args.tasks.forEach((task, index) => {
741
+ if (!task.name) {
742
+ throw new Error(`Task at index ${index} is missing required field 'name'`);
743
+ }
744
+ });
745
+ // Get listId from name if needed
587
746
  let listId = args.listId;
588
747
  if (!listId && args.listName) {
589
748
  const result = await clickup.findListIDByName(args.listName);
@@ -592,12 +751,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
592
751
  }
593
752
  listId = result.id;
594
753
  }
754
+ // Now validate we have a listId
755
+ if (!listId) {
756
+ throw new Error("Either listId or listName must be provided");
757
+ }
595
758
  const { listId: _, listName: __, tasks } = args;
596
759
  const createdTasks = await clickup.createBulkTasks(listId, { tasks });
597
760
  return {
598
761
  content: [{
599
762
  type: "text",
600
- text: `Created ${createdTasks.length} tasks`
763
+ text: JSON.stringify({
764
+ message: `Created ${createdTasks.length} tasks`,
765
+ tasks: createdTasks.map(task => ({
766
+ id: task.id,
767
+ name: task.name,
768
+ url: task.url
769
+ }))
770
+ }, null, 2)
601
771
  }]
602
772
  };
603
773
  }
@@ -750,9 +920,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
750
920
  }
751
921
  case "update_task": {
752
922
  const args = request.params.arguments;
923
+ // Require either taskId or taskName
753
924
  if (!args.taskId && !args.taskName) {
754
925
  throw new Error("Either taskId or taskName is required");
755
926
  }
927
+ // Get taskId from taskName if needed
756
928
  let taskId = args.taskId;
757
929
  if (!taskId && args.taskName) {
758
930
  const result = await clickup.findTaskByName(args.taskName, undefined, args.listName);
@@ -761,7 +933,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
761
933
  }
762
934
  taskId = result.id;
763
935
  }
936
+ // Remove helper fields before updating
764
937
  const { taskId: _, taskName: __, listName: ___, ...updateData } = args;
938
+ // Ensure priority is properly handled
939
+ if (updateData.priority !== undefined && updateData.priority !== null) {
940
+ const priority = Number(updateData.priority);
941
+ if (isNaN(priority) || ![1, 2, 3, 4].includes(priority)) {
942
+ throw new Error("Priority must be a number between 1 and 4, or null to clear priority");
943
+ }
944
+ updateData.priority = priority;
945
+ }
765
946
  const task = await clickup.updateTask(taskId, updateData);
766
947
  return {
767
948
  content: [{
@@ -841,6 +1022,243 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
841
1022
  }]
842
1023
  };
843
1024
  }
1025
+ case "get_folder": {
1026
+ const args = request.params.arguments;
1027
+ if (!args.folderId && !args.folderName) {
1028
+ throw new Error("Either folderId or folderName is required");
1029
+ }
1030
+ let folderId = args.folderId;
1031
+ if (!folderId && args.folderName) {
1032
+ // If we need to look up by name, we might need the space
1033
+ let spaceId = args.spaceId;
1034
+ if (!spaceId && args.spaceName) {
1035
+ const foundId = await clickup.findSpaceIDByName(args.spaceName);
1036
+ if (!foundId) {
1037
+ throw new Error(`Space with name "${args.spaceName}" not found`);
1038
+ }
1039
+ spaceId = foundId;
1040
+ }
1041
+ if (!spaceId) {
1042
+ // Try to find folder directly by name (will search across all spaces)
1043
+ const result = await clickup.findFolderIDByName(args.folderName);
1044
+ if (!result) {
1045
+ throw new Error(`Folder with name "${args.folderName}" not found`);
1046
+ }
1047
+ folderId = result.id;
1048
+ }
1049
+ else {
1050
+ // Look in a specific space
1051
+ const folder = await clickup.findFolderByName(spaceId, args.folderName);
1052
+ if (!folder) {
1053
+ throw new Error(`Folder with name "${args.folderName}" not found in specified space`);
1054
+ }
1055
+ folderId = folder.id;
1056
+ }
1057
+ }
1058
+ // Ensure folderId is defined at this point
1059
+ if (!folderId) {
1060
+ throw new Error("Failed to determine folder ID");
1061
+ }
1062
+ const folder = await clickup.getFolder(folderId);
1063
+ return {
1064
+ content: [{
1065
+ type: "text",
1066
+ text: JSON.stringify(folder, null, 2)
1067
+ }]
1068
+ };
1069
+ }
1070
+ case "update_folder": {
1071
+ const args = request.params.arguments;
1072
+ if (!args.folderId && !args.folderName) {
1073
+ throw new Error("Either folderId or folderName is required");
1074
+ }
1075
+ let folderId = args.folderId;
1076
+ if (!folderId && args.folderName) {
1077
+ // If we need to look up by name, we might need the space
1078
+ let spaceId = args.spaceId;
1079
+ if (!spaceId && args.spaceName) {
1080
+ const foundId = await clickup.findSpaceIDByName(args.spaceName);
1081
+ if (!foundId) {
1082
+ throw new Error(`Space with name "${args.spaceName}" not found`);
1083
+ }
1084
+ spaceId = foundId;
1085
+ }
1086
+ if (!spaceId) {
1087
+ // Try to find folder directly by name (will search across all spaces)
1088
+ const result = await clickup.findFolderIDByName(args.folderName);
1089
+ if (!result) {
1090
+ throw new Error(`Folder with name "${args.folderName}" not found`);
1091
+ }
1092
+ folderId = result.id;
1093
+ }
1094
+ else {
1095
+ // Look in a specific space
1096
+ const folder = await clickup.findFolderByName(spaceId, args.folderName);
1097
+ if (!folder) {
1098
+ throw new Error(`Folder with name "${args.folderName}" not found in specified space`);
1099
+ }
1100
+ folderId = folder.id;
1101
+ }
1102
+ }
1103
+ // Ensure folderId is defined at this point
1104
+ if (!folderId) {
1105
+ throw new Error("Failed to determine folder ID");
1106
+ }
1107
+ // Extract update data
1108
+ const { folderId: _, folderName: __, spaceId: ___, spaceName: ____, ...updateData } = args;
1109
+ // Call the updateFolder method
1110
+ const updatedFolder = await clickup.updateFolder(folderId, updateData);
1111
+ return {
1112
+ content: [{
1113
+ type: "text",
1114
+ text: `Updated folder ${updatedFolder.id}: ${updatedFolder.name}`
1115
+ }]
1116
+ };
1117
+ }
1118
+ case "delete_folder": {
1119
+ const args = request.params.arguments;
1120
+ if (!args.folderId && !args.folderName) {
1121
+ throw new Error("Either folderId or folderName is required");
1122
+ }
1123
+ let folderId = args.folderId;
1124
+ if (!folderId && args.folderName) {
1125
+ // If we need to look up by name, we might need the space
1126
+ let spaceId = args.spaceId;
1127
+ if (!spaceId && args.spaceName) {
1128
+ const foundId = await clickup.findSpaceIDByName(args.spaceName);
1129
+ if (!foundId) {
1130
+ throw new Error(`Space with name "${args.spaceName}" not found`);
1131
+ }
1132
+ spaceId = foundId;
1133
+ }
1134
+ if (!spaceId) {
1135
+ // Try to find folder directly by name (will search across all spaces)
1136
+ const result = await clickup.findFolderIDByName(args.folderName);
1137
+ if (!result) {
1138
+ throw new Error(`Folder with name "${args.folderName}" not found`);
1139
+ }
1140
+ folderId = result.id;
1141
+ }
1142
+ else {
1143
+ // Look in a specific space
1144
+ const folder = await clickup.findFolderByName(spaceId, args.folderName);
1145
+ if (!folder) {
1146
+ throw new Error(`Folder with name "${args.folderName}" not found in specified space`);
1147
+ }
1148
+ folderId = folder.id;
1149
+ }
1150
+ }
1151
+ // Ensure folderId is defined at this point
1152
+ if (!folderId) {
1153
+ throw new Error("Failed to determine folder ID");
1154
+ }
1155
+ // Store the folder name before deletion for the response message
1156
+ let folderName = args.folderName;
1157
+ if (!folderName) {
1158
+ try {
1159
+ const folderDetails = await clickup.getFolder(folderId);
1160
+ folderName = folderDetails.name;
1161
+ }
1162
+ catch (error) {
1163
+ // If we can't get the folder details, just use the ID in the response
1164
+ }
1165
+ }
1166
+ await clickup.deleteFolder(folderId);
1167
+ return {
1168
+ content: [{
1169
+ type: "text",
1170
+ text: `Successfully deleted folder ${folderName || folderId}`
1171
+ }]
1172
+ };
1173
+ }
1174
+ case "get_list": {
1175
+ const args = request.params.arguments;
1176
+ if (!args.listId && !args.listName) {
1177
+ throw new Error("Either listId or listName is required");
1178
+ }
1179
+ let listId = args.listId;
1180
+ if (!listId && args.listName) {
1181
+ const result = await clickup.findListIDByName(args.listName);
1182
+ if (!result) {
1183
+ throw new Error(`List with name "${args.listName}" not found`);
1184
+ }
1185
+ listId = result.id;
1186
+ }
1187
+ // Ensure listId is defined at this point
1188
+ if (!listId) {
1189
+ throw new Error("Failed to determine list ID");
1190
+ }
1191
+ const listDetails = await clickup.getList(listId);
1192
+ return {
1193
+ content: [{
1194
+ type: "text",
1195
+ text: JSON.stringify(listDetails, null, 2)
1196
+ }]
1197
+ };
1198
+ }
1199
+ case "update_list": {
1200
+ const args = request.params.arguments;
1201
+ if (!args.listId && !args.listName) {
1202
+ throw new Error("Either listId or listName is required");
1203
+ }
1204
+ let listId = args.listId;
1205
+ if (!listId && args.listName) {
1206
+ const result = await clickup.findListIDByName(args.listName);
1207
+ if (!result) {
1208
+ throw new Error(`List with name "${args.listName}" not found`);
1209
+ }
1210
+ listId = result.id;
1211
+ }
1212
+ // Ensure listId is defined at this point
1213
+ if (!listId) {
1214
+ throw new Error("Failed to determine list ID");
1215
+ }
1216
+ // Extract update data
1217
+ const { listId: _, listName: __, ...updateData } = args;
1218
+ const updatedList = await clickup.updateList(listId, updateData);
1219
+ return {
1220
+ content: [{
1221
+ type: "text",
1222
+ text: `Updated list ${updatedList.id}: ${updatedList.name}`
1223
+ }]
1224
+ };
1225
+ }
1226
+ case "delete_list": {
1227
+ const args = request.params.arguments;
1228
+ if (!args.listId && !args.listName) {
1229
+ throw new Error("Either listId or listName is required");
1230
+ }
1231
+ let listId = args.listId;
1232
+ if (!listId && args.listName) {
1233
+ const result = await clickup.findListIDByName(args.listName);
1234
+ if (!result) {
1235
+ throw new Error(`List with name "${args.listName}" not found`);
1236
+ }
1237
+ listId = result.id;
1238
+ }
1239
+ // Ensure listId is defined at this point
1240
+ if (!listId) {
1241
+ throw new Error("Failed to determine list ID");
1242
+ }
1243
+ // Store the list name before deletion for the response message
1244
+ let listName = args.listName;
1245
+ if (!listName) {
1246
+ try {
1247
+ const listDetails = await clickup.getList(listId);
1248
+ listName = listDetails.name;
1249
+ }
1250
+ catch (error) {
1251
+ // If we can't get the list details, just use the ID in the response
1252
+ }
1253
+ }
1254
+ await clickup.deleteList(listId);
1255
+ return {
1256
+ content: [{
1257
+ type: "text",
1258
+ text: `Successfully deleted list ${listName || listId}`
1259
+ }]
1260
+ };
1261
+ }
844
1262
  default:
845
1263
  throw new Error(`Unknown tool: ${request.params.name}`);
846
1264
  }
@@ -59,16 +59,7 @@ export class ClickUpService {
59
59
  */
60
60
  async makeRequest(requestFn) {
61
61
  await this.checkRateLimit();
62
- try {
63
- return await requestFn();
64
- }
65
- catch (error) {
66
- if (error.response?.status === 429) {
67
- // Let the interceptor handle it
68
- throw error;
69
- }
70
- throw error;
71
- }
62
+ return await requestFn();
72
63
  }
73
64
  /**
74
65
  * Initializes the ClickUpService singleton instance.
@@ -183,10 +174,15 @@ export class ClickUpService {
183
174
  async createTask(listId, data) {
184
175
  return this.makeRequest(async () => {
185
176
  const taskData = { ...data };
186
- if (taskData.description && /[#*`\-\[\]>]/.test(taskData.description)) {
187
- taskData.markdown_description = taskData.description;
177
+ // If markdown_description is provided, it takes precedence
178
+ if (taskData.markdown_description) {
179
+ // Ensure we don't send both to avoid confusion
188
180
  delete taskData.description;
189
181
  }
182
+ else if (taskData.description) {
183
+ // Only use description as-is, don't auto-convert to markdown
184
+ taskData.description = taskData.description.trim();
185
+ }
190
186
  const response = await this.client.post(`/list/${listId}/task`, taskData);
191
187
  return response.data;
192
188
  });
@@ -200,10 +196,15 @@ export class ClickUpService {
200
196
  for (const taskData of data.tasks) {
201
197
  await this.makeRequest(async () => {
202
198
  const processedTask = { ...taskData };
203
- if (processedTask.description && /[#*`\-\[\]>]/.test(processedTask.description)) {
204
- processedTask.markdown_description = processedTask.description;
199
+ // If markdown_description is provided, it takes precedence
200
+ if (processedTask.markdown_description) {
201
+ // Ensure we don't send both to avoid confusion
205
202
  delete processedTask.description;
206
203
  }
204
+ else if (processedTask.description) {
205
+ // Only use description as-is, don't auto-convert to markdown
206
+ processedTask.description = processedTask.description.trim();
207
+ }
207
208
  const response = await this.client.post(`/list/${listId}/task`, processedTask);
208
209
  createdTasks.push(response.data);
209
210
  });
@@ -216,7 +217,21 @@ export class ClickUpService {
216
217
  */
217
218
  async updateTask(taskId, data) {
218
219
  return this.makeRequest(async () => {
219
- const response = await this.client.put(`/task/${taskId}`, data);
220
+ const updateData = { ...data };
221
+ // If markdown_description is provided, it takes precedence
222
+ if (updateData.markdown_description) {
223
+ // Ensure we don't send both to avoid confusion
224
+ delete updateData.description;
225
+ }
226
+ else if (updateData.description) {
227
+ // Only use description as-is, don't auto-convert to markdown
228
+ updateData.description = updateData.description.trim();
229
+ }
230
+ // Handle null priority explicitly
231
+ if (updateData.priority === null) {
232
+ updateData.priority = null;
233
+ }
234
+ const response = await this.client.put(`/task/${taskId}`, updateData);
220
235
  return response.data;
221
236
  });
222
237
  }
@@ -309,9 +324,16 @@ export class ClickUpService {
309
324
  return response.data;
310
325
  });
311
326
  }
312
- async createFolder(spaceId, data) {
327
+ /**
328
+ * Updates an existing folder with new data.
329
+ * @param folderId - ID of the folder to update
330
+ * @param data - Data to update the folder with (name, override_statuses)
331
+ * @returns Promise resolving to the updated ClickUpFolder
332
+ * @throws Error if the API request fails
333
+ */
334
+ async updateFolder(folderId, data) {
313
335
  return this.makeRequest(async () => {
314
- const response = await this.client.post(`/space/${spaceId}/folder`, data);
336
+ const response = await this.client.put(`/folder/${folderId}`, data);
315
337
  return response.data;
316
338
  });
317
339
  }
@@ -337,6 +359,19 @@ export class ClickUpService {
337
359
  const folders = await this.getFolders(spaceId);
338
360
  return folders.find(folder => folder.name.toLowerCase() === folderName.toLowerCase()) || null;
339
361
  }
362
+ /**
363
+ * Creates a new folder in a space.
364
+ * @param spaceId - ID of the space to create the folder in
365
+ * @param data - Folder creation data (name, override_statuses)
366
+ * @returns Promise resolving to the created ClickUpFolder
367
+ * @throws Error if the API request fails
368
+ */
369
+ async createFolder(spaceId, data) {
370
+ return this.makeRequest(async () => {
371
+ const response = await this.client.post(`/space/${spaceId}/folder`, data);
372
+ return response.data;
373
+ });
374
+ }
340
375
  // Additional helper methods
341
376
  /**
342
377
  * Moves a task to a different list.
@@ -710,11 +745,4 @@ export class ClickUpService {
710
745
  : `${task.space.name} > ${task.list.name} > ${task.name}`;
711
746
  return { id: task.id, path };
712
747
  }
713
- async getTaskStatuses(listId) {
714
- const response = await this.getTasks(listId);
715
- const statuses = [...new Set(response.tasks
716
- .filter((task) => task.status !== undefined)
717
- .map((task) => task.status.status))];
718
- return statuses;
719
- }
720
748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.4.54",
3
+ "version": "0.4.57",
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",
@@ -36,7 +36,7 @@
36
36
  "license": "MIT",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/taazkareem/clickup-mcp-server.git"
39
+ "url": "git+https://github.com/taazkareem/clickup-mcp-server.git"
40
40
  },
41
41
  "bugs": {
42
42
  "url": "https://github.com/taazkareem/clickup-mcp-server/issues"
@@ -44,10 +44,8 @@
44
44
  "homepage": "https://github.com/taazkareem/clickup-mcp-server#readme",
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/sdk": "0.6.0",
47
- "@types/express": "^5.0.0",
48
47
  "axios": "^1.6.7",
49
- "dotenv": "^16.4.1",
50
- "express": "^4.21.2"
48
+ "dotenv": "^16.4.1"
51
49
  },
52
50
  "devDependencies": {
53
51
  "@types/node": "^20.11.16",