@taazkareem/clickup-mcp-server 0.4.70 → 0.4.72

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.
@@ -6,10 +6,12 @@
6
6
  * retrieving task details.
7
7
  */
8
8
  import { clickUpServices } from '../services/shared.js';
9
- import { findListIDByName } from './list.js';
10
- import { parseDueDate, formatDueDate } from './utils.js';
9
+ import { parseDueDate, formatDueDate, enhanceResponseWithSponsor, resolveListId, resolveTaskId } from './utils.js';
10
+ import { BulkService } from '../services/clickup/bulk.js';
11
11
  // Use shared services instance
12
12
  const { task: taskService, workspace: workspaceService } = clickUpServices;
13
+ // Create a bulk service instance that uses the task service
14
+ const bulkService = new BulkService(taskService);
13
15
  /**
14
16
  * Tool definition for creating a single task
15
17
  */
@@ -49,7 +51,7 @@ export const createTaskTool = {
49
51
  },
50
52
  dueDate: {
51
53
  type: "string",
52
- description: "Due date of the task (Unix timestamp in milliseconds). Convert dates to this format before submitting."
54
+ description: "Due date of the task. Supports both Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', 'next week', or '3 days from now'."
53
55
  }
54
56
  },
55
57
  required: ["name"]
@@ -146,7 +148,7 @@ export const updateTaskTool = {
146
148
  },
147
149
  dueDate: {
148
150
  type: "string",
149
- description: "New due date (Unix timestamp in milliseconds)"
151
+ description: "New due date. Supports both Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', 'next week', or '3 days from now'."
150
152
  }
151
153
  },
152
154
  required: []
@@ -588,218 +590,6 @@ export const deleteTaskTool = {
588
590
  }
589
591
  }
590
592
  };
591
- /**
592
- * Tool definition for deleting multiple tasks
593
- */
594
- export const deleteBulkTasksTool = {
595
- name: "delete_bulk_tasks",
596
- 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)",
597
- inputSchema: {
598
- type: "object",
599
- properties: {
600
- tasks: {
601
- type: "array",
602
- description: "Array of tasks to delete",
603
- items: {
604
- type: "object",
605
- properties: {
606
- taskId: {
607
- type: "string",
608
- description: "Task ID (preferred). Use instead of taskName if available."
609
- },
610
- taskName: {
611
- type: "string",
612
- description: "Task name. Requires listName when used."
613
- },
614
- listName: {
615
- type: "string",
616
- description: "REQUIRED with taskName: List containing the task."
617
- }
618
- }
619
- }
620
- }
621
- },
622
- required: ["tasks"]
623
- }
624
- };
625
- /**
626
- * Tool definition for creating multiple tasks at once
627
- */
628
- export const createBulkTasksTool = {
629
- name: "create_bulk_tasks",
630
- 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.",
631
- inputSchema: {
632
- type: "object",
633
- properties: {
634
- listId: {
635
- type: "string",
636
- description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
637
- },
638
- listName: {
639
- type: "string",
640
- description: "Name of list for new tasks. Only use if you don't have listId."
641
- },
642
- tasks: {
643
- type: "array",
644
- description: "Array of tasks to create. Each task must have at least a name.",
645
- items: {
646
- type: "object",
647
- properties: {
648
- name: {
649
- type: "string",
650
- description: "Task name with emoji prefix"
651
- },
652
- description: {
653
- type: "string",
654
- description: "Plain text description"
655
- },
656
- markdown_description: {
657
- type: "string",
658
- description: "Markdown description (overrides plain text)"
659
- },
660
- status: {
661
- type: "string",
662
- description: "Task status (uses list default if omitted)"
663
- },
664
- priority: {
665
- type: "number",
666
- description: "Priority 1-4 (1=urgent, 4=low)"
667
- },
668
- dueDate: {
669
- type: "string",
670
- description: "Due date (Unix timestamp ms)"
671
- }
672
- },
673
- required: ["name"]
674
- }
675
- },
676
- options: {
677
- type: "object",
678
- description: "Optional processing settings",
679
- properties: {
680
- batchSize: {
681
- type: "number",
682
- description: "Tasks per batch (default: 10)"
683
- },
684
- concurrency: {
685
- type: "number",
686
- description: "Parallel operations (default: 1)"
687
- },
688
- continueOnError: {
689
- type: "boolean",
690
- description: "Continue if some tasks fail"
691
- },
692
- retryCount: {
693
- type: "number",
694
- description: "Retry attempts for failures"
695
- }
696
- }
697
- }
698
- },
699
- required: ["tasks"]
700
- }
701
- };
702
- /**
703
- * Tool definition for updating multiple tasks
704
- */
705
- export const updateBulkTasksTool = {
706
- name: "update_bulk_tasks",
707
- 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.",
708
- inputSchema: {
709
- type: "object",
710
- properties: {
711
- tasks: {
712
- type: "array",
713
- description: "Array of tasks to update",
714
- items: {
715
- type: "object",
716
- properties: {
717
- taskId: {
718
- type: "string",
719
- description: "Task ID (preferred). Use instead of taskName if available."
720
- },
721
- taskName: {
722
- type: "string",
723
- description: "Task name. Requires listName when used."
724
- },
725
- listName: {
726
- type: "string",
727
- description: "REQUIRED with taskName: List containing the task."
728
- },
729
- name: {
730
- type: "string",
731
- description: "New name with emoji prefix"
732
- },
733
- description: {
734
- type: "string",
735
- description: "New plain text description"
736
- },
737
- markdown_description: {
738
- type: "string",
739
- description: "New markdown description"
740
- },
741
- status: {
742
- type: "string",
743
- description: "New status"
744
- },
745
- priority: {
746
- type: ["number", "null"],
747
- enum: [1, 2, 3, 4, null],
748
- description: "New priority (1-4 or null)"
749
- },
750
- dueDate: {
751
- type: "string",
752
- description: "New due date (Unix timestamp in milliseconds)"
753
- }
754
- }
755
- }
756
- }
757
- },
758
- required: ["tasks"]
759
- }
760
- };
761
- /**
762
- * Tool definition for moving multiple tasks
763
- */
764
- export const moveBulkTasksTool = {
765
- name: "move_bulk_tasks",
766
- 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.",
767
- inputSchema: {
768
- type: "object",
769
- properties: {
770
- tasks: {
771
- type: "array",
772
- description: "Array of tasks to move",
773
- items: {
774
- type: "object",
775
- properties: {
776
- taskId: {
777
- type: "string",
778
- description: "Task ID (preferred). Use instead of taskName if available."
779
- },
780
- taskName: {
781
- type: "string",
782
- description: "Task name. Requires listName when used."
783
- },
784
- listName: {
785
- type: "string",
786
- description: "REQUIRED with taskName: List containing the task."
787
- }
788
- }
789
- }
790
- },
791
- targetListId: {
792
- type: "string",
793
- description: "ID of destination list (preferred). Use instead of targetListName if available."
794
- },
795
- targetListName: {
796
- type: "string",
797
- description: "Name of destination list. Only use if you don't have targetListId."
798
- }
799
- },
800
- required: ["tasks"]
801
- }
802
- };
803
593
  /**
804
594
  * Tool definition for getting task comments
805
595
  */
