@taazkareem/clickup-mcp-server 0.4.60 → 0.4.62

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,1486 @@
1
+ /**
2
+ * ClickUp MCP Task Tools
3
+ *
4
+ * This module defines task-related tools including creating, updating,
5
+ * moving, duplicating, and deleting tasks. It also provides tools for
6
+ * retrieving task details.
7
+ */
8
+ import { createClickUpServices } from '../services/clickup/index.js';
9
+ import config from '../config.js';
10
+ import { findListIDByName } from './list.js';
11
+ import { parseDueDate } from './utils.js';
12
+ // Initialize ClickUp services using the factory function
13
+ const services = createClickUpServices({
14
+ apiKey: config.clickupApiKey,
15
+ teamId: config.clickupTeamId
16
+ });
17
+ // Extract the services we need for task operations
18
+ const { task: taskService, workspace: workspaceService } = services;
19
+ /**
20
+ * Tool definition for creating a single task
21
+ */
22
+ export const createTaskTool = {
23
+ name: "create_task",
24
+ 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.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ name: {
29
+ type: "string",
30
+ description: "Name of the task. Put a relevant emoji followed by a blank space before the name."
31
+ },
32
+ description: {
33
+ type: "string",
34
+ description: "Plain text description for the task"
35
+ },
36
+ markdown_description: {
37
+ type: "string",
38
+ description: "Markdown formatted description for the task. If provided, this takes precedence over description"
39
+ },
40
+ listId: {
41
+ type: "string",
42
+ 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."
43
+ },
44
+ listName: {
45
+ type: "string",
46
+ 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."
47
+ },
48
+ status: {
49
+ type: "string",
50
+ description: "OPTIONAL: Override the default ClickUp status. In most cases, you should omit this to use ClickUp defaults"
51
+ },
52
+ priority: {
53
+ type: "number",
54
+ description: "Priority of the task (1-4), where 1 is urgent/highest priority and 4 is lowest priority. Only set this when the user explicitly requests a priority level."
55
+ },
56
+ dueDate: {
57
+ type: "string",
58
+ description: "Due date of the task (Unix timestamp in milliseconds). Convert dates to this format before submitting."
59
+ }
60
+ },
61
+ required: ["name"]
62
+ },
63
+ async handler({ name, description, markdown_description, dueDate, priority, status, listId, listName }) {
64
+ let targetListId = listId;
65
+ // If no listId but listName is provided, look up the list ID
66
+ if (!targetListId && listName) {
67
+ // Use workspace service to find list by name
68
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
69
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
70
+ if (!listInfo) {
71
+ throw new Error(`List "${listName}" not found`);
72
+ }
73
+ targetListId = listInfo.id;
74
+ }
75
+ if (!targetListId) {
76
+ throw new Error("Either listId or listName must be provided");
77
+ }
78
+ // Prepare task data
79
+ const taskData = {
80
+ name,
81
+ description,
82
+ markdown_description,
83
+ status,
84
+ priority: priority,
85
+ due_date: dueDate ? parseDueDate(dueDate) : undefined
86
+ };
87
+ // Add due_date_time flag if due date is set
88
+ if (dueDate && taskData.due_date) {
89
+ taskData.due_date_time = true;
90
+ }
91
+ // Create the task
92
+ const createdTask = await taskService.createTask(targetListId, taskData);
93
+ // Format response
94
+ return {
95
+ content: [{
96
+ type: "text",
97
+ text: JSON.stringify({
98
+ id: createdTask.id,
99
+ name: createdTask.name,
100
+ url: createdTask.url,
101
+ status: createdTask.status?.status || "New",
102
+ list: createdTask.list.name,
103
+ space: createdTask.space.name,
104
+ folder: createdTask.folder?.name
105
+ }, null, 2)
106
+ }]
107
+ };
108
+ }
109
+ };
110
+ /**
111
+ * Tool definition for updating a task
112
+ */
113
+ export const updateTaskTool = {
114
+ name: "update_task",
115
+ description: "Modify an existing task's properties. Valid parameter combinations:\n1. Use taskId alone (preferred if you have it)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)\n\nAt least one update field (name, description, status, priority) must be provided. Only specified fields will be updated.",
116
+ inputSchema: {
117
+ type: "object",
118
+ properties: {
119
+ taskId: {
120
+ type: "string",
121
+ description: "ID of the task to update (preferred). Use this instead of taskName if you have it from a previous response."
122
+ },
123
+ taskName: {
124
+ type: "string",
125
+ description: "Name of the task to update. Only use this if you don't have taskId. Warning: Task names may not be unique."
126
+ },
127
+ listName: {
128
+ type: "string",
129
+ description: "Name of the list containing the task. Required when using taskName if multiple tasks have the same name."
130
+ },
131
+ name: {
132
+ type: "string",
133
+ description: "New name for the task. Include emoji prefix if appropriate."
134
+ },
135
+ description: {
136
+ type: "string",
137
+ description: "New plain text description. Will be ignored if markdown_description is provided."
138
+ },
139
+ markdown_description: {
140
+ type: "string",
141
+ description: "New markdown description. Takes precedence over plain text description."
142
+ },
143
+ status: {
144
+ type: "string",
145
+ description: "New status. Must be valid for the task's current list."
146
+ },
147
+ priority: {
148
+ type: ["number", "null"],
149
+ enum: [1, 2, 3, 4, null],
150
+ description: "New priority: 1 (urgent) to 4 (low). Set null to clear priority."
151
+ },
152
+ dueDate: {
153
+ type: "string",
154
+ description: "New due date (Unix timestamp in milliseconds)"
155
+ }
156
+ },
157
+ required: []
158
+ },
159
+ async handler({ taskId, taskName, listName, name, description, markdown_description, status, priority, dueDate }) {
160
+ let targetTaskId = taskId;
161
+ // If no taskId but taskName is provided, look up the task ID
162
+ if (!targetTaskId && taskName) {
163
+ // First find the list ID if listName is provided
164
+ let listId;
165
+ if (listName) {
166
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
167
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
168
+ if (!listInfo) {
169
+ throw new Error(`List "${listName}" not found`);
170
+ }
171
+ listId = listInfo.id;
172
+ }
173
+ // Now find the task
174
+ const tasks = await taskService.getTasks(listId || '');
175
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
176
+ if (!foundTask) {
177
+ throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
178
+ }
179
+ targetTaskId = foundTask.id;
180
+ }
181
+ if (!targetTaskId) {
182
+ throw new Error("Either taskId or taskName must be provided");
183
+ }
184
+ // Prepare update data
185
+ const updateData = {};
186
+ if (name !== undefined)
187
+ updateData.name = name;
188
+ if (description !== undefined)
189
+ updateData.description = description;
190
+ if (markdown_description !== undefined)
191
+ updateData.markdown_description = markdown_description;
192
+ if (status !== undefined)
193
+ updateData.status = status;
194
+ if (priority !== undefined) {
195
+ updateData.priority = priority === null ? null : priority;
196
+ }
197
+ if (dueDate !== undefined) {
198
+ updateData.due_date = dueDate ? parseDueDate(dueDate) : null;
199
+ if (dueDate && updateData.due_date) {
200
+ updateData.due_date_time = true;
201
+ }
202
+ }
203
+ // Update the task
204
+ const updatedTask = await taskService.updateTask(targetTaskId, updateData);
205
+ // Format response
206
+ return {
207
+ content: [{
208
+ type: "text",
209
+ text: JSON.stringify({
210
+ id: updatedTask.id,
211
+ name: updatedTask.name,
212
+ url: updatedTask.url,
213
+ status: updatedTask.status?.status || "Unknown",
214
+ updated: true
215
+ }, null, 2)
216
+ }]
217
+ };
218
+ }
219
+ };
220
+ /**
221
+ * Tool definition for moving a task
222
+ */
223
+ export const moveTaskTool = {
224
+ name: "move_task",
225
+ description: "Move a task to a different list. Valid parameter combinations:\n1. Use taskId + (listId or listName) - preferred\n2. Use taskName + sourceListName + (listId or listName)\n\nWARNING: Task statuses may reset if destination list has different status options.",
226
+ inputSchema: {
227
+ type: "object",
228
+ properties: {
229
+ taskId: {
230
+ type: "string",
231
+ description: "ID of the task to move (preferred). Use this instead of taskName if you have it."
232
+ },
233
+ taskName: {
234
+ type: "string",
235
+ description: "Name of the task to move. When using this, you MUST also provide sourceListName."
236
+ },
237
+ sourceListName: {
238
+ type: "string",
239
+ description: "REQUIRED with taskName: Current list containing the task."
240
+ },
241
+ listId: {
242
+ type: "string",
243
+ description: "ID of destination list (preferred). Use this instead of listName if you have it."
244
+ },
245
+ listName: {
246
+ type: "string",
247
+ description: "Name of destination list. Only use if you don't have listId."
248
+ }
249
+ },
250
+ required: []
251
+ },
252
+ async handler({ taskId, taskName, sourceListName, listId, listName }) {
253
+ let targetTaskId = taskId;
254
+ let targetListId = listId;
255
+ // If no taskId but taskName is provided, look up the task ID
256
+ if (!targetTaskId && taskName) {
257
+ // First find the source list ID if sourceListName is provided
258
+ let sourceListId;
259
+ if (sourceListName) {
260
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
261
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
262
+ if (!listInfo) {
263
+ throw new Error(`Source list "${sourceListName}" not found`);
264
+ }
265
+ sourceListId = listInfo.id;
266
+ }
267
+ // Now find the task
268
+ const tasks = await taskService.getTasks(sourceListId || '');
269
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
270
+ if (!foundTask) {
271
+ throw new Error(`Task "${taskName}" not found${sourceListName ? ` in list "${sourceListName}"` : ""}`);
272
+ }
273
+ targetTaskId = foundTask.id;
274
+ }
275
+ if (!targetTaskId) {
276
+ throw new Error("Either taskId or taskName must be provided");
277
+ }
278
+ // If no listId but listName is provided, look up the list ID
279
+ if (!targetListId && listName) {
280
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
281
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
282
+ if (!listInfo) {
283
+ throw new Error(`List "${listName}" not found`);
284
+ }
285
+ targetListId = listInfo.id;
286
+ }
287
+ if (!targetListId) {
288
+ throw new Error("Either listId or listName must be provided");
289
+ }
290
+ // Move the task
291
+ const movedTask = await taskService.moveTask(targetTaskId, targetListId);
292
+ // Format response
293
+ return {
294
+ content: [{
295
+ type: "text",
296
+ text: JSON.stringify({
297
+ id: movedTask.id,
298
+ name: movedTask.name,
299
+ url: movedTask.url,
300
+ status: movedTask.status?.status || "Unknown",
301
+ list: movedTask.list.name,
302
+ moved: true
303
+ }, null, 2)
304
+ }]
305
+ };
306
+ }
307
+ };
308
+ /**
309
+ * Tool definition for duplicating a task
310
+ */
311
+ export const duplicateTaskTool = {
312
+ name: "duplicate_task",
313
+ description: "Create a copy of a task in the same or different list. Valid parameter combinations:\n1. Use taskId + optional (listId or listName) - preferred\n2. Use taskName + sourceListName + optional (listId or listName)\n\nThe duplicate preserves the original task's properties.",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ taskId: {
318
+ type: "string",
319
+ description: "ID of task to duplicate (preferred). Use this instead of taskName if you have it."
320
+ },
321
+ taskName: {
322
+ type: "string",
323
+ description: "Name of task to duplicate. When using this, you MUST provide sourceListName."
324
+ },
325
+ sourceListName: {
326
+ type: "string",
327
+ description: "REQUIRED with taskName: List containing the original task."
328
+ },
329
+ listId: {
330
+ type: "string",
331
+ description: "ID of list for the duplicate (optional). Defaults to same list as original."
332
+ },
333
+ listName: {
334
+ type: "string",
335
+ description: "Name of list for the duplicate. Only use if you don't have listId."
336
+ }
337
+ },
338
+ required: []
339
+ },
340
+ async handler({ taskId, taskName, sourceListName, listId, listName }) {
341
+ let targetTaskId = taskId;
342
+ let sourceListId;
343
+ // If sourceListName is provided, find the source list ID
344
+ if (sourceListName) {
345
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
346
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
347
+ if (!listInfo) {
348
+ throw new Error(`Source list "${sourceListName}" not found`);
349
+ }
350
+ sourceListId = listInfo.id;
351
+ }
352
+ // If no taskId but taskName is provided, look up the task ID
353
+ if (!targetTaskId && taskName) {
354
+ // Find the task in the source list if specified, otherwise search all tasks
355
+ if (sourceListId) {
356
+ const tasks = await taskService.getTasks(sourceListId);
357
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
358
+ if (!foundTask) {
359
+ throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
360
+ }
361
+ targetTaskId = foundTask.id;
362
+ }
363
+ else {
364
+ // Without a source list, we need to search more broadly
365
+ throw new Error("When using taskName, sourceListName must be provided to find the task");
366
+ }
367
+ }
368
+ if (!targetTaskId) {
369
+ throw new Error("Either taskId or taskName (with sourceListName) must be provided");
370
+ }
371
+ let targetListId = listId;
372
+ // If no listId but listName is provided, look up the list ID
373
+ if (!targetListId && listName) {
374
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
375
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
376
+ if (!listInfo) {
377
+ throw new Error(`Target list "${listName}" not found`);
378
+ }
379
+ targetListId = listInfo.id;
380
+ }
381
+ // Duplicate the task
382
+ const task = await taskService.duplicateTask(targetTaskId, targetListId);
383
+ // Format response
384
+ return {
385
+ content: [{
386
+ type: "text",
387
+ text: JSON.stringify({
388
+ id: task.id,
389
+ name: task.name,
390
+ url: task.url,
391
+ duplicated: true,
392
+ list: task.list.name,
393
+ space: task.space.name,
394
+ folder: task.folder?.name
395
+ }, null, 2)
396
+ }]
397
+ };
398
+ }
399
+ };
400
+ /**
401
+ * Tool definition for getting task details
402
+ */
403
+ export const getTaskTool = {
404
+ name: "get_task",
405
+ description: "Retrieve detailed information about a specific task. Valid parameter combinations:\n1. Use taskId alone (preferred)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)",
406
+ inputSchema: {
407
+ type: "object",
408
+ properties: {
409
+ taskId: {
410
+ type: "string",
411
+ description: "ID of task to retrieve (preferred). Use this instead of taskName if you have it."
412
+ },
413
+ taskName: {
414
+ type: "string",
415
+ description: "Name of task to retrieve. Warning: Task names may not be unique."
416
+ },
417
+ listName: {
418
+ type: "string",
419
+ description: "Name of list containing the task. Helps find the right task when using taskName."
420
+ }
421
+ },
422
+ required: []
423
+ },
424
+ async handler({ taskId, taskName, listName }) {
425
+ let targetTaskId = taskId;
426
+ // If no taskId but taskName is provided, look up the task ID
427
+ if (!targetTaskId && taskName) {
428
+ let listId;
429
+ if (listName) {
430
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
431
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
432
+ if (!listInfo) {
433
+ throw new Error(`List "${listName}" not found`);
434
+ }
435
+ listId = listInfo.id;
436
+ }
437
+ // Now find the task
438
+ const tasks = await taskService.getTasks(listId || '');
439
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
440
+ if (!foundTask) {
441
+ throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
442
+ }
443
+ targetTaskId = foundTask.id;
444
+ }
445
+ if (!targetTaskId) {
446
+ throw new Error("Either taskId or taskName must be provided");
447
+ }
448
+ // Get the task
449
+ const task = await taskService.getTask(targetTaskId);
450
+ // Format response
451
+ return {
452
+ content: [{
453
+ type: "text",
454
+ text: JSON.stringify({
455
+ id: task.id,
456
+ name: task.name,
457
+ description: task.description,
458
+ status: task.status?.status || "Unknown",
459
+ priority: task.priority,
460
+ due_date: task.due_date,
461
+ url: task.url,
462
+ list: task.list.name,
463
+ space: task.space.name,
464
+ folder: task.folder?.name,
465
+ creator: task.creator,
466
+ assignees: task.assignees,
467
+ tags: task.tags,
468
+ time_estimate: task.time_estimate,
469
+ time_spent: task.time_spent,
470
+ }, null, 2)
471
+ }]
472
+ };
473
+ }
474
+ };
475
+ /**
476
+ * Tool definition for getting tasks from a list
477
+ */
478
+ export const getTasksTool = {
479
+ name: "get_tasks",
480
+ description: "Retrieve tasks from a list with optional filtering. You MUST provide either:\n1. listId (preferred)\n2. listName\n\nUse filters to narrow down results by status, dates, etc.",
481
+ inputSchema: {
482
+ type: "object",
483
+ properties: {
484
+ listId: {
485
+ type: "string",
486
+ description: "ID of list to get tasks from (preferred). Use this instead of listName if you have it."
487
+ },
488
+ listName: {
489
+ type: "string",
490
+ description: "Name of list to get tasks from. Only use if you don't have listId."
491
+ },
492
+ archived: {
493
+ type: "boolean",
494
+ description: "Include archived tasks"
495
+ },
496
+ page: {
497
+ type: "number",
498
+ description: "Page number for pagination (starts at 0)"
499
+ },
500
+ order_by: {
501
+ type: "string",
502
+ description: "Sort field: due_date, created, updated"
503
+ },
504
+ reverse: {
505
+ type: "boolean",
506
+ description: "Reverse sort order (descending)"
507
+ },
508
+ subtasks: {
509
+ type: "boolean",
510
+ description: "Include subtasks"
511
+ },
512
+ statuses: {
513
+ type: "array",
514
+ items: {
515
+ type: "string"
516
+ },
517
+ description: "Filter by status names (e.g. ['To Do', 'In Progress'])"
518
+ }
519
+ },
520
+ required: []
521
+ },
522
+ async handler({ listId, listName, archived, page, order_by, reverse, subtasks, statuses }) {
523
+ let targetListId = listId;
524
+ // If no listId but listName is provided, look up the list ID
525
+ if (!targetListId && listName) {
526
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
527
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
528
+ if (!listInfo) {
529
+ throw new Error(`List "${listName}" not found`);
530
+ }
531
+ targetListId = listInfo.id;
532
+ }
533
+ if (!targetListId) {
534
+ throw new Error("Either listId or listName must be provided");
535
+ }
536
+ // Prepare filter options - remove archived as it's not in TaskFilters
537
+ const filters = {
538
+ page,
539
+ order_by,
540
+ reverse,
541
+ subtasks,
542
+ statuses
543
+ };
544
+ // Get tasks with filters
545
+ const tasks = await taskService.getTasks(targetListId, filters);
546
+ // Format response
547
+ return {
548
+ content: [{
549
+ type: "text",
550
+ text: JSON.stringify({
551
+ list_id: targetListId,
552
+ task_count: tasks.length,
553
+ tasks: tasks.map((task) => ({
554
+ id: task.id,
555
+ name: task.name,
556
+ status: task.status?.status || "Unknown",
557
+ priority: task.priority,
558
+ due_date: task.due_date,
559
+ url: task.url
560
+ }))
561
+ }, null, 2)
562
+ }]
563
+ };
564
+ }
565
+ };
566
+ /**
567
+ * Tool definition for deleting a task
568
+ */
569
+ export const deleteTaskTool = {
570
+ name: "delete_task",
571
+ description: "\u26a0\ufe0f PERMANENTLY DELETE a task. This action cannot be undone. Valid parameter combinations:\n1. Use taskId alone (preferred and safest)\n2. Use taskName + optional listName (use with caution)",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ taskId: {
576
+ type: "string",
577
+ description: "ID of task to delete (preferred). Use this instead of taskName for safety."
578
+ },
579
+ taskName: {
580
+ type: "string",
581
+ description: "Name of task to delete. Use with extreme caution as names may not be unique."
582
+ },
583
+ listName: {
584
+ type: "string",
585
+ description: "Name of list containing the task. Helps ensure correct task deletion when using taskName."
586
+ }
587
+ }
588
+ }
589
+ };
590
+ /**
591
+ * Tool definition for deleting multiple tasks
592
+ */
593
+ export const deleteBulkTasksTool = {
594
+ name: "delete_bulk_tasks",
595
+ description: "\u26a0\ufe0f PERMANENTLY DELETE multiple tasks. This action cannot be undone. For each task, you MUST provide either:\n1. taskId alone (preferred and safest)\n2. taskName + listName (use with caution)",
596
+ inputSchema: {
597
+ type: "object",
598
+ properties: {
599
+ tasks: {
600
+ type: "array",
601
+ description: "Array of tasks to delete",
602
+ items: {
603
+ type: "object",
604
+ properties: {
605
+ taskId: {
606
+ type: "string",
607
+ description: "Task ID (preferred). Use instead of taskName if available."
608
+ },
609
+ taskName: {
610
+ type: "string",
611
+ description: "Task name. Requires listName when used."
612
+ },
613
+ listName: {
614
+ type: "string",
615
+ description: "REQUIRED with taskName: List containing the task."
616
+ }
617
+ }
618
+ }
619
+ }
620
+ },
621
+ required: ["tasks"]
622
+ }
623
+ };
624
+ /**
625
+ * Tool definition for creating multiple tasks at once
626
+ */
627
+ export const createBulkTasksTool = {
628
+ name: "create_bulk_tasks",
629
+ description: "Create multiple tasks in a list efficiently. You MUST provide:\n1. An array of tasks with required properties\n2. Either listId or listName to specify the target list\n\nOptional: Configure batch size and concurrency for performance.",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ listId: {
634
+ type: "string",
635
+ description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
636
+ },
637
+ listName: {
638
+ type: "string",
639
+ description: "Name of list for new tasks. Only use if you don't have listId."
640
+ },
641
+ tasks: {
642
+ type: "array",
643
+ description: "Array of tasks to create. Each task must have at least a name.",
644
+ items: {
645
+ type: "object",
646
+ properties: {
647
+ name: {
648
+ type: "string",
649
+ description: "Task name with emoji prefix"
650
+ },
651
+ description: {
652
+ type: "string",
653
+ description: "Plain text description"
654
+ },
655
+ markdown_description: {
656
+ type: "string",
657
+ description: "Markdown description (overrides plain text)"
658
+ },
659
+ status: {
660
+ type: "string",
661
+ description: "Task status (uses list default if omitted)"
662
+ },
663
+ priority: {
664
+ type: "number",
665
+ description: "Priority 1-4 (1=urgent, 4=low)"
666
+ },
667
+ dueDate: {
668
+ type: "string",
669
+ description: "Due date (Unix timestamp ms)"
670
+ }
671
+ },
672
+ required: ["name"]
673
+ }
674
+ },
675
+ options: {
676
+ type: "object",
677
+ description: "Optional processing settings",
678
+ properties: {
679
+ batchSize: {
680
+ type: "number",
681
+ description: "Tasks per batch (default: 10)"
682
+ },
683
+ concurrency: {
684
+ type: "number",
685
+ description: "Parallel operations (default: 1)"
686
+ },
687
+ continueOnError: {
688
+ type: "boolean",
689
+ description: "Continue if some tasks fail"
690
+ },
691
+ retryCount: {
692
+ type: "number",
693
+ description: "Retry attempts for failures"
694
+ }
695
+ }
696
+ }
697
+ },
698
+ required: ["tasks"]
699
+ }
700
+ };
701
+ /**
702
+ * Tool definition for updating multiple tasks
703
+ */
704
+ export const updateBulkTasksTool = {
705
+ name: "update_bulk_tasks",
706
+ description: "Update multiple tasks efficiently. For each task, you MUST provide either:\n1. taskId alone (preferred)\n2. taskName + listName\n\nOnly specified fields will be updated for each task.",
707
+ inputSchema: {
708
+ type: "object",
709
+ properties: {
710
+ tasks: {
711
+ type: "array",
712
+ description: "Array of tasks to update",
713
+ items: {
714
+ type: "object",
715
+ properties: {
716
+ taskId: {
717
+ type: "string",
718
+ description: "Task ID (preferred). Use instead of taskName if available."
719
+ },
720
+ taskName: {
721
+ type: "string",
722
+ description: "Task name. Requires listName when used."
723
+ },
724
+ listName: {
725
+ type: "string",
726
+ description: "REQUIRED with taskName: List containing the task."
727
+ },
728
+ name: {
729
+ type: "string",
730
+ description: "New name with emoji prefix"
731
+ },
732
+ description: {
733
+ type: "string",
734
+ description: "New plain text description"
735
+ },
736
+ markdown_description: {
737
+ type: "string",
738
+ description: "New markdown description"
739
+ },
740
+ status: {
741
+ type: "string",
742
+ description: "New status"
743
+ },
744
+ priority: {
745
+ type: ["number", "null"],
746
+ enum: [1, 2, 3, 4, null],
747
+ description: "New priority (1-4 or null)"
748
+ },
749
+ dueDate: {
750
+ type: "string",
751
+ description: "New due date (Unix timestamp in milliseconds)"
752
+ }
753
+ }
754
+ }
755
+ }
756
+ },
757
+ required: ["tasks"]
758
+ }
759
+ };
760
+ /**
761
+ * Tool definition for moving multiple tasks
762
+ */
763
+ export const moveBulkTasksTool = {
764
+ name: "move_bulk_tasks",
765
+ description: "Move multiple tasks to a different list efficiently. For each task, you MUST provide either:\n1. taskId alone (preferred)\n2. taskName + listName\n\nWARNING: Task statuses may reset if target list has different status options.",
766
+ inputSchema: {
767
+ type: "object",
768
+ properties: {
769
+ tasks: {
770
+ type: "array",
771
+ description: "Array of tasks to move",
772
+ items: {
773
+ type: "object",
774
+ properties: {
775
+ taskId: {
776
+ type: "string",
777
+ description: "Task ID (preferred). Use instead of taskName if available."
778
+ },
779
+ taskName: {
780
+ type: "string",
781
+ description: "Task name. Requires listName when used."
782
+ },
783
+ listName: {
784
+ type: "string",
785
+ description: "REQUIRED with taskName: List containing the task."
786
+ }
787
+ }
788
+ }
789
+ },
790
+ targetListId: {
791
+ type: "string",
792
+ description: "ID of destination list (preferred). Use instead of targetListName if available."
793
+ },
794
+ targetListName: {
795
+ type: "string",
796
+ description: "Name of destination list. Only use if you don't have targetListId."
797
+ }
798
+ },
799
+ required: ["tasks"]
800
+ }
801
+ };
802
+ /**
803
+ * Handler for bulk task updates
804
+ */
805
+ export async function handleUpdateBulkTasks({ tasks }) {
806
+ if (!tasks || !tasks.length) {
807
+ throw new Error("No tasks provided for bulk update");
808
+ }
809
+ const results = {
810
+ total: tasks.length,
811
+ successful: 0,
812
+ failed: 0,
813
+ failures: []
814
+ };
815
+ for (const task of tasks) {
816
+ try {
817
+ let taskId = task.taskId;
818
+ if (!taskId && task.taskName) {
819
+ if (!task.listName) {
820
+ throw new Error(`List name is required when using task name for task "${task.taskName}"`);
821
+ }
822
+ const listInfo = await findListIDByName(workspaceService, task.listName);
823
+ if (!listInfo) {
824
+ throw new Error(`List "${task.listName}" not found`);
825
+ }
826
+ const taskList = await taskService.getTasks(listInfo.id);
827
+ const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
828
+ if (!foundTask) {
829
+ throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
830
+ }
831
+ taskId = foundTask.id;
832
+ }
833
+ if (!taskId) {
834
+ throw new Error("Either taskId or taskName must be provided");
835
+ }
836
+ await taskService.updateTask(taskId, {
837
+ name: task.name,
838
+ description: task.description,
839
+ markdown_description: task.markdown_description,
840
+ status: task.status,
841
+ priority: task.priority,
842
+ due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
843
+ });
844
+ results.successful++;
845
+ }
846
+ catch (error) {
847
+ results.failed++;
848
+ results.failures.push({
849
+ task: task.taskId || task.taskName,
850
+ error: error.message
851
+ });
852
+ }
853
+ }
854
+ return {
855
+ content: [{
856
+ type: "text",
857
+ text: JSON.stringify(results, null, 2)
858
+ }]
859
+ };
860
+ }
861
+ /**
862
+ * Handler for bulk task creation
863
+ */
864
+ export async function handleCreateBulkTasks(parameters) {
865
+ // Validate required parameters
866
+ const { tasks, listId, listName } = parameters;
867
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
868
+ throw new Error('You must provide a non-empty array of tasks to create');
869
+ }
870
+ let targetListId = listId;
871
+ // If no listId but listName is provided, look up the list ID
872
+ if (!targetListId && listName) {
873
+ const listInfo = await findListIDByName(workspaceService, listName);
874
+ if (!listInfo) {
875
+ throw new Error(`List "${listName}" not found`);
876
+ }
877
+ targetListId = listInfo.id;
878
+ }
879
+ if (!targetListId) {
880
+ throw new Error("Either listId or listName must be provided");
881
+ }
882
+ const results = {
883
+ total: tasks.length,
884
+ successful: 0,
885
+ failed: 0,
886
+ failures: []
887
+ };
888
+ // Map tasks to ClickUp format
889
+ const clickupTasks = tasks.map((task) => {
890
+ const taskData = {
891
+ name: task.name,
892
+ description: task.description,
893
+ markdown_description: task.markdown_description,
894
+ status: task.status,
895
+ priority: task.priority,
896
+ due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
897
+ };
898
+ // Add due_date_time flag if due date is set
899
+ if (task.dueDate && taskData.due_date) {
900
+ taskData.due_date_time = true;
901
+ }
902
+ return taskData;
903
+ });
904
+ // Create tasks in bulk using the task service
905
+ try {
906
+ const bulkResult = await taskService.createBulkTasks(targetListId, { tasks: clickupTasks });
907
+ // Update results based on bulk operation outcome
908
+ results.successful = bulkResult.successfulItems.length;
909
+ results.failed = bulkResult.failedItems.length;
910
+ results.failures = bulkResult.failedItems.map(failure => ({
911
+ task: failure.item.name,
912
+ error: failure.error.message
913
+ }));
914
+ }
915
+ catch (error) {
916
+ // If the bulk operation itself fails, mark all tasks as failed
917
+ results.failed = tasks.length;
918
+ results.failures = tasks.map(task => ({
919
+ task: task.name,
920
+ error: error.message
921
+ }));
922
+ }
923
+ return {
924
+ content: [{
925
+ type: "text",
926
+ text: JSON.stringify(results, null, 2)
927
+ }]
928
+ };
929
+ }
930
+ /**
931
+ * Handler for the create_task tool
932
+ */
933
+ export async function handleCreateTask(parameters) {
934
+ const { name, description, markdown_description, listId, listName, status, priority, dueDate } = parameters;
935
+ // Validate required fields
936
+ if (!name) {
937
+ throw new Error("Task name is required");
938
+ }
939
+ let targetListId = listId;
940
+ // If no listId but listName is provided, look up the list ID
941
+ if (!targetListId && listName) {
942
+ // Use workspace service to find the list by name in the hierarchy
943
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
944
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
945
+ if (!listInfo) {
946
+ throw new Error(`List "${listName}" not found`);
947
+ }
948
+ targetListId = listInfo.id;
949
+ }
950
+ if (!targetListId) {
951
+ throw new Error("Either listId or listName must be provided");
952
+ }
953
+ // Prepare task data
954
+ const taskData = {
955
+ name,
956
+ description,
957
+ markdown_description,
958
+ status,
959
+ priority: priority,
960
+ due_date: dueDate ? parseDueDate(dueDate) : undefined
961
+ };
962
+ // Add due_date_time flag if due date is set
963
+ if (dueDate && taskData.due_date) {
964
+ taskData.due_date_time = true;
965
+ }
966
+ // Create the task
967
+ const task = await taskService.createTask(targetListId, taskData);
968
+ // Format response
969
+ return {
970
+ content: [{
971
+ type: "text",
972
+ text: JSON.stringify({
973
+ id: task.id,
974
+ name: task.name,
975
+ url: task.url,
976
+ status: task.status?.status || "New",
977
+ list: task.list.name,
978
+ space: task.space.name,
979
+ folder: task.folder?.name
980
+ }, null, 2)
981
+ }]
982
+ };
983
+ }
984
+ /**
985
+ * Handler for the update_task tool
986
+ */
987
+ export async function handleUpdateTask(parameters) {
988
+ const { taskId, taskName, listName, name, description, markdown_description, status, priority, dueDate } = parameters;
989
+ let targetTaskId = taskId;
990
+ // If no taskId but taskName is provided, look up the task ID
991
+ if (!targetTaskId && taskName) {
992
+ let listId;
993
+ // If listName is provided, find the list ID first
994
+ if (listName) {
995
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
996
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
997
+ if (!listInfo) {
998
+ throw new Error(`List "${listName}" not found`);
999
+ }
1000
+ listId = listInfo.id;
1001
+ }
1002
+ // Now find the task
1003
+ const tasks = await taskService.getTasks(listId || '');
1004
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1005
+ if (!foundTask) {
1006
+ throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
1007
+ }
1008
+ targetTaskId = foundTask.id;
1009
+ }
1010
+ if (!targetTaskId) {
1011
+ throw new Error("Either taskId or taskName must be provided");
1012
+ }
1013
+ // Prepare update data
1014
+ const updateData = {};
1015
+ if (name !== undefined)
1016
+ updateData.name = name;
1017
+ if (description !== undefined)
1018
+ updateData.description = description;
1019
+ if (markdown_description !== undefined)
1020
+ updateData.markdown_description = markdown_description;
1021
+ if (status !== undefined)
1022
+ updateData.status = status;
1023
+ if (priority !== undefined) {
1024
+ updateData.priority = priority === null ? null : priority;
1025
+ }
1026
+ if (dueDate !== undefined) {
1027
+ updateData.due_date = dueDate ? parseDueDate(dueDate) : null;
1028
+ if (dueDate && updateData.due_date) {
1029
+ updateData.due_date_time = true;
1030
+ }
1031
+ }
1032
+ // Update the task
1033
+ const task = await taskService.updateTask(targetTaskId, updateData);
1034
+ // Format response
1035
+ return {
1036
+ content: [{
1037
+ type: "text",
1038
+ text: JSON.stringify({
1039
+ id: task.id,
1040
+ name: task.name,
1041
+ url: task.url,
1042
+ status: task.status?.status || "Unknown",
1043
+ updated: true,
1044
+ list: task.list.name,
1045
+ space: task.space.name,
1046
+ folder: task.folder?.name
1047
+ }, null, 2)
1048
+ }]
1049
+ };
1050
+ }
1051
+ /**
1052
+ * Handler for the move_task tool
1053
+ */
1054
+ export async function handleMoveTask(parameters) {
1055
+ const { taskId, taskName, sourceListName, listId, listName } = parameters;
1056
+ let targetTaskId = taskId;
1057
+ let sourceListId;
1058
+ // If sourceListName is provided, find the source list ID
1059
+ if (sourceListName) {
1060
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1061
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
1062
+ if (!listInfo) {
1063
+ throw new Error(`Source list "${sourceListName}" not found`);
1064
+ }
1065
+ sourceListId = listInfo.id;
1066
+ }
1067
+ // If no taskId but taskName is provided, look up the task ID
1068
+ if (!targetTaskId && taskName) {
1069
+ // Find the task in the source list if specified, otherwise search all tasks
1070
+ if (sourceListId) {
1071
+ const tasks = await taskService.getTasks(sourceListId);
1072
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1073
+ if (!foundTask) {
1074
+ throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
1075
+ }
1076
+ targetTaskId = foundTask.id;
1077
+ }
1078
+ else {
1079
+ // Without a source list, we need to search more broadly
1080
+ // This is less efficient but necessary if source list is unknown
1081
+ throw new Error("When using taskName, sourceListName must be provided to find the task");
1082
+ }
1083
+ }
1084
+ if (!targetTaskId) {
1085
+ throw new Error("Either taskId or taskName (with sourceListName) must be provided");
1086
+ }
1087
+ let targetListId = listId;
1088
+ // If no listId but listName is provided, look up the list ID
1089
+ if (!targetListId && listName) {
1090
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1091
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1092
+ if (!listInfo) {
1093
+ throw new Error(`Target list "${listName}" not found`);
1094
+ }
1095
+ targetListId = listInfo.id;
1096
+ }
1097
+ if (!targetListId) {
1098
+ throw new Error("Either listId or listName must be provided for the target list");
1099
+ }
1100
+ // Move the task
1101
+ const task = await taskService.moveTask(targetTaskId, targetListId);
1102
+ // Format response
1103
+ return {
1104
+ content: [{
1105
+ type: "text",
1106
+ text: JSON.stringify({
1107
+ id: task.id,
1108
+ name: task.name,
1109
+ url: task.url,
1110
+ moved: true,
1111
+ list: task.list.name,
1112
+ space: task.space.name,
1113
+ folder: task.folder?.name
1114
+ }, null, 2)
1115
+ }]
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Handler for the duplicate_task tool
1120
+ */
1121
+ export async function handleDuplicateTask(parameters) {
1122
+ const { taskId, taskName, sourceListName, listId, listName } = parameters;
1123
+ let targetTaskId = taskId;
1124
+ let sourceListId;
1125
+ // If sourceListName is provided, find the source list ID
1126
+ if (sourceListName) {
1127
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1128
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, sourceListName, 'list');
1129
+ if (!listInfo) {
1130
+ throw new Error(`Source list "${sourceListName}" not found`);
1131
+ }
1132
+ sourceListId = listInfo.id;
1133
+ }
1134
+ // If no taskId but taskName is provided, look up the task ID
1135
+ if (!targetTaskId && taskName) {
1136
+ // Find the task in the source list if specified, otherwise search all tasks
1137
+ if (sourceListId) {
1138
+ const tasks = await taskService.getTasks(sourceListId);
1139
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1140
+ if (!foundTask) {
1141
+ throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
1142
+ }
1143
+ targetTaskId = foundTask.id;
1144
+ }
1145
+ else {
1146
+ // Without a source list, we need to search more broadly
1147
+ throw new Error("When using taskName, sourceListName must be provided to find the task");
1148
+ }
1149
+ }
1150
+ if (!targetTaskId) {
1151
+ throw new Error("Either taskId or taskName (with sourceListName) must be provided");
1152
+ }
1153
+ let targetListId = listId;
1154
+ // If no listId but listName is provided, look up the list ID
1155
+ if (!targetListId && listName) {
1156
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1157
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1158
+ if (!listInfo) {
1159
+ throw new Error(`Target list "${listName}" not found`);
1160
+ }
1161
+ targetListId = listInfo.id;
1162
+ }
1163
+ // Duplicate the task
1164
+ const task = await taskService.duplicateTask(targetTaskId, targetListId);
1165
+ // Format response
1166
+ return {
1167
+ content: [{
1168
+ type: "text",
1169
+ text: JSON.stringify({
1170
+ id: task.id,
1171
+ name: task.name,
1172
+ url: task.url,
1173
+ duplicated: true,
1174
+ list: task.list.name,
1175
+ space: task.space.name,
1176
+ folder: task.folder?.name
1177
+ }, null, 2)
1178
+ }]
1179
+ };
1180
+ }
1181
+ /**
1182
+ * Handler for the get_tasks tool
1183
+ */
1184
+ export async function handleGetTasks(parameters) {
1185
+ const { listId, listName, archived, page, order_by, reverse, subtasks, statuses, include_closed, assignees, due_date_gt, due_date_lt, date_created_gt, date_created_lt, date_updated_gt, date_updated_lt, custom_fields } = parameters;
1186
+ let targetListId = listId;
1187
+ // If no listId but listName is provided, look up the list ID
1188
+ if (!targetListId && listName) {
1189
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1190
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1191
+ if (!listInfo) {
1192
+ throw new Error(`List "${listName}" not found`);
1193
+ }
1194
+ targetListId = listInfo.id;
1195
+ }
1196
+ if (!targetListId) {
1197
+ throw new Error("Either listId or listName must be provided");
1198
+ }
1199
+ // Prepare filter options - remove archived as it's not in TaskFilters
1200
+ const filters = {
1201
+ page,
1202
+ order_by,
1203
+ reverse,
1204
+ subtasks,
1205
+ statuses
1206
+ };
1207
+ // Get tasks with filters
1208
+ const tasks = await taskService.getTasks(targetListId, filters);
1209
+ // Format response
1210
+ return {
1211
+ content: [{
1212
+ type: "text",
1213
+ text: JSON.stringify({
1214
+ list_id: targetListId,
1215
+ task_count: tasks.length,
1216
+ tasks: tasks.map((task) => ({
1217
+ id: task.id,
1218
+ name: task.name,
1219
+ status: task.status?.status || "Unknown",
1220
+ priority: task.priority,
1221
+ due_date: task.due_date,
1222
+ url: task.url
1223
+ }))
1224
+ }, null, 2)
1225
+ }]
1226
+ };
1227
+ }
1228
+ /**
1229
+ * Handler for the get_task tool
1230
+ */
1231
+ export async function handleGetTask(parameters) {
1232
+ const { taskId, taskName, listName } = parameters;
1233
+ let targetTaskId = taskId;
1234
+ // If no taskId but taskName is provided, look up the task ID
1235
+ if (!targetTaskId && taskName) {
1236
+ let listId;
1237
+ // If listName is provided, find the list ID first
1238
+ if (listName) {
1239
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1240
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1241
+ if (!listInfo) {
1242
+ throw new Error(`List "${listName}" not found`);
1243
+ }
1244
+ listId = listInfo.id;
1245
+ }
1246
+ // Now find the task
1247
+ const tasks = await taskService.getTasks(listId || '');
1248
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1249
+ if (!foundTask) {
1250
+ throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
1251
+ }
1252
+ targetTaskId = foundTask.id;
1253
+ }
1254
+ if (!targetTaskId) {
1255
+ throw new Error("Either taskId or taskName must be provided");
1256
+ }
1257
+ // Get the task
1258
+ const task = await taskService.getTask(targetTaskId);
1259
+ // Format response
1260
+ return {
1261
+ content: [{
1262
+ type: "text",
1263
+ text: JSON.stringify(task, null, 2)
1264
+ }]
1265
+ };
1266
+ }
1267
+ /**
1268
+ * Handler for the delete_task tool
1269
+ */
1270
+ export async function handleDeleteTask(parameters) {
1271
+ const { taskId, taskName, listName } = parameters;
1272
+ let targetTaskId = taskId;
1273
+ // If no taskId but taskName is provided, look up the task ID
1274
+ if (!targetTaskId && taskName) {
1275
+ let listId;
1276
+ // If listName is provided, find the list ID first
1277
+ if (listName) {
1278
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
1279
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1280
+ if (!listInfo) {
1281
+ throw new Error(`List "${listName}" not found`);
1282
+ }
1283
+ listId = listInfo.id;
1284
+ }
1285
+ // Now find the task
1286
+ const tasks = await taskService.getTasks(listId || '');
1287
+ const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1288
+ if (!foundTask) {
1289
+ throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
1290
+ }
1291
+ targetTaskId = foundTask.id;
1292
+ }
1293
+ if (!targetTaskId) {
1294
+ throw new Error("Either taskId or taskName must be provided");
1295
+ }
1296
+ // Get task info before deleting (for the response)
1297
+ let taskInfo;
1298
+ try {
1299
+ taskInfo = await taskService.getTask(targetTaskId);
1300
+ }
1301
+ catch (error) {
1302
+ // If we can't get the task info, we'll continue with deletion anyway
1303
+ console.error("Error fetching task before deletion:", error);
1304
+ }
1305
+ // Delete the task
1306
+ await taskService.deleteTask(targetTaskId);
1307
+ // Format response
1308
+ return {
1309
+ content: [{
1310
+ type: "text",
1311
+ text: JSON.stringify({
1312
+ id: targetTaskId,
1313
+ name: taskInfo?.name || "Unknown",
1314
+ deleted: true,
1315
+ list: taskInfo?.list?.name || "Unknown",
1316
+ space: taskInfo?.space?.name || "Unknown"
1317
+ }, null, 2)
1318
+ }]
1319
+ };
1320
+ }
1321
+ /**
1322
+ * Handler for the delete_bulk_tasks tool
1323
+ */
1324
+ export async function handleDeleteBulkTasks({ tasks }) {
1325
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1326
+ throw new Error('You must provide a non-empty array of tasks to delete');
1327
+ }
1328
+ const results = {
1329
+ total: tasks.length,
1330
+ successful: 0,
1331
+ failed: 0,
1332
+ failures: [],
1333
+ deleted: []
1334
+ };
1335
+ // Collect all task IDs for deletion
1336
+ const taskIdsToDelete = [];
1337
+ const taskMap = new Map();
1338
+ // First, resolve all task IDs
1339
+ for (const task of tasks) {
1340
+ try {
1341
+ let taskId = task.taskId;
1342
+ if (!taskId && task.taskName) {
1343
+ if (!task.listName) {
1344
+ throw new Error(`List name is required when using task name for task "${task.taskName}"`);
1345
+ }
1346
+ const listInfo = await findListIDByName(workspaceService, task.listName);
1347
+ if (!listInfo) {
1348
+ throw new Error(`List "${task.listName}" not found`);
1349
+ }
1350
+ const taskList = await taskService.getTasks(listInfo.id);
1351
+ const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
1352
+ if (!foundTask) {
1353
+ throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
1354
+ }
1355
+ taskId = foundTask.id;
1356
+ // Store original task info for the response
1357
+ taskMap.set(taskId, { id: taskId, name: foundTask.name, originalTask: task });
1358
+ }
1359
+ else if (taskId) {
1360
+ // Store task ID with basic info for the response
1361
+ taskMap.set(taskId, { id: taskId, name: task.taskName || "Unknown", originalTask: task });
1362
+ }
1363
+ else {
1364
+ throw new Error("Either taskId or taskName must be provided for each task");
1365
+ }
1366
+ taskIdsToDelete.push(taskId);
1367
+ }
1368
+ catch (error) {
1369
+ results.failed++;
1370
+ results.failures.push({
1371
+ task: task.taskId || task.taskName,
1372
+ error: error.message
1373
+ });
1374
+ }
1375
+ }
1376
+ // Perform the bulk delete operation if we have tasks to delete
1377
+ if (taskIdsToDelete.length > 0) {
1378
+ try {
1379
+ const bulkResult = await taskService.deleteBulkTasks(taskIdsToDelete);
1380
+ // Process successful deletions
1381
+ for (const deletedId of bulkResult.successfulItems) {
1382
+ results.successful++;
1383
+ const taskInfo = taskMap.get(deletedId);
1384
+ results.deleted.push({
1385
+ id: deletedId,
1386
+ name: taskInfo?.name || "Unknown",
1387
+ deleted: true
1388
+ });
1389
+ }
1390
+ // Process failed deletions
1391
+ for (const failure of bulkResult.failedItems) {
1392
+ results.failed++;
1393
+ const taskInfo = taskMap.get(failure.item);
1394
+ results.failures.push({
1395
+ task: taskInfo?.name || failure.item,
1396
+ error: failure.error.message
1397
+ });
1398
+ }
1399
+ }
1400
+ catch (error) {
1401
+ // If the bulk delete fails entirely, mark all remaining tasks as failed
1402
+ for (const taskId of taskIdsToDelete) {
1403
+ const taskInfo = taskMap.get(taskId);
1404
+ if (taskInfo && !results.deleted.some(t => t.id === taskId) &&
1405
+ !results.failures.some(f => f.task === taskId || f.task === taskInfo.name)) {
1406
+ results.failed++;
1407
+ results.failures.push({
1408
+ task: taskInfo.name || taskId,
1409
+ error: error.message
1410
+ });
1411
+ }
1412
+ }
1413
+ }
1414
+ }
1415
+ return {
1416
+ content: [{
1417
+ type: "text",
1418
+ text: JSON.stringify(results, null, 2)
1419
+ }]
1420
+ };
1421
+ }
1422
+ /**
1423
+ * Handler for bulk task moves
1424
+ */
1425
+ export async function handleMoveBulkTasks(parameters) {
1426
+ const { tasks, targetListId, targetListName } = parameters;
1427
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1428
+ throw new Error('You must provide a non-empty array of tasks to move');
1429
+ }
1430
+ let finalTargetListId = targetListId;
1431
+ // If no targetListId but targetListName is provided, look up the list ID
1432
+ if (!finalTargetListId && targetListName) {
1433
+ const listInfo = await findListIDByName(workspaceService, targetListName);
1434
+ if (!listInfo) {
1435
+ throw new Error(`Target list "${targetListName}" not found`);
1436
+ }
1437
+ finalTargetListId = listInfo.id;
1438
+ }
1439
+ if (!finalTargetListId) {
1440
+ throw new Error("Either targetListId or targetListName must be provided");
1441
+ }
1442
+ const results = {
1443
+ total: tasks.length,
1444
+ successful: 0,
1445
+ failed: 0,
1446
+ failures: []
1447
+ };
1448
+ for (const task of tasks) {
1449
+ try {
1450
+ let taskId = task.taskId;
1451
+ if (!taskId && task.taskName) {
1452
+ if (!task.listName) {
1453
+ throw new Error(`List name is required when using task name for task "${task.taskName}"`);
1454
+ }
1455
+ const listInfo = await findListIDByName(workspaceService, task.listName);
1456
+ if (!listInfo) {
1457
+ throw new Error(`List "${task.listName}" not found`);
1458
+ }
1459
+ const taskList = await taskService.getTasks(listInfo.id);
1460
+ const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
1461
+ if (!foundTask) {
1462
+ throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
1463
+ }
1464
+ taskId = foundTask.id;
1465
+ }
1466
+ if (!taskId) {
1467
+ throw new Error("Either taskId or taskName must be provided");
1468
+ }
1469
+ await taskService.moveTask(taskId, finalTargetListId);
1470
+ results.successful++;
1471
+ }
1472
+ catch (error) {
1473
+ results.failed++;
1474
+ results.failures.push({
1475
+ task: task.taskId || task.taskName,
1476
+ error: error.message
1477
+ });
1478
+ }
1479
+ }
1480
+ return {
1481
+ content: [{
1482
+ type: "text",
1483
+ text: JSON.stringify(results, null, 2)
1484
+ }]
1485
+ };
1486
+ }