@taazkareem/clickup-mcp-server 0.6.5 → 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.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
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/server.js CHANGED
@@ -7,7 +7,7 @@
7
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
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
13
  import { getSpaceTagsTool, handleGetSpaceTags, addTagToTaskTool, handleAddTagToTask, removeTagFromTaskTool, handleRemoveTagFromTask } from "./tools/tag.js";
@@ -19,7 +19,7 @@ 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.5",
22
+ version: "0.6.4",
23
23
  }, {
24
24
  capabilities: {
25
25
  tools: {},
@@ -40,7 +40,6 @@ export function configureServer() {
40
40
  workspaceHierarchyTool,
41
41
  createTaskTool,
42
42
  getTaskTool,
43
- getTasksTool,
44
43
  updateTaskTool,
45
44
  moveTaskTool,
46
45
  duplicateTaskTool,
@@ -75,7 +74,7 @@ export function configureServer() {
75
74
  });
76
75
  // Register CallTool handler with proper logging
77
76
  logger.info("Registering tool handlers", {
78
- toolCount: 28,
77
+ toolCount: 31,
79
78
  categories: ["workspace", "task", "list", "folder", "tag"]
80
79
  });
81
80
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -99,8 +98,6 @@ export function configureServer() {
99
98
  return handleDuplicateTask(params);
100
99
  case "get_task":
101
100
  return handleGetTask(params);
102
- case "get_tasks":
103
- return handleGetTasks(params);
104
101
  case "delete_task":
105
102
  return handleDeleteTask(params);
106
103
  case "get_task_comments":
@@ -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) {
@@ -23,6 +23,8 @@ export class TaskServiceCore extends BaseClickUpService {
23
23
  tasks: new Map(),
24
24
  lists: new Map()
25
25
  };
26
+ // Cache for task name to ID mapping
27
+ this.nameToIdCache = new Map();
26
28
  // Cache TTL in milliseconds (5 minutes)
27
29
  this.CACHE_TTL = 5 * 60 * 1000;
28
30
  if (workspaceService) {
@@ -155,24 +157,40 @@ export class TaskServiceCore extends BaseClickUpService {
155
157
  try {
156
158
  return await this.makeRequest(async () => {
157
159
  const response = await this.client.post(`/list/${listId}/task`, taskData);
158
- return response.data;
160
+ // Handle both JSON and text responses
161
+ const data = response.data;
162
+ if (typeof data === 'string') {
163
+ // If we got a text response, try to extract task ID from common patterns
164
+ const idMatch = data.match(/task.*?(\w{9})/i);
165
+ if (idMatch) {
166
+ // If we found an ID, fetch the full task details
167
+ return await this.getTask(idMatch[1]);
168
+ }
169
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
170
+ }
171
+ return data;
159
172
  });
160
173
  }
161
174
  catch (error) {
162
- throw this.handleError(error, `Failed to create task in list ${listId}`);
175
+ throw this.handleError(error, 'Failed to create task');
163
176
  }
164
177
  }
165
178
  /**
166
- * Get a specific task by ID
179
+ * Get a task by its ID
167
180
  * @param taskId The ID of the task to retrieve
168
- * @returns The task details
181
+ * @returns The task
169
182
  */
170
183
  async getTask(taskId) {
171
184
  this.logOperation('getTask', { taskId });
172
185
  try {
173
186
  return await this.makeRequest(async () => {
174
187
  const response = await this.client.get(`/task/${taskId}`);
175
- return response.data;
188
+ // Handle both JSON and text responses
189
+ const data = response.data;
190
+ if (typeof data === 'string') {
191
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
192
+ }
193
+ return data;
176
194
  });
177
195
  }
178
196
  catch (error) {
@@ -180,22 +198,27 @@ export class TaskServiceCore extends BaseClickUpService {
180
198
  }
181
199
  }
182
200
  /**
183
- * Get all tasks in a list with optional filtering
201
+ * Get all tasks in a list
184
202
  * @param listId The ID of the list to get tasks from
185
203
  * @param filters Optional filters to apply
186
- * @returns List of tasks matching the filters
204
+ * @returns Array of tasks
187
205
  */
188
206
  async getTasks(listId, filters = {}) {
189
207
  this.logOperation('getTasks', { listId, filters });
190
208
  try {
191
209
  return await this.makeRequest(async () => {
192
210
  const params = this.buildTaskFilterParams(filters);
193
- const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
194
- return response.data.tasks;
211
+ const response = await this.client.get(`/list/${listId}/task`, { params });
212
+ // Handle both JSON and text responses
213
+ const data = response.data;
214
+ if (typeof data === 'string') {
215
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
216
+ }
217
+ return Array.isArray(data) ? data : data.tasks || [];
195
218
  });
196
219
  }
197
220
  catch (error) {
198
- throw this.handleError(error, `Failed to get tasks from list ${listId}`);
221
+ throw this.handleError(error, `Failed to get tasks for list ${listId}`);
199
222
  }
200
223
  }
201
224
  /**
@@ -247,7 +270,18 @@ export class TaskServiceCore extends BaseClickUpService {
247
270
  try {
248
271
  return await this.makeRequest(async () => {
249
272
  const response = await this.client.put(`/task/${taskId}`, updateData);
250
- return response.data;
273
+ // Handle both JSON and text responses
274
+ const data = response.data;
275
+ if (typeof data === 'string') {
276
+ // If we got a text response, try to extract task ID from common patterns
277
+ const idMatch = data.match(/task.*?(\w{9})/i);
278
+ if (idMatch) {
279
+ // If we found an ID, fetch the full task details
280
+ return await this.getTask(idMatch[1]);
281
+ }
282
+ throw new ClickUpServiceError('Received unexpected text response from API', ErrorCode.UNKNOWN, data);
283
+ }
284
+ return data;
251
285
  });
252
286
  }
253
287
  catch (error) {
@@ -436,4 +470,35 @@ export class TaskServiceCore extends BaseClickUpService {
436
470
  throw error;
437
471
  }
438
472
  }
473
+ /**
474
+ * Try to get a task ID from the name cache
475
+ * @param taskName The name of the task
476
+ * @param listId Optional list ID for context
477
+ * @returns The cached task ID if found and not expired, otherwise null
478
+ */
479
+ getCachedTaskId(taskName, listId) {
480
+ const cached = this.nameToIdCache.get(taskName);
481
+ if (cached && Date.now() - cached.validatedAt < this.CACHE_TTL) {
482
+ // If listId is provided, ensure it matches the cached context
483
+ if (!listId || cached.listId === listId) {
484
+ this.logger.debug('Using cached task ID for name', { taskName, cachedId: cached.taskId });
485
+ return cached.taskId;
486
+ }
487
+ }
488
+ return null;
489
+ }
490
+ /**
491
+ * Cache a task name to ID mapping
492
+ * @param taskName The name of the task
493
+ * @param taskId The ID of the task
494
+ * @param listId Optional list ID for context
495
+ */
496
+ cacheTaskNameToId(taskName, taskId, listId) {
497
+ this.nameToIdCache.set(taskName, {
498
+ taskId,
499
+ validatedAt: Date.now(),
500
+ listId
501
+ });
502
+ this.logger.debug('Cached task name to ID mapping', { taskName, taskId, listId });
503
+ }
439
504
  }