@taazkareem/clickup-mcp-server 0.6.4 → 0.6.6

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.5 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
 
@@ -18,7 +18,7 @@ A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI appl
18
18
 
19
19
  ## Smithery Installation (Quick Start)
20
20
 
21
- [![smithery badge](https://smithery.ai/badge/@TaazKareem/clickup-mcp-server)](https://smithery.ai/server/@TaazKareem/clickup-mcp-server)
21
+ [![smithery badge](https://smithery.ai/badge/@taazkareem/clickup-mcp-server)](https://smithery.ai/server/@TaazKareem/clickup-mcp-server)
22
22
 
23
23
  The server is hosted on [Smithery](https://smithery.ai/server/@TaazKareem/clickup-mcp-server). There, you can preview the available tools or copy the commands to run on your specific client app.
24
24
 
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
- 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";
10
+ import { createTaskTool, updateTaskTool, moveTaskTool, duplicateTaskTool, getTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool, createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool, attachTaskFileTool, getWorkspaceTasksTool, handleCreateTask, handleUpdateTask, handleMoveTask, handleDuplicateTask, 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
@@ -24,6 +24,7 @@ export const server = new Server({
24
24
  capabilities: {
25
25
  tools: {},
26
26
  prompts: {},
27
+ resources: {},
27
28
  },
28
29
  });
29
30
  /**
@@ -39,7 +40,6 @@ export function configureServer() {
39
40
  workspaceHierarchyTool,
40
41
  createTaskTool,
41
42
  getTaskTool,
42
- getTasksTool,
43
43
  updateTaskTool,
44
44
  moveTaskTool,
45
45
  duplicateTaskTool,
@@ -62,14 +62,16 @@ export function configureServer() {
62
62
  updateFolderTool,
63
63
  deleteFolderTool,
64
64
  getSpaceTagsTool,
65
- createSpaceTagTool,
66
- updateSpaceTagTool,
67
- deleteSpaceTagTool,
68
65
  addTagToTaskTool,
69
66
  removeTagFromTaskTool
70
67
  ]
71
68
  };
72
69
  });
70
+ // Add handler for resources/list
71
+ server.setRequestHandler(ListResourcesRequestSchema, async (req) => {
72
+ logger.debug("Received ListResources request");
73
+ return { resources: [] };
74
+ });
73
75
  // Register CallTool handler with proper logging
74
76
  logger.info("Registering tool handlers", {
75
77
  toolCount: 31,
@@ -96,8 +98,6 @@ export function configureServer() {
96
98
  return handleDuplicateTask(params);
97
99
  case "get_task":
98
100
  return handleGetTask(params);
99
- case "get_tasks":
100
- return handleGetTasks(params);
101
101
  case "delete_task":
102
102
  return handleDeleteTask(params);
103
103
  case "get_task_comments":
@@ -136,24 +136,39 @@ export function configureServer() {
136
136
  return handleDeleteFolder(params);
137
137
  case "get_space_tags":
138
138
  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
139
  case "add_tag_to_task":
146
140
  return handleAddTagToTask(params);
147
141
  case "remove_tag_from_task":
148
142
  return handleRemoveTagFromTask(params);
149
143
  default:
150
144
  logger.error(`Unknown tool requested: ${name}`);
151
- throw new Error(`Unknown tool: ${name}`);
145
+ const error = new Error(`Unknown tool: ${name}`);
146
+ error.name = "UnknownToolError";
147
+ throw error;
152
148
  }
153
149
  }
154
150
  catch (err) {
155
151
  logger.error(`Error executing tool: ${name}`, err);
156
- throw err;
152
+ // Transform error to a more descriptive JSON-RPC error
153
+ if (err.name === "UnknownToolError") {
154
+ throw {
155
+ code: -32601,
156
+ message: `Method not found: ${name}`
157
+ };
158
+ }
159
+ else if (err.name === "ValidationError") {
160
+ throw {
161
+ code: -32602,
162
+ message: `Invalid params for tool ${name}: ${err.message}`
163
+ };
164
+ }
165
+ else {
166
+ // Generic server error
167
+ throw {
168
+ code: -32000,
169
+ message: `Error executing tool ${name}: ${err.message}`
170
+ };
171
+ }
157
172
  }
158
173
  });
159
174
  server.setRequestHandler(ListPromptsRequestSchema, async () => {
@@ -40,6 +40,23 @@ export class ClickUpServiceError extends Error {
40
40
  this.context = context;
41
41
  }
42
42
  }
43
+ /**
44
+ * Helper function to safely parse JSON
45
+ * @param data Data to parse
46
+ * @param fallback Optional fallback value if parsing fails
47
+ * @returns Parsed JSON or fallback value
48
+ */
49
+ function safeJsonParse(data, fallback = undefined) {
50
+ if (typeof data !== 'string') {
51
+ return data;
52
+ }
53
+ try {
54
+ return JSON.parse(data);
55
+ }
56
+ catch (error) {
57
+ return fallback;
58
+ }
59
+ }
43
60
  /**
44
61
  * Base ClickUp service class that handles common functionality
45
62
  */
@@ -70,7 +87,20 @@ export class BaseClickUpService {
70
87
  'Authorization': apiKey,
71
88
  'Content-Type': 'application/json'
72
89
  },
73
- timeout: this.timeout
90
+ timeout: this.timeout,
91
+ transformResponse: [
92
+ // Add custom response transformer to handle both JSON and text responses
93
+ (data) => {
94
+ if (!data)
95
+ return data;
96
+ // If it's already an object, return as is
97
+ if (typeof data !== 'string')
98
+ return data;
99
+ // Try to parse as JSON, fall back to raw text if parsing fails
100
+ const parsed = safeJsonParse(data, null);
101
+ return parsed !== null ? parsed : data;
102
+ }
103
+ ]
74
104
  });
75
105
  this.logger.debug(`Initialized ${className}`, { teamId, baseUrl });
76
106
  // Add response interceptor for error handling
@@ -93,80 +123,69 @@ export class BaseClickUpService {
93
123
  path,
94
124
  status,
95
125
  method: error.config?.method?.toUpperCase() || 'UNKNOWN',
96
- requestData: error.config?.data ? JSON.parse(error.config.data) : undefined
126
+ requestData: error.config?.data ? safeJsonParse(error.config.data, error.config.data) : undefined
97
127
  };
98
128
  // Pick the appropriate error code based on status
99
129
  let code;
100
130
  let logMessage;
131
+ let errorMessage;
101
132
  if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
102
133
  code = ErrorCode.NETWORK_ERROR;
103
134
  logMessage = `Request timeout for ${path}`;
135
+ errorMessage = 'Request timed out. Please try again.';
104
136
  }
105
137
  else if (!error.response) {
106
138
  code = ErrorCode.NETWORK_ERROR;
107
139
  logMessage = `Network error accessing ${path}: ${error.message}`;
140
+ errorMessage = 'Network error. Please check your connection and try again.';
108
141
  }
109
142
  else if (status === 429) {
110
143
  code = ErrorCode.RATE_LIMIT;
111
144
  this.handleRateLimitHeaders(error.response.headers);
145
+ // Calculate time until reset
146
+ const reset = error.response.headers['x-ratelimit-reset'];
147
+ const now = Date.now() / 1000; // Convert to seconds
148
+ const timeToReset = Math.max(0, reset - now);
149
+ const resetMinutes = Math.ceil(timeToReset / 60);
112
150
  logMessage = `Rate limit exceeded for ${path}`;
151
+ errorMessage = `Rate limit exceeded. Please wait ${resetMinutes} minute${resetMinutes === 1 ? '' : 's'} before trying again.`;
152
+ // Add more context to the error
153
+ errorContext.rateLimitInfo = {
154
+ limit: error.response.headers['x-ratelimit-limit'],
155
+ remaining: error.response.headers['x-ratelimit-remaining'],
156
+ reset: reset,
157
+ timeToReset: timeToReset
158
+ };
113
159
  }
114
160
  else if (status === 401 || status === 403) {
115
161
  code = ErrorCode.UNAUTHORIZED;
116
162
  logMessage = `Authorization failed for ${path}`;
163
+ errorMessage = 'Authorization failed. Please check your API key.';
117
164
  }
118
165
  else if (status === 404) {
119
166
  code = ErrorCode.NOT_FOUND;
120
167
  logMessage = `Resource not found: ${path}`;
168
+ errorMessage = 'Resource not found.';
121
169
  }
122
170
  else if (status >= 400 && status < 500) {
123
171
  code = ErrorCode.VALIDATION;
124
172
  logMessage = `Validation error for ${path}: ${errorMsg}`;
173
+ errorMessage = errorMsg;
125
174
  }
126
175
  else if (status >= 500) {
127
176
  code = ErrorCode.SERVER_ERROR;
128
177
  logMessage = `ClickUp server error: ${errorMsg}`;
178
+ errorMessage = 'ClickUp server error. Please try again later.';
129
179
  }
130
180
  else {
131
181
  code = ErrorCode.UNKNOWN;
132
182
  logMessage = `Unknown API error: ${errorMsg}`;
183
+ errorMessage = 'An unexpected error occurred. Please try again.';
133
184
  }
134
185
  // Log the error with context
135
186
  this.logger.error(logMessage, errorContext);
136
- // Throw a well-structured error
137
- throw new ClickUpServiceError(errorMsg, code, responseData, status, errorContext);
138
- }
139
- /**
140
- * Process the request queue, respecting rate limits by spacing out requests
141
- * @private
142
- */
143
- async processQueue() {
144
- if (this.requestQueue.length === 0) {
145
- this.logger.debug('Queue empty, exiting queue processing mode');
146
- this.processingQueue = false;
147
- return;
148
- }
149
- this.logger.debug(`Processing request queue (${this.requestQueue.length} items)`);
150
- const startTime = Date.now();
151
- try {
152
- // Take the first request from the queue
153
- const request = this.requestQueue.shift();
154
- if (request) {
155
- // Wait for the request spacing interval
156
- await new Promise(resolve => setTimeout(resolve, this.requestSpacing));
157
- // Run the request
158
- await request();
159
- }
160
- }
161
- catch (error) {
162
- this.logger.error('Error executing queued request', error);
163
- }
164
- finally {
165
- const duration = Date.now() - startTime;
166
- this.logger.trace(`Queue item processed in ${duration}ms, ${this.requestQueue.length} items remaining`);
167
- // Continue processing the queue after a short delay
168
- setTimeout(() => this.processQueue(), this.requestSpacing);
169
- }
187
+ // Throw a formatted error with user-friendly message
188
+ throw new ClickUpServiceError(errorMessage, code, error);
170
189
  }
171
190
  /**
172
191
  * Handle rate limit headers from ClickUp API
@@ -192,14 +211,44 @@ export class BaseClickUpService {
192
211
  const now = Date.now();
193
212
  const resetTime = reset * 1000; // convert to milliseconds
194
213
  const timeToReset = Math.max(0, resetTime - now);
195
- if (timeToReset > 0 && remaining > 0) {
196
- // Calculate time between requests to stay under limit
197
- // Add 10% buffer to be safe
198
- const safeSpacing = Math.ceil((timeToReset / remaining) * 1.1);
214
+ // Proactively adjust spacing when remaining requests get low
215
+ // This helps avoid hitting rate limits in the first place
216
+ if (remaining < limit * 0.3) {
217
+ // More aggressive spacing when close to limit
218
+ let safeSpacing;
219
+ if (remaining <= 5) {
220
+ // Very aggressive spacing for last few requests
221
+ safeSpacing = Math.ceil((timeToReset / remaining) * 2);
222
+ // Start processing in queue mode preemptively
223
+ if (!this.processingQueue) {
224
+ this.logger.info('Preemptively switching to queue mode (low remaining requests)', {
225
+ remaining,
226
+ limit
227
+ });
228
+ this.processingQueue = true;
229
+ this.processQueue().catch(err => {
230
+ this.logger.error('Error processing request queue', err);
231
+ });
232
+ }
233
+ }
234
+ else if (remaining <= 20) {
235
+ // More aggressive spacing
236
+ safeSpacing = Math.ceil((timeToReset / remaining) * 1.5);
237
+ }
238
+ else {
239
+ // Standard safe spacing with buffer
240
+ safeSpacing = Math.ceil((timeToReset / remaining) * 1.1);
241
+ }
242
+ // Apply updated spacing, but with a reasonable maximum
243
+ const maxSpacing = 5000; // 5 seconds max spacing
244
+ const adjustedSpacing = Math.min(safeSpacing, maxSpacing);
199
245
  // Only adjust if it's greater than our current spacing
200
- if (safeSpacing > this.requestSpacing) {
201
- this.logger.debug(`Adjusting request spacing: ${this.requestSpacing}ms → ${safeSpacing}ms`);
202
- this.requestSpacing = safeSpacing;
246
+ if (adjustedSpacing > this.requestSpacing) {
247
+ this.logger.debug(`Adjusting request spacing: ${this.requestSpacing}ms → ${adjustedSpacing}ms`, {
248
+ remaining,
249
+ timeToReset
250
+ });
251
+ this.requestSpacing = adjustedSpacing;
203
252
  }
204
253
  }
205
254
  }
@@ -208,6 +257,55 @@ export class BaseClickUpService {
208
257
  this.logger.warn('Failed to parse rate limit headers', error);
209
258
  }
210
259
  }
260
+ /**
261
+ * Process the request queue, respecting rate limits by spacing out requests
262
+ * @private
263
+ */
264
+ async processQueue() {
265
+ if (this.requestQueue.length === 0) {
266
+ this.logger.debug('Queue empty, exiting queue processing mode');
267
+ this.processingQueue = false;
268
+ return;
269
+ }
270
+ const queueLength = this.requestQueue.length;
271
+ this.logger.debug(`Processing request queue (${queueLength} items)`);
272
+ const startTime = Date.now();
273
+ try {
274
+ // Take the first request from the queue
275
+ const request = this.requestQueue.shift();
276
+ if (request) {
277
+ // Adjust delay based on queue size
278
+ // Longer delays for bigger queues to prevent overwhelming the API
279
+ let delay = this.requestSpacing;
280
+ if (queueLength > 20) {
281
+ delay = this.requestSpacing * 2;
282
+ }
283
+ else if (queueLength > 10) {
284
+ delay = this.requestSpacing * 1.5;
285
+ }
286
+ // Wait for the calculated delay
287
+ await new Promise(resolve => setTimeout(resolve, delay));
288
+ // Run the request
289
+ await request();
290
+ }
291
+ }
292
+ catch (error) {
293
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
294
+ // If we still hit rate limits, increase the spacing
295
+ this.requestSpacing = Math.min(this.requestSpacing * 1.5, 10000); // Max 10s
296
+ this.logger.warn(`Rate limit hit during queue processing, increasing delay to ${this.requestSpacing}ms`);
297
+ }
298
+ else {
299
+ this.logger.error('Error executing queued request', error);
300
+ }
301
+ }
302
+ finally {
303
+ const duration = Date.now() - startTime;
304
+ this.logger.trace(`Queue item processed in ${duration}ms, ${this.requestQueue.length} items remaining`);
305
+ // Continue processing the queue after the calculated delay
306
+ setTimeout(() => this.processQueue(), this.requestSpacing);
307
+ }
308
+ }
211
309
  /**
212
310
  * Makes an API request with rate limiting.
213
311
  * @protected
@@ -217,7 +315,13 @@ export class BaseClickUpService {
217
315
  async makeRequest(fn) {
218
316
  // If we're being rate limited, queue the request rather than executing immediately
219
317
  if (this.processingQueue) {
220
- this.logger.debug('Queue active, adding request to queue');
318
+ const queuePosition = this.requestQueue.length + 1;
319
+ const estimatedWaitTime = Math.ceil((queuePosition * this.requestSpacing) / 1000);
320
+ this.logger.info('Request queued due to rate limiting', {
321
+ queuePosition,
322
+ estimatedWaitSeconds: estimatedWaitTime,
323
+ currentSpacing: this.requestSpacing
324
+ });
221
325
  return new Promise((resolve, reject) => {
222
326
  this.requestQueue.push(async () => {
223
327
  try {
@@ -225,7 +329,14 @@ export class BaseClickUpService {
225
329
  resolve(result);
226
330
  }
227
331
  catch (error) {
228
- reject(error);
332
+ // Enhance error message with queue context if it's a rate limit error
333
+ if (error instanceof ClickUpServiceError && error.code === ErrorCode.RATE_LIMIT) {
334
+ const enhancedError = new ClickUpServiceError(`${error.message} (Request was queued at position ${queuePosition})`, error.code, error.data);
335
+ reject(enhancedError);
336
+ }
337
+ else {
338
+ reject(error);
339
+ }
229
340
  }
230
341
  });
231
342
  });
@@ -65,30 +65,18 @@ export class BulkService {
65
65
  */
66
66
  async findTaskInList(taskName, listName) {
67
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) {
68
+ const result = await this.taskService.findTasks({
69
+ taskName,
70
+ listName,
71
+ allowMultipleMatches: false,
72
+ useSmartDisambiguation: true,
73
+ includeFullDetails: false
74
+ });
75
+ if (!result || Array.isArray(result)) {
88
76
  throw new ClickUpServiceError(`Task "${taskName}" not found in list "${listName}"`, ErrorCode.NOT_FOUND);
89
77
  }
90
- logger.info(`Task "${taskName}" found with ID: ${matchingTask.id}`);
91
- return matchingTask.id;
78
+ logger.info(`Task "${taskName}" found with ID: ${result.id}`);
79
+ return result.id;
92
80
  }
93
81
  catch (error) {
94
82
  // Enhance the error message
@@ -135,16 +135,65 @@ export class ClickUpTagService extends BaseClickUpService {
135
135
  * Add a tag to a task
136
136
  * @param taskId - ID of the task
137
137
  * @param tagName - Name of the tag to add
138
- * @returns Promise with success status
138
+ * @returns Promise with success status and tag data
139
139
  */
140
140
  async addTagToTask(taskId, tagName) {
141
141
  try {
142
142
  this.logger.debug(`Adding tag "${tagName}" to task: ${taskId}`);
143
+ // First get the task to get its space ID
144
+ const taskResponse = await this.client.get(`/task/${taskId}`);
145
+ if (!taskResponse.data?.space?.id) {
146
+ return {
147
+ success: false,
148
+ error: {
149
+ message: 'Could not determine space ID from task',
150
+ code: 'SPACE_NOT_FOUND'
151
+ }
152
+ };
153
+ }
154
+ // Get space tags to verify tag exists
155
+ const spaceId = taskResponse.data.space.id;
156
+ const spaceTags = await this.getSpaceTags(spaceId);
157
+ if (!spaceTags.success || !spaceTags.data) {
158
+ return {
159
+ success: false,
160
+ error: {
161
+ message: 'Failed to verify tag existence in space',
162
+ code: 'TAG_VERIFICATION_FAILED',
163
+ details: spaceTags.error
164
+ }
165
+ };
166
+ }
167
+ // Check if tag exists
168
+ const tagExists = spaceTags.data.some(tag => tag.name === tagName);
169
+ if (!tagExists) {
170
+ return {
171
+ success: false,
172
+ error: {
173
+ message: `Tag "${tagName}" does not exist in the space`,
174
+ code: 'TAG_NOT_FOUND'
175
+ }
176
+ };
177
+ }
143
178
  // Encode the tag name in the URL
144
179
  const encodedTagName = encodeURIComponent(tagName);
180
+ // Add the tag
145
181
  await this.client.post(`/task/${taskId}/tag/${encodedTagName}`, {});
182
+ // Verify the tag was added by getting the task again
183
+ const verifyResponse = await this.client.get(`/task/${taskId}`);
184
+ const tagAdded = verifyResponse.data?.tags?.some(tag => tag.name === tagName) ?? false;
185
+ if (!tagAdded) {
186
+ return {
187
+ success: false,
188
+ error: {
189
+ message: 'Tag addition failed verification',
190
+ code: 'TAG_VERIFICATION_FAILED'
191
+ }
192
+ };
193
+ }
146
194
  return {
147
- success: true
195
+ success: true,
196
+ data: { tagAdded: true }
148
197
  };
149
198
  }
150
199
  catch (error) {