@taazkareem/clickup-mcp-server 0.4.56 → 0.4.58

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/README.md CHANGED
@@ -38,7 +38,6 @@ npx -y @taazkareem/clickup-mcp-server \
38
38
  - Full CRUD operations for workspace components
39
39
  - Efficient path-based navigation
40
40
 
41
-
42
41
  - 🔄 **Integration Features**
43
42
  - Name or ID-based item lookup
44
43
  - Case-insensitive name matching
package/build/index.js CHANGED
@@ -109,7 +109,11 @@ 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: ["name"],
113
+ oneOf: [
114
+ { required: ["listId"] },
115
+ { required: ["listName"] }
116
+ ]
113
117
  }
114
118
  },
115
119
  {
@@ -168,7 +172,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
168
172
  }
169
173
  }
170
174
  },
171
- required: ["listId", "tasks"]
175
+ required: ["tasks"],
176
+ oneOf: [
177
+ { required: ["listId"] },
178
+ { required: ["listName"] }
179
+ ]
172
180
  }
173
181
  },
174
182
  {
@@ -179,11 +187,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
179
187
  properties: {
180
188
  spaceId: {
181
189
  type: "string",
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."
190
+ description: "ID of the space to create the list in (required if not using folderId). If you have this ID from a previous response, use it directly rather than looking up by name."
183
191
  },
184
- spaceName: {
192
+ folderId: {
185
193
  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). Only use this if you don't already have the space ID from previous responses."
194
+ description: "ID of the folder to create the list in (required if not using spaceId). If you have this ID from a previous response, use it directly rather than looking up by name."
187
195
  },
188
196
  name: {
189
197
  type: "string",
@@ -193,24 +201,34 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
193
201
  type: "string",
194
202
  description: "Description or content of the list"
195
203
  },
196
- dueDate: {
197
- type: "string",
198
- description: "Due date for the list (Unix timestamp in milliseconds). Convert dates to this format before submitting."
204
+ assignee: {
205
+ type: "number",
206
+ description: "User ID to assign the list to"
199
207
  },
200
208
  priority: {
201
209
  type: "number",
202
210
  description: "Priority of the list (1-4), where 1 is urgent/highest priority and 4 is lowest priority. Only set when explicitly requested."
203
211
  },
204
- assignee: {
205
- type: "number",
206
- description: "User ID to assign the list to"
212
+ dueDate: {
213
+ type: "string",
214
+ description: "Due date for the list (Unix timestamp in milliseconds). Convert dates to this format before submitting."
207
215
  },
208
216
  status: {
209
217
  type: "string",
210
218
  description: "Status of the list"
211
219
  }
212
220
  },
213
- required: ["name"]
221
+ allOf: [
222
+ {
223
+ oneOf: [
224
+ { required: ["spaceId"] },
225
+ { required: ["folderId"] }
226
+ ]
227
+ },
228
+ {
229
+ required: ["name"]
230
+ }
231
+ ]
214
232
  }
215
233
  },
216
234
  {
@@ -236,7 +254,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
236
254
  description: "Whether to override space statuses with folder-specific statuses"
237
255
  }
238
256
  },
239
- required: ["name"]
257
+ required: ["name"],
258
+ oneOf: [
259
+ { required: ["spaceId"] },
260
+ { required: ["spaceName"] }
261
+ ]
240
262
  }
241
263
  },
242
264
  {
@@ -274,7 +296,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
274
296
  description: "Status of the list (uses folder default if not specified)"
275
297
  }
276
298
  },
277
- required: ["name"]
299
+ required: ["name"],
300
+ oneOf: [
301
+ { required: ["folderId"] },
302
+ { required: ["folderName", "spaceId"] },
303
+ { required: ["folderName", "spaceName"] }
304
+ ]
278
305
  }
279
306
  },
280
307
  {
@@ -304,7 +331,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
304
331
  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
332
  }