@@ -907,78 +697,19 @@ export const getTaskCommentsTool = {
907
697
  };
908
698
  }
909
699
  };
910
- /**
911
- * Handler for bulk task updates
912
- */
913
- export async function handleUpdateBulkTasks({ tasks }) {
914
- if (!tasks || !tasks.length) {
915
- throw new Error("No tasks provided for bulk update");
916
- }
917
- const results = {
918
- total: tasks.length,
919
- successful: 0,
920
- failed: 0,
921
- failures: []
922
- };
923
- for (const task of tasks) {
924
- try {
925
- let taskId = task.taskId;
926
- if (!taskId && task.taskName) {
927
- if (!task.listName) {
928
- throw new Error(`List name is required when using task name for task "${task.taskName}"`);
929
- }
930
- const listInfo = await findListIDByName(workspaceService, task.listName);
931
- if (!listInfo) {
932
- throw new Error(`List "${task.listName}" not found`);
933
- }
934
- const taskList = await taskService.getTasks(listInfo.id);
935
- const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
936
- if (!foundTask) {
937
- throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
938
- }
939
- taskId = foundTask.id;
940
- }
941
- if (!taskId) {
942
- throw new Error("Either taskId or taskName must be provided");
943
- }
944
- await taskService.updateTask(taskId, {
945
- name: task.name,
946
- description: task.description,
947
- markdown_description: task.markdown_description,
948
- status: task.status,
949
- priority: task.priority,
950
- due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
951
- });
952
- results.successful++;
953
- }
954
- catch (error) {
955
- results.failed++;
956
- results.failures.push({
957
- task: task.taskId || task.taskName,
958
- error: error.message
959
- });
960
- }
961
- }
962
- return {
963
- content: [{
964
- type: "text",
965
- text: JSON.stringify(results, null, 2)
966
- }]
967
- };
968
- }
969
- /**
970
- * Handler for bulk task creation
971
- */
972
- export async function handleCreateBulkTasks(parameters) {
973
- // Validate required parameters
974
- const { tasks, listId, listName } = parameters;
975
- if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
976
- throw new Error('You must provide a non-empty array of tasks to create');
700
+ // Export handlers to be used by the server
701
+ export async function handleCreateTask(parameters) {
702
+ const { name, description, markdown_description, listId, listName, status, priority, dueDate } = parameters;
703
+ // Validate required fields
704
+ if (!name) {
705
+ throw new Error("Task name is required");
977
706
  }
978
707
  let targetListId = listId;
979
708
  // If no listId but listName is provided, look up the list ID
980
709
  if (!targetListId && listName) {
981
- const listInfo = await findListIDByName(workspaceService, listName);
710
+ // Use workspace service to find the list by name in the hierarchy
711
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
712
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
982
713
  if (!listInfo) {
983
714
  throw new Error(`List "${listName}" not found`);
984
715
  }
@@ -987,94 +718,23 @@ export async function handleCreateBulkTasks(parameters) {
987
718
  if (!targetListId) {
988
719
  throw new Error("Either listId or listName must be provided");
989
720
  }
990
- const results = {
991
- total: tasks.length,
992
- successful: 0,
993
- failed: 0,
994
- failures: []
721
+ // Prepare task data
722
+ const taskData = {
723
+ name,
724
+ description,
725
+ markdown_description,
726
+ status,
727
+ priority: priority,
728
+ due_date: dueDate ? parseDueDate(dueDate) : undefined
995
729
  };
996
- // Map tasks to ClickUp format
997
- const clickupTasks = tasks.map((task) => {
998
- const taskData = {
999
- name: task.name,
1000
- description: task.description,
1001
- markdown_description: task.markdown_description,
1002
- status: task.status,
1003
- priority: task.priority,
1004
- due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined
1005
- };
1006
- // Add due_date_time flag if due date is set
1007
- if (task.dueDate && taskData.due_date) {
1008
- taskData.due_date_time = true;
1009
- }
1010
- return taskData;
1011
- });
1012
- // Create tasks in bulk using the task service
1013
- try {
1014
- const bulkResult = await taskService.createBulkTasks(targetListId, { tasks: clickupTasks });
1015
- // Update results based on bulk operation outcome
1016
- results.successful = bulkResult.successfulItems.length;
1017
- results.failed = bulkResult.failedItems.length;
1018
- results.failures = bulkResult.failedItems.map(failure => ({
1019
- task: failure.item.name,
1020
- error: failure.error.message
1021
- }));
1022
- }
1023
- catch (error) {
1024
- // If the bulk operation itself fails, mark all tasks as failed
1025
- results.failed = tasks.length;
1026
- results.failures = tasks.map(task => ({
1027
- task: task.name,
1028
- error: error.message
1029
- }));
1030
- }
1031
- return {
1032
- content: [{
1033
- type: "text",
1034
- text: JSON.stringify(results, null, 2)
1035
- }]
1036
- };
1037
- }
1038
- /**
1039
- * Handler for the create_task tool
1040
- */
1041
- export async function handleCreateTask(parameters) {
1042
- const { name, description, markdown_description, listId, listName, status, priority, dueDate } = parameters;
1043
- // Validate required fields
1044
- if (!name) {
1045
- throw new Error("Task name is required");
1046
- }
1047
- let targetListId = listId;
1048
- // If no listId but listName is provided, look up the list ID
1049
- if (!targetListId && listName) {
1050
- // Use workspace service to find the list by name in the hierarchy
1051
- const hierarchy = await workspaceService.getWorkspaceHierarchy();
1052
- const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1053
- if (!listInfo) {
1054
- throw new Error(`List "${listName}" not found`);
1055
- }
1056
- targetListId = listInfo.id;
1057
- }
1058
- if (!targetListId) {
1059
- throw new Error("Either listId or listName must be provided");
1060
- }
1061
- // Prepare task data
1062
- const taskData = {
1063
- name,
1064
- description,
1065
- markdown_description,
1066
- status,
1067
- priority: priority,
1068
- due_date: dueDate ? parseDueDate(dueDate) : undefined
1069
- };
1070
- // Add due_date_time flag if due date is set
1071
- if (dueDate && taskData.due_date) {
1072
- taskData.due_date_time = true;
730
+ // Add due_date_time flag if due date is set
731
+ if (dueDate && taskData.due_date) {
732
+ taskData.due_date_time = true;
1073
733
  }
1074
734
  // Create the task
1075
735
  const task = await taskService.createTask(targetListId, taskData);
1076
- // Format response
1077
- return {
736
+ // Create response object
737
+ const response = {
1078
738
  content: [{
1079
739
  type: "text",
1080
740
  text: JSON.stringify({
@@ -1089,10 +749,9 @@ export async function handleCreateTask(parameters) {
1089
749
  }, null, 2)
1090
750
  }]
1091
751
  };
752
+ // Add sponsor message to response
753
+ return enhanceResponseWithSponsor(response);
1092
754
  }
1093
- /**
1094
- * Handler for the update_task tool
1095
- */
1096
755
  export async function handleUpdateTask(parameters) {
1097
756
  const { taskId, taskName, listName, name, description, markdown_description, status, priority, dueDate } = parameters;
1098
757
  let targetTaskId = taskId;
@@ -1156,9 +815,6 @@ export async function handleUpdateTask(parameters) {
1156
815
  }]
1157
816
  };
1158
817
  }
1159
- /**
1160
- * Handler for the move_task tool
1161
- */
1162
818
  export async function handleMoveTask(parameters) {
1163
819
  const { taskId, taskName, sourceListName, listId, listName } = parameters;
1164
820
  let targetTaskId = taskId;
@@ -1220,9 +876,6 @@ export async function handleMoveTask(parameters) {
1220
876
  }]
1221
877
  };
1222
878
  }
1223
- /**
1224
- * Handler for the duplicate_task tool
1225
- */
1226
879
  export async function handleDuplicateTask(parameters) {
1227
880
  const { taskId, taskName, sourceListName, listId, listName } = parameters;
1228
881
  let targetTaskId = taskId;
@@ -1284,9 +937,6 @@ export async function handleDuplicateTask(parameters) {
1284
937
  }]
1285
938
  };
1286
939
  }
1287
- /**
1288
- * Handler for the get_tasks tool
1289
- */
1290
940
  export async function handleGetTasks(parameters) {
1291
941
  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;
1292
942
  let targetListId = listId;
@@ -1334,9 +984,6 @@ export async function handleGetTasks(parameters) {
1334
984
  }]
1335
985
  };
