@alanse/clickup-multi-mcp-server 1.0.0

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 (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -0,0 +1,397 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Workspace Service Module
6
+ *
7
+ * Handles workspace hierarchy and space-related operations
8
+ */
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');
13
+ /**
14
+ * Service for workspace-related operations
15
+ */
16
+ export class WorkspaceService extends BaseClickUpService {
17
+ /**
18
+ * Creates an instance of WorkspaceService
19
+ * @param apiKey - ClickUp API key
20
+ * @param teamId - ClickUp team ID
21
+ * @param baseUrl - Optional custom API URL
22
+ */
23
+ constructor(apiKey, teamId, baseUrl) {
24
+ super(apiKey, teamId, baseUrl);
25
+ // Store the workspace hierarchy in memory
26
+ this.workspaceHierarchy = null;
27
+ }
28
+ /**
29
+ * Helper method to handle errors consistently
30
+ * @param error - Error caught from a try/catch
31
+ * @param message - Optional message to add to the error
32
+ * @returns - A standardized ClickUpServiceError
33
+ */
34
+ handleError(error, message) {
35
+ logger.error('WorkspaceService error:', error);
36
+ // If the error is already a ClickUpServiceError, return it
37
+ if (error instanceof ClickUpServiceError) {
38
+ return error;
39
+ }
40
+ // Otherwise, create a new ClickUpServiceError
41
+ const errorMessage = message || 'An error occurred in WorkspaceService';
42
+ return new ClickUpServiceError(errorMessage, ErrorCode.WORKSPACE_ERROR, error);
43
+ }
44
+ /**
45
+ * Get all spaces for the team
46
+ * @returns - Promise resolving to array of spaces
47
+ */
48
+ async getSpaces() {
49
+ try {
50
+ const response = await this.makeRequest(async () => {
51
+ const result = await this.client.get(`/team/${this.teamId}/space`);
52
+ return result.data;
53
+ });
54
+ return response.spaces || [];
55
+ }
56
+ catch (error) {
57
+ throw this.handleError(error, 'Failed to get spaces');
58
+ }
59
+ }
60
+ /**
61
+ * Get a specific space by ID
62
+ * @param spaceId - The ID of the space to retrieve
63
+ * @returns - Promise resolving to the space object
64
+ */
65
+ async getSpace(spaceId) {
66
+ try {
67
+ // Validate spaceId
68
+ if (!spaceId) {
69
+ throw new ClickUpServiceError('Space ID is required', ErrorCode.INVALID_PARAMETER);
70
+ }
71
+ return await this.makeRequest(async () => {
72
+ const result = await this.client.get(`/space/${spaceId}`);
73
+ return result.data;
74
+ });
75
+ }
76
+ catch (error) {
77
+ throw this.handleError(error, `Failed to get space with ID ${spaceId}`);
78
+ }
79
+ }
80
+ /**
81
+ * Find a space by name
82
+ * @param spaceName - The name of the space to find
83
+ * @returns - Promise resolving to the space or null if not found
84
+ */
85
+ async findSpaceByName(spaceName) {
86
+ try {
87
+ // Validate spaceName
88
+ if (!spaceName) {
89
+ throw new ClickUpServiceError('Space name is required', ErrorCode.INVALID_PARAMETER);
90
+ }
91
+ // Get all spaces and find the one with the matching name
92
+ const spaces = await this.getSpaces();
93
+ const space = spaces.find(s => s.name === spaceName);
94
+ return space || null;
95
+ }
96
+ catch (error) {
97
+ throw this.handleError(error, `Failed to find space with name ${spaceName}`);
98
+ }
99
+ }
100
+ /**
101
+ * Get the complete workspace hierarchy including spaces, folders, and lists
102
+ * @param forceRefresh - Whether to force a refresh of the hierarchy
103
+ * @returns - Promise resolving to the workspace tree
104
+ */
105
+ async getWorkspaceHierarchy(forceRefresh = false) {
106
+ try {
107
+ // If we have the hierarchy in memory and not forcing refresh, return it
108
+ if (this.workspaceHierarchy && !forceRefresh) {
109
+ logger.debug('Returning cached workspace hierarchy');
110
+ return this.workspaceHierarchy;
111
+ }
112
+ const startTime = Date.now();
113
+ logger.info('Starting workspace hierarchy fetch');
114
+ // Start building the workspace tree
115
+ const workspaceTree = {
116
+ root: {
117
+ id: this.teamId,
118
+ name: 'Workspace',
119
+ children: []
120
+ }
121
+ };
122
+ // Get all spaces
123
+ const spacesStartTime = Date.now();
124
+ const spaces = await this.getSpaces();
125
+ const spacesTime = Date.now() - spacesStartTime;
126
+ logger.info(`Fetched ${spaces.length} spaces in ${spacesTime}ms`);
127
+ // Process spaces in batches to respect rate limits
128
+ const batchSize = 3; // Process 3 spaces at a time
129
+ const spaceNodes = [];
130
+ let totalFolders = 0;
131
+ let totalLists = 0;
132
+ for (let i = 0; i < spaces.length; i += batchSize) {
133
+ const batchStartTime = Date.now();
134
+ const spaceBatch = spaces.slice(i, i + batchSize);
135
+ logger.debug(`Processing space batch ${i / batchSize + 1} of ${Math.ceil(spaces.length / batchSize)} (${spaceBatch.length} spaces)`);
136
+ const batchNodes = await Promise.all(spaceBatch.map(async (space) => {
137
+ const spaceStartTime = Date.now();
138
+ const spaceNode = {
139
+ id: space.id,
140
+ name: space.name,
141
+ type: 'space',
142
+ children: []
143
+ };
144
+ // Fetch initial space data
145
+ const [folders, listsInSpace] = await Promise.all([
146
+ this.getFoldersInSpace(space.id),
147
+ this.getListsInSpace(space.id)
148
+ ]);
149
+ totalFolders += folders.length;
150
+ totalLists += listsInSpace.length;
151
+ // Process folders in smaller batches
152
+ const folderBatchSize = 5; // Process 5 folders at a time
153
+ const folderNodes = [];
154
+ for (let j = 0; j < folders.length; j += folderBatchSize) {
155
+ const folderBatchStartTime = Date.now();
156
+ const folderBatch = folders.slice(j, j + folderBatchSize);
157
+ const batchFolderNodes = await Promise.all(folderBatch.map(async (folder) => {
158
+ const folderNode = {
159
+ id: folder.id,
160
+ name: folder.name,
161
+ type: 'folder',
162
+ parentId: space.id,
163
+ children: []
164
+ };
165
+ // Get lists in the folder
166
+ const listsInFolder = await this.getListsInFolder(folder.id);
167
+ totalLists += listsInFolder.length;
168
+ folderNode.children = listsInFolder.map(list => ({
169
+ id: list.id,
170
+ name: list.name,
171
+ type: 'list',
172
+ parentId: folder.id
173
+ }));
174
+ return folderNode;
175
+ }));
176
+ folderNodes.push(...batchFolderNodes);
177
+ const folderBatchTime = Date.now() - folderBatchStartTime;
178
+ logger.debug(`Processed folder batch in space ${space.name} in ${folderBatchTime}ms (${folderBatch.length} folders)`);
179
+ }
180
+ // Add folder nodes to space
181
+ spaceNode.children?.push(...folderNodes);
182
+ // Add folderless lists to space
183
+ logger.debug(`Adding ${listsInSpace.length} lists directly to space ${space.name}`);
184
+ const listNodes = listsInSpace.map(list => ({
185
+ id: list.id,
186
+ name: list.name,
187
+ type: 'list',
188
+ parentId: space.id
189
+ }));
190
+ spaceNode.children?.push(...listNodes);
191
+ const spaceTime = Date.now() - spaceStartTime;
192
+ logger.info(`Processed space ${space.name} in ${spaceTime}ms (${folders.length} folders, ${listsInSpace.length} lists)`);
193
+ return spaceNode;
194
+ }));
195
+ spaceNodes.push(...batchNodes);
196
+ const batchTime = Date.now() - batchStartTime;
197
+ logger.info(`Processed space batch in ${batchTime}ms (${spaceBatch.length} spaces)`);
198
+ }
199
+ // Add all space nodes to the workspace tree
200
+ workspaceTree.root.children.push(...spaceNodes);
201
+ const totalTime = Date.now() - startTime;
202
+ logger.info('Workspace hierarchy fetch completed', {
203
+ duration: totalTime,
204
+ spaces: spaces.length,
205
+ folders: totalFolders,
206
+ lists: totalLists,
207
+ averageTimePerSpace: totalTime / spaces.length,
208
+ averageTimePerNode: totalTime / (spaces.length + totalFolders + totalLists)
209
+ });
210
+ // Store the hierarchy for later use
211
+ this.workspaceHierarchy = workspaceTree;
212
+ return workspaceTree;
213
+ }
214
+ catch (error) {
215
+ throw this.handleError(error, 'Failed to get workspace hierarchy');
216
+ }
217
+ }
218
+ /**
219
+ * Clear the stored workspace hierarchy, forcing a fresh fetch on next request
220
+ */
221
+ clearWorkspaceHierarchy() {
222
+ this.workspaceHierarchy = null;
223
+ }
224
+ /**
225
+ * Find a node in the workspace tree by name and type
226
+ * @param node - The node to start searching from
227
+ * @param name - The name to search for
228
+ * @param type - The type of node to search for
229
+ * @returns - The node and its path if found, null otherwise
230
+ */
231
+ findNodeInTree(node, name, type) {
232
+ // If this is the node we're looking for, return it
233
+ if ('type' in node && node.type === type && node.name === name) {
234
+ return { node, path: node.name };
235
+ }
236
+ // Otherwise, search its children recursively
237
+ for (const child of (node.children || [])) {
238
+ const result = this.findNodeInTree(child, name, type);
239
+ if (result) {
240
+ // Prepend this node's name to the path
241
+ const currentNodeName = 'name' in node ? node.name : 'Workspace';
242
+ result.path = `${currentNodeName} > ${result.path}`;
243
+ return result;
244
+ }
245
+ }
246
+ // Not found in this subtree
247
+ return null;
248
+ }
249
+ /**
250
+ * Find an ID by name and type in the workspace hierarchy
251
+ * @param hierarchy - The workspace hierarchy
252
+ * @param name - The name to search for
253
+ * @param type - The type of node to search for
254
+ * @returns - The ID and path if found, null otherwise
255
+ */
256
+ findIDByNameInHierarchy(hierarchy, name, type) {
257
+ const result = this.findNodeInTree(hierarchy.root, name, type);
258
+ if (result) {
259
+ return { id: result.node.id, path: result.path };
260
+ }
261
+ return null;
262
+ }
263
+ /**
264
+ * Find a space ID by name
265
+ * @param spaceName - The name of the space to find
266
+ * @returns - Promise resolving to the space ID or null if not found
267
+ */
268
+ async findSpaceIDByName(spaceName) {
269
+ const space = await this.findSpaceByName(spaceName);
270
+ return space ? space.id : null;
271
+ }
272
+ /**
273
+ * Get folderless lists from the API (lists that are directly in a space)
274
+ * @param spaceId - The ID of the space
275
+ * @returns - Promise resolving to array of lists
276
+ */
277
+ async getFolderlessLists(spaceId) {
278
+ try {
279
+ const response = await this.makeRequest(async () => {
280
+ const result = await this.client.get(`/space/${spaceId}/list`);
281
+ return result.data;
282
+ });
283
+ return response.lists || [];
284
+ }
285
+ catch (error) {
286
+ throw this.handleError(error, `Failed to get folderless lists for space ${spaceId}`);
287
+ }
288
+ }
289
+ /**
290
+ * Get lists in a space (not in any folder)
291
+ * @param spaceId - The ID of the space
292
+ * @returns - Promise resolving to array of lists
293
+ */
294
+ async getListsInSpace(spaceId) {
295
+ try {
296
+ // The /space/{space_id}/list endpoint already returns folderless lists only
297
+ const lists = await this.getFolderlessLists(spaceId);
298
+ logger.debug(`Found ${lists.length} folderless lists in space ${spaceId}`);
299
+ // Return all lists without filtering since the API already returns folderless lists
300
+ return lists;
301
+ }
302
+ catch (error) {
303
+ throw this.handleError(error, `Failed to get lists in space ${spaceId}`);
304
+ }
305
+ }
306
+ /**
307
+ * Get folders from the API
308
+ * @param spaceId - The ID of the space
309
+ * @returns - Promise resolving to array of folders
310
+ */
311
+ async getFolders(spaceId) {
312
+ try {
313
+ const response = await this.makeRequest(async () => {
314
+ const result = await this.client.get(`/space/${spaceId}/folder`);
315
+ return result.data;
316
+ });
317
+ return response.folders || [];
318
+ }
319
+ catch (error) {
320
+ throw this.handleError(error, `Failed to get folders for space ${spaceId}`);
321
+ }
322
+ }
323
+ /**
324
+ * Get a specific folder by ID
325
+ * @param folderId - The ID of the folder to retrieve
326
+ * @returns - Promise resolving to the folder
327
+ */
328
+ async getFolder(folderId) {
329
+ try {
330
+ return await this.makeRequest(async () => {
331
+ const result = await this.client.get(`/folder/${folderId}`);
332
+ return result.data;
333
+ });
334
+ }
335
+ catch (error) {
336
+ throw this.handleError(error, `Failed to get folder with ID ${folderId}`);
337
+ }
338
+ }
339
+ /**
340
+ * Get folders in a space
341
+ * @param spaceId - The ID of the space
342
+ * @returns - Promise resolving to array of folders
343
+ */
344
+ async getFoldersInSpace(spaceId) {
345
+ try {
346
+ return await this.getFolders(spaceId);
347
+ }
348
+ catch (error) {
349
+ throw this.handleError(error, `Failed to get folders in space ${spaceId}`);
350
+ }
351
+ }
352
+ /**
353
+ * Get lists in a folder
354
+ * @param folderId - The ID of the folder
355
+ * @returns - Promise resolving to array of lists
356
+ */
357
+ async getListsInFolder(folderId) {
358
+ try {
359
+ const response = await this.makeRequest(async () => {
360
+ const result = await this.client.get(`/folder/${folderId}/list`);
361
+ return result.data;
362
+ });
363
+ return response.lists || [];
364
+ }
365
+ catch (error) {
366
+ throw this.handleError(error, `Failed to get lists in folder ${folderId}`);
367
+ }
368
+ }
369
+ /**
370
+ * Get all members in a workspace
371
+ * @returns Array of workspace members
372
+ */
373
+ async getWorkspaceMembers() {
374
+ try {
375
+ // Use the existing team/workspace endpoint which typically returns member information
376
+ const teamId = this.teamId;
377
+ const response = await this.client.get(`/team/${teamId}`);
378
+ if (!response || !response.data || !response.data.team) {
379
+ throw new Error('Invalid response from ClickUp API');
380
+ }
381
+ // Extract and normalize member data
382
+ const members = response.data.team.members || [];
383
+ return members.map((member) => ({
384
+ id: member.user?.id,
385
+ name: member.user?.username || member.user?.email,
386
+ username: member.user?.username,
387
+ email: member.user?.email,
388
+ role: member.role,
389
+ profilePicture: member.user?.profilePicture
390
+ }));
391
+ }
392
+ catch (error) {
393
+ console.error('Error getting workspace members:', error);
394
+ throw error;
395
+ }
396
+ }
397
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Shared Services Module
6
+ *
7
+ * This module maintains singleton instances of services that should be shared
8
+ * across the application to ensure consistent state.
9
+ * Supports multiple workspaces with lazy initialization.
10
+ */
11
+ import { createClickUpServices } from './clickup/index.js';
12
+ import { getWorkspaceConfig, getDefaultWorkspace, getAvailableWorkspaces } from '../config.js';
13
+ import { Logger } from '../logger.js';
14
+ const logger = new Logger('SharedServices');
15
+ // Map of workspace ID to services instance
16
+ const workspaceServicesMap = new Map();
17
+ /**
18
+ * Get or create the ClickUp services instance for a specific workspace
19
+ * @param workspaceId - Workspace identifier (optional, uses default if not specified)
20
+ * @returns ClickUp services instance for the workspace
21
+ */
22
+ export function getClickUpServices(workspaceId) {
23
+ const wsId = workspaceId || getDefaultWorkspace();
24
+ // Check if services already exist for this workspace
25
+ let services = workspaceServicesMap.get(wsId);
26
+ if (!services) {
27
+ logger.info(`Creating ClickUp services for workspace: ${wsId}`);
28
+ // Get workspace configuration
29
+ const workspaceConfig = getWorkspaceConfig(wsId);
30
+ // Create the services instance
31
+ services = createClickUpServices({
32
+ apiKey: workspaceConfig.token,
33
+ teamId: workspaceConfig.teamId
34
+ });
35
+ // Store in map
36
+ workspaceServicesMap.set(wsId, services);
37
+ // Log what services were initialized
38
+ logger.info(`Services initialization complete for workspace: ${wsId}`, {
39
+ services: Object.keys(services).join(', '),
40
+ teamId: workspaceConfig.teamId,
41
+ description: workspaceConfig.description
42
+ });
43
+ }
44
+ return services;
45
+ }
46
+ /**
47
+ * Get services for all configured workspaces
48
+ * @returns Map of workspace ID to services
49
+ */
50
+ export function getAllWorkspaceServices() {
51
+ const availableWorkspaces = getAvailableWorkspaces();
52
+ // Initialize services for all workspaces
53
+ for (const wsId of availableWorkspaces) {
54
+ getClickUpServices(wsId);
55
+ }
56
+ return workspaceServicesMap;
57
+ }
58
+ // Create a default instance of ClickUp services to be shared (for backwards compatibility)
59
+ export const clickUpServices = getClickUpServices();
60
+ // Export individual services for convenience (uses default workspace)
61
+ export const { list: listService, task: taskService, folder: folderService, workspace: workspaceService, timeTracking: timeTrackingService, document: documentService } = clickUpServices;