@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 +1 -1
- package/build/config.js +2 -3
- package/build/index.js +17 -33
- package/build/logger.js +5 -6
- package/build/server.js +33 -15
- package/build/services/clickup/bulk.js +92 -81
- package/build/services/clickup/task/task-core.js +18 -56
- package/build/tools/tag.js +7 -14
- package/build/tools/task/attachments.js +16 -16
- package/build/tools/task/bulk-operations.js +68 -100
- package/build/tools/task/handlers.js +51 -33
- package/build/tools/task/main.js +125 -31
- package/build/tools/task/single-operations.js +49 -58
- package/build/tools/task/utilities.js +23 -4
- package/build/tools/task/workspace-operations.js +8 -15
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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,
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
//
|
|
161
|
+
// Validate the destination list exists
|
|
162
|
+
await this.taskService.validateListExists(resolvedTargetListId);
|
|
124
163
|
return await processBatch(tasks, async (task) => {
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
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(
|
|
159
|
-
logger.info(
|
|
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(
|
|
165
|
-
|
|
166
|
-
|
|
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(
|
|
175
|
-
|
|
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
|
|
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
|
|
289
|
-
const [
|
|
288
|
+
// First, get task and validate destination list
|
|
289
|
+
const [sourceTask, _] = await Promise.all([
|
|
290
290
|
this.validateTaskExists(taskId),
|
|
291
|
-
this.validateListExists(destinationListId)
|
|
291
|
+
this.validateListExists(destinationListId)
|
|
292
292
|
]);
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
//
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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');
|
package/build/tools/tag.js
CHANGED
|
@@ -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
|
|
30
|
-
2. Provide spaceName
|
|
29
|
+
1. Provide spaceId (preferred)
|
|
30
|
+
2. Provide spaceName
|
|
31
31
|
|
|
32
32
|
Requirements:
|
|
33
|
-
- EITHER spaceId OR spaceName
|
|
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
|
|
61
|
-
2. Provide spaceName
|
|
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
|
|
61
|
+
- Space identification: EITHER spaceId OR spaceName REQUIRED
|
|
66
62
|
|
|
67
63
|
Notes:
|
|
68
|
-
-
|
|
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: {
|