@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,239 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Tag Service
6
+ *
7
+ * Provides access to ClickUp API endpoints for tag management:
8
+ * - Space tags (get, create, update, delete)
9
+ * - Task tags (add, remove)
10
+ */
11
+ import { BaseClickUpService } from './base.js';
12
+ /**
13
+ * ClickUp Tag Service class for managing tags
14
+ */
15
+ export class ClickUpTagService extends BaseClickUpService {
16
+ /**
17
+ * Get all tags in a space
18
+ * @param spaceId - ID of the space to get tags from
19
+ * @returns Promise with tags array
20
+ */
21
+ async getSpaceTags(spaceId) {
22
+ try {
23
+ this.logger.debug(`Getting tags for space: ${spaceId}`);
24
+ const response = await this.client.get(`/space/${spaceId}/tag`);
25
+ return {
26
+ success: true,
27
+ data: response.data.tags
28
+ };
29
+ }
30
+ catch (error) {
31
+ this.logger.error(`Failed to get tags for space: ${spaceId}`, error);
32
+ return {
33
+ success: false,
34
+ error: {
35
+ message: error.message || 'Failed to get space tags',
36
+ code: error.code,
37
+ details: error.data
38
+ }
39
+ };
40
+ }
41
+ }
42
+ /**
43
+ * Create a new tag in a space
44
+ * @param spaceId - ID of the space
45
+ * @param tagData - Tag data (name, background color, foreground color)
46
+ * @returns Promise with created tag
47
+ */
48
+ async createSpaceTag(spaceId, tagData) {
49
+ try {
50
+ this.logger.debug(`Creating tag "${tagData.tag_name}" in space: ${spaceId}`);
51
+ // Send tag data wrapped in a 'tag' object
52
+ const response = await this.client.post(`/space/${spaceId}/tag`, {
53
+ tag: {
54
+ name: tagData.tag_name,
55
+ tag_bg: tagData.tag_bg,
56
+ tag_fg: tagData.tag_fg
57
+ }
58
+ });
59
+ return {
60
+ success: true,
61
+ data: response.data.tag
62
+ };
63
+ }
64
+ catch (error) {
65
+ this.logger.error(`Failed to create tag in space: ${spaceId}`, error);
66
+ return {
67
+ success: false,
68
+ error: {
69
+ message: error.message || 'Failed to create space tag',
70
+ code: error.code,
71
+ details: error.data
72
+ }
73
+ };
74
+ }
75
+ }
76
+ /**
77
+ * Update an existing tag in a space
78
+ * @param spaceId - ID of the space
79
+ * @param tagName - Current name of the tag to update
80
+ * @param updateData - Tag data to update (name, colors)
81
+ * @returns Promise with updated tag
82
+ */
83
+ async updateSpaceTag(spaceId, tagName, updateData) {
84
+ try {
85
+ this.logger.debug(`Updating tag "${tagName}" in space: ${spaceId}`);
86
+ // Encode the tag name in the URL
87
+ const encodedTagName = encodeURIComponent(tagName);
88
+ const response = await this.client.put(`/space/${spaceId}/tag/${encodedTagName}`, updateData);
89
+ return {
90
+ success: true,
91
+ data: response.data.tag
92
+ };
93
+ }
94
+ catch (error) {
95
+ this.logger.error(`Failed to update tag "${tagName}" in space: ${spaceId}`, error);
96
+ return {
97
+ success: false,
98
+ error: {
99
+ message: error.message || 'Failed to update space tag',
100
+ code: error.code,
101
+ details: error.data
102
+ }
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Delete a tag from a space
108
+ * @param spaceId - ID of the space
109
+ * @param tagName - Name of the tag to delete
110
+ * @returns Promise with success status
111
+ */
112
+ async deleteSpaceTag(spaceId, tagName) {
113
+ try {
114
+ this.logger.debug(`Deleting tag "${tagName}" from space: ${spaceId}`);
115
+ // Encode the tag name in the URL
116
+ const encodedTagName = encodeURIComponent(tagName);
117
+ await this.client.delete(`/space/${spaceId}/tag/${encodedTagName}`);
118
+ return {
119
+ success: true
120
+ };
121
+ }
122
+ catch (error) {
123
+ this.logger.error(`Failed to delete tag "${tagName}" from space: ${spaceId}`, error);
124
+ return {
125
+ success: false,
126
+ error: {
127
+ message: error.message || 'Failed to delete space tag',
128
+ code: error.code,
129
+ details: error.data
130
+ }
131
+ };
132
+ }
133
+ }
134
+ /**
135
+ * Add a tag to a task
136
+ * @param taskId - ID of the task
137
+ * @param tagName - Name of the tag to add
138
+ * @returns Promise with success status and tag data
139
+ */
140
+ async addTagToTask(taskId, tagName) {
141
+ try {
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
+ }
178
+ // Encode the tag name in the URL
179
+ const encodedTagName = encodeURIComponent(tagName);
180
+ // Add the tag
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
+ }
194
+ return {
195
+ success: true,
196
+ data: { tagAdded: true }
197
+ };
198
+ }
199
+ catch (error) {
200
+ this.logger.error(`Failed to add tag "${tagName}" to task: ${taskId}`, error);
201
+ return {
202
+ success: false,
203
+ error: {
204
+ message: error.message || 'Failed to add tag to task',
205
+ code: error.code,
206
+ details: error.data
207
+ }
208
+ };
209
+ }
210
+ }
211
+ /**
212
+ * Remove a tag from a task
213
+ * @param taskId - ID of the task
214
+ * @param tagName - Name of the tag to remove
215
+ * @returns Promise with success status
216
+ */
217
+ async removeTagFromTask(taskId, tagName) {
218
+ try {
219
+ this.logger.debug(`Removing tag "${tagName}" from task: ${taskId}`);
220
+ // Encode the tag name in the URL
221
+ const encodedTagName = encodeURIComponent(tagName);
222
+ await this.client.delete(`/task/${taskId}/tag/${encodedTagName}`);
223
+ return {
224
+ success: true
225
+ };
226
+ }
227
+ catch (error) {
228
+ this.logger.error(`Failed to remove tag "${tagName}" from task: ${taskId}`, error);
229
+ return {
230
+ success: false,
231
+ error: {
232
+ message: error.message || 'Failed to remove tag from task',
233
+ code: error.code,
234
+ details: error.data
235
+ }
236
+ };
237
+ }
238
+ }
239
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Module Exports
6
+ *
7
+ * Exports all task-related functionality:
8
+ * - Core task operations (CRUD)
9
+ * - Task searching and filtering
10
+ * - Task comments
11
+ * - File attachments
12
+ * - Task tags
13
+ * - Custom fields
14
+ */
15
+ // Export the main TaskService class
16
+ export { TaskService } from './task-service.js';
17
+ // Export all component services
18
+ export { TaskServiceCore } from './task-core.js';
19
+ export { TaskServiceSearch } from './task-search.js';
20
+ export { TaskServiceAttachments } from './task-attachments.js';
21
+ export { TaskServiceComments } from './task-comments.js';
22
+ export { TaskServiceTags } from './task-tags.js';
23
+ export { TaskServiceCustomFields } from './task-custom-fields.js';
24
+ // Export types and interfaces from all modules
25
+ export * from './task-core.js';
26
+ export * from './task-search.js';
27
+ export * from './task-attachments.js';
28
+ export * from './task-comments.js';
29
+ export * from './task-tags.js';
30
+ export * from './task-custom-fields.js';
31
+ // Re-export TaskService as the default export
32
+ export { TaskService as default } from './task-service.js';
@@ -0,0 +1,105 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Attachments Module
6
+ *
7
+ * Handles file attachment operations for ClickUp tasks, supporting three methods:
8
+ * - Uploading file attachments from base64/buffer data
9
+ * - Uploading file attachments from a URL (web URLs like http/https)
10
+ * - Uploading file attachments from local file paths (absolute paths)
11
+ *
12
+ * REFACTORED: Now uses composition instead of inheritance.
13
+ * Only depends on TaskServiceCore for base functionality.
14
+ */
15
+ /**
16
+ * Attachment functionality for the TaskService
17
+ *
18
+ * This service handles all file attachment operations for ClickUp tasks.
19
+ * It uses composition to access core functionality instead of inheritance.
20
+ */
21
+ export class TaskServiceAttachments {
22
+ constructor(core) {
23
+ this.core = core;
24
+ }
25
+ /**
26
+ * Upload a file attachment to a ClickUp task
27
+ * @param taskId The ID of the task to attach the file to
28
+ * @param fileData The file data as a Buffer
29
+ * @param fileName The name of the file
30
+ * @returns Promise resolving to the attachment response from ClickUp
31
+ */
32
+ async uploadTaskAttachment(taskId, fileData, fileName) {
33
+ this.core.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
34
+ try {
35
+ return await this.core.makeRequest(async () => {
36
+ // Create FormData for multipart/form-data upload
37
+ const FormData = (await import('form-data')).default;
38
+ const formData = new FormData();
39
+ // Add the file to the form data
40
+ formData.append('attachment', fileData, {
41
+ filename: fileName,
42
+ contentType: 'application/octet-stream' // Let ClickUp determine the content type
43
+ });
44
+ // Use the raw axios client for this request since we need to handle FormData
45
+ const response = await this.core.client.post(`/task/${taskId}/attachment`, formData, {
46
+ headers: {
47
+ ...formData.getHeaders(),
48
+ 'Authorization': this.core.apiKey
49
+ }
50
+ });
51
+ return response.data;
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw this.core.handleError(error, `Failed to upload attachment to task ${taskId}`);
56
+ }
57
+ }
58
+ /**
59
+ * Upload a file attachment to a ClickUp task from a URL
60
+ * @param taskId The ID of the task to attach the file to
61
+ * @param fileUrl The URL of the file to download and attach
62
+ * @param fileName Optional file name (if not provided, it will be extracted from the URL)
63
+ * @param authHeader Optional authorization header for the URL
64
+ * @returns Promise resolving to the attachment response from ClickUp
65
+ */
66
+ async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
67
+ this.core.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
68
+ try {
69
+ return await this.core.makeRequest(async () => {
70
+ // Import required modules
71
+ const axios = (await import('axios')).default;
72
+ const FormData = (await import('form-data')).default;
73
+ // Download the file from the URL
74
+ const headers = {};
75
+ if (authHeader) {
76
+ headers['Authorization'] = authHeader;
77
+ }
78
+ const response = await axios.get(fileUrl, {
79
+ responseType: 'arraybuffer',
80
+ headers
81
+ });
82
+ // Extract filename from URL if not provided
83
+ const actualFileName = fileName || fileUrl.split('/').pop() || 'downloaded-file';
84
+ // Create FormData for multipart/form-data upload
85
+ const formData = new FormData();
86
+ // Add the file to the form data
87
+ formData.append('attachment', Buffer.from(response.data), {
88
+ filename: actualFileName,
89
+ contentType: 'application/octet-stream'
90
+ });
91
+ // Upload the file to ClickUp
92
+ const uploadResponse = await this.core.client.post(`/task/${taskId}/attachment`, formData, {
93
+ headers: {
94
+ ...formData.getHeaders(),
95
+ 'Authorization': this.core.apiKey
96
+ }
97
+ });
98
+ return uploadResponse.data;
99
+ });
100
+ }
101
+ catch (error) {
102
+ throw this.core.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Task Service - Comments Module
6
+ *
7
+ * Handles comment operations for ClickUp tasks, including:
8
+ * - Retrieving comments for a task
9
+ * - Creating comments on a task
10
+ *
11
+ * REFACTORED: Now uses composition instead of inheritance.
12
+ * Only depends on TaskServiceCore for base functionality.
13
+ */
14
+ /**
15
+ * Comments functionality for the TaskService
16
+ *
17
+ * This service handles all comment-related operations for ClickUp tasks.
18
+ * It uses composition to access core functionality instead of inheritance.
19
+ */
20
+ export class TaskServiceComments {
21
+ constructor(core) {
22
+ this.core = core;
23
+ }
24
+ /**
25
+ * Get all comments for a task
26
+ *
27
+ * @param taskId ID of the task to get comments for
28
+ * @param start Optional pagination start
29
+ * @param startId Optional comment ID to start from
30
+ * @returns Array of task comments
31
+ */
32
+ async getTaskComments(taskId, start, startId) {
33
+ this.core.logOperation('getTaskComments', { taskId, start, startId });
34
+ try {
35
+ // Build query parameters for pagination
36
+ const queryParams = new URLSearchParams();
37
+ if (start !== undefined) {
38
+ queryParams.append('start', start.toString());
39
+ }
40
+ if (startId) {
41
+ queryParams.append('start_id', startId);
42
+ }
43
+ const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
44
+ return await this.core.makeRequest(async () => {
45
+ const response = await this.core.client.get(`/task/${taskId}/comment${queryString}`);
46
+ return response.data.comments || [];
47
+ });
48
+ }
49
+ catch (error) {
50
+ throw this.core.handleError(error, 'Failed to get task comments');
51
+ }
52
+ }
53
+ /**
54
+ * Create a comment on a task
55
+ *
56
+ * @param taskId ID of the task to comment on
57
+ * @param commentText Text content of the comment
58
+ * @param notifyAll Whether to notify all assignees
59
+ * @param assignee Optional user ID to assign the comment to
60
+ * @returns The created comment
61
+ */
62
+ async createTaskComment(taskId, commentText, notifyAll = false, assignee) {
63
+ this.core.logOperation('createTaskComment', { taskId, commentText, notifyAll, assignee });
64
+ try {
65
+ const payload = {
66
+ comment_text: commentText,
67
+ notify_all: notifyAll
68
+ };
69
+ if (assignee) {
70
+ payload.assignee = assignee;
71
+ }
72
+ // Make the request directly without using makeRequest for better error handling
73
+ const response = await this.core.client.post(`/task/${taskId}/comment`, payload);
74
+ // Handle different response formats from ClickUp API
75
+ if (response.data) {
76
+ if (response.data.comment) {
77
+ // Standard format: { comment: ClickUpComment }
78
+ return response.data.comment;
79
+ }
80
+ else if (response.data.id && (response.data.comment_text || response.data.comment)) {
81
+ // Direct format: the comment object itself
82
+ return response.data;
83
+ }
84
+ else {
85
+ // Fallback: construct a minimal valid comment object
86
+ return {
87
+ id: response.data.id || `custom-${Date.now()}`,
88
+ comment: response.data.comment || commentText,
89
+ comment_text: response.data.comment_text || commentText,
90
+ user: response.data.user || { id: 0, username: 'Unknown', email: '', color: '' },
91
+ date: response.data.date || new Date().toISOString(),
92
+ resolved: false
93
+ };
94
+ }
95
+ }
96
+ throw new Error('Invalid response from ClickUp API');
97
+ }
98
+ catch (error) {
99
+ // Check if comment might have been created despite error
100
+ if (error.response?.status === 200 || error.response?.status === 201) {
101
+ // Try to construct a comment object from what we know
102
+ return {
103
+ id: `fallback-${Date.now()}`,
104
+ comment: commentText,
105
+ comment_text: commentText,
106
+ user: { id: 0, username: 'Unknown', email: '', color: '' },
107
+ date: new Date().toISOString(),
108
+ resolved: false
109
+ };
110
+ }
111
+ throw this.core.handleError(error, 'Failed to create task comment');
112
+ }
113
+ }
114
+ }