1336
986
  }
1337
- /**
1338
- * Handler for the delete_task tool
1339
- */
1340
987
  export async function handleDeleteTask(parameters) {
1341
988
  const { taskId, taskName, listName } = parameters;
1342
989
  let targetTaskId = taskId;
@@ -1388,204 +1035,634 @@ export async function handleDeleteTask(parameters) {
1388
1035
  }]
1389
1036
  };
1390
1037
  }
1038
+ export async function handleGetTaskComments(parameters) {
1039
+ const { taskId, taskName, listName, start, startId } = parameters;
1040
+ try {
1041
+ // Call the handler with validation
1042
+ const result = await getTaskCommentsTool.handler({
1043
+ taskId,
1044
+ taskName,
1045
+ listName,
1046
+ start: start ? Number(start) : undefined,
1047
+ startId
1048
+ });
1049
+ return {
1050
+ content: [{
1051
+ type: "text",
1052
+ text: JSON.stringify(result, null, 2)
1053
+ }]
1054
+ };
1055
+ }
1056
+ catch (error) {
1057
+ // Handle and format error response with proper content array
1058
+ console.error('Error getting task comments:', error);
1059
+ return {
1060
+ content: [{
1061
+ type: "text",
1062
+ text: JSON.stringify({
1063
+ error: error.message || 'Failed to get task comments',
1064
+ taskId: taskId || null,
1065
+ taskName: taskName || null,
1066
+ listName: listName || null
1067
+ }, null, 2)
1068
+ }]
1069
+ };
1070
+ }
1071
+ }
1072
+ // Helper function for path extraction
1073
+ export function extractPath(node) {
1074
+ if (!node)
1075
+ return '';
1076
+ if (!node.parent)
1077
+ return node.name;
1078
+ return `${extractPath(node.parent)} > ${node.name}`;
1079
+ }
1080
+ // Helper function for path traversal
1081
+ export function extractTreePath(root, targetId) {
1082
+ if (!root)
1083
+ return [];
1084
+ // If this node is the target, return it in an array
1085
+ if (root.id === targetId) {
1086
+ return [root];
1087
+ }
1088
+ // Check children if they exist
1089
+ if (root.children) {
1090
+ for (const child of root.children) {
1091
+ const path = extractTreePath(child, targetId);
1092
+ if (path.length > 0) {
1093
+ return [root, ...path];
1094
+ }
1095
+ }
1096
+ }
1097
+ // Not found in this branch
1098
+ return [];
1099
+ }
1100
+ // After the existing tools, add the bulk tools:
1391
1101
  /**
1392
- * Handler for the delete_bulk_tasks tool
1102
+ * Tool definition for creating multiple tasks at once
1393
1103
  */
