UNPKG

@taazkareem/clickup-mcp-server

Version:

ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol

920 lines (919 loc) 37.7 kB
/** * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com> * SPDX-License-Identifier: MIT * * ClickUp MCP Task Operation Handlers * * This module implements the handlers for task operations, both for single task * and bulk operations. These handlers are used by the tool definitions. */ import { toTaskPriority } from '../../services/clickup/types.js'; import { clickUpServices } from '../../services/shared.js'; import { BulkService } from '../../services/clickup/bulk.js'; import { parseDueDate } from '../utils.js'; import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js'; import { handleResolveAssignees } from '../member.js'; import { workspaceService } from '../../services/shared.js'; import { isNameMatch } from '../../utils/resolver-utils.js'; import { Logger } from '../../logger.js'; // Use shared services instance const { task: taskService, list: listService } = clickUpServices; // Create a bulk service instance that uses the task service const bulkService = new BulkService(taskService); // Create a logger instance for task handlers const logger = new Logger('TaskHandlers'); // Token limit constant for workspace tasks const WORKSPACE_TASKS_TOKEN_LIMIT = 50000; // Cache for task context between sequential operations const taskContextCache = new Map(); const TASK_CONTEXT_TTL = 5 * 60 * 1000; // 5 minutes /** * Store task context for sequential operations */ function storeTaskContext(taskName, taskId) { taskContextCache.set(taskName, { id: taskId, timestamp: Date.now() }); } /** * Get cached task context if valid */ function getCachedTaskContext(taskName) { const context = taskContextCache.get(taskName); if (!context) return null; if (Date.now() - context.timestamp > TASK_CONTEXT_TTL) { taskContextCache.delete(taskName); return null; } return context.id; } //============================================================================= // SHARED UTILITY FUNCTIONS //============================================================================= /** * Parse time estimate string into minutes * Supports formats like "2h 30m", "150m", "2.5h" */ function parseTimeEstimate(timeEstimate) { // If it's already a number, return it directly if (typeof timeEstimate === 'number') { return timeEstimate; } if (!timeEstimate || typeof timeEstimate !== 'string') return 0; // If it's just a number as string, parse it if (/^\d+$/.test(timeEstimate)) { return parseInt(timeEstimate, 10); } let totalMinutes = 0; // Extract hours const hoursMatch = timeEstimate.match(/(\d+\.?\d*)h/); if (hoursMatch) { totalMinutes += parseFloat(hoursMatch[1]) * 60; } // Extract minutes const minutesMatch = timeEstimate.match(/(\d+)m/); if (minutesMatch) { totalMinutes += parseInt(minutesMatch[1], 10); } return Math.round(totalMinutes); // Return minutes } /** * Resolve assignees from mixed input (user IDs, emails, usernames) to user IDs */ async function resolveAssignees(assignees) { if (!assignees || !Array.isArray(assignees) || assignees.length === 0) { return []; } const resolved = []; const toResolve = []; // Separate numeric IDs from strings that need resolution for (const assignee of assignees) { if (typeof assignee === 'number') { resolved.push(assignee); } else if (typeof assignee === 'string') { // Check if it's a numeric string const numericId = parseInt(assignee, 10); if (!isNaN(numericId) && numericId.toString() === assignee) { resolved.push(numericId); } else { // It's an email or username that needs resolution toResolve.push(assignee); } } } // Resolve emails/usernames to user IDs if any if (toResolve.length > 0) { try { const result = await handleResolveAssignees({ assignees: toResolve }); // The result is wrapped by sponsorService.createResponse, so we need to parse the JSON if (result.content && Array.isArray(result.content) && result.content.length > 0) { const dataText = result.content[0].text; const parsedData = JSON.parse(dataText); if (parsedData.userIds && Array.isArray(parsedData.userIds)) { for (const userId of parsedData.userIds) { if (userId !== null && typeof userId === 'number') { resolved.push(userId); } } } } } catch (error) { console.warn('Failed to resolve some assignees:', error.message); // Continue with the IDs we could resolve } } return resolved; } /** * Build task update data from parameters */ async function buildUpdateData(params) { const updateData = {}; if (params.name !== undefined) updateData.name = params.name; if (params.description !== undefined) updateData.description = params.description; if (params.markdown_description !== undefined) updateData.markdown_description = params.markdown_description; if (params.status !== undefined) updateData.status = params.status; // Use toTaskPriority to properly handle null values and validation if (params.priority !== undefined) { updateData.priority = toTaskPriority(params.priority); } if (params.dueDate !== undefined) { const parsedDueDate = parseDueDate(params.dueDate); if (parsedDueDate !== undefined) { updateData.due_date = parsedDueDate; updateData.due_date_time = true; } else { // Clear the due date by setting it to null updateData.due_date = null; updateData.due_date_time = false; } } if (params.startDate !== undefined) { const parsedStartDate = parseDueDate(params.startDate); if (parsedStartDate !== undefined) { updateData.start_date = parsedStartDate; updateData.start_date_time = true; } else { // Clear the start date by setting it to null updateData.start_date = null; updateData.start_date_time = false; } } // Handle time estimate if provided - convert from string to minutes if (params.time_estimate !== undefined) { // Log the time estimate for debugging console.log(`Original time_estimate: ${params.time_estimate}, typeof: ${typeof params.time_estimate}`); // Parse and convert to number in minutes const minutes = parseTimeEstimate(params.time_estimate); console.log(`Converted time_estimate: ${minutes}`); updateData.time_estimate = minutes; } // Handle custom fields if provided if (params.custom_fields !== undefined) { updateData.custom_fields = params.custom_fields; } // Handle assignees if provided - resolve emails/usernames to user IDs if (params.assignees !== undefined) { // Parse assignees if it's a string (from MCP serialization) let assigneesArray = params.assignees; if (typeof params.assignees === 'string') { try { assigneesArray = JSON.parse(params.assignees); } catch (error) { console.warn('Failed to parse assignees string:', params.assignees, error); assigneesArray = []; } } const resolvedAssignees = await resolveAssignees(assigneesArray); // Store the resolved assignees for processing in the updateTask method // The actual add/rem logic will be handled there based on current vs new assignees updateData.assignees = resolvedAssignees; } return updateData; } /** * Core function to find a task by ID or name * This consolidates all task lookup logic in one place for consistency */ async function findTask(params) { const { taskId, taskName, listName, customTaskId, requireId = false, includeSubtasks = false } = params; // Validate that we have enough information to identify a task const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { requireTaskId: requireId, useGlobalLookup: true }); if (!validationResult.isValid) { throw new Error(validationResult.errorMessage); } try { // Direct path for taskId - most efficient (now includes automatic custom ID detection) if (taskId) { const task = await taskService.getTask(taskId); // Add subtasks if requested if (includeSubtasks) { const subtasks = await taskService.getSubtasks(task.id); return { task, subtasks }; } return { task }; } // Direct path for customTaskId - for explicit custom ID requests // Note: This is now mainly for backward compatibility since getTask() handles custom IDs automatically if (customTaskId) { const task = await taskService.getTaskByCustomId(customTaskId); // Add subtasks if requested if (includeSubtasks) { const subtasks = await taskService.getSubtasks(task.id); return { task, subtasks }; } return { task }; } // Special optimized path for taskName + listName combination if (taskName && listName) { const listId = await resolveListIdWithValidation(null, listName); // Get all tasks in the list const allTasks = await taskService.getTasks(listId); // Find the task that matches the name const matchingTask = findTaskByName(allTasks, taskName); if (!matchingTask) { throw new Error(`Task "${taskName}" not found in list "${listName}"`); } // Add subtasks if requested if (includeSubtasks) { const subtasks = await taskService.getSubtasks(matchingTask.id); return { task: matchingTask, subtasks }; } return { task: matchingTask }; } // Fallback to searching all lists for taskName-only case if (taskName) { logger.debug(`Searching all lists for task: "${taskName}"`); // Get workspace hierarchy which contains all lists const hierarchy = await workspaceService.getWorkspaceHierarchy(); // Extract all list IDs from the hierarchy const listIds = []; const extractListIds = (node) => { if (node.type === 'list') { listIds.push(node.id); } if (node.children) { node.children.forEach(extractListIds); } }; // Start from the root's children hierarchy.root.children.forEach(extractListIds); // Search through each list const searchPromises = listIds.map(async (listId) => { try { const tasks = await taskService.getTasks(listId); const matchingTask = findTaskByName(tasks, taskName); if (matchingTask) { logger.debug(`Found task "${matchingTask.name}" (ID: ${matchingTask.id}) in list with ID "${listId}"`); return matchingTask; } return null; } catch (error) { logger.warn(`Error searching list ${listId}: ${error.message}`); return null; } }); // Wait for all searches to complete const results = await Promise.all(searchPromises); // Filter out null results and sort by match quality and recency const matchingTasks = results .filter(task => task !== null) .sort((a, b) => { const aMatch = isNameMatch(a.name, taskName); const bMatch = isNameMatch(b.name, taskName); // First sort by match quality if (bMatch.score !== aMatch.score) { return bMatch.score - aMatch.score; } // Then sort by recency return parseInt(b.date_updated) - parseInt(a.date_updated); }); if (matchingTasks.length === 0) { throw new Error(`Task "${taskName}" not found in any list across your workspace. Please check the task name and try again.`); } const bestMatch = matchingTasks[0]; // Add subtasks if requested if (includeSubtasks) { const subtasks = await taskService.getSubtasks(bestMatch.id); return { task: bestMatch, subtasks }; } return { task: bestMatch }; } // We shouldn't reach here if validation is working correctly throw new Error("No valid task identification provided"); } catch (error) { // Enhance error message for non-existent tasks if (taskName && error.message.includes('not found')) { throw new Error(`Task "${taskName}" not found. Please check the task name and try again.`); } // Pass along other formatted errors throw error; } } /** * Helper function to find a task by name in an array of tasks */ function findTaskByName(tasks, name) { if (!tasks || !Array.isArray(tasks) || !name) return null; const normalizedSearchName = name.toLowerCase().trim(); // Get match scores for all tasks const taskMatchScores = tasks.map(task => { const matchResult = isNameMatch(task.name, name); return { task, matchResult, // Parse the date_updated field as a number for sorting updatedAt: task.date_updated ? parseInt(task.date_updated, 10) : 0 }; }).filter(result => result.matchResult.isMatch); if (taskMatchScores.length === 0) { return null; } // First, try to find exact matches const exactMatches = taskMatchScores .filter(result => result.matchResult.exactMatch) .sort((a, b) => { // For exact matches with the same score, sort by most recently updated if (b.matchResult.score === a.matchResult.score) { return b.updatedAt - a.updatedAt; } return b.matchResult.score - a.matchResult.score; }); // Get the best matches based on whether we have exact matches or need to fall back to fuzzy matches const bestMatches = exactMatches.length > 0 ? exactMatches : taskMatchScores.sort((a, b) => { // First sort by match score (highest first) if (b.matchResult.score !== a.matchResult.score) { return b.matchResult.score - a.matchResult.score; } // Then sort by most recently updated return b.updatedAt - a.updatedAt; }); // Get the best match return bestMatches[0].task; } /** * Handler for getting a task - uses the consolidated findTask function */ export async function getTaskHandler(params) { try { const result = await findTask({ taskId: params.taskId, taskName: params.taskName, listName: params.listName, customTaskId: params.customTaskId, includeSubtasks: params.subtasks }); if (result.subtasks) { return { ...result.task, subtasks: result.subtasks }; } return result.task; } catch (error) { throw error; } } /** * Get task ID from various identifiers - uses the consolidated findTask function */ export async function getTaskId(taskId, taskName, listName, customTaskId, requireId, includeSubtasks) { // Check task context cache first if we have a task name if (taskName && !taskId && !customTaskId) { const cachedId = getCachedTaskContext(taskName); if (cachedId) { return cachedId; } } const result = await findTask({ taskId, taskName, listName, customTaskId, requireId, includeSubtasks }); // Store task context for future operations if (taskName && result.task.id) { storeTaskContext(taskName, result.task.id); } return result.task.id; } /** * Process a list identification validation, returning the list ID */ async function getListId(listId, listName) { validateListIdentification(listId, listName); return await resolveListIdWithValidation(listId, listName); } /** * Extract and build task filters from parameters */ function buildTaskFilters(params) { const { subtasks, statuses, page, order_by, reverse } = params; const filters = {}; if (subtasks !== undefined) filters.subtasks = subtasks; if (statuses !== undefined) filters.statuses = statuses; if (page !== undefined) filters.page = page; if (order_by !== undefined) filters.order_by = order_by; if (reverse !== undefined) filters.reverse = reverse; return filters; } /** * Map tasks for bulk operations, resolving task IDs * Uses smart disambiguation for tasks without list context */ async function mapTaskIds(tasks) { return Promise.all(tasks.map(async (task) => { const validationResult = validateTaskIdentification({ taskId: task.taskId, taskName: task.taskName, listName: task.listName, customTaskId: task.customTaskId }, { useGlobalLookup: true }); if (!validationResult.isValid) { throw new Error(validationResult.errorMessage); } return await getTaskId(task.taskId, task.taskName, task.listName, task.customTaskId); })); } //============================================================================= // SINGLE TASK OPERATIONS //============================================================================= /** * Handler for creating a task */ export async function createTaskHandler(params) { const { name, description, markdown_description, status, dueDate, startDate, parent, tags, custom_fields, check_required_custom_fields, assignees } = params; if (!name) throw new Error("Task name is required"); // Use our helper function to validate and convert priority const priority = toTaskPriority(params.priority); const listId = await getListId(params.listId, params.listName); // Resolve assignees if provided let resolvedAssignees = undefined; if (assignees) { // Parse assignees if it's a string (from MCP serialization) let assigneesArray = assignees; if (typeof assignees === 'string') { try { assigneesArray = JSON.parse(assignees); } catch (error) { console.warn('Failed to parse assignees string in createTask:', assignees, error); assigneesArray = []; } } resolvedAssignees = await resolveAssignees(assigneesArray); } const taskData = { name, description, markdown_description, status, parent, tags, custom_fields, check_required_custom_fields, assignees: resolvedAssignees }; // Only include priority if explicitly provided by the user if (priority !== undefined) { taskData.priority = priority; } // Add due date if specified if (dueDate) { taskData.due_date = parseDueDate(dueDate); taskData.due_date_time = true; } // Add start date if specified if (startDate) { taskData.start_date = parseDueDate(startDate); taskData.start_date_time = true; } return await taskService.createTask(listId, taskData); } /** * Handler for updating a task */ export async function updateTaskHandler(taskService, params) { const { taskId, taskName, listName, customTaskId, ...rawUpdateData } = params; // Validate task identification with global lookup enabled const validationResult = validateTaskIdentification(params, { useGlobalLookup: true }); if (!validationResult.isValid) { throw new Error(validationResult.errorMessage); } // Build properly formatted update data from raw parameters (now async) const updateData = await buildUpdateData(rawUpdateData); // Validate update data validateTaskUpdateData(updateData); try { // Get the task ID using global lookup const id = await getTaskId(taskId, taskName, listName, customTaskId); return await taskService.updateTask(id, updateData); } catch (error) { throw new Error(`Failed to update task: ${error instanceof Error ? error.message : String(error)}`); } } /** * Handler for moving a task */ export async function moveTaskHandler(params) { const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false); const listId = await getListId(params.listId, params.listName); return await taskService.moveTask(taskId, listId); } /** * Handler for duplicating a task */ export async function duplicateTaskHandler(params) { const taskId = await getTaskId(params.taskId, params.taskName, undefined, params.customTaskId, false); let listId; if (params.listId || params.listName) { listId = await getListId(params.listId, params.listName); } return await taskService.duplicateTask(taskId, listId); } /** * Handler for getting tasks */ export async function getTasksHandler(params) { const listId = await getListId(params.listId, params.listName); return await taskService.getTasks(listId, buildTaskFilters(params)); } /** * Handler for getting task comments */ export async function getTaskCommentsHandler(params) { const taskId = await getTaskId(params.taskId, params.taskName, params.listName); const { start, startId } = params; return await taskService.getTaskComments(taskId, start, startId); } /** * Handler for creating a task comment */ export async function createTaskCommentHandler(params) { // Validate required parameters if (!params.commentText) { throw new Error('Comment text is required'); } try { // Resolve the task ID const taskId = await getTaskId(params.taskId, params.taskName, params.listName); // Extract other parameters with defaults const { commentText, notifyAll = false, assignee = null } = params; // Create the comment return await taskService.createTaskComment(taskId, commentText, notifyAll, assignee); } catch (error) { // If this is a task lookup error, provide more helpful message if (error.message?.includes('not found') || error.message?.includes('identify task')) { if (params.taskName) { throw new Error(`Could not find task "${params.taskName}" in list "${params.listName}"`); } else { throw new Error(`Task with ID "${params.taskId}" not found`); } } // Otherwise, rethrow the original error throw error; } } /** * Estimate tokens for a task response * This is a simplified estimation - adjust based on actual token counting needs */ function estimateTaskResponseTokens(task) { // Base estimation for task structure let tokenCount = 0; // Core fields tokenCount += (task.name?.length || 0) / 4; // Approximate tokens for name tokenCount += (task.description?.length || 0) / 4; // Approximate tokens for description tokenCount += (task.text_content?.length || 0) / 4; // Use text_content instead of markdown_description // Status and other metadata tokenCount += 5; // Basic metadata fields // Custom fields if (task.custom_fields) { tokenCount += Object.keys(task.custom_fields).length * 10; // Rough estimate per custom field } // Add overhead for JSON structure tokenCount *= 1.1; return Math.ceil(tokenCount); } /** * Check if response would exceed token limit */ function wouldExceedTokenLimit(response) { if (!response.tasks?.length) return false; // Calculate total estimated tokens const totalTokens = response.tasks.reduce((sum, task) => sum + estimateTaskResponseTokens(task), 0); // Add overhead for response structure const estimatedTotal = totalTokens * 1.1; return estimatedTotal > WORKSPACE_TASKS_TOKEN_LIMIT; } /** * Handler for getting workspace tasks with filtering */ export async function getWorkspaceTasksHandler(taskService, params) { try { // Require at least one filter parameter const hasFilter = [ 'tags', 'list_ids', 'folder_ids', 'space_ids', 'statuses', 'assignees', 'date_created_gt', 'date_created_lt', 'date_updated_gt', 'date_updated_lt', 'due_date_gt', 'due_date_lt' ].some(key => params[key] !== undefined); if (!hasFilter) { throw new Error('At least one filter parameter is required (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)'); } // Check if list_ids are provided for enhanced filtering via Views API if (params.list_ids && params.list_ids.length > 0) { logger.info('Using Views API for enhanced list filtering', { listIds: params.list_ids, listCount: params.list_ids.length }); // Warning for broad queries const hasOnlyListIds = Object.keys(params).filter(key => params[key] !== undefined && key !== 'list_ids' && key !== 'detail_level').length === 0; if (hasOnlyListIds && params.list_ids.length > 5) { logger.warn('Broad query detected: many lists with no additional filters', { listCount: params.list_ids.length, recommendation: 'Consider adding additional filters (tags, statuses, assignees, etc.) for better performance' }); } // Use Views API for enhanced list filtering let allTasks = []; const processedTaskIds = new Set(); // Create promises for concurrent fetching const fetchPromises = params.list_ids.map(async (listId) => { try { // Get the default list view ID const viewId = await taskService.getListViews(listId); if (!viewId) { logger.warn(`No default view found for list ${listId}, skipping`); return []; } // Extract filters supported by the Views API const supportedFilters = { subtasks: params.subtasks, include_closed: params.include_closed, archived: params.archived, order_by: params.order_by, reverse: params.reverse, page: params.page, statuses: params.statuses, assignees: params.assignees, date_created_gt: params.date_created_gt, date_created_lt: params.date_created_lt, date_updated_gt: params.date_updated_gt, date_updated_lt: params.date_updated_lt, due_date_gt: params.due_date_gt, due_date_lt: params.due_date_lt, custom_fields: params.custom_fields }; // Get tasks from the view const tasksFromView = await taskService.getTasksFromView(viewId, supportedFilters); return tasksFromView; } catch (error) { logger.error(`Failed to get tasks from list ${listId}`, { error: error.message }); return []; // Continue with other lists even if one fails } }); // Execute all fetches concurrently const taskArrays = await Promise.all(fetchPromises); // Aggregate tasks and remove duplicates for (const tasks of taskArrays) { for (const task of tasks) { if (!processedTaskIds.has(task.id)) { allTasks.push(task); processedTaskIds.add(task.id); } } } logger.info('Aggregated tasks from Views API', { totalTasks: allTasks.length, uniqueTasks: processedTaskIds.size }); // Apply client-side filtering for unsupported filters if (params.tags && params.tags.length > 0) { allTasks = allTasks.filter(task => params.tags.every((tag) => task.tags.some(t => t.name === tag))); logger.debug('Applied client-side tag filtering', { tags: params.tags, remainingTasks: allTasks.length }); } if (params.folder_ids && params.folder_ids.length > 0) { allTasks = allTasks.filter(task => task.folder && params.folder_ids.includes(task.folder.id)); logger.debug('Applied client-side folder filtering', { folderIds: params.folder_ids, remainingTasks: allTasks.length }); } if (params.space_ids && params.space_ids.length > 0) { allTasks = allTasks.filter(task => params.space_ids.includes(task.space.id)); logger.debug('Applied client-side space filtering', { spaceIds: params.space_ids, remainingTasks: allTasks.length }); } // Check token limit and format response const shouldUseSummary = params.detail_level === 'summary' || wouldExceedTokenLimit({ tasks: allTasks }); if (shouldUseSummary) { logger.info('Using summary format for Views API response', { totalTasks: allTasks.length, reason: params.detail_level === 'summary' ? 'requested' : 'token_limit' }); return { summaries: allTasks.map(task => ({ id: task.id, name: task.name, status: task.status.status, list: { id: task.list.id, name: task.list.name }, due_date: task.due_date, url: task.url, priority: task.priority?.priority || null, tags: task.tags.map(tag => ({ name: tag.name, tag_bg: tag.tag_bg, tag_fg: tag.tag_fg })) })), total_count: allTasks.length, has_more: false, next_page: 0 }; } return { tasks: allTasks, total_count: allTasks.length, has_more: false, next_page: 0 }; } // Fallback to existing workspace-wide task retrieval when list_ids are not provided logger.info('Using standard workspace task retrieval'); const filters = { tags: params.tags, list_ids: params.list_ids, folder_ids: params.folder_ids, space_ids: params.space_ids, statuses: params.statuses, include_closed: params.include_closed, include_archived_lists: params.include_archived_lists, include_closed_lists: params.include_closed_lists, archived: params.archived, order_by: params.order_by, reverse: params.reverse, due_date_gt: params.due_date_gt, due_date_lt: params.due_date_lt, date_created_gt: params.date_created_gt, date_created_lt: params.date_created_lt, date_updated_gt: params.date_updated_gt, date_updated_lt: params.date_updated_lt, assignees: params.assignees, page: params.page, detail_level: params.detail_level || 'detailed', subtasks: params.subtasks, include_subtasks: params.include_subtasks, include_compact_time_entries: params.include_compact_time_entries, custom_fields: params.custom_fields }; // Get tasks with adaptive response format support const response = await taskService.getWorkspaceTasks(filters); // Check token limit at handler level if (params.detail_level !== 'summary' && wouldExceedTokenLimit(response)) { logger.info('Response would exceed token limit, fetching summary format instead'); // Refetch with summary format const summaryResponse = await taskService.getWorkspaceTasks({ ...filters, detail_level: 'summary' }); return summaryResponse; } // Return the response without adding the redundant _note field return response; } catch (error) { throw new Error(`Failed to get workspace tasks: ${error.message}`); } } //============================================================================= // BULK TASK OPERATIONS //============================================================================= /** * Handler for creating multiple tasks */ export async function createBulkTasksHandler(params) { const { tasks, listId, listName, options } = params; // Validate tasks array validateBulkTasks(tasks, 'create'); // Validate and resolve list ID const targetListId = await resolveListIdWithValidation(listId, listName); // Format tasks for creation - resolve assignees for each task const formattedTasks = await Promise.all(tasks.map(async (task) => { // Resolve assignees if provided const resolvedAssignees = task.assignees ? await resolveAssignees(task.assignees) : undefined; const taskData = { name: task.name, description: task.description, markdown_description: task.markdown_description, status: task.status, tags: task.tags, custom_fields: task.custom_fields, assignees: resolvedAssignees }; // Only include priority if explicitly provided by the user const priority = toTaskPriority(task.priority); if (priority !== undefined) { taskData.priority = priority; } // Add due date if specified if (task.dueDate) { taskData.due_date = parseDueDate(task.dueDate); taskData.due_date_time = true; } // Add start date if specified if (task.startDate) { taskData.start_date = parseDueDate(task.startDate); taskData.start_date_time = true; } return taskData; })); // Parse bulk options const bulkOptions = parseBulkOptions(options); // Create tasks - pass arguments in correct order: listId, tasks, options return await bulkService.createTasks(targetListId, formattedTasks, bulkOptions); } /** * Handler for updating multiple tasks */ export async function updateBulkTasksHandler(params) { const { tasks, options } = params; // Validate tasks array validateBulkTasks(tasks, 'update'); // Parse bulk options const bulkOptions = parseBulkOptions(options); // Update tasks return await bulkService.updateTasks(tasks, bulkOptions); } /** * Handler for moving multiple tasks */ export async function moveBulkTasksHandler(params) { const { tasks, targetListId, targetListName, options } = params; // Validate tasks array validateBulkTasks(tasks, 'move'); // Validate and resolve target list ID const resolvedTargetListId = await resolveListIdWithValidation(targetListId, targetListName); // Parse bulk options const bulkOptions = parseBulkOptions(options); // Move tasks return await bulkService.moveTasks(tasks, resolvedTargetListId, bulkOptions); } /** * Handler for deleting multiple tasks */ export async function deleteBulkTasksHandler(params) { const { tasks, options } = params; // Validate tasks array validateBulkTasks(tasks, 'delete'); // Parse bulk options const bulkOptions = parseBulkOptions(options); // Delete tasks return await bulkService.deleteTasks(tasks, bulkOptions); } /** * Handler for deleting a task */ export async function deleteTaskHandler(params) { const taskId = await getTaskId(params.taskId, params.taskName, params.listName); await taskService.deleteTask(taskId); return true; }