306
333
  },
307
- required: ["taskName", "listName"]
334
+ allOf: [
335
+ {
336
+ oneOf: [
337
+ { required: ["taskId"] },
338
+ { required: ["taskName"] }
339
+ ]
340
+ },
341
+ {
342
+ oneOf: [
343
+ { required: ["listId"] },
344
+ { required: ["listName"] }
345
+ ]
346
+ }
347
+ ]
308
348
  }
309
349
  },
310
350
  {
@@ -334,7 +374,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
334
374
  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
375
  }
336
376
  },
337
- required: ["taskName", "listName"]
377
+ allOf: [
378
+ {
379
+ oneOf: [
380
+ { required: ["taskId"] },
381
+ { required: ["taskName"] }
382
+ ]
383
+ },
384
+ {
385
+ oneOf: [
386
+ { required: ["listId"] },
387
+ { required: ["listName"] }
388
+ ]
389
+ }
390
+ ]
338
391
  }
339
392
  },
340
393
  {
@@ -363,20 +416,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
363
416
  type: "string",
364
417
  description: "New plain text description for the task"
365
418
  },
419
+ markdown_description: {
420
+ type: "string",
421
+ description: "New markdown formatted description for the task. If provided, this takes precedence over description"
422
+ },
366
423
  status: {
367
424
  type: "string",
368
425
  description: "New status for the task (must be a valid status in the task's list)"
369
426
  },
370
427
  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."
428
+ type: ["number", "null"],
429
+ enum: [1, 2, 3, 4, null],
430
+ 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.",
431
+ optional: true
377
432
  }
378
433
  },
379
- required: ["taskName"]
434
+ oneOf: [
435
+ { required: ["taskId"] },
436
+ { required: ["taskName"] }
437
+ ]
380
438
  }
381
439
  },
382
440
  {
@@ -456,7 +514,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
456
514
  description: "Object with custom field IDs as keys and desired values for filtering"
457
515
  }
458
516
  },
459
- required: ["listName"]
517
+ oneOf: [
518
+ { required: ["listId"] },
519
+ { required: ["listName"] }
520
+ ]
460
521
  }
461
522
  },
462
523
  {
@@ -478,7 +539,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
478
539
  description: "Optional: Name of the list to narrow down task search when multiple tasks have the same name"
479
540
  }
480
541
  },
481
- required: ["taskName"]
542
+ oneOf: [
543
+ { required: ["taskId"] },
544
+ { required: ["taskName"] }
545
+ ]
482
546
  }
483
547
  },
484
548
  {
@@ -526,7 +590,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
526
590
  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."
527
591
  }
528
592
  },
529
- required: []
593
+ oneOf: [
594
+ { required: ["folderId"] },
595
+ { required: ["folderName", "spaceId"] },
596
+ { required: ["folderName", "spaceName"] }
597
+ ]
530
598
  }
531
599
  },
532
600
  {
@@ -560,7 +628,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
560
628
  description: "Whether to override space statuses with folder-specific statuses"
561
629
  }
562
630
  },
563
- required: []
631
+ oneOf: [
632
+ { required: ["folderId"] },
633
+ { required: ["folderName", "spaceId"] },
634
+ { required: ["folderName", "spaceName"] }
635
+ ]
564
636
  }
565
637
  },
566
638
  {
@@ -586,7 +658,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
586
658
  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."
587
659
  }
588
660
  },
589
- required: []
661
+ oneOf: [
662
+ { required: ["folderId"] },
663
+ { required: ["folderName", "spaceId"] },
664
+ { required: ["folderName", "spaceName"] }
665
+ ]
590
666
  }
591
667
  },
592
668
  {
@@ -604,7 +680,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
604
680
  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."
605
681
  }
606
682
  },
607
- required: []
683
+ oneOf: [
684
+ { required: ["listId"] },
685
+ { required: ["listName"] }
686
+ ]
608
687
  }
