@taazkareem/clickup-mcp-server 0.6.2 → 0.6.4

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.
Files changed (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +111 -69
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +439 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +49 -20
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +102 -18
  21. package/build/tools/task/handlers.js +216 -53
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +161 -32
  24. package/build/tools/task/single-operations.js +82 -17
  25. package/build/tools/task/utilities.js +47 -75
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
@@ -104,8 +104,11 @@ export class WorkspaceService extends BaseClickUpService {
104
104
  try {
105
105
  // If we have the hierarchy in memory and not forcing refresh, return it
106
106
  if (this.workspaceHierarchy && !forceRefresh) {
107
+ this.logger.debug('Returning cached workspace hierarchy');
107
108
  return this.workspaceHierarchy;
108
109
  }
110
+ const startTime = Date.now();
111
+ this.logger.info('Starting workspace hierarchy fetch');
109
112
  // Start building the workspace tree
110
113
  const workspaceTree = {
111
114
  root: {
@@ -115,51 +118,93 @@ export class WorkspaceService extends BaseClickUpService {
115
118
  }
116
119
  };
117
120
  // Get all spaces
121
+ const spacesStartTime = Date.now();
118
122
  const spaces = await this.getSpaces();
119
- // Process each space
120
- for (const space of spaces) {
121
- const spaceNode = {
122
- id: space.id,
123
- name: space.name,
124
- type: 'space',
125
- children: []
126
- };
127
- // Get folders for the space
128
- const folders = await this.getFoldersInSpace(space.id);
129
- for (const folder of folders) {
130
- const folderNode = {
131
- id: folder.id,
132
- name: folder.name,
133
- type: 'folder',
134
- parentId: space.id,
123
+ const spacesTime = Date.now() - spacesStartTime;
124
+ this.logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
125
+ // Process spaces in batches to respect rate limits
126
+ const batchSize = 3; // Process 3 spaces at a time
127
+ const spaceNodes = [];
128
+ let totalFolders = 0;
129
+ let totalLists = 0;
130
+ for (let i = 0; i < spaces.length; i += batchSize) {
131
+ const batchStartTime = Date.now();
132
+ const spaceBatch = spaces.slice(i, i + batchSize);
133
+ this.logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
134
+ const batchNodes = await Promise.all(spaceBatch.map(async (space) => {
135
+ const spaceStartTime = Date.now();
136
+ const spaceNode = {
137
+ id: space.id,
138
+ name: space.name,
139
+ type: 'space',
135
140
  children: []
136
141
  };
137
- // Get lists in the folder
138
- const listsInFolder = await this.getListsInFolder(folder.id);
139
- for (const list of listsInFolder) {
140
- folderNode.children?.push({
141
- id: list.id,
142
- name: list.name,
143
- type: 'list',
144
- parentId: folder.id
145
- });
142
+ // Fetch initial space data
143
+ const [folders, listsInSpace] = await Promise.all([
144
+ this.getFoldersInSpace(space.id),
145
+ this.getListsInSpace(space.id)
146
+ ]);
147
+ totalFolders += folders.length;
148
+ totalLists += listsInSpace.length;
149
+ // Process folders in smaller batches
150
+ const folderBatchSize = 5; // Process 5 folders at a time
151
+ const folderNodes = [];
152
+ for (let j = 0; j < folders.length; j += folderBatchSize) {
153
+ const folderBatchStartTime = Date.now();
154
+ const folderBatch = folders.slice(j, j + folderBatchSize);
155
+ const batchFolderNodes = await Promise.all(folderBatch.map(async (folder) => {
156
+ const folderNode = {
157
+ id: folder.id,
158
+ name: folder.name,
159
+ type: 'folder',
160
+ parentId: space.id,
161
+ children: []
162
+ };
163
+ // Get lists in the folder
164
+ const listsInFolder = await this.getListsInFolder(folder.id);
165
+ totalLists += listsInFolder.length;
166
+ folderNode.children = listsInFolder.map(list => ({
167
+ id: list.id,
168
+ name: list.name,
169
+ type: 'list',
170
+ parentId: folder.id
171
+ }));
172
+ return folderNode;
173
+ }));
174
+ folderNodes.push(...batchFolderNodes);
175
+ const folderBatchTime = Date.now() - folderBatchStartTime;
176
+ this.logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
146
177
  }
147
- spaceNode.children?.push(folderNode);
148
- }
149
- // Get lists directly in the space (not in any folder)
150
- const listsInSpace = await this.getListsInSpace(space.id);
151
- this.logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name} (${space.id})`);
152
- for (const list of listsInSpace) {
153
- this.logger.debug(`Adding list directly to space: ${list.name} (${list.id})`);
154
- spaceNode.children?.push({
178
+ // Add folder nodes to space
179
+ spaceNode.children?.push(...folderNodes);
180
+ // Add folderless lists to space
181
+ this.logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
182
+ const listNodes = listsInSpace.map(list => ({
155
183
  id: list.id,
156
184
  name: list.name,
157
185
  type: 'list',
158
186
  parentId: space.id
159
- });
160
- }
161
- workspaceTree.root.children.push(spaceNode);
187
+ }));
188
+ spaceNode.children?.push(...listNodes);
189
+ const spaceTime = Date.now() - spaceStartTime;
190
+ this.logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
191
+ return spaceNode;
192
+ }));
193
+ spaceNodes.push(...batchNodes);
194
+ const batchTime = Date.now() - batchStartTime;
195
+ this.logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
162
196
  }
197
+ // Add all space nodes to the workspace tree
198
+ workspaceTree.root.children.push(...spaceNodes);
199
+ const totalTime = Date.now() - startTime;
200
+ this.logger.info('Workspace hierarchy fetch completed', {
201
+ duration: totalTime,
202
+ spaces: spaces.length,
203
+ folders: totalFolders,
204
+ lists: totalLists,
205
+ averageTimePerSpace: totalTime / spaces.length,
206
+ averageTimePerNode: totalTime / (spaces.length + totalFolders + totalLists)
207
+ });
163
208
  // Store the hierarchy for later use
164
209
  this.workspaceHierarchy = workspaceTree;
165
210
  return workspaceTree;
@@ -156,7 +156,7 @@ Requirements:
156
156
  - EITHER folderId OR (folderName + space information) is REQUIRED
157
157
  - When using folderName, you MUST provide EITHER spaceId OR spaceName
158
158
 
159
- ⚠️ CRITICAL WARNING:
159
+ Warning:
160
160
  - This action CANNOT be undone
161
161
  - All lists and tasks within the folder will also be permanently deleted
162
162
  - Using folderName is risky as names may not be unique across different spaces`,
@@ -8,11 +8,9 @@
8
8
  * retrieving, and deleting lists. It supports creating lists both in spaces
9
9
  * and in folders.
10
10
  */
11
- import { clickUpServices } from '../services/shared.js';
11
+ import { listService, workspaceService } from '../services/shared.js';
12
12
  import config from '../config.js';
13
13
  import { sponsorService } from '../utils/sponsor-service.js';
14
- // Use shared services instance
15
- const { list: listService, workspace: workspaceService } = clickUpServices;
16
14
  /**
17
15
  * Tool definition for creating a list directly in a space
18
16
  */
@@ -216,7 +214,7 @@ Valid Usage:
216
214
  Requirements:
217
215
  - EITHER listId OR listName: REQUIRED
218
216
 
219
- ⚠️ CRITICAL WARNING:
217
+ Warning:
220
218
  - This action CANNOT be undone
221
219
  - All tasks within the list will also be permanently deleted
222
220
  - Using listName is risky as names may not be unique`,
@@ -7,11 +7,12 @@
7
7
  * This module implements a tool for attaching files to ClickUp tasks
8
8
  * with automatic method selection based on file source and size.
9
9
  */
10
- import { taskService } from '../../services/shared.js';
11
- import { validateTaskIdentification, resolveTaskIdWithValidation } from './utilities.js';
10
+ import { clickUpServices } from '../../services/shared.js';
11
+ import { validateTaskIdentification } from './utilities.js';
12
12
  import { sponsorService } from '../../utils/sponsor-service.js';
13
+ // Use shared services instance
14
+ const { task: taskService } = clickUpServices;
13
15
  // Session storage for chunked uploads (in-memory for demonstration)
14
- // In production, this should use a more persistent store
15
16
  const chunkSessions = new Map();
16
17
  // Clean up expired sessions periodically
17
18
  setInterval(() => {
@@ -32,22 +33,39 @@ export const attachTaskFileTool = {
32
33
  description: `Purpose: Attaches a file to a ClickUp task.
33
34
 
34
35
  Valid Usage:
36
+ 1. Use taskId alone (preferred) - works with both regular and custom IDs
37
+ 2. Use taskName alone (will search across all lists)
38
+ 3. Use taskName + listName (for faster, targeted search)
39
+
40
+ File Source Options:
35
41
  1. Upload from base64: Provide file_data + file_name
36
- 2. Upload from URL or local file: Provide file_url + optional file_name
37
- - For web URLs: Use http:// or https:// URLs
38
- - For local files: Use absolute file paths (starting with / or drive letter)
39
- 3. For large files, advanced options are available via chunk_* parameters
42
+ 2. Upload from URL: Provide file_url starting with http:// or https://
43
+ 3. Upload from local file: Provide file_url as absolute path (starting with / or drive letter)
44
+ 4. For large files: Use chunk_* parameters for advanced chunked uploading
40
45
 
41
46
  Requirements:
42
- - EITHER taskId OR (taskName + listName) is REQUIRED
43
- - EITHER file_data OR file_url is REQUIRED
47
+ - EITHER taskId OR taskName: REQUIRED
48
+ - listName: Optional, but recommended when using taskName
49
+ - File Source: ONE of the following is REQUIRED:
50
+ - file_data + file_name
51
+ - file_url (web URL or local path)
52
+ - chunk_session (for continuing chunked upload)
44
53
 
45
54
  Notes:
46
- - The system automatically selects the best upload method based on file size and source
47
- - Base64 method has a 10MB size limit due to encoding overhead (file_data parameter)
48
- - URL method works for files hosted online (file_url parameter with http/https)
49
- - Local file method works with absolute paths only (file_url parameter with / or drive letter)
50
- - For large files, the system may use chunked uploading automatically`,
55
+ - The tool automatically searches for tasks using smart name matching
56
+ - When only taskName is provided, it searches across all lists
57
+ - Adding listName narrows the search to a specific list for better performance
58
+ - The system automatically selects the best upload method based on file size and source:
59
+ - Base64 method: Limited to 10MB due to encoding overhead
60
+ - URL method: Works for files hosted online
61
+ - Local file method: Works with absolute paths only
62
+ - Large files: Automatically uses chunked uploading
63
+
64
+ Warning:
65
+ - Using taskName without listName may match multiple tasks
66
+ - If multiple matches are found, the operation will fail with a disambiguation error
67
+ - For local files, relative paths are not supported
68
+ - Base64 uploads over 10MB will automatically switch to chunked upload mode`,
51
69
  inputSchema: {
52
70
  type: "object",
53
71
  properties: {
@@ -57,11 +75,11 @@ Notes:
57
75
  },
58
76
  taskName: {
59
77
  type: "string",
60
- description: "Name of the task to attach the file to. When using this parameter, you MUST also provide listName."
78
+ description: "Name of the task to attach the file to. The tool will search for tasks with this name across all lists unless listName is specified."
61
79
  },
62
80
  listName: {
63
81
  type: "string",
64
- description: "Name of the list containing the task. REQUIRED when using taskName."
82
+ description: "Optional: Name of list containing the task. Providing this narrows the search to a specific list, improving performance and reducing ambiguity."
65
83
  },
66
84
  file_name: {
67
85
  type: "string",
@@ -96,8 +114,7 @@ Notes:
96
114
  type: "boolean",
97
115
  description: "Optional: For advanced usage with large file chunking. Whether this is the final chunk."
98
116
  }
99
- },
100
- required: [] // Will validate based on context in the handler
117
+ }
101
118
  }
102
119
  };
103
120
  /**
@@ -113,7 +130,18 @@ async function attachTaskFileHandler(params) {
113
130
  throw new Error("Either file_data, file_url, or session_id must be provided");
114
131
  }
115
132
  // Resolve task ID
116
- const resolvedTaskId = await resolveTaskIdWithValidation(taskId, taskName, listName);
133
+ const result = await taskService.findTasks({
134
+ taskId,
135
+ taskName,
136
+ listName,
137
+ allowMultipleMatches: false,
138
+ useSmartDisambiguation: true,
139
+ includeFullDetails: false
140
+ });
141
+ if (!result || Array.isArray(result)) {
142
+ throw new Error("Task not found");
143
+ }
144
+ const resolvedTaskId = result.id;
117
145
  try {
118
146
  // CASE 1: Chunked upload continuation
119
147
  if (session_id) {
@@ -254,7 +282,8 @@ async function handleChunkUpload(taskId, sessionToken, chunkIndex, chunkData, is
254
282
  // Sort chunks by index
255
283
  const sortedChunks = Array.from(session.chunks.entries())
256
284
  .sort((a, b) => a[0] - b[0]);
257
- for (const [index, chunk] of sortedChunks) {
285
+ for (const entry of sortedChunks) {
286
+ const [index, chunk] = entry;
258
287
  chunk.copy(fileData, offset);
259
288
  offset += chunk.length;
260
289
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp MCP Task Attachment Types
6
+ *
7
+ * This module defines types for file attachment operations.
8
+ */
9
+ export {};
@@ -76,28 +76,21 @@ export const createBulkTasksTool = {
76
76
  description: `Purpose: Create multiple tasks in a list efficiently.
77
77
 
78
78
  Valid Usage:
79
- 1. An array of tasks with required properties + listId (preferred)
80
- 2. An array of tasks with required properties + listName
79
+ 1. Provide listId + array of tasks (preferred)
80
+ 2. Provide listName + array of tasks
81
81
 
82
82
  Requirements:
83
83
  - tasks: REQUIRED (array of tasks, each with at least a name)
84
84
  - EITHER listId OR listName: REQUIRED
85
+ - All tasks will be created in the specified list
85
86
 
86
87
  Notes:
87
88
  - Configure batch size and concurrency via options for performance
88
89
  - Each task should have a name with emoji prefix
89
- - All tasks will be created in the same list`,
90
+ - Custom fields can be set for each task using the custom_fields property (array of {id, value} objects)`,
90
91
  inputSchema: {
91
92
  type: "object",
92
93
  properties: {
93
- listId: {
94
- type: "string",
95
- description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
96
- },
97
- listName: {
98
- type: "string",
99
- description: "Name of list for new tasks. Only use if you don't have listId."
100
- },
101
94
  tasks: {
102
95
  type: "array",
103
96
  description: "Array of tasks to create. Each task must have at least a name.",
@@ -134,18 +127,43 @@ Notes:
134
127
  type: "string"
135
128
  },
136
129
  description: "Optional array of tag names to assign to the task. The tags must already exist in the space."
130
+ },
131
+ custom_fields: {
132
+ type: "array",
133
+ items: {
134
+ type: "object",
135
+ properties: {
136
+ id: {
137
+ type: "string",
138
+ description: "ID of the custom field"
139
+ },
140
+ value: {
141
+ description: "Value for the custom field. Type depends on the field type."
142
+ }
143
+ },
144
+ required: ["id", "value"]
145
+ },
146
+ description: "Optional array of custom field values to set on the task."
137
147
  }
138
148
  },
139
149
  required: ["name"]
140
150
  }
141
151
  },
152
+ listId: {
153
+ type: "string",
154
+ description: "ID of list for new tasks (preferred). Use this instead of listName if you have it."
155
+ },
156
+ listName: {
157
+ type: "string",
158
+ description: "Name of list for new tasks. Only use if you don't have listId."
159
+ },
142
160
  options: bulkOptionsSchema
143
161
  },
144
162
  required: ["tasks"]
145
163
  }
146
164
  };
147
165
  /**
148
- * Tool definition for updating multiple tasks at once
166
+ * Tool definition for updating multiple tasks efficiently
149
167
  */
150
168
  export const updateBulkTasksTool = {
151
169
  name: "update_bulk_tasks",
@@ -163,7 +181,11 @@ Requirements:
163
181
  Notes:
164
182
  - Only specified fields will be updated for each task
165
183
  - Configure batch size and concurrency via options for performance
166
- - Each task can have different fields to update`,
184
+ - Each task can have different fields to update
185
+ - Custom fields can be updated using the custom_fields property (array of {id, value} objects)
186
+
187
+ Warning:
188
+ - Using taskName without listName will fail as tasks may have identical names across lists`,
167
189
  inputSchema: {
168
190
  type: "object",
169
191
  properties: {
@@ -173,7 +195,22 @@ Notes:
173
195
  items: {
174
196
  type: "object",
175
197
  properties: {
176
- ...taskIdentifierSchema,
198
+ taskId: {
199
+ type: "string",
200
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
201
+ },
202
+ taskName: {
203
+ type: "string",
204
+ description: "Task name. Requires listName when used."
205
+ },
206
+ listName: {
207
+ type: "string",
208
+ description: "REQUIRED with taskName: List containing the task."
209
+ },
210
+ customTaskId: {
211
+ type: "string",
212
+ description: "Custom task ID (e.g., 'DEV-1234'). Only use if you want to explicitly force custom ID lookup. In most cases, use taskId which auto-detects ID format."
213
+ },
177
214
  name: {
178
215
  type: "string",
179
216
  description: "New name with emoji prefix"
@@ -198,6 +235,23 @@ Notes:
198
235
  dueDate: {
199
236
  type: "string",
200
237
  description: "New due date. Supports Unix timestamps (in milliseconds) and natural language expressions like '1 hour from now', 'tomorrow', etc."
238
+ },
239
+ custom_fields: {
240
+ type: "array",
241
+ items: {
242
+ type: "object",
243
+ properties: {
244
+ id: {
245
+ type: "string",
246
+ description: "ID of the custom field"
247
+ },
248
+ value: {
249
+ description: "Value for the custom field. Type depends on the field type."
250
+ }
251
+ },
252
+ required: ["id", "value"]
253
+ },
254
+ description: "Optional array of custom field values to set on the task."
201
255
  }
202
256
  }
203
257
  }
@@ -227,7 +281,7 @@ Notes:
227
281
  - Configure batch size and concurrency via options for performance
228
282
  - All tasks will be moved to the same destination list
229
283
 
230
- ⚠️ Warning:
284
+ Warning:
231
285
  - Task statuses may reset if destination list has different status options
232
286
  - Using taskName without listName will fail as tasks may have identical names across lists`,
233
287
  inputSchema: {
@@ -239,7 +293,22 @@ Notes:
239
293
  items: {
240
294
  type: "object",
241
295
  properties: {
242
- ...taskIdentifierSchema
296
+ taskId: {
297
+ type: "string",
298
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
299
+ },
300
+ taskName: {
301
+ type: "string",
302
+ description: "Task name. Requires listName when used."
303
+ },
304
+ listName: {
305
+ type: "string",
306
+ description: "REQUIRED with taskName: List containing the task."
307
+ },
308
+ customTaskId: {
309
+ type: "string",
310
+ description: "Custom task ID (e.g., 'DEV-1234'). Only use if you want to explicitly force custom ID lookup. In most cases, use taskId which auto-detects ID format."
311
+ }
243
312
  }
244
313
  }
245
314
  },
@@ -274,7 +343,7 @@ Requirements:
274
343
  Notes:
275
344
  - Configure batch size and concurrency via options for performance
276
345
 
277
- ⚠️ CRITICAL WARNING:
346
+ Warning:
278
347
  - This action CANNOT be undone for any of the tasks
279
348
  - Using taskName without listName is dangerous as names may not be unique
280
349
  - Always provide listName when using taskName for safer targeting`,
@@ -287,7 +356,22 @@ Notes:
287
356
  items: {
288
357
  type: "object",
289
358
  properties: {
290
- ...taskIdentifierSchema
359
+ taskId: {
360
+ type: "string",
361
+ description: "Task ID (preferred). Works with both regular task IDs (9 characters) and custom IDs with uppercase prefixes (like 'DEV-1234')."
362
+ },
363
+ taskName: {
364
+ type: "string",
365
+ description: "Task name. Requires listName when used."
366
+ },
367
+ listName: {
368
+ type: "string",
369
+ description: "REQUIRED with taskName: List containing the task."
370
+ },
371
+ customTaskId: {
372
+ type: "string",
373
+ description: "Custom task ID (e.g., 'DEV-1234'). Only use if you want to explicitly force custom ID lookup. In most cases, use taskId which auto-detects ID format."
374
+ }
291
375
  }
292
376
  }
293
377
  },