@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 +2 -2
- package/build/config.js +2 -3
- package/build/index.js +17 -33
- package/build/logger.js +5 -6
- package/build/server.js +32 -17
- package/build/services/clickup/base.js +156 -45
- package/build/services/clickup/bulk.js +10 -22
- package/build/services/clickup/tag.js +51 -2
- package/build/services/clickup/task/task-core.js +76 -11
- package/build/services/clickup/task/task-search.js +378 -25
- package/build/services/clickup/workspace.js +14 -12
- package/build/tools/tag.js +88 -36
- package/build/tools/task/attachments.js +15 -9
- package/build/tools/task/handlers.js +253 -136
- package/build/tools/task/main.js +9 -33
- package/build/tools/task/single-operations.js +9 -7
- package/build/tools/task/utilities.js +59 -12
- package/build/utils/date-utils.js +7 -4
- package/build/utils/resolver-utils.js +102 -29
- package/package.json +1 -1
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* Handles workspace hierarchy and space-related operations
|
|
8
8
|
*/
|
|
9
9
|
import { BaseClickUpService, ClickUpServiceError, ErrorCode } from './base.js';
|
|
10
|
+
import { Logger } from '../../logger.js';
|
|
11
|
+
// Create a logger instance for workspace service
|
|
12
|
+
const logger = new Logger('WorkspaceService');
|
|
10
13
|
/**
|
|
11
14
|
* Service for workspace-related operations
|
|
12
15
|
*/
|
|
@@ -29,8 +32,7 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
29
32
|
* @returns - A standardized ClickUpServiceError
|
|
30
33
|
*/
|
|
31
34
|
handleError(error, message) {
|
|
32
|
-
|
|
33
|
-
console.error('WorkspaceService error:', error);
|
|
35
|
+
logger.error('WorkspaceService error:', error);
|
|
34
36
|
// If the error is already a ClickUpServiceError, return it
|
|
35
37
|
if (error instanceof ClickUpServiceError) {
|
|
36
38
|
return error;
|
|
@@ -104,11 +106,11 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
104
106
|
try {
|
|
105
107
|
// If we have the hierarchy in memory and not forcing refresh, return it
|
|
106
108
|
if (this.workspaceHierarchy && !forceRefresh) {
|
|
107
|
-
|
|
109
|
+
logger.debug('Returning cached workspace hierarchy');
|
|
108
110
|
return this.workspaceHierarchy;
|
|
109
111
|
}
|
|
110
112
|
const startTime = Date.now();
|
|
111
|
-
|
|
113
|
+
logger.info('Starting workspace hierarchy fetch');
|
|
112
114
|
// Start building the workspace tree
|
|
113
115
|
const workspaceTree = {
|
|
114
116
|
root: {
|
|
@@ -121,7 +123,7 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
121
123
|
const spacesStartTime = Date.now();
|
|
122
124
|
const spaces = await this.getSpaces();
|
|
123
125
|
const spacesTime = Date.now() - spacesStartTime;
|
|
124
|
-
|
|
126
|
+
logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
|
|
125
127
|
// Process spaces in batches to respect rate limits
|
|
126
128
|
const batchSize = 3; // Process 3 spaces at a time
|
|
127
129
|
const spaceNodes = [];
|
|
@@ -130,7 +132,7 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
130
132
|
for (let i = 0; i < spaces.length; i += batchSize) {
|
|
131
133
|
const batchStartTime = Date.now();
|
|
132
134
|
const spaceBatch = spaces.slice(i, i + batchSize);
|
|
133
|
-
|
|
135
|
+
logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
|
|
134
136
|
const batchNodes = await Promise.all(spaceBatch.map(async (space) => {
|
|
135
137
|
const spaceStartTime = Date.now();
|
|
136
138
|
const spaceNode = {
|
|
@@ -173,12 +175,12 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
173
175
|
}));
|
|
174
176
|
folderNodes.push(...batchFolderNodes);
|
|
175
177
|
const folderBatchTime = Date.now() - folderBatchStartTime;
|
|
176
|
-
|
|
178
|
+
logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
|
|
177
179
|
}
|
|
178
180
|
// Add folder nodes to space
|
|
179
181
|
spaceNode.children?.push(...folderNodes);
|
|
180
182
|
// Add folderless lists to space
|
|
181
|
-
|
|
183
|
+
logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
|
|
182
184
|
const listNodes = listsInSpace.map(list => ({
|
|
183
185
|
id: list.id,
|
|
184
186
|
name: list.name,
|
|
@@ -187,17 +189,17 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
187
189
|
}));
|
|
188
190
|
spaceNode.children?.push(...listNodes);
|
|
189
191
|
const spaceTime = Date.now() - spaceStartTime;
|
|
190
|
-
|
|
192
|
+
logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
|
|
191
193
|
return spaceNode;
|
|
192
194
|
}));
|
|
193
195
|
spaceNodes.push(...batchNodes);
|
|
194
196
|
const batchTime = Date.now() - batchStartTime;
|
|
195
|
-
|
|
197
|
+
logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
|
|
196
198
|
}
|
|
197
199
|
// Add all space nodes to the workspace tree
|
|
198
200
|
workspaceTree.root.children.push(...spaceNodes);
|
|
199
201
|
const totalTime = Date.now() - startTime;
|
|
200
|
-
|
|
202
|
+
logger.info('Workspace hierarchy fetch completed', {
|
|
201
203
|
duration: totalTime,
|
|
202
204
|
spaces: spaces.length,
|
|
203
205
|
folders: totalFolders,
|
|
@@ -293,7 +295,7 @@ export class WorkspaceService extends BaseClickUpService {
|
|
|
293
295
|
try {
|
|
294
296
|
// The /space/{space_id}/list endpoint already returns folderless lists only
|
|
295
297
|
const lists = await this.getFolderlessLists(spaceId);
|
|
296
|
-
|
|
298
|
+
logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
|
|
297
299
|
// Return all lists without filtering since the API already returns folderless lists
|
|
298
300
|
return lists;
|
|
299
301
|
}
|
package/build/tools/tag.js
CHANGED
|
@@ -13,8 +13,11 @@ import { clickUpServices } from '../services/shared.js';
|
|
|
13
13
|
import { Logger } from '../logger.js';
|
|
14
14
|
import { sponsorService } from '../utils/sponsor-service.js';
|
|
15
15
|
import { processColorCommand } from '../utils/color-processor.js';
|
|
16
|
+
import { validateTaskIdentification } from './task/utilities.js';
|
|
16
17
|
// Create a logger specific to tag tools
|
|
17
18
|
const logger = new Logger('TagTools');
|
|
19
|
+
// Use shared services instance
|
|
20
|
+
const { task: taskService } = clickUpServices;
|
|
18
21
|
//=============================================================================
|
|
19
22
|
// TOOL DEFINITIONS
|
|
20
23
|
//=============================================================================
|
|
@@ -201,17 +204,18 @@ export const addTagToTaskTool = {
|
|
|
201
204
|
|
|
202
205
|
Valid Usage:
|
|
203
206
|
1. Provide taskId (preferred if available)
|
|
204
|
-
2. Provide taskName
|
|
207
|
+
2. Provide taskName (optionally with listName for disambiguation)
|
|
205
208
|
|
|
206
209
|
Requirements:
|
|
207
210
|
- tagName: REQUIRED
|
|
208
|
-
- EITHER taskId OR
|
|
211
|
+
- EITHER taskId OR customTaskId OR taskName: REQUIRED
|
|
209
212
|
- The tag MUST exist in the space containing the task before calling this tool
|
|
210
213
|
|
|
211
214
|
Warning:
|
|
212
215
|
- The operation will fail if the tag does not exist in the space
|
|
213
216
|
- Always use get_space_tags first to verify the tag exists
|
|
214
217
|
- If the tag doesn't exist, create it using create_space_tag before adding it to the task
|
|
218
|
+
- If multiple tasks have the same name, provide listName to disambiguate
|
|
215
219
|
|
|
216
220
|
Notes:
|
|
217
221
|
- Use get_space_tags to see available tags
|
|
@@ -229,11 +233,11 @@ Notes:
|
|
|
229
233
|
},
|
|
230
234
|
taskName: {
|
|
231
235
|
type: "string",
|
|
232
|
-
description: "Name of the task to add tag to.
|
|
236
|
+
description: "Name of the task to add tag to. Will search across all lists unless listName is provided."
|
|
233
237
|
},
|
|
234
238
|
listName: {
|
|
235
239
|
type: "string",
|
|
236
|
-
description: "Name of the list containing the task.
|
|
240
|
+
description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
|
|
237
241
|
},
|
|
238
242
|
tagName: {
|
|
239
243
|
type: "string",
|
|
@@ -252,15 +256,16 @@ export const removeTagFromTaskTool = {
|
|
|
252
256
|
|
|
253
257
|
Valid Usage:
|
|
254
258
|
1. Provide taskId (preferred if available)
|
|
255
|
-
2. Provide taskName
|
|
259
|
+
2. Provide taskName (optionally with listName for disambiguation)
|
|
256
260
|
|
|
257
261
|
Requirements:
|
|
258
262
|
- tagName: REQUIRED
|
|
259
|
-
- EITHER taskId OR
|
|
263
|
+
- EITHER taskId OR customTaskId OR taskName: REQUIRED
|
|
260
264
|
|
|
261
265
|
Notes:
|
|
262
266
|
- This only removes the association between the tag and task
|
|
263
|
-
- The tag will still exist in the space
|
|
267
|
+
- The tag will still exist in the space
|
|
268
|
+
- If multiple tasks have the same name, provide listName to disambiguate`,
|
|
264
269
|
inputSchema: {
|
|
265
270
|
type: "object",
|
|
266
271
|
properties: {
|
|
@@ -274,11 +279,11 @@ Notes:
|
|
|
274
279
|
},
|
|
275
280
|
taskName: {
|
|
276
281
|
type: "string",
|
|
277
|
-
description: "Name of the task to remove tag from.
|
|
282
|
+
description: "Name of the task to remove tag from. Will search across all lists unless listName is provided."
|
|
278
283
|
},
|
|
279
284
|
listName: {
|
|
280
285
|
type: "string",
|
|
281
|
-
description: "Name of the list containing the task.
|
|
286
|
+
description: "Optional: Name of the list containing the task. Use to disambiguate when multiple tasks have the same name."
|
|
282
287
|
},
|
|
283
288
|
tagName: {
|
|
284
289
|
type: "string",
|
|
@@ -341,10 +346,18 @@ export const handleDeleteSpaceTag = createHandlerWrapper(deleteSpaceTag, () => (
|
|
|
341
346
|
/**
|
|
342
347
|
* Wrapper for addTagToTask handler
|
|
343
348
|
*/
|
|
344
|
-
export const handleAddTagToTask = createHandlerWrapper(addTagToTask, () =>
|
|
345
|
-
success
|
|
346
|
-
|
|
347
|
-
|
|
349
|
+
export const handleAddTagToTask = createHandlerWrapper(addTagToTask, (result) => {
|
|
350
|
+
if (!result.success) {
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
error: result.error
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
message: "Tag added to task successfully"
|
|
359
|
+
};
|
|
360
|
+
});
|
|
348
361
|
/**
|
|
349
362
|
* Wrapper for removeTagFromTask handler
|
|
350
363
|
*/
|
|
@@ -673,28 +686,42 @@ export async function deleteSpaceTag(params) {
|
|
|
673
686
|
*/
|
|
674
687
|
async function resolveTaskId(params) {
|
|
675
688
|
const { taskId, customTaskId, taskName, listName } = params;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
689
|
+
try {
|
|
690
|
+
// First validate task identification with global lookup enabled
|
|
691
|
+
const validationResult = validateTaskIdentification({ taskId, customTaskId, taskName, listName }, { useGlobalLookup: true });
|
|
692
|
+
if (!validationResult.isValid) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
error: { message: validationResult.errorMessage }
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
const result = await taskService.findTasks({
|
|
699
|
+
taskId,
|
|
700
|
+
customTaskId,
|
|
701
|
+
taskName,
|
|
702
|
+
listName,
|
|
703
|
+
allowMultipleMatches: false,
|
|
704
|
+
useSmartDisambiguation: true,
|
|
705
|
+
includeFullDetails: false
|
|
706
|
+
});
|
|
707
|
+
if (!result || Array.isArray(result)) {
|
|
708
|
+
return {
|
|
709
|
+
success: false,
|
|
710
|
+
error: { message: 'Task not found with the provided identification' }
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
return { success: true, taskId: result.id };
|
|
683
714
|
}
|
|
684
|
-
|
|
685
|
-
if (taskName && listName) {
|
|
686
|
-
// Implementation would go here
|
|
715
|
+
catch (error) {
|
|
687
716
|
return {
|
|
688
717
|
success: false,
|
|
689
|
-
error: {
|
|
718
|
+
error: {
|
|
719
|
+
message: error.message || 'Failed to resolve task ID',
|
|
720
|
+
code: error.code,
|
|
721
|
+
details: error.data
|
|
722
|
+
}
|
|
690
723
|
};
|
|
691
724
|
}
|
|
692
|
-
return {
|
|
693
|
-
success: false,
|
|
694
|
-
error: {
|
|
695
|
-
message: 'Task identifier is required (taskId, customTaskId, or taskName+listName)'
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
725
|
}
|
|
699
726
|
/**
|
|
700
727
|
* Add a tag to a task
|
|
@@ -712,12 +739,12 @@ export async function addTagToTask(params) {
|
|
|
712
739
|
}
|
|
713
740
|
};
|
|
714
741
|
}
|
|
715
|
-
if (!taskId && !customTaskId && !
|
|
716
|
-
logger.error('addTagToTask called without
|
|
742
|
+
if (!taskId && !customTaskId && !taskName) {
|
|
743
|
+
logger.error('addTagToTask called without task identifier');
|
|
717
744
|
return {
|
|
718
745
|
success: false,
|
|
719
746
|
error: {
|
|
720
|
-
message: 'Either taskId, customTaskId, or
|
|
747
|
+
message: 'Either taskId, customTaskId, or taskName is required'
|
|
721
748
|
}
|
|
722
749
|
};
|
|
723
750
|
}
|
|
@@ -735,6 +762,31 @@ export async function addTagToTask(params) {
|
|
|
735
762
|
const result = await clickUpServices.tag.addTagToTask(taskIdResult.taskId, tagName);
|
|
736
763
|
if (!result.success) {
|
|
737
764
|
logger.error('Failed to add tag to task', result.error);
|
|
765
|
+
// Provide more specific error messages based on error code
|
|
766
|
+
if (result.error?.code === 'TAG_NOT_FOUND') {
|
|
767
|
+
return {
|
|
768
|
+
success: false,
|
|
769
|
+
error: {
|
|
770
|
+
message: `The tag "${tagName}" does not exist in the space. Please create it first using create_space_tag.`
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
else if (result.error?.code === 'SPACE_NOT_FOUND') {
|
|
775
|
+
return {
|
|
776
|
+
success: false,
|
|
777
|
+
error: {
|
|
778
|
+
message: 'Could not determine which space the task belongs to.'
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
else if (result.error?.code === 'TAG_VERIFICATION_FAILED') {
|
|
783
|
+
return {
|
|
784
|
+
success: false,
|
|
785
|
+
error: {
|
|
786
|
+
message: 'The tag addition could not be verified. Please check if the tag was added manually.'
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
738
790
|
return {
|
|
739
791
|
success: false,
|
|
740
792
|
error: result.error || {
|
|
@@ -775,12 +827,12 @@ export async function removeTagFromTask(params) {
|
|
|
775
827
|
}
|
|
776
828
|
};
|
|
777
829
|
}
|
|
778
|
-
if (!taskId && !customTaskId && !
|
|
779
|
-
logger.error('removeTagFromTask called without
|
|
830
|
+
if (!taskId && !customTaskId && !taskName) {
|
|
831
|
+
logger.error('removeTagFromTask called without task identifier');
|
|
780
832
|
return {
|
|
781
833
|
success: false,
|
|
782
834
|
error: {
|
|
783
|
-
message: 'Either taskId, customTaskId, or
|
|
835
|
+
message: 'Either taskId, customTaskId, or taskName is required'
|
|
784
836
|
}
|
|
785
837
|
};
|
|
786
838
|
}
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
import { clickUpServices } from '../../services/shared.js';
|
|
11
11
|
import { validateTaskIdentification } from './utilities.js';
|
|
12
12
|
import { sponsorService } from '../../utils/sponsor-service.js';
|
|
13
|
+
import { Logger } from '../../logger.js';
|
|
13
14
|
// Use shared services instance
|
|
14
15
|
const { task: taskService } = clickUpServices;
|
|
16
|
+
// Create a logger instance for attachments
|
|
17
|
+
const logger = new Logger('TaskAttachments');
|
|
15
18
|
// Session storage for chunked uploads (in-memory for demonstration)
|
|
16
19
|
const chunkSessions = new Map();
|
|
17
20
|
// Clean up expired sessions periodically
|
|
@@ -21,7 +24,7 @@ setInterval(() => {
|
|
|
21
24
|
for (const [token, session] of chunkSessions.entries()) {
|
|
22
25
|
if (now - session.timestamp > expired) {
|
|
23
26
|
chunkSessions.delete(token);
|
|
24
|
-
|
|
27
|
+
logger.debug(`Cleaned up expired upload session: ${token}`);
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
}, 3600 * 1000); // Check every hour
|
|
@@ -122,9 +125,12 @@ Warning:
|
|
|
122
125
|
*/
|
|
123
126
|
async function attachTaskFileHandler(params) {
|
|
124
127
|
// Extract common parameters
|
|
125
|
-
const { taskId, taskName, listName, file_name, file_data, file_url, auth_header, chunk_total, chunk_size, chunk_index, session_id } = params;
|
|
128
|
+
const { taskId, taskName, listName, customTaskId, file_name, file_data, file_url, auth_header, chunk_total, chunk_size, chunk_index, session_id } = params;
|
|
126
129
|
// Validate task identification
|
|
127
|
-
validateTaskIdentification(
|
|
130
|
+
const validationResult = validateTaskIdentification({ taskId, taskName, listName, customTaskId }, { useGlobalLookup: true });
|
|
131
|
+
if (!validationResult.isValid) {
|
|
132
|
+
throw new Error(validationResult.errorMessage);
|
|
133
|
+
}
|
|
128
134
|
// Validate file source - either file_data or file_url must be provided
|
|
129
135
|
if (!file_data && !file_url && !session_id) {
|
|
130
136
|
throw new Error("Either file_data, file_url, or session_id must be provided");
|
|
@@ -150,13 +156,13 @@ async function attachTaskFileHandler(params) {
|
|
|
150
156
|
// CASE 2: URL-based upload or local file path
|
|
151
157
|
if (file_url) {
|
|
152
158
|
// Check if it's a local file path
|
|
153
|
-
|
|
159
|
+
logger.debug(`Checking if path is local: ${file_url}`);
|
|
154
160
|
if (file_url.startsWith('/') || /^[A-Za-z]:\\/.test(file_url)) {
|
|
155
|
-
|
|
161
|
+
logger.debug(`Detected as local path, proceeding to handle: ${file_url}`);
|
|
156
162
|
return await handleLocalFileUpload(resolvedTaskId, file_url, file_name);
|
|
157
163
|
}
|
|
158
164
|
else if (file_url.startsWith('http://') || file_url.startsWith('https://')) {
|
|
159
|
-
|
|
165
|
+
logger.debug(`Detected as URL, proceeding with URL upload: ${file_url}`);
|
|
160
166
|
return await handleUrlUpload(resolvedTaskId, file_url, file_name, auth_header);
|
|
161
167
|
}
|
|
162
168
|
else {
|
|
@@ -183,7 +189,7 @@ async function attachTaskFileHandler(params) {
|
|
|
183
189
|
throw new Error("Invalid parameters: Unable to determine upload method");
|
|
184
190
|
}
|
|
185
191
|
catch (error) {
|
|
186
|
-
|
|
192
|
+
logger.error(`Error attaching file to task:`, error);
|
|
187
193
|
throw error;
|
|
188
194
|
}
|
|
189
195
|
}
|
|
@@ -331,7 +337,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
|
|
|
331
337
|
// Import fs and path modules
|
|
332
338
|
const fs = await import('fs');
|
|
333
339
|
const path = await import('path');
|
|
334
|
-
|
|
340
|
+
logger.debug(`Processing absolute file path: ${filePath}`);
|
|
335
341
|
// Normalize the path to prevent directory traversal attacks
|
|
336
342
|
const normalizedPath = path.normalize(filePath);
|
|
337
343
|
// Check if file exists
|
|
@@ -348,7 +354,7 @@ async function handleLocalFileUpload(taskId, filePath, fileName) {
|
|
|
348
354
|
// Read file
|
|
349
355
|
const fileBuffer = fs.readFileSync(normalizedPath);
|
|
350
356
|
const fileSize = fileBuffer.length;
|
|
351
|
-
|
|
357
|
+
logger.debug(`Successfully read file: ${extractedFileName} (${fileSize} bytes)`);
|
|
352
358
|
// Choose upload method based on file size
|
|
353
359
|
if (fileSize > 10 * 1024 * 1024) {
|
|
354
360
|
// For large files, start chunked upload process
|