609
688
  },
610
689
  {
@@ -634,7 +713,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
634
713
  description: "New status for the list"
635
714
  }
636
715
  },
637
- required: []
716
+ oneOf: [
717
+ { required: ["listId"] },
718
+ { required: ["listName"] }
719
+ ]
638
720
  }
639
721
  },
640
722
  {
@@ -652,7 +734,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
652
734
  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."
653
735
  }
654
736
  },
655
- required: []
737
+ oneOf: [
738
+ { required: ["listId"] },
739
+ { required: ["listName"] }
740
+ ]
656
741
  }
657
742
  }
658
743
  ]
@@ -730,12 +815,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
730
815
  }
731
816
  case "create_bulk_tasks": {
732
817
  const args = request.params.arguments;
733
- if (!args.listId && !args.listName) {
734
- throw new Error("Either listId or listName is required");
735
- }
736
- if (!args.tasks || args.tasks.length === 0) {
818
+ // First validate tasks array
819
+ if (!args.tasks || !Array.isArray(args.tasks) || args.tasks.length === 0) {
737
820
  throw new Error("tasks array is required and must not be empty");
738
821
  }
822
+ // Validate each task has required fields
823
+ args.tasks.forEach((task, index) => {
824
+ if (!task.name) {
825
+ throw new Error(`Task at index ${index} is missing required field 'name'`);
826
+ }
827
+ });
828
+ // Get listId from name if needed
739
829
  let listId = args.listId;
740
830
  if (!listId && args.listName) {
741
831
  const result = await clickup.findListIDByName(args.listName);
@@ -744,33 +834,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
744
834
  }
745
835
  listId = result.id;
746
836
  }
837
+ // Now validate we have a listId
838
+ if (!listId) {
839
+ throw new Error("Either listId or listName must be provided");
840
+ }
747
841
  const { listId: _, listName: __, tasks } = args;
748
842
  const createdTasks = await clickup.createBulkTasks(listId, { tasks });
749
843
  return {
750
844
  content: [{
751
845
  type: "text",
752
- text: `Created ${createdTasks.length} tasks`
846
+ text: JSON.stringify({
847
+ message: `Created ${createdTasks.length} tasks`,
848
+ tasks: createdTasks.map(task => ({
849
+ id: task.id,
850
+ name: task.name,
851
+ url: task.url
852
+ }))
853
+ }, null, 2)
753
854
  }]
754
855
  };
755
856
  }
