@taazkareem/clickup-mcp-server 0.4.60 → 0.4.63

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