@taazkareem/clickup-mcp-server 0.6.3 → 0.6.5

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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI applications. This server allows AI agents to interact with ClickUp tasks, spaces, lists, and folders through a standardized protocol.
8
8
 
9
- > 🚧 **Status Update:** Rolling out v0.6.3 will add Global Task Lookup with smart disambiguation, Start Date Support for tasks with natural language expressions, Complete Tag Support including natural language tag color commands, Subtasks Support, Custom ID Support, and Logging Fixes
9
+ > 🚧 **Status Update:** Rolling out v0.6.4 will add Global Task Lookup with smart disambiguation, Start Date Support for tasks with natural language expressions, Complete Tag Support including natural language tag color commands, Subtasks Support, Custom ID Support, and Logging Fixes
10
10
 
11
11
  ## Setup
12
12
 
package/build/config.js CHANGED
@@ -43,7 +43,7 @@ export const parseLogLevel = (levelStr) => {
43
43
  case 'WARN': return LogLevel.WARN;
44
44
  case 'ERROR': return LogLevel.ERROR;
45
45
  default:
46
- console.error(`Invalid LOG_LEVEL: ${levelStr}, defaulting to ERROR`);
46
+ // Don't use console.error as it interferes with JSON-RPC communication
47
47
  return LogLevel.ERROR;
48
48
  }
49
49
  };
@@ -54,8 +54,7 @@ const configuration = {
54
54
  enableSponsorMessage: process.env.ENABLE_SPONSOR_MESSAGE !== 'false',
55
55
  logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL)
56
56
  };
57
- // Log the configured log level (but only to console to avoid circular dependency)
58
- console.debug(`Log level set to: ${LogLevel[configuration.logLevel]}`);
57
+ // Don't log to console as it interferes with JSON-RPC communication
59
58
  // Validate only the required variables are present
60
59
  const requiredVars = ['clickupApiKey', 'clickupTeamId'];
61
60
  const missingEnvVars = requiredVars
package/build/index.js CHANGED
@@ -23,43 +23,27 @@
23
23
  */
24
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
25
  import { configureServer, server } from "./server.js";
26
- import { info, error, warn } from "./logger.js";
27
- import { exec } from 'child_process';
28
- import { promisify } from 'util';
29
- const execAsync = promisify(exec);
30
- /**
31
- * Check if another instance of the server is already running
32
- * @returns Boolean indicating if this is the only instance
33
- */
34
- async function checkSingleInstance() {
35
- try {
36
- const { stdout } = await execAsync(`ps aux | grep "node.*clickup-mcp-server" | grep -v grep | wc -l`);
37
- const instanceCount = parseInt(stdout.trim(), 10);
38
- // If there's more than one instance (including this one), warn and exit
39
- if (instanceCount > 1) {
40
- error(`Multiple server instances detected (${instanceCount}). This may cause issues.`);
41
- info("Use 'pkill -9 -f clickup-mcp-server' to kill all instances before starting a new one.");
42
- return false;
43
- }
44
- return true;
45
- }
46
- catch (err) {
47
- error("Failed to check for other running instances", err);
48
- // Continue execution even if check fails
49
- return true;
50
- }
51
- }
26
+ import { info, error } from "./logger.js";
27
+ import { dirname } from 'path';
28
+ import { fileURLToPath } from 'url';
29
+ // Get directory name for module paths
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+ // Handle uncaught exceptions
32
+ process.on('uncaughtException', (err) => {
33
+ error("Uncaught Exception", { message: err.message, stack: err.stack });
34
+ process.exit(1);
35
+ });
36
+ // Handle unhandled promise rejections
37
+ process.on('unhandledRejection', (reason, promise) => {
38
+ error("Unhandled Rejection", { reason });
39
+ process.exit(1);
40
+ });
52
41
  /**
53
42
  * Application entry point that configures and starts the MCP server.
54
43
  */