756
857
  case "create_list": {
757
- const args = request.params.arguments;
858
+ const args = request.params.arguments ? request.params.arguments : { name: '' };
758
859
  if (!args.name) {
759
- throw new Error("name is required");
760
- }
761
- let spaceId = args.spaceId;
762
- if (!spaceId && args.spaceName) {
763
- const foundId = await clickup.findSpaceIDByName(args.spaceName);
764
- if (!foundId) {
765
- throw new Error(`Space with name "${args.spaceName}" not found`);
860
+ throw new Error("List name is required");
861
+ }
862
+ // Validate that we have either spaceId/spaceName OR folderId/folderName, but not both
863
+ const hasSpace = !!(args.spaceId || args.spaceName);
864
+ const hasFolder = !!(args.folderId || args.folderName);
865
+ if (!hasSpace && !hasFolder) {
866
+ throw new Error("Either spaceId/spaceName or folderId/folderName must be provided");
867
+ }
868
+ if (hasSpace && hasFolder) {
869
+ throw new Error("Cannot provide both space and folder identifiers. Use either spaceId/spaceName OR folderId/folderName");
870
+ }
871
+ // Prepare the list data
872
+ const listData = {
873
+ name: args.name,
874
+ content: args.content,
875
+ due_date: args.dueDate,
876
+ priority: args.priority,
877
+ assignee: args.assignee,
878
+ status: args.status
879
+ };
880
+ let list;
881
+ if (hasSpace) {
882
+ // Handle space-based creation
883
+ let spaceId = args.spaceId;
884
+ if (!spaceId && args.spaceName) {
885
+ const teamId = await clickup.getTeamId();
886
+ const space = await clickup.findSpaceByName(args.spaceName, teamId);
887
+ if (!space) {
888
+ throw new Error(`Space with name "${args.spaceName}" not found`);
889
+ }
890
+ spaceId = space.id;
766
891
  }
767
- spaceId = foundId;
892
+ list = await clickup.createList(spaceId, listData);
768
893
  }
769
- if (!spaceId) {
770
- throw new Error("Either spaceId or spaceName must be provided");
894
+ else {
895
+ // Handle folder-based creation
896
+ let folderId = args.folderId;
897
+ if (!folderId && args.folderName) {
898
+ const result = await clickup.findFolderIDByName(args.folderName);
899
+ if (!result) {
900
+ throw new Error(`Folder with name "${args.folderName}" not found`);
901
+ }
902
+ folderId = result.id;
903
+ }
904
+ list = await clickup.createListInFolder(folderId, listData);
771
905
  }
772
- const { spaceId: _, spaceName: __, ...listData } = args;
773
- const list = await clickup.createList(spaceId, listData);
774
906
  return {
775
907
  content: [{
776
908
  type: "text",
@@ -902,9 +1034,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
902
1034
  }
903
1035
  case "update_task": {
904
1036
  const args = request.params.arguments;
1037
+ // Require either taskId or taskName
905
1038
  if (!args.taskId && !args.taskName) {
906
1039
  throw new Error("Either taskId or taskName is required");
907
1040
  }
1041
+ // Get taskId from taskName if needed
908
1042
  let taskId = args.taskId;
909
1043
  if (!taskId && args.taskName) {
910
1044
  const result = await clickup.findTaskByName(args.taskName, undefined, args.listName);
@@ -913,7 +1047,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
913
1047
  }
914
1048
  taskId = result.id;
915
1049
  }
1050
+ // Remove helper fields before updating
916
1051
  const { taskId: _, taskName: __, listName: ___, ...updateData } = args;
1052
+ // Ensure priority is properly handled
1053
+ if (updateData.priority !== undefined && updateData.priority !== null) {
1054
+ const priority = Number(updateData.priority);
1055
+ if (isNaN(priority) || ![1, 2, 3, 4].includes(priority)) {
1056
+ throw new Error("Priority must be a number between 1 and 4, or null to clear priority");
1057
+ }
1058
+ updateData.priority = priority;
1059
+ }
917
1060
  const task = await clickup.updateTask(taskId, updateData);
918
1061
  return {
919
1062
  content: [{
@@ -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.
@@ -94,6 +85,13 @@ export class ClickUpService {
94
85
  }
95
86
  return ClickUpService.instance;
96
87
  }
88
+ /**
89
+ * Gets the team/workspace ID that was set during initialization.
90
+ * @returns The team/workspace ID
91
+ */
92
+ getTeamId() {
93
+ return this.clickupTeamId;
94
+ }
97
95
  // Tasks
98
96
  /**
99
97
  * Retrieves tasks from a specific list with optional filtering.
@@ -183,10 +181,15 @@ export class ClickUpService {
183
181
  async createTask(listId, data) {
184
182
  return this.makeRequest(async () => {
185
183
  const taskData = { ...data };
186
- if (taskData.description && /[#*`\-\[\]>]/.test(taskData.description)) {
187
- taskData.markdown_description = taskData.description;
184
+ // If markdown_description is provided, it takes precedence
185
+ if (taskData.markdown_description) {
186
+ // Ensure we don't send both to avoid confusion
188
187
  delete taskData.description;
189
188
  }
189
+ else if (taskData.description) {
190
+ // Only use description as-is, don't auto-convert to markdown
191
+ taskData.description = taskData.description.trim();
192
+ }
190
193
  const response = await this.client.post(`/list/${listId}/task`, taskData);
191
194
  return response.data;
192
195
  });
@@ -200,10 +203,15 @@ export class ClickUpService {
200
203
  for (const taskData of data.tasks) {
201
204
  await this.makeRequest(async () => {
202
205
  const processedTask = { ...taskData };
203
- if (processedTask.description && /[#*`\-\[\]>]/.test(processedTask.description)) {
204
- processedTask.markdown_description = processedTask.description;
206
+ // If markdown_description is provided, it takes precedence
207
+ if (processedTask.markdown_description) {
208
+ // Ensure we don't send both to avoid confusion
205
209
  delete processedTask.description;
206
210
  }
211
+ else if (processedTask.description) {
212
+ // Only use description as-is, don't auto-convert to markdown
213
+ processedTask.description = processedTask.description.trim();
214
+ }
207
215
  const response = await this.client.post(`/list/${listId}/task`, processedTask);
208
216
  createdTasks.push(response.data);
209
217
  });
@@ -216,7 +224,21 @@ export class ClickUpService {
216
224
  */
217
225
  async updateTask(taskId, data) {
218
226
  return this.makeRequest(async () => {
219
- const response = await this.client.put(`/task/${taskId}`, data);
227
+ const updateData = { ...data };
228
+ // If markdown_description is provided, it takes precedence
229
+ if (updateData.markdown_description) {
230
+ // Ensure we don't send both to avoid confusion
231
+ delete updateData.description;
232
+ }
233
+ else if (updateData.description) {
234
+ // Only use description as-is, don't auto-convert to markdown
235
+ updateData.description = updateData.description.trim();
236
+ }
237
+ // Handle null priority explicitly
238
+ if (updateData.priority === null) {
239
+ updateData.priority = null;
240
+ }
241
+ const response = await this.client.put(`/task/${taskId}`, updateData);
220
242
  return response.data;
221
243
  });
222
244
  }
@@ -285,6 +307,9 @@ export class ClickUpService {
285
307
  }
286
308
  /**
287
309
  * Creates a new list in a space.
310
+ * Note: ClickUp API requires lists to be in folders, so this will:
311
+ * 1. Create a default folder if none specified
312
+ * 2. Create the list within that folder
288
313
  * @param spaceId - ID of the space to create the list in
289
314
  * @param data - List creation data (name, content, due date, etc.)
290
315
  * @returns Promise resolving to the created ClickUpList
@@ -292,7 +317,17 @@ export class ClickUpService {
292
317
  */
293
318
  async createList(spaceId, data) {
294
319
  return this.makeRequest(async () => {
295
- const response = await this.client.post(`/space/${spaceId}/list`, data);
320
+ // First, get or create a default folder
321
+ const folders = await this.getFolders(spaceId);
322
+ let defaultFolder = folders.find(f => f.name === 'Default Lists');
323
+ if (!defaultFolder) {
324
+ // Create a default folder if none exists
325
+ defaultFolder = await this.createFolder(spaceId, {
326
+ name: 'Default Lists'
327
+ });
328
+ }
329
+ // Create the list within the default folder
330
+ const response = await this.client.post(`/folder/${defaultFolder.id}/list`, data);
296
331
  return response.data;
297
332
  });
298
333
  }
@@ -730,11 +765,4 @@ export class ClickUpService {
730
765
  : `${task.space.name} > ${task.list.name} > ${task.name}`;
731
766
  return { id: task.id, path };
732
767
  }
733
- async getTaskStatuses(listId) {
734
- const response = await this.getTasks(listId);
735
- const statuses = [...new Set(response.tasks
736
- .filter((task) => task.status !== undefined)
737
- .map((task) => task.status.status))];
738
- return statuses;
739
- }
740
768
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.4.56",
3
+ "version": "0.4.58",
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",
@@ -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",