1394
- export async function handleDeleteBulkTasks({ tasks }) {
1395
- if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1396
- throw new Error('You must provide a non-empty array of tasks to delete');
1397
- }
1398
- const results = {
1399
- total: tasks.length,
1400
- successful: 0,
1401
- failed: 0,
1402
- failures: [],
1403
- deleted: []
1404
- };
1405
- // Collect all task IDs for deletion
1406
- const taskIdsToDelete = [];
1407
- const taskMap = new Map();
1408
- // First, resolve all task IDs
1409
- for (const task of tasks) {
1410
- try {
1411
- let taskId = task.taskId;
1412
- if (!taskId && task.taskName) {
1413
- if (!task.listName) {
1414
- throw new Error(`List name is required when using task name for task "${task.taskName}"`);
1415
- }
1416
- const listInfo = await findListIDByName(workspaceService, task.listName);
1417
- if (!listInfo) {
1418
- throw new Error(`List "${task.listName}" not found`);
1104
+ export const createBulkTasksTool = {
1105
+ name: "create_bulk_tasks",
1106
+ 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.",
1107
+ inputSchema: {
1108
+ type: "object",
1109
+ properties: {
1110
+ listId: {
1111
+ type: "string",
1112
+ description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
1113
+ },
1114
+ listName: {
1115
+ type: "string",
1116
+ description: "Name of list for new tasks. Only use if you don't have listId."
1117
+ },
1118
+ tasks: {
1119
+ type: "array",
1120
+ description: "Array of tasks to create. Each task must have at least a name.",
1121
+ items: {
1122
+ type: "object",
1123
+ properties: {
1124
+ name: {
1125
+ type: "string",
1126
+ description: "Task name with emoji prefix"
1127
+ },
1128
+ description: {
1129
+ type: "string",
1130
+ description: "Plain text description"
1131
+ },
1132
+ markdown_description: {
1133
+ type: "string",
1134
+ description: "Markdown description (overrides plain text)"
1135
+ },
1136
+ status: {
1137
+ type: "string",
1138
+ description: "Task status (uses list default if omitted)"
1139
+ },
1140
+ priority: {
1141
+ type: "number",
1142
+ description: "Priority 1-4 (1=urgent, 4=low)"
1143
+ },
1144
+ dueDate: {
1145
+ type: "string",
1146
+ description: "Due date. Supports Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', 'next week', etc."
1147
+ }
1148
+ },
1149
+ required: ["name"]
1419
1150
  }
1420
- // Use the improved findTaskByName method
1421
- const foundTask = await taskService.findTaskByName(listInfo.id, task.taskName);
1422
- if (!foundTask) {
1423
- throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
1151
+ },
1152
+ options: {
1153
+ oneOf: [
1154
+ {
1155
+ type: "object",
1156
+ description: "Optional processing settings",
1157
+ properties: {
1158
+ batchSize: {
1159
+ type: "number",
1160
+ description: "Tasks per batch (default: 10)"
1161
+ },
1162
+ concurrency: {
1163
+ type: "number",
1164
+ description: "Parallel operations (default: 3)"
1165
+ },
1166
+ continueOnError: {
1167
+ type: "boolean",
1168
+ description: "Continue if some tasks fail"
1169
+ },
1170
+ retryCount: {
1171
+ type: "number",
1172
+ description: "Retry attempts for failures"
1173
+ }
1174
+ }
1175
+ },
1176
+ {
1177
+ type: "string",
1178
+ description: "JSON string representing options. Will be parsed automatically."
1179
+ }
1180
+ ],
1181
+ description: "Processing options (or JSON string representing options)"
1182
+ }
1183
+ },
1184
+ required: ["tasks"]
1185
+ }
1186
+ };
1187
+ /**
1188
+ * Tool definition for updating multiple tasks
1189
+ */
1190
+ export const updateBulkTasksTool = {
1191
+ name: "update_bulk_tasks",
1192
+ 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.",
1193
+ inputSchema: {
1194
+ type: "object",
1195
+ properties: {
1196
+ tasks: {
1197
+ type: "array",
1198
+ description: "Array of tasks to update",
1199
+ items: {
1200
+ type: "object",
1201
+ properties: {
1202
+ taskId: {
1203
+ type: "string",
1204
+ description: "Task ID (preferred). Use instead of taskName if available."
1205
+ },
1206
+ taskName: {
1207
+ type: "string",
1208
+ description: "Task name. Requires listName when used."
1209
+ },
1210
+ listName: {
1211
+ type: "string",
1212
+ description: "REQUIRED with taskName: List containing the task."
1213
+ },
1214
+ name: {
1215
+ type: "string",
1216
+ description: "New name with emoji prefix"
1217
+ },
1218
+ description: {
1219
+ type: "string",
1220
+ description: "New plain text description"
1221
+ },
1222
+ markdown_description: {
1223
+ type: "string",
1224
+ description: "New markdown description"
1225
+ },
1226
+ status: {
1227
+ type: "string",
1228
+ description: "New status"
1229
+ },
1230
+ priority: {
1231
+ type: ["number", "null"],
1232
+ enum: [1, 2, 3, 4, null],
1233
+ description: "New priority (1-4 or null)"
1234
+ },
1235
+ dueDate: {
1236
+ type: "string",
1237
+ description: "New due date. Supports Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', etc."
1238
+ }
1239
+ }
1424
1240
  }
1425
- taskId = foundTask.id;
1426
- // Store original task info for the response
1427
- taskMap.set(taskId, { id: taskId, name: foundTask.name, originalTask: task });
1241
+ },
1242
+ options: {
1243
+ oneOf: [
1244
+ {
1245
+ type: "object",
1246
+ description: "Optional processing settings",
1247
+ properties: {
1248
+ batchSize: {
1249
+ type: "number",
1250
+ description: "Tasks per batch (default: 10)"
1251
+ },
1252
+ concurrency: {
1253
+ type: "number",
1254
+ description: "Parallel operations (default: 3)"
1255
+ },
1256
+ continueOnError: {
1257
+ type: "boolean",
1258
+ description: "Continue if some tasks fail"
1259
+ },
1260
+ retryCount: {
1261
+ type: "number",
1262
+ description: "Retry attempts for failures"
1263
+ }
1264
+ }
1265
+ },
1266
+ {
1267
+ type: "string",
1268
+ description: "JSON string representing options. Will be parsed automatically."
1269
+ }
1270
+ ],
1271
+ description: "Processing options (or JSON string representing options)"
1428
1272
  }
1429
- else if (taskId) {
1430
- // Store task ID with basic info for the response
1431
- taskMap.set(taskId, { id: taskId, name: task.taskName || "Unknown", originalTask: task });
1273
+ },
1274
+ required: ["tasks"]
1275
+ }
1276
+ };
1277
+ /**
1278
+ * Tool definition for moving multiple tasks
1279
+ */
1280
+ export const moveBulkTasksTool = {
1281
+ name: "move_bulk_tasks",
1282
+ 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.",
1283
+ inputSchema: {
1284
+ type: "object",
1285
+ properties: {
1286
+ tasks: {
1287
+ type: "array",
1288
+ description: "Array of tasks to move",
1289
+ items: {
1290
+ type: "object",
1291
+ properties: {
1292
+ taskId: {
1293
+ type: "string",
1294
+ description: "Task ID (preferred). Use instead of taskName if available."
1295
+ },
1296
+ taskName: {
1297
+ type: "string",
1298
+ description: "Task name. Requires listName when used."
1299
+ },
1300
+ listName: {
1301
+ type: "string",
1302
+ description: "REQUIRED with taskName: List containing the task."
1303
+ }
1304
+ }
1305
+ }
1306
+ },
1307
+ targetListId: {
1308
+ type: "string",
1309
+ description: "ID of destination list (preferred). Use instead of targetListName if available."
1310
+ },
1311
+ targetListName: {
1312
+ type: "string",
1313
+ description: "Name of destination list. Only use if you don't have targetListId."
1314
+ },
1315
+ options: {
1316
+ oneOf: [
1317
+ {
1318
+ type: "object",
1319
+ description: "Optional processing settings",
1320
+ properties: {
1321
+ batchSize: {
1322
+ type: "number",
1323
+ description: "Tasks per batch (default: 10)"
1324
+ },
1325
+ concurrency: {
1326
+ type: "number",
1327
+ description: "Parallel operations (default: 3)"
1328
+ },
1329
+ continueOnError: {
1330
+ type: "boolean",
1331
+ description: "Continue if some tasks fail"
1332
+ },
1333
+ retryCount: {
1334
+ type: "number",
1335
+ description: "Retry attempts for failures"
1336
+ }
1337
+ }
1338
+ },
1339
+ {
1340
+ type: "string",
1341
+ description: "JSON string representing options. Will be parsed automatically."
1342
+ }
1343
+ ],
1344
+ description: "Processing options (or JSON string representing options)"
1432
1345
  }
1433
- else {
1434
- throw new Error("Either taskId or taskName must be provided for each task");
1346
+ },
1347
+ required: ["tasks"]
1348
+ }
1349
+ };
1350
+ /**
1351
+ * Tool definition for deleting multiple tasks
1352
+ */
1353
+ export const deleteBulkTasksTool = {
1354
+ name: "delete_bulk_tasks",
1355
+ description: "⚠️ 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)",
1356
+ inputSchema: {
1357
+ type: "object",
1358
+ properties: {
1359
+ tasks: {
1360
+ type: "array",
1361
+ description: "Array of tasks to delete",
1362
+ items: {
1363
+ type: "object",
1364
+ properties: {
1365
+ taskId: {
1366
+ type: "string",
1367
+ description: "Task ID (preferred). Use instead of taskName if available."
1368
+ },
1369
+ taskName: {
1370
+ type: "string",
1371
+ description: "Task name. Requires listName when used."
1372
+ },
1373
+ listName: {
1374
+ type: "string",
1375
+ description: "REQUIRED with taskName: List containing the task."
1376
+ }
1377
+ }
1378
+ }
1379
+ },
1380
+ options: {
1381
+ oneOf: [
1382
+ {
1383
+ type: "object",
1384
+ description: "Optional processing settings",
1385
+ properties: {
1386
+ batchSize: {
1387
+ type: "number",
1388
+ description: "Tasks per batch (default: 10)"
1389
+ },
1390
+ concurrency: {
1391
+ type: "number",
1392
+ description: "Parallel operations (default: 3)"
1393
+ },
1394
+ continueOnError: {
1395
+ type: "boolean",
1396
+ description: "Continue if some tasks fail"
1397
+ },
1398
+ retryCount: {
1399
+ type: "number",
1400
+ description: "Retry attempts for failures"
1401
+ }
1402
+ }
1403
+ },
1404
+ {
1405
+ type: "string",
1406
+ description: "JSON string representing options. Will be parsed automatically."
1407
+ }
1408
+ ],
1409
+ description: "Processing options (or JSON string representing options)"
1435
1410
  }
1436
- taskIdsToDelete.push(taskId);
1411
+ },
1412
+ required: ["tasks"]
1413
+ }
1414
+ };
1415
+ /**
1416
+ * Handler for bulk task creation
1417
+ */
1418
+ export async function handleCreateBulkTasks(parameters) {
1419
+ const { listId, listName, tasks, options: rawOptions } = parameters;
1420
+ // Handle options parameter - may be a string that needs to be parsed
1421
+ let options = rawOptions;
1422
+ if (typeof rawOptions === 'string') {
1423
+ try {
1424
+ options = JSON.parse(rawOptions);
1437
1425
  }
1438
1426
  catch (error) {
1439
- results.failed++;
1440
- results.failures.push({
1441
- task: task.taskId || task.taskName,
1442
- error: error.message
1443
- });
1427
+ // Just use default options on parse error
1428
+ options = undefined;
1429
+ }
1430
+ }
1431
+ try {
1432
+ // Validate tasks parameter
1433
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1434
+ throw new Error('You must provide a non-empty array of tasks to create');
1444
1435
  }
1436
+ // Resolve list ID
1437
+ const targetListId = await resolveListId(listId, listName);
1438
+ // Format tasks with proper data types
1439
+ const formattedTasks = tasks.map((task) => ({
1440
+ name: task.name,
1441
+ description: task.description,
1442
+ markdown_description: task.markdown_description,
1443
+ status: task.status,
1444
+ priority: task.priority,
1445
+ due_date: task.dueDate ? parseDueDate(task.dueDate) : undefined,
1446
+ due_date_time: task.dueDate ? true : undefined
1447
+ }));
1448
+ // Use bulk service to create tasks
1449
+ const result = await bulkService.createTasks(targetListId, formattedTasks, options);
1450
+ // Format response
1451
+ const response = {
1452
+ content: [{
1453
+ type: "text",
1454
+ text: JSON.stringify({
1455
+ total: result.totals.total,
1456
+ successful: result.totals.success,
1457
+ failed: result.totals.failure,
1458
+ failures: result.failed.map(failure => ({
1459
+ task: failure.item.name,
1460
+ error: failure.error.message
1461
+ }))
1462
+ }, null, 2)
1463
+ }]
1464
+ };
1465
+ return enhanceResponseWithSponsor(response);
1445
1466
  }
1446
- // Perform the bulk delete operation if we have tasks to delete
1447
- if (taskIdsToDelete.length > 0) {
1467
+ catch (error) {
1468
+ return {
1469
+ content: [{
1470
+ type: "text",
1471
+ text: JSON.stringify({
1472
+ error: error.message || 'Failed to create tasks in bulk',
1473
+ listId,
1474
+ listName
1475
+ }, null, 2)
1476
+ }]
1477
+ };
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Handler for bulk task updates
1482
+ */
1483
+ export async function handleUpdateBulkTasks(parameters) {
1484
+ const { tasks, options: rawOptions } = parameters;
1485
+ // Handle options parameter - may be a string that needs to be parsed
1486
+ let options = rawOptions;
1487
+ if (typeof rawOptions === 'string') {
1448
1488
  try {
1449
- const bulkResult = await taskService.deleteBulkTasks(taskIdsToDelete);
1450
- // Process successful deletions
1451
- for (const deletedId of bulkResult.successfulItems) {
1452
- results.successful++;
1453
- const taskInfo = taskMap.get(deletedId);
1454
- results.deleted.push({
1455
- id: deletedId,
1456
- name: taskInfo?.name || "Unknown",
1457
- deleted: true
1458
- });
1459
- }
1460
- // Process failed deletions
1461
- for (const failure of bulkResult.failedItems) {
1462
- results.failed++;
1463
- const taskInfo = taskMap.get(failure.item);
1464
- results.failures.push({
1465
- task: taskInfo?.name || failure.item,
1466
- error: failure.error.message
1467
- });
1468
- }
1489
+ options = JSON.parse(rawOptions);
1469
1490
  }
1470
1491
  catch (error) {
1471
- // If the bulk delete fails entirely, mark all remaining tasks as failed
1472
- for (const taskId of taskIdsToDelete) {
1473
- const taskInfo = taskMap.get(taskId);
1474
- if (taskInfo && !results.deleted.some(t => t.id === taskId) &&
1475
- !results.failures.some(f => f.task === taskId || f.task === taskInfo.name)) {
1476
- results.failed++;
1477
- results.failures.push({
1478
- task: taskInfo.name || taskId,
1479
- error: error.message
1480
- });
1492
+ // Just use default options on parse error
1493
+ options = undefined;
1494
+ }
1495
+ }
1496
+ try {
1497
+ // Validate tasks parameter
1498
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1499
+ throw new Error('You must provide a non-empty array of tasks to update');
1500
+ }
1501
+ // Process the tasks using TaskResolver
1502
+ const tasksToUpdate = await Promise.all(tasks.map(async (task) => {
1503
+ const taskId = await resolveTaskId(task.taskId, task.taskName, undefined, task.listName);
1504
+ // Create update data object
1505
+ const updateData = {};
1506
+ if (task.name !== undefined)
1507
+ updateData.name = task.name;
1508
+ if (task.description !== undefined)
1509
+ updateData.description = task.description;
1510
+ if (task.markdown_description !== undefined)
1511
+ updateData.markdown_description = task.markdown_description;
1512
+ if (task.status !== undefined)
1513
+ updateData.status = task.status;
1514
+ if (task.priority !== undefined) {
1515
+ updateData.priority = task.priority === null ? null : task.priority;
1516
+ }
1517
+ if (task.dueDate !== undefined) {
1518
+ updateData.due_date = task.dueDate ? parseDueDate(task.dueDate) : null;
1519
+ if (task.dueDate && updateData.due_date) {
1520
+ updateData.due_date_time = true;
1481
1521
  }
1482
1522
  }
1483
- }
1523
+ return { id: taskId, data: updateData };
1524
+ }));
1525
+ // Use bulk service to update tasks
1526
+ const result = await bulkService.updateTasks(tasksToUpdate, options);
1527
+ // Format response
1528
+ const response = {
1529
+ content: [{
1530
+ type: "text",
1531
+ text: JSON.stringify({
1532
+ total: result.totals.total,
1533
+ successful: result.totals.success,
1534
+ failed: result.totals.failure,
1535
+ failures: result.failed.map(failure => ({
1536
+ taskId: failure.item.id,
1537
+ error: failure.error.message
1538
+ }))
1539
+ }, null, 2)
1540
+ }]
1541
+ };
1542
+ return enhanceResponseWithSponsor(response);
1543
+ }
1544
+ catch (error) {
1545
+ return {
1546
+ content: [{
1547
+ type: "text",
1548
+ text: JSON.stringify({
1549
+ error: error.message || 'Failed to update tasks in bulk',
1550
+ taskCount: tasks?.length || 0
1551
+ }, null, 2)
1552
+ }]
1553
+ };
1484
1554
  }
1485
- return {
1486
- content: [{
1487
- type: "text",
1488
- text: JSON.stringify(results, null, 2)
1489
- }]
1490
- };
1491
1555
  }
1492
1556
  /**
1493
1557
  * Handler for bulk task moves
1494
1558
  */
1495
1559
  export async function handleMoveBulkTasks(parameters) {
1496
- const { tasks, targetListId, targetListName } = parameters;
1497
- if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1498
- throw new Error('You must provide a non-empty array of tasks to move');
1560
+ const { tasks, targetListId, targetListName, options: rawOptions } = parameters;
1561
+ // Handle options parameter - may be a string that needs to be parsed
1562
+ let options = rawOptions;
1563
+ if (typeof rawOptions === 'string') {
1564
+ try {
1565
+ options = JSON.parse(rawOptions);
1566
+ }
1567
+ catch (error) {
1568
+ // Just use default options on parse error
1569
+ options = undefined;
1570
+ }
1499
1571
  }
1500
- let finalTargetListId = targetListId;
1501
- // If no targetListId but targetListName is provided, look up the list ID
1502
- if (!finalTargetListId && targetListName) {
1503
- const listInfo = await findListIDByName(workspaceService, targetListName);
1504
- if (!listInfo) {
1505
- throw new Error(`Target list "${targetListName}" not found`);
1572
+ try {
1573
+ // Validate tasks parameter
1574
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1575
+ throw new Error('You must provide a non-empty array of tasks to move');
1576
+ }
1577
+ // Resolve target list ID
1578
+ const resolvedTargetListId = await resolveListId(targetListId, targetListName);
1579
+ if (!resolvedTargetListId) {
1580
+ throw new Error('Either targetListId or targetListName must be provided');
1506
1581
  }
1507
- finalTargetListId = listInfo.id;
1582
+ // Resolve task IDs
1583
+ const taskIds = await Promise.all(tasks.map((task) => resolveTaskId(task.taskId, task.taskName, undefined, task.listName)));
1584
+ // Use bulk service to move tasks
1585
+ const result = await bulkService.moveTasks(taskIds, resolvedTargetListId, options);
1586
+ // Format response
1587
+ const response = {
1588
+ content: [{
1589
+ type: "text",
1590
+ text: JSON.stringify({
1591
+ total: result.totals.total,
1592
+ successful: result.totals.success,
1593
+ failed: result.totals.failure,
1594
+ targetList: targetListName || resolvedTargetListId,
1595
+ failures: result.failed.map(failure => ({
1596
+ taskId: failure.item,
1597
+ error: failure.error.message
1598
+ }))
1599
+ }, null, 2)
1600
+ }]
1601
+ };
1602
+ return enhanceResponseWithSponsor(response);
1508
1603
  }
1509
- if (!finalTargetListId) {
1510
- throw new Error("Either targetListId or targetListName must be provided");
1604
+ catch (error) {
1605
+ return {
1606
+ content: [{
1607
+ type: "text",
1608
+ text: JSON.stringify({
1609
+ error: error.message || 'Failed to move tasks in bulk',
1610
+ targetListId: targetListId || targetListName,
1611
+ taskCount: tasks?.length || 0
1612
+ }, null, 2)
1613
+ }]
1614
+ };
1511
1615
  }
1512
- const results = {
1513
- total: tasks.length,
1514
- successful: 0,
1515
- failed: 0,
1516
- failures: []
1517
- };
1518
- for (const task of tasks) {
1616
+ }
1617
+ /**
1618
+ * Handler for bulk task deletion
1619
+ */
1620
+ export async function handleDeleteBulkTasks(parameters) {
1621
+ const { tasks, options: rawOptions } = parameters;
1622
+ // Handle options parameter - may be a string that needs to be parsed
1623
+ let options = rawOptions;
1624
+ if (typeof rawOptions === 'string') {
1519
1625
  try {
1520
- let taskId = task.taskId;
1521
- if (!taskId && task.taskName) {
1522
- if (!task.listName) {
1523
- throw new Error(`List name is required when using task name for task "${task.taskName}"`);
1524
- }
1525
- const listInfo = await findListIDByName(workspaceService, task.listName);
1526
- if (!listInfo) {
1527
- throw new Error(`List "${task.listName}" not found`);
1528
- }
1529
- const taskList = await taskService.getTasks(listInfo.id);
1530
- const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
1531
- if (!foundTask) {
1532
- throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
1533
- }
1534
- taskId = foundTask.id;
1535
- }
1536
- if (!taskId) {
1537
- throw new Error("Either taskId or taskName must be provided");
1538
- }
1539
- await taskService.moveTask(taskId, finalTargetListId);
1540
- results.successful++;
1626
+ options = JSON.parse(rawOptions);
1541
1627
  }
1542
1628
  catch (error) {
1543
- results.failed++;
1544
- results.failures.push({
1545
- task: task.taskId || task.taskName,
1546
- error: error.message
1547
- });
1629
+ // Just use default options on parse error
1630
+ options = undefined;
1548
1631
  }
1549
1632
  }
1550
- return {
1551
- content: [{
1552
- type: "text",
1553
- text: JSON.stringify(results, null, 2)
1554
- }]
1555
- };
1556
- }
1557
- /**
1558
- * Handler for getting task comments
1559
- */
1560
- export async function handleGetTaskComments(parameters) {
1561
- const { taskId, taskName, listName, start, startId } = parameters;
1562
1633
  try {
1563
- // Call the handler with validation
1564
- const result = await getTaskCommentsTool.handler({
1565
- taskId,
1566
- taskName,
1567
- listName,
1568
- start: start ? Number(start) : undefined,
1569
- startId
1570
- });
1571
- return {
1634
+ // Validate tasks parameter
1635
+ if (!tasks || !Array.isArray(tasks) || tasks.length === 0) {
1636
+ throw new Error('You must provide a non-empty array of tasks to delete');
1637
+ }
1638
+ // Resolve task IDs
1639
+ const taskIds = await Promise.all(tasks.map((task) => resolveTaskId(task.taskId, task.taskName, undefined, task.listName)));
1640
+ // Use bulk service to delete tasks
1641
+ const result = await bulkService.deleteTasks(taskIds, options);
1642
+ // Format response
1643
+ const response = {
1572
1644
  content: [{
1573
1645
  type: "text",
1574
- text: JSON.stringify(result, null, 2)
1646
+ text: JSON.stringify({
1647
+ total: result.totals.total,
1648
+ successful: result.totals.success,
1649
+ failed: result.totals.failure,
1650
+ failures: result.failed.map(failure => ({
1651
+ taskId: failure.item,
1652
+ error: failure.error.message
1653
+ }))
1654
+ }, null, 2)
1575
1655
  }]
1576
1656
  };
1657
+ return enhanceResponseWithSponsor(response);
1577
1658
  }
1578
1659
  catch (error) {
1579
- // Handle and format error response with proper content array
1580
- console.error('Error getting task comments:', error);
1581
1660
  return {
1582
1661
  content: [{
1583
1662
  type: "text",
1584
1663
  text: JSON.stringify({
1585
- error: error.message || 'Failed to get task comments',
1586
- taskId: taskId || null,
1587
- taskName: taskName || null,
1588
- listName: listName || null
1664
+ error: error.message || 'Failed to delete tasks in bulk',
1665
+ taskCount: tasks?.length || 0
1589
1666
  }, null, 2)
1590
1667
  }]
1591
1668
  };