55
44
  async function main() {
56
45
  try {
57
46
  info("Starting ClickUp MCP Server...");
58
- // Check if we're the only instance
59
- const isSingleInstance = await checkSingleInstance();
60
- if (!isSingleInstance) {
61
- warn("Continuing startup despite multiple instances detected");
62
- }
63
47
  // Log essential information about the environment
64
48
  info("Server environment", {
65
49
  pid: process.pid,
@@ -77,11 +61,11 @@ async function main() {
77
61
  info("Server startup complete - ready to handle requests");
78
62
  }
79
63
  catch (err) {
80
- error("Error during server startup", err);
64
+ error("Error during server startup", { message: err.message, stack: err.stack });
81
65
  process.exit(1);
82
66
  }
83
67
  }
84
68
  main().catch((err) => {
85
- error("Unhandled server error", err);
69
+ error("Unhandled server error", { message: err.message, stack: err.stack });
86
70
  process.exit(1);
87
71
  });
package/build/logger.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Logger module for MCP Server
6
6
  *
7
7
  * This module provides logging functionality for the server,
8
- * writing logs to both the console and a log file in the build folder.
8
+ * writing logs to only the log file to avoid interfering with JSON-RPC.
9
9
  */
10
10
  import { createWriteStream } from 'fs';
11
11
  import { join, dirname } from 'path';
@@ -18,7 +18,8 @@ const pid = process.pid;
18
18
  // Create a write stream for logging - use a fixed filename in the build directory
19
19
  const logFileName = 'server.log';
20
20
  const logStream = createWriteStream(join(__dirname, logFileName), { flags: 'w' });
21
- console.error(`Logging to ${join(__dirname, logFileName)}`);
21
+ // Write init message to log file only
22
+ logStream.write(`Logging initialized to ${join(__dirname, logFileName)}\n`);
22
23
  // Use the configured log level from config.ts
23
24
  const configuredLevel = config.logLevel;
24
25
  // Re-export LogLevel enum
@@ -32,7 +33,7 @@ export function isLevelEnabled(level) {
32
33
  return level >= configuredLevel;
33
34
  }
34
35
  /**
35
- * Log function that writes to both console and file
36
+ * Log function that writes only to file to avoid interfering with JSON-RPC
36
37
  * @param level Log level (trace, debug, info, warn, error)
37
38
  * @param message Message to log
38
39
  * @param data Optional data to include in log
@@ -74,9 +75,7 @@ export function log(level, message, data) {
74
75
  logMessage += '\n' + JSON.stringify(data, null, 2);
75
76
  }
76
77
  }
77
- // When using stdio transport, log to stderr which is captured by host application
78
- console.error(logMessage);
79
- // Write to file
78
+ // Write to file only, not to stderr which would interfere with JSON-RPC
80
79
  logStream.write(logMessage + '\n');
81
80
  }
82
81
  /**
package/build/server.js CHANGED
@@ -5,12 +5,12 @@
5
5
  * MCP Server for ClickUp integration
6
6
  */
7
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
- import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { workspaceHierarchyTool, handleGetWorkspaceHierarchy } from "./tools/workspace.js";
10
10
  import { createTaskTool, updateTaskTool, moveTaskTool, duplicateTaskTool, getTaskTool, getTasksTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool, createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool, attachTaskFileTool, getWorkspaceTasksTool, handleCreateTask, handleUpdateTask, handleMoveTask, handleDuplicateTask, handleGetTasks, handleDeleteTask, handleGetTaskComments, handleCreateTaskComment, handleCreateBulkTasks, handleUpdateBulkTasks, handleMoveBulkTasks, handleDeleteBulkTasks, handleGetTask, handleAttachTaskFile, handleGetWorkspaceTasks } from "./tools/task/index.js";
11
11
  import { createListTool, handleCreateList, createListInFolderTool, handleCreateListInFolder, getListTool, handleGetList, updateListTool, handleUpdateList, deleteListTool, handleDeleteList } from "./tools/list.js";
12
12
  import { createFolderTool, handleCreateFolder, getFolderTool, handleGetFolder, updateFolderTool, handleUpdateFolder, deleteFolderTool, handleDeleteFolder } from "./tools/folder.js";
13
- import { getSpaceTagsTool, handleGetSpaceTags, createSpaceTagTool, handleCreateSpaceTag, updateSpaceTagTool, handleUpdateSpaceTag, deleteSpaceTagTool, handleDeleteSpaceTag, addTagToTaskTool, handleAddTagToTask, removeTagFromTaskTool, handleRemoveTagFromTask } from "./tools/tag.js";
13
+ import { getSpaceTagsTool, handleGetSpaceTags, addTagToTaskTool, handleAddTagToTask, removeTagFromTaskTool, handleRemoveTagFromTask } from "./tools/tag.js";
14
14
  import { Logger } from "./logger.js";
15
15
  import { clickUpServices } from "./services/shared.js";
16
16
  // Create a logger instance for server
@@ -19,11 +19,12 @@ const logger = new Logger('Server');
19
19
  const { workspace } = clickUpServices;
20
20
  export const server = new Server({
21
21
  name: "clickup-mcp-server",
22
- version: "0.6.3",
22
+ version: "0.6.5",
23
23
  }, {
24
24
  capabilities: {
25
25
  tools: {},
26
26
  prompts: {},
27
+ resources: {},
27
28
  },
28
29
  });
29
30
  /**
@@ -62,17 +63,19 @@ export function configureServer() {
62
63
  updateFolderTool,
63
64
  deleteFolderTool,
64
65
  getSpaceTagsTool,
65
- createSpaceTagTool,
66
- updateSpaceTagTool,
67
- deleteSpaceTagTool,
68
66
  addTagToTaskTool,
69
67
  removeTagFromTaskTool
70
68
  ]
71
69
  };
72
70
  });
71
+ // Add handler for resources/list
72
+ server.setRequestHandler(ListResourcesRequestSchema, async (req) => {
73
+ logger.debug("Received ListResources request");
74
+ return { resources: [] };
75
+ });
73
76
  // Register CallTool handler with proper logging
74
77
  logger.info("Registering tool handlers", {
75
- toolCount: 31,
78
+ toolCount: 28,
76
79
  categories: ["workspace", "task", "list", "folder", "tag"]
77
80
  });
78
81
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -136,24 +139,39 @@ export function configureServer() {
136
139
  return handleDeleteFolder(params);
137
140
  case "get_space_tags":
138
141
  return handleGetSpaceTags(params);
139
- case "create_space_tag":
140
- return handleCreateSpaceTag(params);
141
- case "update_space_tag":
142
- return handleUpdateSpaceTag(params);
143
- case "delete_space_tag":
144
- return handleDeleteSpaceTag(params);
145
142
  case "add_tag_to_task":
146
143
  return handleAddTagToTask(params);
147
144
  case "remove_tag_from_task":
148
145
  return handleRemoveTagFromTask(params);
149
146
  default:
150
147
  logger.error(`Unknown tool requested: ${name}`);
151
- throw new Error(`Unknown tool: ${name}`);
148
+ const error = new Error(`Unknown tool: ${name}`);
149
+ error.name = "UnknownToolError";
150
+ throw error;
152
151
  }
153
152
  }
154
153
  catch (err) {
155
154
  logger.error(`Error executing tool: ${name}`, err);
156
- throw err;
155
+ // Transform error to a more descriptive JSON-RPC error
156
+ if (err.name === "UnknownToolError") {
157
+ throw {
158
+ code: -32601,
159
+ message: `Method not found: ${name}`
160
+ };
161
+ }
162
+ else if (err.name === "ValidationError") {
163
+ throw {
164
+ code: -32602,
165
+ message: `Invalid params for tool ${name}: ${err.message}`
166
+ };
167
+ }
168
+ else {
169
+ // Generic server error
170
+ throw {
171
+ code: -32000,
172
+ message: `Error executing tool ${name}: ${err.message}`
173
+ };
174
+ }
157
175
  }
158
176
  });
159
177
  server.setRequestHandler(ListPromptsRequestSchema, async () => {
@@ -7,15 +7,21 @@
7
7
  * Enhanced implementation for bulk operations that leverages the existing single-operation methods.
8
8
  * This approach reduces code duplication while offering powerful concurrency management.
9
9
  */
10
- import { ClickUpServiceError, ErrorCode } from './base.js';
11
- import { processBatch } from '../../utils/concurrency-utils.js';
12
10
  import { Logger } from '../../logger.js';
11
+ import { processBatch } from '../../utils/concurrency-utils.js';
12
+ import { ClickUpServiceError, ErrorCode } from './base.js';
13
+ import { clickUpServices } from '../shared.js';
14
+ import { findListIDByName } from '../../tools/list.js';
13
15
  // Create logger instance
14
16
  const logger = new Logger('BulkService');
15
17
  /**
16
- * Service for handling bulk operations in ClickUp
18
+ * Service for performing bulk operations in ClickUp
17
19
  */
18
20
  export class BulkService {
21
+ /**
22
+ * Create a new bulk service
23
+ * @param taskService ClickUp Task Service instance
24
+ */
19
25
  constructor(taskService) {
20
26
  this.taskService = taskService;
21
27
  logger.info('BulkService initialized');
@@ -54,6 +60,61 @@ export class BulkService {
54
60
  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 });
55
61
  }
56
62
  }
63
+ /**
64
+ * Find task by name within a specific list
65
+ */
66
+ async findTaskInList(taskName, listName) {
67
+ try {
68
+ // First get the list ID using the global lookup utility
69
+ const listInfo = await findListIDByName(clickUpServices.workspace, listName);
70
+ if (!listInfo) {
71
+ throw new ClickUpServiceError(`List "${listName}" not found`, ErrorCode.NOT_FOUND);
72
+ }
73
+ logger.info(`List "${listName}" resolved to ID: ${listInfo.id}`);
74
+ // Get tasks from the list using the resolved ID
75
+ const taskList = await this.taskService.getTasks(listInfo.id);
76
+ // Find the task by name - first try exact match
77
+ let matchingTask = taskList.find(t => t.name === taskName);
78
+ // If no exact match, try case-insensitive match
79
+ if (!matchingTask) {
80
+ matchingTask = taskList.find(t => t.name.toLowerCase() === taskName.toLowerCase());
81
+ // If still no match, try substring match as a fallback
82
+ if (!matchingTask) {
83
+ matchingTask = taskList.find(t => t.name.toLowerCase().includes(taskName.toLowerCase()) ||
84
+ taskName.toLowerCase().includes(t.name.toLowerCase()));
85
+ }
86
+ }
87
+ if (!matchingTask) {
88
+ throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
89
+ }
90
+ logger.info(`Task "${taskName}" found with ID: ${matchingTask.id}`);
91
+ return matchingTask.id;
92
+ }
93
+ catch (error) {
94
+ // Enhance the error message
95
+ if (error instanceof ClickUpServiceError) {
96
+ throw error;
97
+ }
98
+ throw new ClickUpServiceError(`Error finding task "${taskName}" in list "${listName}": ${error instanceof Error ? error.message : String(error)}`, ErrorCode.UNKNOWN);
99
+ }
100
+ }
101
+ /**
102
+ * Resolve task ID using provided identifiers
103
+ */
104
+ async resolveTaskId(task) {
105
+ const { taskId, taskName, listName, customTaskId } = task;
106
+ if (taskId) {
107
+ return taskId;
108
+ }
109
+ if (customTaskId) {
110
+ const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
111
+ return resolvedTask.id;
112
+ }
113
+ if (taskName && listName) {
114
+ return await this.findTaskInList(taskName, listName);
115
+ }
116
+ throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
117
+ }
57
118
  /**
58
119
  * Update multiple tasks
59
120
  * @param tasks Array of tasks to update with their new data
@@ -63,36 +124,10 @@ export class BulkService {
63
124
  async updateTasks(tasks, options) {
64
125
  logger.info('Starting bulk update operation', { taskCount: tasks.length });
65
126
  try {
66
- // Extract all task IDs that need validation
67
- const taskIds = tasks
68
- .map(task => task.taskId)
69
- .filter((id) => !!id);
70
- // Validate all tasks exist in parallel
71
- if (taskIds.length > 0) {
72
- await this.taskService.validateTasksExist(taskIds);
73
- }
74
- // Process updates in batches
75
127
  return await processBatch(tasks, async (task) => {
76
128
  const { taskId, taskName, listName, customTaskId, ...updateData } = task;
77
- if (taskId) {
78
- return await this.taskService.updateTask(taskId, updateData);
79
- }
80
- else if (customTaskId) {
81
- const resolvedTask = await this.taskService.getTaskByCustomId(customTaskId);
82
- return await this.taskService.updateTask(resolvedTask.id, updateData);
83
- }
84
- else if (taskName && listName) {
85
- // For tasks identified by name, we need to resolve the ID first
86
- const taskList = await this.taskService.getTasks(listName);
87
- const matchingTask = taskList.find(t => t.name === taskName);
88
- if (!matchingTask) {
89
- throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
90
- }
91
- return await this.taskService.updateTask(matchingTask.id, updateData);
92
- }
93
- else {
94
- throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
95
- }
129
+ const resolvedTaskId = await this.resolveTaskId({ taskId, taskName, listName, customTaskId });
130
+ return await this.taskService.updateTask(resolvedTaskId, updateData);
96
131
  }, options);
97
132
  }
98
133
  catch (error) {
@@ -103,44 +138,31 @@ export class BulkService {
103
138
  /**
104
139
  * Move multiple tasks to a different list
105
140
  * @param tasks Array of tasks to move (each with taskId or taskName + listName)
106
- * @param targetListId ID of the destination list
141
+ * @param targetListId ID of the destination list or list name
107
142
  * @param options Optional batch processing settings
108
143
  * @returns Array of moved tasks
109
144
  */
110
145
  async moveTasks(tasks, targetListId, options) {
111
146
  logger.info('Starting bulk move operation', { taskCount: tasks.length, targetListId });
112
147
  try {
113
- // First validate the destination list exists
114
- await this.taskService.validateListExists(targetListId);
115
- // Extract all task IDs that need validation
116
- const taskIds = tasks
117
- .map(task => task.taskId)
118
- .filter((id) => !!id);
119
- // Validate all tasks exist in parallel
120
- if (taskIds.length > 0) {
121
- await this.taskService.validateTasksExist(taskIds);
148
+ // Determine if targetListId is actually an ID or a name
149
+ let resolvedTargetListId = targetListId;
150
+ // If the targetListId doesn't match the pattern of a list ID (usually just numbers),
151
+ // assume it's a list name and try to resolve it
152
+ if (!/^\d+$/.test(targetListId)) {
153
+ logger.info(`Target list appears to be a name: "${targetListId}", attempting to resolve`);
154
+ const listInfo = await findListIDByName(clickUpServices.workspace, targetListId);
155
+ if (!listInfo) {
156
+ throw new ClickUpServiceError(`Target list "${targetListId}" not found`, ErrorCode.NOT_FOUND);
157
+ }
158
+ resolvedTargetListId = listInfo.id;
159
+ logger.info(`Resolved target list to ID: ${resolvedTargetListId}`);
122
160
  }
123
- // Process moves in batches
161
+ // Validate the destination list exists
162
+ await this.taskService.validateListExists(resolvedTargetListId);
124
163
  return await processBatch(tasks, async (task) => {
125
- if (task.taskId) {
126
- return await this.taskService.moveTask(task.taskId, targetListId);
127
- }
128
- else if (task.customTaskId) {
129
- const resolvedTask = await this.taskService.getTaskByCustomId(task.customTaskId);
130
- return await this.taskService.moveTask(resolvedTask.id, targetListId);
131
- }
132
- else if (task.taskName && task.listName) {
133
- // For tasks identified by name, we need to resolve the ID first
134
- const taskList = await this.taskService.getTasks(task.listName);
135
- const matchingTask = taskList.find(t => t.name === task.taskName);
136
- if (!matchingTask) {
137
- throw new ClickUpServiceError(`Task "${task.taskName}" not found in list "${task.listName}"`, ErrorCode.NOT_FOUND);
138
- }
139
- return await this.taskService.moveTask(matchingTask.id, targetListId);
140
- }
141
- else {
142
- throw new ClickUpServiceError('Invalid task identification. Provide either taskId, customTaskId, or both taskName and listName', ErrorCode.INVALID_PARAMETER);
143
- }
164
+ const resolvedTaskId = await this.resolveTaskId(task);
165
+ return await this.taskService.moveTask(resolvedTaskId, resolvedTargetListId);
144
166
  }, options);
145
167
  }
146
168
  catch (error) {
@@ -149,33 +171,22 @@ export class BulkService {
149
171
  }
150
172
  }
151
173
  /**
152
- * Delete multiple tasks efficiently
153
- *
154
- * @param taskIds Array of task IDs to delete
174
+ * Delete multiple tasks
175
+ * @param tasks Array of tasks to delete (each with taskId or taskName + listName)
155
176
  * @param options Batch processing options
156
177
  * @returns Results containing successful and failed deletions
157
178
  */
158
- async deleteTasks(taskIds, options) {
159
- logger.info(`Deleting ${taskIds.length} tasks`, {
160
- batchSize: options?.batchSize,
161
- concurrency: options?.concurrency
162
- });
179
+ async deleteTasks(tasks, options) {
180
+ logger.info('Starting bulk delete operation', { taskCount: tasks.length });
163
181
  try {
164
- return await processBatch(taskIds, async (taskId, index) => {
165
- logger.debug(`Deleting task ${index + 1}/${taskIds.length}`, {
166
- taskId
167
- });
168
- // Reuse the single-task delete method
169
- await this.taskService.deleteTask(taskId);
170
- return taskId; // Return the ID for successful deletions
182
+ return await processBatch(tasks, async (task) => {
183
+ const resolvedTaskId = await this.resolveTaskId(task);
184
+ await this.taskService.deleteTask(resolvedTaskId);
171
185
  }, options);
172
186
  }
173
187
  catch (error) {
174
- logger.error(`Failed to delete tasks in bulk`, {
175
- taskCount: taskIds.length,
176
- error: error instanceof Error ? error.message : String(error)
177
- });
178
- throw new ClickUpServiceError(`Failed to delete tasks in bulk: ${error instanceof Error ? error.message : String(error)}`, error instanceof ClickUpServiceError ? error.code : ErrorCode.UNKNOWN, { taskCount: taskIds.length });
188
+ logger.error('Bulk delete operation failed', error);
189
+ throw error;
179
190
  }
180
191
  }
181
192
  }
@@ -276,7 +276,7 @@ export class TaskServiceCore extends BaseClickUpService {
276
276
  }
277
277
  }
278
278
  /**
279
- * Move a task to a different list
279
+ * Move a task to another list
280
280
  * @param taskId The ID of the task to move
281
281
  * @param destinationListId The ID of the list to move the task to
282
282
  * @returns The updated task
@@ -285,70 +285,32 @@ export class TaskServiceCore extends BaseClickUpService {
285
285
  const startTime = Date.now();
286
286
  this.logOperation('moveTask', { taskId, destinationListId, operation: 'start' });
287
287
  try {
288
- // First, get both the task and list info in parallel
289
- const [task, listResult] = await Promise.all([
288
+ // First, get task and validate destination list
289
+ const [sourceTask, _] = await Promise.all([
290
290
  this.validateTaskExists(taskId),
291
- this.validateListExists(destinationListId).then(() => this.listService.getList(destinationListId))
291
+ this.validateListExists(destinationListId)
292
292
  ]);
293
- const originalTask = task;
294
- const destinationList = listResult;
295
- // Log parallel request timing
296
- const parallelRequestTime = Date.now() - startTime;
297
- this.logOperation('moveTask', {
298
- taskId,
299
- destinationListId,
300
- operation: 'parallel_requests_complete',
301
- timing: { parallelRequestTime }
302
- });
303
- const currentStatus = originalTask.status?.status;
304
- const availableStatuses = destinationList.statuses?.map(s => s.status) || [];
305
- // Enhanced status mapping logic
306
- let newStatus = currentStatus;
307
- if (currentStatus && availableStatuses.length > 0) {
308
- // Only map status if current status isn't available in destination list
309
- if (!availableStatuses.includes(currentStatus)) {
310
- // Try to find a similar status
311
- const similarStatus = availableStatuses.find(s => s.toLowerCase().includes(currentStatus.toLowerCase()) ||
312
- currentStatus.toLowerCase().includes(s.toLowerCase()));
313
- // If no similar status found, use the first available status
314
- newStatus = similarStatus || availableStatuses[0];
315
- this.logger.debug('Status mapping', {
316
- original: currentStatus,
317
- mapped: newStatus,
318
- available: availableStatuses
319
- });
320
- }
321
- }
322
- // Make the move request
323
- const movedTask = await this.makeRequest(async () => {
324
- const response = await this.client.post(`/task/${taskId}`, {
325
- list: destinationListId,
326
- status: newStatus
327
- });
328
- return response.data;
329
- });
330
- // Cache the moved task
331
- this.validationCache.tasks.set(taskId, {
293
+ // Extract task data for creating the new task
294
+ const taskData = this.extractTaskData(sourceTask);
295
+ // Create the task in the new list
296
+ const newTask = await this.createTask(destinationListId, taskData);
297
+ // Delete the original task
298
+ await this.deleteTask(taskId);
299
+ // Update the cache
300
+ this.validationCache.tasks.delete(taskId);
301
+ this.validationCache.tasks.set(newTask.id, {
332
302
  validatedAt: Date.now(),
333
- task: movedTask
303
+ task: newTask
334
304
  });
335
305
  const totalTime = Date.now() - startTime;
336
306
  this.logOperation('moveTask', {
337
307
  taskId,
338
308
  destinationListId,
339
309
  operation: 'complete',
340
- timing: {
341
- totalTime,
342
- parallelRequestTime,
343
- moveOperationTime: totalTime - parallelRequestTime
344
- },
345
- statusMapping: {
346
- original: currentStatus,
347
- new: newStatus,
348
- wasMapped: currentStatus !== newStatus
349
- }
310
+ timing: { totalTime },
311
+ newTaskId: newTask.id
350
312
  });
351
- return movedTask;
313
+ return newTask;
352
314
  }
353
315
  catch (error) {
354
316
  // Log failure
@@ -356,7 +318,7 @@ export class TaskServiceCore extends BaseClickUpService {
356
318
  taskId,
357
319
  destinationListId,
358
320
  operation: 'failed',
359
- error: error.message,
321
+ error: error instanceof Error ? error.message : String(error),
360
322
  timing: { totalTime: Date.now() - startTime }
361
323
  });
362
324
  throw this.handleError(error, 'Failed to move task');
@@ -26,15 +26,11 @@ export const getSpaceTagsTool = {
26
26
  description: `Purpose: Get all tags available in a ClickUp space.
27
27
 
28
28
  Valid Usage:
29
- 1. Provide spaceId (preferred if available)
30
- 2. Provide spaceName (will be resolved to a space ID)
29
+ 1. Provide spaceId (preferred)
30
+ 2. Provide spaceName
31
31
 
32
32
  Requirements:
33
- - EITHER spaceId OR spaceName is REQUIRED
34
-
35
- Notes:
36
- - Tags are defined at the space level in ClickUp
37
- - You need to know the available tags before adding them to tasks`,
33
+ - Space identification: EITHER spaceId OR spaceName REQUIRED`,
38
34
  inputSchema: {
39
35
  type: "object",
40
36
  properties: {
@@ -57,18 +53,15 @@ export const createSpaceTagTool = {
57
53
  description: `Purpose: Create a new tag in a ClickUp space.
58
54
 
59
55
  Valid Usage:
60
- 1. Provide spaceId (preferred if available)
61
- 2. Provide spaceName (will be resolved to a space ID)
56
+ 1. Provide spaceId + tagName (preferred)
57
+ 2. Provide spaceName + tagName
62
58
 
63
59
  Requirements:
64
60
  - tagName: REQUIRED
65
- - EITHER spaceId OR spaceName: REQUIRED
61
+ - Space identification: EITHER spaceId OR spaceName REQUIRED
66
62
 
67
63
  Notes:
68
- - New tag will be available for all tasks in the space
69
- - You can specify background and foreground colors in HEX format (e.g., #FF0000)
70
- - You can also provide a color command (e.g., "blue tag") to automatically generate colors
71
- - After creating a tag, you can add it to tasks using add_tag_to_task`,
64
+ - Specify colors with HEX values or natural language command`,
72
65
  inputSchema: {
73
66
  type: "object",
74
67
  properties: {