@taazkareem/clickup-mcp-server
Version:
ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol
181 lines (180 loc) • 7.73 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
* SPDX-License-Identifier: MIT
*
* ClickUp Bulk Service
*
* Enhanced implementation for bulk operations that leverages the existing single-operation methods.
* This approach reduces code duplication while offering powerful concurrency management.
*/
import { Logger } from '../../logger.js';
import { processBatch } from '../../utils/concurrency-utils.js';
import { ClickUpServiceError, ErrorCode } from './base.js';
import { clickUpServices } from '../shared.js';
import { findListIDByName } from '../../tools/list.js';
// Create logger instance
const logger = new Logger('BulkService');
/**
* Service for performing bulk operations in ClickUp
*/
export class BulkService {
/**
* Create a new bulk service
* @param taskService ClickUp Task Service instance
*/
constructor(taskService) {
this.taskService = taskService;
logger.info('BulkService initialized');
}
/**
* Create multiple tasks in a list efficiently
*
* @param listId ID of the list to create tasks in
* @param tasks Array of task data
* @param options Batch processing options
* @returns Results containing successful and failed tasks
*/
async createTasks(listId, tasks, options) {
logger.info(`Creating ${tasks.length} tasks in list ${listId}`, {
batchSize: options?.batchSize,
concurrency: options?.concurrency
});
try {
// First validate that the list exists - do this once for all tasks
await this.taskService.validateListExists(listId);
// Process the tasks in batches
return await processBatch(tasks, (task, index) => {
logger.debug(`Creating task ${index + 1}/${tasks.length}`, {
taskName: task.name
});
// Reuse the single-task creation method
return this.taskService.createTask(listId, task);
}, options);
}
catch (error) {
logger.error(`Failed to create tasks in bulk`, {
listId,
taskCount: tasks.length,
error: error instanceof Error ? error.message : String(error)
});
throw new ClickUpServiceError(`Failed to create tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { listId, taskCount: tasks.length });
}
}
/**
* Find task by name within a specific list
*/
async findTaskInList(taskName, listName) {
try {
const result = await this.taskService.findTasks({
taskName,
listName,
allowMultipleMatches: false,
useSmartDisambiguation: true,
includeFullDetails: false
});
if (!result || Array.isArray(result)) {
throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
}
logger.info(`Task "${taskName}" found with ID: ${result.id}`);
return result.id;
}
catch (error) {
// Enhance the error message
if (error instanceof ClickUpServiceError) {
throw error;
}
throw new ClickUpServiceError(`Error finding task "${taskName}" in list "${listName}": ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN);
}
}
/**
* Resolve task ID using provided identifiers
*/
async resolveTaskId(task) {
const { taskId, taskName, listName, customTaskId } = task;
if (taskId) {
return taskId;
}
if (customTaskId) {
const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
return resolvedTask.id;
}
if (taskName && listName) {
return await this.findTaskInList(taskName, listName);
}
throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
}
/**
* Update multiple tasks
* @param tasks Array of tasks to update with their new data
* @param options Optional batch processing settings
* @returns Array of updated tasks
*/
async updateTasks(tasks, options) {
logger.info('Starting bulk update operation', { taskCount: tasks.length });
try {
return await processBatch(tasks, async (task) => {
const { taskId, taskName, listName, customTaskId, ...updateData } = task;
const resolvedTaskId = await this.resolveTaskId({ taskId, taskName, listName, customTaskId });
return await this.taskService.updateTask(resolvedTaskId, updateData);
}, options);
}
catch (error) {
logger.error('Bulk update operation failed', error);
throw error;
}
}
/**
* Move multiple tasks to a different list
* @param tasks Array of tasks to move (each with taskId or taskName + listName)
* @param targetListId ID of the destination list or list name
* @param options Optional batch processing settings
* @returns Array of moved tasks
*/
async moveTasks(tasks, targetListId, options) {
logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
try {
// Determine if targetListId is actually an ID or a name
let resolvedTargetListId = targetListId;
// If the targetListId doesn't match the pattern of a list ID (usually just numbers),
// assume it's a list name and try to resolve it
if (!/^\d+$/.test(targetListId)) {
logger.info(`Target list appears to be a name: "${targetListId}", attempting to resolve`);
const listInfo = await findListIDByName(clickUpServices.workspace, targetListId);
if (!listInfo) {
throw new ClickUpServiceError(`Target list "${targetListId}" not found`, ErrorCode.NOT_FOUND);
}
resolvedTargetListId = listInfo.id;
logger.info(`Resolved target list to ID: ${resolvedTargetListId}`);
}
// Validate the destination list exists
await this.taskService.validateListExists(resolvedTargetListId);
return await processBatch(tasks, async (task) => {
const resolvedTaskId = await this.resolveTaskId(task);
return await this.taskService.moveTask(resolvedTaskId, resolvedTargetListId);
}, options);
}
catch (error) {
logger.error('Bulk move operation failed', error);
throw error;
}
}
/**
* Delete multiple tasks
* @param tasks Array of tasks to delete (each with taskId or taskName + listName)
* @param options Batch processing options
* @returns Results containing successful and failed deletions
*/
async deleteTasks(tasks, options) {
logger.info('Starting bulk delete operation', { taskCount: tasks.length });
try {
return await processBatch(tasks, async (task) => {
const resolvedTaskId = await this.resolveTaskId(task);
await this.taskService.deleteTask(resolvedTaskId);
}, options);
}
catch (error) {
logger.error('Bulk delete operation failed', error);
throw error;
}
}
}