@taazkareem/clickup-mcp-server 0.4.72 → 0.4.74

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.
@@ -14,7 +14,6 @@ export class TaskService extends BaseClickUpService {
14
14
  constructor(apiKey, teamId, baseUrl, workspaceService) {
15
15
  super(apiKey, teamId, baseUrl);
16
16
  this.workspaceService = null;
17
- // Cache workspace service if provided
18
17
  if (workspaceService) {
19
18
  this.workspaceService = workspaceService;
20
19
  this.logOperation('constructor', { usingSharedWorkspaceService: true });
@@ -35,6 +34,122 @@ export class TaskService extends BaseClickUpService {
35
34
  }
36
35
  return new ClickUpServiceError(message || `Task service error: ${error.message}`, ErrorCode.UNKNOWN, error);
37
36
  }
37
+ /**
38
+ * Build URL parameters from task filters
39
+ * @param filters Task filters to convert to URL parameters
40
+ * @returns URLSearchParams object
41
+ */
42
+ buildTaskFilterParams(filters) {
43
+ const params = new URLSearchParams();
44
+ // Add all filters to the query parameters
45
+ if (filters.include_closed)
46
+ params.append('include_closed', String(filters.include_closed));
47
+ if (filters.subtasks)
48
+ params.append('subtasks', String(filters.subtasks));
49
+ if (filters.page)
50
+ params.append('page', String(filters.page));
51
+ if (filters.order_by)
52
+ params.append('order_by', filters.order_by);
53
+ if (filters.reverse)
54
+ params.append('reverse', String(filters.reverse));
55
+ // Array parameters
56
+ if (filters.statuses && filters.statuses.length > 0) {
57
+ filters.statuses.forEach(status => params.append('statuses[]', status));
58
+ }
59
+ if (filters.assignees && filters.assignees.length > 0) {
60
+ filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
61
+ }
62
+ // Date filters
63
+ if (filters.due_date_gt)
64
+ params.append('due_date_gt', String(filters.due_date_gt));
65
+ if (filters.due_date_lt)
66
+ params.append('due_date_lt', String(filters.due_date_lt));
67
+ if (filters.date_created_gt)
68
+ params.append('date_created_gt', String(filters.date_created_gt));
69
+ if (filters.date_created_lt)
70
+ params.append('date_created_lt', String(filters.date_created_lt));
71
+ if (filters.date_updated_gt)
72
+ params.append('date_updated_gt', String(filters.date_updated_gt));
73
+ if (filters.date_updated_lt)
74
+ params.append('date_updated_lt', String(filters.date_updated_lt));
75
+ // Handle custom fields if present
76
+ if (filters.custom_fields) {
77
+ Object.entries(filters.custom_fields).forEach(([key, value]) => {
78
+ params.append(`custom_fields[${key}]`, String(value));
79
+ });
80
+ }
81
+ return params;
82
+ }
83
+ /**
84
+ * Extract priority value from a task
85
+ * @param task The task to extract priority from
86
+ * @returns TaskPriority or null
87
+ */
88
+ extractPriorityValue(task) {
89
+ if (!task.priority || !task.priority.id) {
90
+ return null;
91
+ }
92
+ const priorityValue = parseInt(task.priority.id);
93
+ // Ensure it's in the valid range 1-4
94
+ if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
95
+ return null;
96
+ }
97
+ return priorityValue;
98
+ }
99
+ /**
100
+ * Extract task data for creation/duplication
101
+ * @param task The source task
102
+ * @param nameOverride Optional override for the task name
103
+ * @returns CreateTaskData object
104
+ */
105
+ extractTaskData(task, nameOverride) {
106
+ return {
107
+ name: nameOverride || task.name,
108
+ description: task.description || '',
109
+ status: task.status?.status,
110
+ priority: this.extractPriorityValue(task),
111
+ due_date: task.due_date ? Number(task.due_date) : undefined,
112
+ assignees: task.assignees?.map(a => a.id) || []
113
+ };
114
+ }
115
+ /**
116
+ * Find a matching task by name using different matching strategies
117
+ * @param tasks List of tasks to search
118
+ * @param taskName Name to search for
119
+ * @returns Matching task or null
120
+ */
121
+ findMatchingTask(tasks, taskName) {
122
+ // Normalize the search term
123
+ const normalizedSearchTerm = taskName.toLowerCase().trim();
124
+ // First try exact match
125
+ let matchingTask = tasks.find(task => task.name.toLowerCase().trim() === normalizedSearchTerm);
126
+ // If no exact match, try substring match
127
+ if (!matchingTask) {
128
+ matchingTask = tasks.find(task => task.name.toLowerCase().trim().includes(normalizedSearchTerm) ||
129
+ normalizedSearchTerm.includes(task.name.toLowerCase().trim()));
130
+ }
131
+ // If still no match and there are emoji characters, try matching without emoji
132
+ if (!matchingTask && /[\p{Emoji}]/u.test(normalizedSearchTerm)) {
133
+ matchingTask = this.findMatchingTaskWithoutEmoji(tasks, normalizedSearchTerm);
134
+ }
135
+ return matchingTask || null;
136
+ }
137
+ /**
138
+ * Find matching task with emoji characters removed
139
+ * @param tasks List of tasks to search
140
+ * @param searchTerm Search term (with emoji)
141
+ * @returns Matching task or null
142
+ */
143
+ findMatchingTaskWithoutEmoji(tasks, searchTerm) {
144
+ // Remove emoji and try again (simple approximation)
145
+ const withoutEmoji = searchTerm.replace(/[\p{Emoji}]/gu, '').trim();
146
+ return tasks.find(task => {
147
+ const taskNameWithoutEmoji = task.name.toLowerCase().replace(/[\p{Emoji}]/gu, '').trim();
148
+ return taskNameWithoutEmoji === withoutEmoji ||
149
+ taskNameWithoutEmoji.includes(withoutEmoji) ||
150
+ withoutEmoji.includes(taskNameWithoutEmoji);
151
+ }) || null;
152
+ }
38
153
  /**
39
154
  * Create a new task in the specified list
40
155
  * @param listId The ID of the list to create the task in
@@ -63,42 +178,7 @@ export class TaskService extends BaseClickUpService {
63
178
  this.logOperation('getTasks', { listId, filters });
64
179
  try {
65
180
  return await this.makeRequest(async () => {
66
- const params = new URLSearchParams();
67
- // Add all filters to the query parameters
68
- if (filters.include_closed)
69
- params.append('include_closed', String(filters.include_closed));
70
- if (filters.subtasks)
71
- params.append('subtasks', String(filters.subtasks));
72
- if (filters.page)
73
- params.append('page', String(filters.page));
74
- if (filters.order_by)
75
- params.append('order_by', filters.order_by);
76
- if (filters.reverse)
77
- params.append('reverse', String(filters.reverse));
78
- if (filters.statuses && filters.statuses.length > 0) {
79
- filters.statuses.forEach(status => params.append('statuses[]', status));
80
- }
81
- if (filters.assignees && filters.assignees.length > 0) {
82
- filters.assignees.forEach(assignee => params.append('assignees[]', assignee));
83
- }
84
- if (filters.due_date_gt)
85
- params.append('due_date_gt', String(filters.due_date_gt));
86
- if (filters.due_date_lt)
87
- params.append('due_date_lt', String(filters.due_date_lt));
88
- if (filters.date_created_gt)
89
- params.append('date_created_gt', String(filters.date_created_gt));
90
- if (filters.date_created_lt)
91
- params.append('date_created_lt', String(filters.date_created_lt));
92
- if (filters.date_updated_gt)
93
- params.append('date_updated_gt', String(filters.date_updated_gt));
94
- if (filters.date_updated_lt)
95
- params.append('date_updated_lt', String(filters.date_updated_lt));
96
- // Handle custom fields if present
97
- if (filters.custom_fields) {
98
- Object.entries(filters.custom_fields).forEach(([key, value]) => {
99
- params.append(`custom_fields[${key}]`, String(value));
100
- });
101
- }
181
+ const params = this.buildTaskFilterParams(filters);
102
182
  const response = await this.client.get(`/list/${listId}/task?${params.toString()}`);
103
183
  return response.data.tasks;
104
184
  });
@@ -171,27 +251,7 @@ export class TaskService extends BaseClickUpService {
171
251
  this.logOperation('findTaskByName', { listId, taskName });
172
252
  try {
173
253
  const tasks = await this.getTasks(listId);
174
- // Normalize the search term
175
- const normalizedSearchTerm = taskName.toLowerCase().trim();
176
- // First try exact match
177
- let matchingTask = tasks.find(task => task.name.toLowerCase().trim() === normalizedSearchTerm);
178
- // If no exact match, try substring match
179
- if (!matchingTask) {
180
- matchingTask = tasks.find(task => task.name.toLowerCase().trim().includes(normalizedSearchTerm) ||
181
- normalizedSearchTerm.includes(task.name.toLowerCase().trim()));
182
- }
183
- // If still no match and there are emoji characters, try matching without emoji
184
- if (!matchingTask && /[\p{Emoji}]/u.test(normalizedSearchTerm)) {
185
- // Remove emoji and try again (simple approximation)
186
- const withoutEmoji = normalizedSearchTerm.replace(/[\p{Emoji}]/gu, '').trim();
187
- matchingTask = tasks.find(task => {
188
- const taskNameWithoutEmoji = task.name.toLowerCase().replace(/[\p{Emoji}]/gu, '').trim();
189
- return taskNameWithoutEmoji === withoutEmoji ||
190
- taskNameWithoutEmoji.includes(withoutEmoji) ||
191
- withoutEmoji.includes(taskNameWithoutEmoji);
192
- });
193
- }
194
- return matchingTask || null;
254
+ return this.findMatchingTask(tasks, taskName);
195
255
  }
196
256
  catch (error) {
197
257
  throw this.handleError(error, `Failed to find task by name: ${error instanceof Error ? error.message : String(error)}`);
@@ -217,28 +277,9 @@ export class TaskService extends BaseClickUpService {
217
277
  let newStatus = availableStatuses.includes(currentStatus || '')
218
278
  ? currentStatus // Keep the same status if available in destination list
219
279
  : destinationList.statuses?.[0]?.status; // Otherwise use the default (first) status
220
- // Priority mapping: convert string priority to numeric value if needed
221
- let priorityValue = null;
222
- if (originalTask.priority) {
223
- // If priority.id exists and is numeric, use that
224
- if (originalTask.priority.id) {
225
- priorityValue = parseInt(originalTask.priority.id);
226
- // Ensure it's in the valid range 1-4
227
- if (isNaN(priorityValue) || priorityValue < 1 || priorityValue > 4) {
228
- priorityValue = null;
229
- }
230
- }
231
- }
232
280
  // Prepare the task data for the new list
233
- const taskData = {
234
- name: originalTask.name,
235
- description: originalTask.description,
236
- status: newStatus,
237
- priority: priorityValue,
238
- due_date: originalTask.due_date ? Number(originalTask.due_date) : undefined,
239
- assignees: originalTask.assignees?.map(a => a.id) || [],
240
- // Add any other relevant fields from the original task
241
- };
281
+ const taskData = this.extractTaskData(originalTask);
282
+ taskData.status = newStatus;
242
283
  // Create new task and delete old one in a single makeRequest call
243
284
  return await this.makeRequest(async () => {
244
285
  // First create the new task
@@ -269,15 +310,8 @@ export class TaskService extends BaseClickUpService {
269
310
  try {
270
311
  // Get the original task to duplicate
271
312
  const originalTask = await this.getTask(taskId);
272
- // Create a copy of the task data
273
- const newTaskData = {
274
- name: `${originalTask.name} (copy)`,
275
- description: originalTask.description || '',
276
- status: originalTask.status?.status,
277
- priority: originalTask.priority?.id ? parseInt(originalTask.priority.id) : null,
278
- due_date: originalTask.due_date ? new Date(originalTask.due_date).getTime() : undefined,
279
- assignees: originalTask.assignees?.map(a => a.id) || []
280
- };
313
+ // Create a copy of the task data with "(copy)" appended to the name
314
+ const newTaskData = this.extractTaskData(originalTask, `${originalTask.name} (copy)`);
281
315
  // Create the new task in the specified list or original list
282
316
  const targetListId = listId || originalTask.list.id;
283
317
  return await this.createTask(targetListId, newTaskData);
@@ -1,4 +1,19 @@
1
1
  /**
2
2
  * Common type definitions for ClickUp API entities
3
3
  */
4
- export {};
4
+ // Helper function to validate and convert priority values
5
+ export function toTaskPriority(value) {
6
+ if (value === null)
7
+ return null;
8
+ if (value === undefined)
9
+ return undefined;
10
+ if (value === "null")
11
+ return null;
12
+ // Convert string to number if needed
13
+ const numValue = typeof value === 'string' ? parseInt(value, 10) : value;
14
+ // Validate it's a valid priority number
15
+ if (typeof numValue === 'number' && !isNaN(numValue) && [1, 2, 3, 4].includes(numValue)) {
16
+ return numValue;
17
+ }
18
+ return undefined;
19
+ }
@@ -8,26 +8,26 @@ import { createClickUpServices } from './clickup/index.js';
8
8
  import config from '../config.js';
9
9
  import { Logger } from '../logger.js';
10
10
  const logger = new Logger('SharedServices');
11
- // Singleton instance
12
- let servicesInstance = null;
11
+ // Singleton instances
12
+ let clickUpServicesInstance = null;
13
13
  /**
14
14
  * Get or create the ClickUp services instance
15
15
  */
16
16
  function getClickUpServices() {
17
- if (!servicesInstance) {
17
+ if (!clickUpServicesInstance) {
18
18
  logger.info('Creating shared ClickUp services singleton');
19
19
  // Create the services instance
20
- servicesInstance = createClickUpServices({
20
+ clickUpServicesInstance = createClickUpServices({
21
21
  apiKey: config.clickupApiKey,
22
22
  teamId: config.clickupTeamId
23
23
  });
24
24
  // Log what services were initialized with more clarity
25
25
  logger.info('Services initialization complete', {
26
- services: Object.keys(servicesInstance).join(', '),
26
+ services: Object.keys(clickUpServicesInstance).join(', '),
27
27
  teamId: config.clickupTeamId
28
28
  });
29
29
  }
30
- return servicesInstance;
30
+ return clickUpServicesInstance;
31
31
  }
32
32
  // Create a single instance of ClickUp services to be shared
33
33
  export const clickUpServices = getClickUpServices();
@@ -5,6 +5,7 @@
5
5
  * updating, and deleting folders in the ClickUp workspace hierarchy.
6
6
  */
7
7
  import { clickUpServices } from '../services/shared.js';
8
+ import { sponsorService } from '../utils/sponsor-service.js';
8
9
  // Use shared services instance
9
10
  const { folder: folderService, workspace: workspaceService } = clickUpServices;
10
11
  /**
@@ -12,7 +13,19 @@ const { folder: folderService, workspace: workspaceService } = clickUpServices;
12
13
  */
13
14
  export const createFolderTool = {
14
15
  name: "create_folder",
15
- description: "Create a new folder in a ClickUp space for organizing related lists. You MUST provide:\n1. A folder name\n2. Either spaceId (preferred) or spaceName\n\nAfter creating a folder, you can add lists to it using create_list_in_folder.",
16
+ description: `Purpose: Create a new folder in a ClickUp space for organizing related lists.
17
+
18
+ Valid Usage:
19
+ 1. Provide spaceId (preferred) + folder name
20
+ 2. Provide spaceName + folder name
21
+
22
+ Requirements:
23
+ - name: REQUIRED
24
+ - EITHER spaceId OR spaceName: REQUIRED
25
+
26
+ Notes:
27
+ - After creating a folder, you can add lists to it using create_list_in_folder
28
+ - Use override_statuses to set folder-specific statuses`,
16
29
  inputSchema: {
17
30
  type: "object",
18
31
  properties: {
@@ -41,7 +54,18 @@ export const createFolderTool = {
41
54
  */
42
55
  export const getFolderTool = {
43
56
  name: "get_folder",
44
- description: "Retrieve details about a specific folder including name, status, and metadata. Valid parameter combinations:\n1. Use folderId alone (preferred)\n2. Use folderName + (spaceId or spaceName)\n\nHelps you understand folder structure before creating or updating lists.",
57
+ description: `Purpose: Retrieve details about a specific folder including name, status, and metadata.
58
+
59
+ Valid Usage:
60
+ 1. Use folderId alone (preferred)
61
+ 2. Use folderName + (spaceId or spaceName)
62
+
63
+ Requirements:
64
+ - EITHER folderId OR (folderName + space information) is REQUIRED
65
+ - When using folderName, you MUST provide EITHER spaceId OR spaceName
66
+
67
+ Notes:
68
+ - Helps you understand folder structure before creating or updating lists`,
45
69
  inputSchema: {
46
70
  type: "object",
47
71
  properties: {
@@ -70,7 +94,19 @@ export const getFolderTool = {
70
94
  */
71
95
  export const updateFolderTool = {
72
96
  name: "update_folder",
73
- description: "Modify an existing folder's properties. Valid parameter combinations:\n1. Use folderId alone (preferred)\n2. Use folderName + (spaceId or spaceName)\n\nAt least one update field (name or override_statuses) must be provided.",
97
+ description: `Purpose: Modify an existing folder's properties.
98
+
99
+ Valid Usage:
100
+ 1. Use folderId alone (preferred)
101
+ 2. Use folderName + (spaceId or spaceName)
102
+
103
+ Requirements:
104
+ - At least one update field (name or override_statuses) must be provided
105
+ - EITHER folderId OR (folderName + space information) is REQUIRED
106
+ - When using folderName, you MUST provide EITHER spaceId OR spaceName
107
+
108
+ Notes:
109
+ - Changes apply immediately to all lists within the folder`,
74
110
  inputSchema: {
75
111
  type: "object",
76
112
  properties: {
@@ -107,7 +143,20 @@ export const updateFolderTool = {
107
143
  */
108
144
  export const deleteFolderTool = {
109
145
  name: "delete_folder",
110
- description: "⚠️ PERMANENTLY DELETE a folder and all its contents. This action cannot be undone. Valid parameter combinations:\n1. Use folderId alone (preferred and safest)\n2. Use folderName + (spaceId or spaceName)\n\nWARNING: This will also delete all lists and tasks within the folder.",
146
+ description: `Purpose: PERMANENTLY DELETE a folder and all its contents.
147
+
148
+ Valid Usage:
149
+ 1. Use folderId alone (preferred and safest)
150
+ 2. Use folderName + (spaceId or spaceName)
151
+
152
+ Requirements:
153
+ - EITHER folderId OR (folderName + space information) is REQUIRED
154
+ - When using folderName, you MUST provide EITHER spaceId OR spaceName
155
+
156
+ ⚠️ CRITICAL WARNING:
157
+ - This action CANNOT be undone
158
+ - All lists and tasks within the folder will also be permanently deleted
159
+ - Using folderName is risky as names may not be unique across different spaces`,
111
160
  inputSchema: {
112
161
  type: "object",
113
162
  properties: {
@@ -163,23 +212,18 @@ export async function handleCreateFolder(parameters) {
163
212
  try {
164
213
  // Create the folder
165
214
  const newFolder = await folderService.createFolder(targetSpaceId, folderData);
166
- return {
167
- content: [{
168
- type: "text",
169
- text: JSON.stringify({
170
- id: newFolder.id,
171
- name: newFolder.name,
172
- space: {
173
- id: newFolder.space.id,
174
- name: newFolder.space.name
175
- },
176
- message: `Folder "${newFolder.name}" created successfully`
177
- }, null, 2)
178
- }]
179
- };
215
+ return sponsorService.createResponse({
216
+ id: newFolder.id,
217
+ name: newFolder.name,
218
+ space: {
219
+ id: newFolder.space.id,
220
+ name: newFolder.space.name
221
+ },
222
+ message: `Folder "${newFolder.name}" created successfully`
223
+ }, true);
180
224
  }
181
225
  catch (error) {
182
- throw new Error(`Failed to create folder: ${error.message}`);
226
+ return sponsorService.createErrorResponse(`Failed to create folder: ${error.message}`);
183
227
  }
184
228
  }
185
229
  /**
@@ -215,26 +259,17 @@ export async function handleGetFolder(parameters) {
215
259
  try {
216
260
  // Get the folder
217
261
  const folder = await folderService.getFolder(targetFolderId);
218
- return {
219
- content: [{
220
- type: "text",
221
- text: JSON.stringify({
222
- id: folder.id,
223
- name: folder.name,
224
- lists: folder.lists.map((list) => ({
225
- id: list.id,
226
- name: list.name
227
- })),
228
- space: {
229
- id: folder.space.id,
230
- name: folder.space.name
231
- }
232
- }, null, 2)
233
- }]
234
- };
262
+ return sponsorService.createResponse({
263
+ id: folder.id,
264
+ name: folder.name,
265
+ space: {
266
+ id: folder.space.id,
267
+ name: folder.space.name
268
+ }
269
+ }, true);
235
270
  }
236
271
  catch (error) {
237
- throw new Error(`Failed to retrieve folder: ${error.message}`);
272
+ return sponsorService.createErrorResponse(`Failed to retrieve folder: ${error.message}`);
238
273
  }
239
274
  }
240
275
  /**
@@ -280,23 +315,18 @@ export async function handleUpdateFolder(parameters) {
280
315
  try {
281
316
  // Update the folder
282
317
  const updatedFolder = await folderService.updateFolder(targetFolderId, updateData);
283
- return {
284
- content: [{
285
- type: "text",
286
- text: JSON.stringify({
287
- id: updatedFolder.id,
288
- name: updatedFolder.name,
289
- space: {
290
- id: updatedFolder.space.id,
291
- name: updatedFolder.space.name
292
- },
293
- message: `Folder "${updatedFolder.name}" updated successfully`
294
- }, null, 2)
295
- }]
296
- };
318
+ return sponsorService.createResponse({
319
+ id: updatedFolder.id,
320
+ name: updatedFolder.name,
321
+ space: {
322
+ id: updatedFolder.space.id,
323
+ name: updatedFolder.space.name
324
+ },
325
+ message: `Folder "${updatedFolder.name}" updated successfully`
326
+ }, true);
297
327
  }
298
328
  catch (error) {
299
- throw new Error(`Failed to update folder: ${error.message}`);
329
+ return sponsorService.createErrorResponse(`Failed to update folder: ${error.message}`);
300
330
  }
301
331
  }
302
332
  /**
@@ -335,16 +365,12 @@ export async function handleDeleteFolder(parameters) {
335
365
  const folderName = folder.name;
336
366
  // Delete the folder
337
367
  await folderService.deleteFolder(targetFolderId);
338
- return {
339
- content: [{
340
- type: "text",
341
- text: JSON.stringify({
342
- message: `Folder "${folderName}" deleted successfully`
343
- }, null, 2)
344
- }]
345
- };
368
+ return sponsorService.createResponse({
369
+ success: true,
370
+ message: `Folder "${folderName || targetFolderId}" deleted successfully`
371
+ }, true);
346
372
  }
347
373
  catch (error) {
348
- throw new Error(`Failed to delete folder: ${error.message}`);
374
+ return sponsorService.createErrorResponse(`Failed to delete folder: ${error.message}`);
349
375
  }
350
376
  }
@@ -6,6 +6,6 @@
6
6
  * for better maintainability.
7
7
  */
8
8
  export * from './workspace.js';
9
- export * from './task.js';
9
+ export * from './task/index.js';
10
10
  export * from './list.js';
11
11
  export * from './folder.js';