@taazkareem/clickup-mcp-server 0.8.5 → 0.9.1

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.
@@ -0,0 +1,281 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Service Adapter
6
+ *
7
+ * Provides backward compatibility for existing ClickUp services
8
+ * while supporting both API key and OAuth2 authentication.
9
+ * This adapter wraps the existing services without modifying their constructors.
10
+ */
11
+ import { WorkspaceService } from './workspace.js';
12
+ import { TaskService } from './task/task-service.js';
13
+ import { ListService } from './list.js';
14
+ import { FolderService } from './folder.js';
15
+ import { TimeService } from './time.js';
16
+ import { TagService } from './tag.js';
17
+ import { BulkService } from './bulk.js';
18
+ import { DocumentService } from './document.js';
19
+ import { Logger } from '../../logger.js';
20
+ import config from '../../config.js';
21
+ /**
22
+ * ClickUp service adapter that provides authentication-aware service creation
23
+ */
24
+ export class ClickUpServiceAdapter {
25
+ constructor() {
26
+ this.logger = new Logger('ClickUpServiceAdapter');
27
+ }
28
+ /**
29
+ * Get authentication parameters from context
30
+ * @private
31
+ */
32
+ getAuthParams(context) {
33
+ if (context.authMethod === 'oauth2') {
34
+ if (!context.accessToken) {
35
+ throw new Error('OAuth2 access token is required but not provided');
36
+ }
37
+ if (!context.teamId) {
38
+ throw new Error('Team ID is required for OAuth2 authentication');
39
+ }
40
+ // For OAuth2, we'll use the access token as the "API key"
41
+ // and modify the service after creation
42
+ return {
43
+ apiKey: `Bearer ${context.accessToken}`,
44
+ teamId: context.teamId
45
+ };
46
+ }
47
+ else {
48
+ // API key authentication
49
+ const apiKey = context.apiKey || config.clickupApiKey;
50
+ const teamId = context.teamId || config.clickupTeamId;
51
+ if (!apiKey) {
52
+ throw new Error('API key is required but not provided');
53
+ }
54
+ if (!teamId) {
55
+ throw new Error('Team ID is required but not provided');
56
+ }
57
+ return { apiKey, teamId };
58
+ }
59
+ }
60
+ /**
61
+ * Create AuthConfig from context
62
+ * @private
63
+ */
64
+ createAuthConfig(context) {
65
+ if (context.authMethod === 'oauth2') {
66
+ if (!context.accessToken) {
67
+ throw new Error('Access token is required for OAuth2 authentication');
68
+ }
69
+ return {
70
+ type: 'oauth2',
71
+ accessToken: context.accessToken,
72
+ teamId: context.teamId || config.clickupTeamId
73
+ };
74
+ }
75
+ else {
76
+ const apiKey = context.apiKey || config.clickupApiKey;
77
+ const teamId = context.teamId || config.clickupTeamId;
78
+ if (!apiKey) {
79
+ throw new Error('API key is required for API key authentication');
80
+ }
81
+ return {
82
+ type: 'api_key',
83
+ apiKey,
84
+ teamId
85
+ };
86
+ }
87
+ }
88
+ /**
89
+ * Create WorkspaceService instance
90
+ */
91
+ createWorkspaceService(context) {
92
+ try {
93
+ const { apiKey, teamId } = this.getAuthParams(context);
94
+ const service = WorkspaceService.withApiKey(apiKey, teamId);
95
+ this.logger.debug('WorkspaceService created', {
96
+ authMethod: context.authMethod,
97
+ teamId
98
+ });
99
+ return service;
100
+ }
101
+ catch (error) {
102
+ this.logger.error('Failed to create WorkspaceService', {
103
+ error: error.message,
104
+ context
105
+ });
106
+ throw error;
107
+ }
108
+ }
109
+ /**
110
+ * Create TaskService instance
111
+ */
112
+ createTaskService(context) {
113
+ try {
114
+ const { apiKey, teamId } = this.getAuthParams(context);
115
+ const service = TaskService.withApiKey(apiKey, teamId);
116
+ this.logger.debug('TaskService created', {
117
+ authMethod: context.authMethod,
118
+ teamId
119
+ });
120
+ return service;
121
+ }
122
+ catch (error) {
123
+ this.logger.error('Failed to create TaskService', {
124
+ error: error.message,
125
+ context
126
+ });
127
+ throw error;
128
+ }
129
+ }
130
+ /**
131
+ * Create ListService instance
132
+ */
133
+ createListService(context) {
134
+ try {
135
+ const { apiKey, teamId } = this.getAuthParams(context);
136
+ const service = new ListService(apiKey, teamId);
137
+ this.logger.debug('ListService created', {
138
+ authMethod: context.authMethod,
139
+ teamId
140
+ });
141
+ return service;
142
+ }
143
+ catch (error) {
144
+ this.logger.error('Failed to create ListService', {
145
+ error: error.message,
146
+ context
147
+ });
148
+ throw error;
149
+ }
150
+ }
151
+ /**
152
+ * Create FolderService instance
153
+ */
154
+ createFolderService(context) {
155
+ try {
156
+ const { apiKey, teamId } = this.getAuthParams(context);
157
+ const service = new FolderService(apiKey, teamId);
158
+ this.logger.debug('FolderService created', {
159
+ authMethod: context.authMethod,
160
+ teamId
161
+ });
162
+ return service;
163
+ }
164
+ catch (error) {
165
+ this.logger.error('Failed to create FolderService', {
166
+ error: error.message,
167
+ context
168
+ });
169
+ throw error;
170
+ }
171
+ }
172
+ /**
173
+ * Create TimeService instance
174
+ */
175
+ createTimeService(context) {
176
+ try {
177
+ const authConfig = this.createAuthConfig(context);
178
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
179
+ const service = new TimeService(apiKey, authConfig.teamId);
180
+ this.logger.debug('TimeService created', {
181
+ authMethod: context.authMethod,
182
+ teamId: authConfig.teamId
183
+ });
184
+ return service;
185
+ }
186
+ catch (error) {
187
+ this.logger.error('Failed to create TimeService', {
188
+ error: error.message,
189
+ context
190
+ });
191
+ throw error;
192
+ }
193
+ }
194
+ /**
195
+ * Create TagService instance
196
+ */
197
+ createTagService(context) {
198
+ try {
199
+ const authConfig = this.createAuthConfig(context);
200
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
201
+ const service = new TagService(apiKey, authConfig.teamId);
202
+ this.logger.debug('TagService created', {
203
+ authMethod: context.authMethod,
204
+ teamId: authConfig.teamId
205
+ });
206
+ return service;
207
+ }
208
+ catch (error) {
209
+ this.logger.error('Failed to create TagService', {
210
+ error: error.message,
211
+ context
212
+ });
213
+ throw error;
214
+ }
215
+ }
216
+ /**
217
+ * Create BulkService instance
218
+ */
219
+ createBulkService(context) {
220
+ try {
221
+ // BulkService needs a TaskService instance
222
+ const taskService = this.createTaskService(context);
223
+ const service = new BulkService(taskService);
224
+ this.logger.debug('BulkService created', {
225
+ authMethod: context.authMethod,
226
+ teamId: context.teamId
227
+ });
228
+ return service;
229
+ }
230
+ catch (error) {
231
+ this.logger.error('Failed to create BulkService', {
232
+ error: error.message,
233
+ context
234
+ });
235
+ throw error;
236
+ }
237
+ }
238
+ /**
239
+ * Create DocumentService instance
240
+ */
241
+ createDocumentService(context) {
242
+ try {
243
+ const { apiKey, teamId } = this.getAuthParams(context);
244
+ const service = new DocumentService(apiKey, teamId);
245
+ this.logger.debug('DocumentService created', {
246
+ authMethod: context.authMethod,
247
+ teamId
248
+ });
249
+ return service;
250
+ }
251
+ catch (error) {
252
+ this.logger.error('Failed to create DocumentService', {
253
+ error: error.message,
254
+ context
255
+ });
256
+ throw error;
257
+ }
258
+ }
259
+ /**
260
+ * Create service context from API key authentication (backward compatibility)
261
+ */
262
+ static createApiKeyContext(apiKey, teamId) {
263
+ return {
264
+ authMethod: 'api_key',
265
+ apiKey: apiKey || config.clickupApiKey,
266
+ teamId: teamId || config.clickupTeamId
267
+ };
268
+ }
269
+ /**
270
+ * Create service context from OAuth2 authentication
271
+ */
272
+ static createOAuth2Context(accessToken, teamId, userId, clickupUserId) {
273
+ return {
274
+ authMethod: 'oauth2',
275
+ accessToken,
276
+ teamId,
277
+ userId,
278
+ clickupUserId
279
+ };
280
+ }
281
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp Service Factory
6
+ *
7
+ * Creates ClickUp service instances with appropriate authentication
8
+ * based on the request context (OAuth2 or API key).
9
+ */
10
+ import { WorkspaceService } from './workspace.js';
11
+ import { TaskService } from './task/task-service.js';
12
+ import { ListService } from './list.js';
13
+ import { FolderService } from './folder.js';
14
+ import { TimeService } from './time.js';
15
+ import { TagService } from './tag.js';
16
+ import { BulkService } from './bulk.js';
17
+ import { DocumentService } from './document.js';
18
+ import { Logger } from '../../logger.js';
19
+ import config from '../../config.js';
20
+ /**
21
+ * Factory class for creating ClickUp services with appropriate authentication
22
+ */
23
+ export class ClickUpServiceFactory {
24
+ constructor() {
25
+ this.logger = new Logger('ClickUpServiceFactory');
26
+ }
27
+ /**
28
+ * Create authentication configuration from service context
29
+ * @private
30
+ */
31
+ createAuthConfig(context) {
32
+ if (context.authMethod === 'oauth2') {
33
+ if (!context.accessToken) {
34
+ throw new Error('OAuth2 access token is required but not provided');
35
+ }
36
+ if (!context.teamId) {
37
+ throw new Error('Team ID is required for OAuth2 authentication');
38
+ }
39
+ return {
40
+ type: 'oauth2',
41
+ accessToken: context.accessToken,
42
+ teamId: context.teamId
43
+ };
44
+ }
45
+ else {
46
+ // API key authentication
47
+ const apiKey = context.apiKey || config.clickupApiKey;
48
+ const teamId = context.teamId || config.clickupTeamId;
49
+ if (!apiKey) {
50
+ throw new Error('API key is required but not provided');
51
+ }
52
+ if (!teamId) {
53
+ throw new Error('Team ID is required but not provided');
54
+ }
55
+ return {
56
+ type: 'api_key',
57
+ apiKey,
58
+ teamId
59
+ };
60
+ }
61
+ }
62
+ /**
63
+ * Create WorkspaceService instance
64
+ */
65
+ createWorkspaceService(context) {
66
+ try {
67
+ const authConfig = this.createAuthConfig(context);
68
+ const service = new WorkspaceService(authConfig);
69
+ this.logger.debug('WorkspaceService created', {
70
+ authMethod: context.authMethod,
71
+ teamId: authConfig.teamId
72
+ });
73
+ return {
74
+ success: true,
75
+ service
76
+ };
77
+ }
78
+ catch (error) {
79
+ this.logger.error('Failed to create WorkspaceService', {
80
+ error: error.message,
81
+ context
82
+ });
83
+ return {
84
+ success: false,
85
+ error: {
86
+ message: error.message,
87
+ code: 'SERVICE_CREATION_FAILED'
88
+ }
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Create TaskService instance
94
+ */
95
+ createTaskService(context) {
96
+ try {
97
+ const authConfig = this.createAuthConfig(context);
98
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
99
+ // Create workspace service first
100
+ const workspaceService = new WorkspaceService(authConfig);
101
+ const service = new TaskService(apiKey, authConfig.teamId, undefined, workspaceService);
102
+ this.logger.debug('TaskService created', {
103
+ authMethod: context.authMethod,
104
+ teamId: authConfig.teamId
105
+ });
106
+ return {
107
+ success: true,
108
+ service
109
+ };
110
+ }
111
+ catch (error) {
112
+ this.logger.error('Failed to create TaskService', {
113
+ error: error.message,
114
+ context
115
+ });
116
+ return {
117
+ success: false,
118
+ error: {
119
+ message: error.message,
120
+ code: 'SERVICE_CREATION_FAILED'
121
+ }
122
+ };
123
+ }
124
+ }
125
+ /**
126
+ * Create ListService instance
127
+ */
128
+ createListService(context) {
129
+ try {
130
+ const authConfig = this.createAuthConfig(context);
131
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
132
+ const service = new ListService(apiKey, authConfig.teamId);
133
+ this.logger.debug('ListService created', {
134
+ authMethod: context.authMethod,
135
+ teamId: authConfig.teamId
136
+ });
137
+ return {
138
+ success: true,
139
+ service
140
+ };
141
+ }
142
+ catch (error) {
143
+ this.logger.error('Failed to create ListService', {
144
+ error: error.message,
145
+ context
146
+ });
147
+ return {
148
+ success: false,
149
+ error: {
150
+ message: error.message,
151
+ code: 'SERVICE_CREATION_FAILED'
152
+ }
153
+ };
154
+ }
155
+ }
156
+ /**
157
+ * Create FolderService instance
158
+ */
159
+ createFolderService(context) {
160
+ try {
161
+ const authConfig = this.createAuthConfig(context);
162
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
163
+ const service = new FolderService(apiKey, authConfig.teamId);
164
+ this.logger.debug('FolderService created', {
165
+ authMethod: context.authMethod,
166
+ teamId: authConfig.teamId
167
+ });
168
+ return {
169
+ success: true,
170
+ service
171
+ };
172
+ }
173
+ catch (error) {
174
+ this.logger.error('Failed to create FolderService', {
175
+ error: error.message,
176
+ context
177
+ });
178
+ return {
179
+ success: false,
180
+ error: {
181
+ message: error.message,
182
+ code: 'SERVICE_CREATION_FAILED'
183
+ }
184
+ };
185
+ }
186
+ }
187
+ /**
188
+ * Create TimeService instance
189
+ */
190
+ createTimeService(context) {
191
+ try {
192
+ const authConfig = this.createAuthConfig(context);
193
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
194
+ const service = new TimeService(apiKey, authConfig.teamId);
195
+ this.logger.debug('TimeService created', {
196
+ authMethod: context.authMethod,
197
+ teamId: authConfig.teamId
198
+ });
199
+ return {
200
+ success: true,
201
+ service
202
+ };
203
+ }
204
+ catch (error) {
205
+ this.logger.error('Failed to create TimeService', {
206
+ error: error.message,
207
+ context
208
+ });
209
+ return {
210
+ success: false,
211
+ error: {
212
+ message: error.message,
213
+ code: 'SERVICE_CREATION_FAILED'
214
+ }
215
+ };
216
+ }
217
+ }
218
+ /**
219
+ * Create TagService instance
220
+ */
221
+ createTagService(context) {
222
+ try {
223
+ const authConfig = this.createAuthConfig(context);
224
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
225
+ const service = new TagService(apiKey, authConfig.teamId);
226
+ this.logger.debug('TagService created', {
227
+ authMethod: context.authMethod,
228
+ teamId: authConfig.teamId
229
+ });
230
+ return {
231
+ success: true,
232
+ service
233
+ };
234
+ }
235
+ catch (error) {
236
+ this.logger.error('Failed to create TagService', {
237
+ error: error.message,
238
+ context
239
+ });
240
+ return {
241
+ success: false,
242
+ error: {
243
+ message: error.message,
244
+ code: 'SERVICE_CREATION_FAILED'
245
+ }
246
+ };
247
+ }
248
+ }
249
+ /**
250
+ * Create BulkService instance
251
+ */
252
+ createBulkService(context) {
253
+ try {
254
+ // BulkService needs a TaskService instance
255
+ const taskServiceResult = this.createTaskService(context);
256
+ if (!taskServiceResult.success) {
257
+ return {
258
+ success: false,
259
+ error: taskServiceResult.error
260
+ };
261
+ }
262
+ const service = new BulkService(taskServiceResult.service);
263
+ this.logger.debug('BulkService created', {
264
+ authMethod: context.authMethod,
265
+ teamId: context.teamId
266
+ });
267
+ return {
268
+ success: true,
269
+ service
270
+ };
271
+ }
272
+ catch (error) {
273
+ this.logger.error('Failed to create BulkService', {
274
+ error: error.message,
275
+ context
276
+ });
277
+ return {
278
+ success: false,
279
+ error: {
280
+ message: error.message,
281
+ code: 'SERVICE_CREATION_FAILED'
282
+ }
283
+ };
284
+ }
285
+ }
286
+ /**
287
+ * Create DocumentService instance
288
+ */
289
+ createDocumentService(context) {
290
+ try {
291
+ const authConfig = this.createAuthConfig(context);
292
+ const apiKey = authConfig.type === 'api_key' ? authConfig.apiKey : authConfig.accessToken;
293
+ const service = new DocumentService(apiKey, authConfig.teamId);
294
+ this.logger.debug('DocumentService created', {
295
+ authMethod: context.authMethod,
296
+ teamId: authConfig.teamId
297
+ });
298
+ return {
299
+ success: true,
300
+ service
301
+ };
302
+ }
303
+ catch (error) {
304
+ this.logger.error('Failed to create DocumentService', {
305
+ error: error.message,
306
+ context
307
+ });
308
+ return {
309
+ success: false,
310
+ error: {
311
+ message: error.message,
312
+ code: 'SERVICE_CREATION_FAILED'
313
+ }
314
+ };
315
+ }
316
+ }
317
+ /**
318
+ * Create service context from API key authentication (backward compatibility)
319
+ */
320
+ static createApiKeyContext(apiKey, teamId) {
321
+ return {
322
+ authMethod: 'api_key',
323
+ apiKey: apiKey || config.clickupApiKey,
324
+ teamId: teamId || config.clickupTeamId
325
+ };
326
+ }
327
+ /**
328
+ * Create service context from OAuth2 authentication
329
+ */
330
+ static createOAuth2Context(accessToken, teamId, userId, clickupUserId) {
331
+ return {
332
+ authMethod: 'oauth2',
333
+ accessToken,
334
+ teamId,
335
+ userId,
336
+ clickupUserId
337
+ };
338
+ }
339
+ }
@@ -8,12 +8,20 @@
8
8
  * - Uploading file attachments from base64/buffer data
9
9
  * - Uploading file attachments from a URL (web URLs like http/https)
10
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.
11
14
  */
12
- import { TaskServiceSearch } from './task-search.js';
13
15
  /**
14
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.
15
20
  */
16
- export class TaskServiceAttachments extends TaskServiceSearch {
21
+ export class TaskServiceAttachments {
22
+ constructor(core) {
23
+ this.core = core;
24
+ }
17
25
  /**
18
26
  * Upload a file attachment to a ClickUp task
19
27
  * @param taskId The ID of the task to attach the file to
@@ -22,9 +30,9 @@ export class TaskServiceAttachments extends TaskServiceSearch {
22
30
  * @returns Promise resolving to the attachment response from ClickUp
23
31
  */
24
32
  async uploadTaskAttachment(taskId, fileData, fileName) {
25
- this.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
33
+ this.core.logOperation('uploadTaskAttachment', { taskId, fileName, fileSize: fileData.length });
26
34
  try {
27
- return await this.makeRequest(async () => {
35
+ return await this.core.makeRequest(async () => {
28
36
  // Create FormData for multipart/form-data upload
29
37
  const FormData = (await import('form-data')).default;
30
38
  const formData = new FormData();
@@ -34,17 +42,17 @@ export class TaskServiceAttachments extends TaskServiceSearch {
34
42
  contentType: 'application/octet-stream' // Let ClickUp determine the content type
35
43
  });
36
44
  // Use the raw axios client for this request since we need to handle FormData
37
- const response = await this.client.post(`/task/${taskId}/attachment`, formData, {
45
+ const response = await this.core.client.post(`/task/${taskId}/attachment`, formData, {
38
46
  headers: {
39
47
  ...formData.getHeaders(),
40
- 'Authorization': this.apiKey
48
+ 'Authorization': this.core.apiKey
41
49
  }
42
50
  });
43
51
  return response.data;
44
52
  });
45
53
  }
46
54
  catch (error) {
47
- throw this.handleError(error, `Failed to upload attachment to task ${taskId}`);
55
+ throw this.core.handleError(error, `Failed to upload attachment to task ${taskId}`);
48
56
  }
49
57
  }
50
58
  /**
@@ -56,9 +64,9 @@ export class TaskServiceAttachments extends TaskServiceSearch {
56
64
  * @returns Promise resolving to the attachment response from ClickUp
57
65
  */
58
66
  async uploadTaskAttachmentFromUrl(taskId, fileUrl, fileName, authHeader) {
59
- this.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
67
+ this.core.logOperation('uploadTaskAttachmentFromUrl', { taskId, fileUrl, fileName });
60
68
  try {
61
- return await this.makeRequest(async () => {
69
+ return await this.core.makeRequest(async () => {
62
70
  // Import required modules
63
71
  const axios = (await import('axios')).default;
64
72
  const FormData = (await import('form-data')).default;
@@ -81,17 +89,17 @@ export class TaskServiceAttachments extends TaskServiceSearch {
81
89
  contentType: 'application/octet-stream'
82
90
  });
83
91
  // Upload the file to ClickUp
84
- const uploadResponse = await this.client.post(`/task/${taskId}/attachment`, formData, {
92
+ const uploadResponse = await this.core.client.post(`/task/${taskId}/attachment`, formData, {
85
93
  headers: {
86
94
  ...formData.getHeaders(),
87
- 'Authorization': this.apiKey
95
+ 'Authorization': this.core.apiKey
88
96
  }
89
97
  });
90
98
  return uploadResponse.data;
91
99
  });
92
100
  }
93
101
  catch (error) {
94
- throw this.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
102
+ throw this.core.handleError(error, `Failed to upload attachment from URL to task ${taskId}`);
95
103
  }
96
104
  }
97
105
  }