@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,428 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * ClickUp MCP List Tools
6
+ *
7
+ * This module defines list-related tools including creating, updating,
8
+ * retrieving, and deleting lists. It supports creating lists both in spaces
9
+ * and in folders.
10
+ */
11
+ import { listService, workspaceService } from '../services/shared.js';
12
+ import config from '../config.js';
13
+ import { sponsorService } from '../utils/sponsor-service.js';
14
+ /**
15
+ * Tool definition for creating a list directly in a space
16
+ */
17
+ export const createListTool = {
18
+ name: "create_list",
19
+ description: `Creates a list in a ClickUp space. Use spaceId (preferred) or spaceName + list name. Name is required. For lists in folders, use create_list_in_folder. Optional: content, dueDate, priority, assignee, status.`,
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ name: {
24
+ type: "string",
25
+ description: "Name of the list"
26
+ },
27
+ spaceId: {
28
+ type: "string",
29
+ description: "ID of the space to create the list in. Use this instead of spaceName if you have the ID."
30
+ },
31
+ spaceName: {
32
+ type: "string",
33
+ description: "Name of the space to create the list in. Alternative to spaceId - one of them MUST be provided."
34
+ },
35
+ content: {
36
+ type: "string",
37
+ description: "Description or content of the list"
38
+ },
39
+ dueDate: {
40
+ type: "string",
41
+ description: "Due date for the list (Unix timestamp in milliseconds)"
42
+ },
43
+ priority: {
44
+ type: "number",
45
+ description: "Priority level: 1 (urgent), 2 (high), 3 (normal), 4 (low)"
46
+ },
47
+ assignee: {
48
+ type: "number",
49
+ description: "User ID to assign the list to"
50
+ },
51
+ status: {
52
+ type: "string",
53
+ description: "Status of the list"
54
+ }
55
+ },
56
+ required: ["name"]
57
+ }
58
+ };
59
+ /**
60
+ * Tool definition for creating a list within a folder
61
+ */
62
+ export const createListInFolderTool = {
63
+ name: "create_list_in_folder",
64
+ description: `Creates a list in a ClickUp folder. Use folderId (preferred) or folderName + space info + list name. Name is required. When using folderName, spaceId/spaceName required as folder names may not be unique. Optional: content, status.`,
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ name: {
69
+ type: "string",
70
+ description: "Name of the list"
71
+ },
72
+ folderId: {
73
+ type: "string",
74
+ description: "ID of the folder to create the list in. If you have this, you don't need folderName or space information."
75
+ },
76
+ folderName: {
77
+ type: "string",
78
+ description: "Name of the folder to create the list in. When using this, you MUST also provide either spaceName or spaceId."
79
+ },
80
+ spaceId: {
81
+ type: "string",
82
+ description: "ID of the space containing the folder. Required when using folderName instead of folderId."
83
+ },
84
+ spaceName: {
85
+ type: "string",
86
+ description: "Name of the space containing the folder. Required when using folderName instead of folderId."
87
+ },
88
+ content: {
89
+ type: "string",
90
+ description: "Description or content of the list"
91
+ },
92
+ status: {
93
+ type: "string",
94
+ description: "Status of the list (uses folder default if not specified)"
95
+ }
96
+ },
97
+ required: ["name"]
98
+ }
99
+ };
100
+ /**
101
+ * Tool definition for retrieving list details
102
+ */
103
+ export const getListTool = {
104
+ name: "get_list",
105
+ description: `Gets details of a ClickUp list. Use listId (preferred) or listName. Returns list details including name, content, and space info. ListId more reliable as names may not be unique.`,
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ listId: {
110
+ type: "string",
111
+ description: "ID of the list to retrieve. Use this instead of listName if you have the ID."
112
+ },
113
+ listName: {
114
+ type: "string",
115
+ description: "Name of the list to retrieve. May be ambiguous if multiple lists have the same name."
116
+ }
117
+ },
118
+ required: []
119
+ }
120
+ };
121
+ /**
122
+ * Tool definition for updating a list
123
+ */
124
+ export const updateListTool = {
125
+ name: "update_list",
126
+ description: `Updates a ClickUp list. Use listId (preferred) or listName + at least one update field (name/content/status). ListId more reliable as names may not be unique. Only specified fields updated.`,
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ listId: {
131
+ type: "string",
132
+ description: "ID of the list to update. Use this instead of listName if you have the ID."
133
+ },
134
+ listName: {
135
+ type: "string",
136
+ description: "Name of the list to update. May be ambiguous if multiple lists have the same name."
137
+ },
138
+ name: {
139
+ type: "string",
140
+ description: "New name for the list"
141
+ },
142
+ content: {
143
+ type: "string",
144
+ description: "New description or content for the list"
145
+ },
146
+ status: {
147
+ type: "string",
148
+ description: "New status for the list"
149
+ }
150
+ },
151
+ required: []
152
+ }
153
+ };
154
+ /**
155
+ * Tool definition for deleting a list
156
+ */
157
+ export const deleteListTool = {
158
+ name: "delete_list",
159
+ description: `PERMANENTLY deletes a ClickUp list and all its tasks. Use listId (preferred/safest) or listName. WARNING: Cannot be undone, all tasks will be deleted, listName risky if not unique.`,
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ listId: {
164
+ type: "string",
165
+ description: "ID of the list to delete. Use this instead of listName if you have the ID."
166
+ },
167
+ listName: {
168
+ type: "string",
169
+ description: "Name of the list to delete. May be ambiguous if multiple lists have the same name."
170
+ }
171
+ },
172
+ required: []
173
+ }
174
+ };
175
+ /**
176
+ * Helper function to find a list ID by name
177
+ * Uses the ClickUp service's global list search functionality
178
+ */
179
+ export async function findListIDByName(workspaceService, listName) {
180
+ // Use workspace service to find the list in the hierarchy
181
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
182
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
183
+ if (!listInfo)
184
+ return null;
185
+ return { id: listInfo.id, name: listName };
186
+ }
187
+ /**
188
+ * Handler for the create_list tool
189
+ * Creates a new list directly in a space
190
+ */
191
+ export async function handleCreateList(parameters) {
192
+ const { name, spaceId, spaceName, content, dueDate, priority, assignee, status } = parameters;
193
+ // Validate required fields
194
+ if (!name) {
195
+ throw new Error("List name is required");
196
+ }
197
+ let targetSpaceId = spaceId;
198
+ // If no spaceId but spaceName is provided, look up the space ID
199
+ if (!targetSpaceId && spaceName) {
200
+ const spaceIdResult = await workspaceService.findSpaceIDByName(spaceName);
201
+ if (!spaceIdResult) {
202
+ throw new Error(`Space "${spaceName}" not found`);
203
+ }
204
+ targetSpaceId = spaceIdResult;
205
+ }
206
+ if (!targetSpaceId) {
207
+ throw new Error("Either spaceId or spaceName must be provided");
208
+ }
209
+ // Prepare list data
210
+ const listData = {
211
+ name
212
+ };
213
+ // Add optional fields if provided
214
+ if (content)
215
+ listData.content = content;
216
+ if (dueDate)
217
+ listData.due_date = parseInt(dueDate);
218
+ if (priority)
219
+ listData.priority = priority;
220
+ if (assignee)
221
+ listData.assignee = assignee;
222
+ if (status)
223
+ listData.status = status;
224
+ try {
225
+ // Create the list
226
+ const newList = await listService.createList(targetSpaceId, listData);
227
+ return sponsorService.createResponse({
228
+ id: newList.id,
229
+ name: newList.name,
230
+ content: newList.content,
231
+ space: {
232
+ id: newList.space.id,
233
+ name: newList.space.name
234
+ },
235
+ url: `https://app.clickup.com/${config.clickupTeamId}/v/l/${newList.id}`,
236
+ message: `List "${name}" created successfully`
237
+ }, true);
238
+ }
239
+ catch (error) {
240
+ return sponsorService.createErrorResponse(`Failed to create list: ${error.message}`);
241
+ }
242
+ }
243
+ /**
244
+ * Handler for the create_list_in_folder tool
245
+ * Creates a new list inside a folder
246
+ */
247
+ export async function handleCreateListInFolder(parameters) {
248
+ const { name, folderId, folderName, spaceId, spaceName, content, status } = parameters;
249
+ // Validate required fields
250
+ if (!name) {
251
+ throw new Error("List name is required");
252
+ }
253
+ let targetFolderId = folderId;
254
+ // If no folderId but folderName is provided, look up the folder ID
255
+ if (!targetFolderId && folderName) {
256
+ let targetSpaceId = spaceId;
257
+ // If no spaceId provided but spaceName is, look up the space ID first
258
+ if (!targetSpaceId && spaceName) {
259
+ const spaceIdResult = await workspaceService.findSpaceByName(spaceName);
260
+ if (!spaceIdResult) {
261
+ throw new Error(`Space "${spaceName}" not found`);
262
+ }
263
+ targetSpaceId = spaceIdResult.id;
264
+ }
265
+ if (!targetSpaceId) {
266
+ throw new Error("When using folderName to identify a folder, you must also provide either spaceId or spaceName to locate the correct folder. This is because folder names might not be unique across different spaces.");
267
+ }
268
+ // Find the folder in the workspace hierarchy
269
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
270
+ const folderInfo = workspaceService.findIDByNameInHierarchy(hierarchy, folderName, 'folder');
271
+ if (!folderInfo) {
272
+ throw new Error(`Folder "${folderName}" not found in space`);
273
+ }
274
+ targetFolderId = folderInfo.id;
275
+ }
276
+ if (!targetFolderId) {
277
+ throw new Error("Either folderId or folderName must be provided");
278
+ }
279
+ // Prepare list data
280
+ const listData = {
281
+ name
282
+ };
283
+ // Add optional fields if provided
284
+ if (content)
285
+ listData.content = content;
286
+ if (status)
287
+ listData.status = status;
288
+ try {
289
+ // Create the list in the folder
290
+ const newList = await listService.createListInFolder(targetFolderId, listData);
291
+ return sponsorService.createResponse({
292
+ id: newList.id,
293
+ name: newList.name,
294
+ content: newList.content,
295
+ folder: {
296
+ id: newList.folder.id,
297
+ name: newList.folder.name
298
+ },
299
+ space: {
300
+ id: newList.space.id,
301
+ name: newList.space.name
302
+ },
303
+ url: `https://app.clickup.com/${config.clickupTeamId}/v/l/${newList.id}`,
304
+ message: `List "${name}" created successfully in folder "${newList.folder.name}"`
305
+ }, true);
306
+ }
307
+ catch (error) {
308
+ return sponsorService.createErrorResponse(`Failed to create list in folder: ${error.message}`);
309
+ }
310
+ }
311
+ /**
312
+ * Handler for the get_list tool
313
+ * Retrieves details about a specific list
314
+ */
315
+ export async function handleGetList(parameters) {
316
+ const { listId, listName } = parameters;
317
+ let targetListId = listId;
318
+ // If no listId provided but listName is, look up the list ID
319
+ if (!targetListId && listName) {
320
+ const listResult = await findListIDByName(workspaceService, listName);
321
+ if (!listResult) {
322
+ throw new Error(`List "${listName}" not found`);
323
+ }
324
+ targetListId = listResult.id;
325
+ }
326
+ if (!targetListId) {
327
+ throw new Error("Either listId or listName must be provided");
328
+ }
329
+ try {
330
+ // Get the list
331
+ const list = await listService.getList(targetListId);
332
+ return sponsorService.createResponse({
333
+ id: list.id,
334
+ name: list.name,
335
+ content: list.content,
336
+ space: {
337
+ id: list.space.id,
338
+ name: list.space.name
339
+ },
340
+ url: `https://app.clickup.com/${config.clickupTeamId}/v/l/${list.id}`
341
+ }, true);
342
+ }
343
+ catch (error) {
344
+ return sponsorService.createErrorResponse(`Failed to retrieve list: ${error.message}`);
345
+ }
346
+ }
347
+ /**
348
+ * Handler for the update_list tool
349
+ * Updates an existing list's properties
350
+ */
351
+ export async function handleUpdateList(parameters) {
352
+ const { listId, listName, name, content, status } = parameters;
353
+ let targetListId = listId;
354
+ // If no listId provided but listName is, look up the list ID
355
+ if (!targetListId && listName) {
356
+ const listResult = await findListIDByName(workspaceService, listName);
357
+ if (!listResult) {
358
+ throw new Error(`List "${listName}" not found`);
359
+ }
360
+ targetListId = listResult.id;
361
+ }
362
+ if (!targetListId) {
363
+ throw new Error("Either listId or listName must be provided");
364
+ }
365
+ // Ensure at least one update field is provided
366
+ if (!name && !content && !status) {
367
+ throw new Error("At least one of name, content, or status must be provided for update");
368
+ }
369
+ // Prepare update data
370
+ const updateData = {};
371
+ if (name)
372
+ updateData.name = name;
373
+ if (content)
374
+ updateData.content = content;
375
+ if (status)
376
+ updateData.status = status;
377
+ try {
378
+ // Update the list
379
+ const updatedList = await listService.updateList(targetListId, updateData);
380
+ return sponsorService.createResponse({
381
+ id: updatedList.id,
382
+ name: updatedList.name,
383
+ content: updatedList.content,
384
+ space: {
385
+ id: updatedList.space.id,
386
+ name: updatedList.space.name
387
+ },
388
+ url: `https://app.clickup.com/${config.clickupTeamId}/v/l/${updatedList.id}`,
389
+ message: `List "${updatedList.name}" updated successfully`
390
+ }, true);
391
+ }
392
+ catch (error) {
393
+ return sponsorService.createErrorResponse(`Failed to update list: ${error.message}`);
394
+ }
395
+ }
396
+ /**
397
+ * Handler for the delete_list tool
398
+ * Permanently removes a list from the workspace
399
+ */
400
+ export async function handleDeleteList(parameters) {
401
+ const { listId, listName } = parameters;
402
+ let targetListId = listId;
403
+ // If no listId provided but listName is, look up the list ID
404
+ if (!targetListId && listName) {
405
+ const listResult = await findListIDByName(workspaceService, listName);
406
+ if (!listResult) {
407
+ throw new Error(`List "${listName}" not found`);
408
+ }
409
+ targetListId = listResult.id;
410
+ }
411
+ if (!targetListId) {
412
+ throw new Error("Either listId or listName must be provided");
413
+ }
414
+ try {
415
+ // Get list details before deletion for confirmation message
416
+ const list = await listService.getList(targetListId);
417
+ const listName = list.name;
418
+ // Delete the list
419
+ await listService.deleteList(targetListId);
420
+ return sponsorService.createResponse({
421
+ success: true,
422
+ message: `List "${listName || targetListId}" deleted successfully`
423
+ }, true);
424
+ }
425
+ catch (error) {
426
+ return sponsorService.createErrorResponse(`Failed to delete list: ${error.message}`);
427
+ }
428
+ }
@@ -0,0 +1,106 @@
1
+ import { workspaceService } from '../services/shared.js';
2
+ import { sponsorService } from '../utils/sponsor-service.js';
3
+ /**
4
+ * Tool definition for getting all members in a ClickUp workspace
5
+ */
6
+ export const getWorkspaceMembersTool = {
7
+ name: 'get_workspace_members',
8
+ description: 'Returns all members (users) in the ClickUp workspace/team. Useful for resolving assignees by name or email.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {},
12
+ required: []
13
+ }
14
+ };
15
+ /**
16
+ * Tool definition for finding a member by name or email
17
+ */
18
+ export const findMemberByNameTool = {
19
+ name: 'find_member_by_name',
20
+ description: 'Finds a member in the ClickUp workspace by name or email. Returns the member object if found, or null if not found.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ nameOrEmail: {
25
+ type: 'string',
26
+ description: 'The name or email of the member to find.'
27
+ }
28
+ },
29
+ required: ['nameOrEmail']
30
+ }
31
+ };
32
+ /**
33
+ * Tool definition for resolving an array of assignee names/emails to ClickUp user IDs
34
+ */
35
+ export const resolveAssigneesTool = {
36
+ name: 'resolve_assignees',
37
+ description: 'Resolves an array of assignee names or emails to ClickUp user IDs. Returns an array of user IDs, or errors for any that cannot be resolved.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ assignees: {
42
+ type: 'array',
43
+ items: { type: 'string' },
44
+ description: 'Array of assignee names or emails to resolve.'
45
+ }
46
+ },
47
+ required: ['assignees']
48
+ }
49
+ };
50
+ /// src/tools/member.ts
51
+ /**
52
+ * Handler for get_workspace_members
53
+ */
54
+ export async function handleGetWorkspaceMembers() {
55
+ try {
56
+ const members = await workspaceService.getWorkspaceMembers();
57
+ return sponsorService.createResponse({ members }, true);
58
+ }
59
+ catch (error) {
60
+ const errorMessage = error instanceof Error ? error.message : String(error);
61
+ return sponsorService.createErrorResponse(`Failed to get workspace members: ${errorMessage}`);
62
+ }
63
+ }
64
+ /**
65
+ * Handler for find_member_by_name
66
+ */
67
+ export async function handleFindMemberByName(parameters) {
68
+ const { nameOrEmail } = parameters;
69
+ if (!nameOrEmail) {
70
+ throw new Error('nameOrEmail is required');
71
+ }
72
+ try {
73
+ const members = await workspaceService.getWorkspaceMembers();
74
+ const found = members.find((m) => m.email?.toLowerCase() === nameOrEmail.toLowerCase() ||
75
+ m.username?.toLowerCase() === nameOrEmail.toLowerCase() ||
76
+ m.name?.toLowerCase() === nameOrEmail.toLowerCase());
77
+ return sponsorService.createResponse({ member: found || null }, true);
78
+ }
79
+ catch (error) {
80
+ const errorMessage = error instanceof Error ? error.message : String(error);
81
+ return sponsorService.createErrorResponse(`Failed to find member: ${errorMessage}`);
82
+ }
83
+ }
84
+ /**
85
+ * Handler for resolve_assignees
86
+ */
87
+ export async function handleResolveAssignees(parameters) {
88
+ const { assignees } = parameters;
89
+ if (!Array.isArray(assignees)) {
90
+ throw new Error('assignees must be an array');
91
+ }
92
+ try {
93
+ const members = await workspaceService.getWorkspaceMembers();
94
+ const resolved = assignees.map((input) => {
95
+ const found = members.find((m) => m.email?.toLowerCase() === input.toLowerCase() ||
96
+ m.username?.toLowerCase() === input.toLowerCase() ||
97
+ m.name?.toLowerCase() === input.toLowerCase());
98
+ return found ? found.id : null;
99
+ });
100
+ return sponsorService.createResponse({ userIds: resolved }, true);
101
+ }
102
+ catch (error) {
103
+ const errorMessage = error instanceof Error ? error.message : String(error);
104
+ return sponsorService.createErrorResponse(`Failed to resolve assignees: ${errorMessage}`);
105
+ }
106
+ }