@taazkareem/clickup-mcp-server 0.4.62 → 0.4.64

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.
@@ -239,15 +239,6 @@ export class BaseClickUpService {
239
239
  * @param details - Details about the operation
240
240
  */
241
241
  logOperation(operation, details) {
242
- // This could be enhanced to use a proper logging framework
243
- console.log(`[ClickUpService] ${operation}:`, details);
244
- }
245
- /**
246
- * Protected helper method to check if cache is available
247
- * @param cacheService Optional cache service instance
248
- * @returns True if caching is available and enabled
249
- */
250
- isCacheEnabled(cacheService) {
251
- return !!cacheService && typeof cacheService.isEnabled === 'function' && cacheService.isEnabled();
242
+ console.log(`[${new Date().toISOString()}] ${operation}:`, details);
252
243
  }
253
244
  }
@@ -13,13 +13,11 @@ export { WorkspaceService } from './workspace.js';
13
13
  export { TaskService } from './task.js';
14
14
  export { ListService } from './list.js';
15
15
  export { FolderService } from './folder.js';
16
- export { InitializationService } from './initialization.js';
17
16
  // Import service classes for the factory function
18
17
  import { WorkspaceService } from './workspace.js';
19
18
  import { TaskService } from './task.js';
20
19
  import { ListService } from './list.js';
21
20
  import { FolderService } from './folder.js';
22
- import { InitializationService } from './initialization.js';
23
21
  /**
24
22
  * Factory function to create instances of all ClickUp services
25
23
  * @param config Configuration for the services
@@ -33,11 +31,6 @@ export function createClickUpServices(config) {
33
31
  workspace: workspaceService,
34
32
  task: new TaskService(apiKey, teamId, baseUrl, workspaceService),
35
33
  list: new ListService(apiKey, teamId, baseUrl, workspaceService),
36
- folder: new FolderService(apiKey, teamId, baseUrl, workspaceService),
37
- initialization: new InitializationService({
38
- apiKey,
39
- teamId,
40
- baseUrl
41
- })
34
+ folder: new FolderService(apiKey, teamId, baseUrl, workspaceService)
42
35
  };
43
36
  }
@@ -99,7 +99,7 @@ export class WorkspaceService extends BaseClickUpService {
99
99
  */
100
100
  async getWorkspaceHierarchy(forceRefresh = false) {
101
101
  try {
102
- // If we have a cached result and not forcing refresh, return it
102
+ // If we have the hierarchy in memory and not forcing refresh, return it
103
103
  if (this.workspaceHierarchy && !forceRefresh) {
104
104
  return this.workspaceHierarchy;
105
105
  }
@@ -164,9 +164,9 @@ export class WorkspaceService extends BaseClickUpService {
164
164
  }
165
165
  }
166
166
  /**
167
- * Invalidate the workspace hierarchy cache
167
+ * Clear the stored workspace hierarchy, forcing a fresh fetch on next request
168
168
  */
169
- invalidateWorkspaceCache() {
169
+ clearWorkspaceHierarchy() {
170
170
  this.workspaceHierarchy = null;
171
171
  }
172
172
  /**
@@ -8,7 +8,7 @@
8
8
  import { createClickUpServices } from '../services/clickup/index.js';
9
9
  import config from '../config.js';
10
10
  import { findListIDByName } from './list.js';
11
- import { parseDueDate } from './utils.js';
11
+ import { parseDueDate, formatDueDate } from './utils.js';
12
12
  // Initialize ClickUp services using the factory function
13
13
  const services = createClickUpServices({
14
14
  apiKey: config.clickupApiKey,
@@ -99,6 +99,7 @@ export const createTaskTool = {
99
99
  name: createdTask.name,
100
100
  url: createdTask.url,
101
101
  status: createdTask.status?.status || "New",
102
+ due_date: createdTask.due_date ? formatDueDate(Number(createdTask.due_date)) : undefined,
102
103
  list: createdTask.list.name,
103
104
  space: createdTask.space.name,
104
105
  folder: createdTask.folder?.name
@@ -211,7 +212,10 @@ export const updateTaskTool = {
211
212
  name: updatedTask.name,
212
213
  url: updatedTask.url,
213
214
  status: updatedTask.status?.status || "Unknown",
214
- updated: true
215
+ updated: true,
216
+ due_date: updatedTask.due_date ? formatDueDate(Number(updatedTask.due_date)) : undefined,
217
+ list: updatedTask.list.name,
218
+ folder: updatedTask.folder?.name
215
219
  }, null, 2)
216
220
  }]
217
221
  };
@@ -298,6 +302,7 @@ export const moveTaskTool = {
298
302
  name: movedTask.name,
299
303
  url: movedTask.url,
300
304
  status: movedTask.status?.status || "Unknown",
305
+ due_date: movedTask.due_date ? formatDueDate(Number(movedTask.due_date)) : undefined,
301
306
  list: movedTask.list.name,
302
307
  moved: true
303
308
  }, null, 2)
@@ -389,6 +394,7 @@ export const duplicateTaskTool = {
389
394
  name: task.name,
390
395
  url: task.url,
391
396
  duplicated: true,
397
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
392
398
  list: task.list.name,
393
399
  space: task.space.name,
394
400
  folder: task.folder?.name
@@ -457,7 +463,8 @@ export const getTaskTool = {
457
463
  description: task.description,
458
464
  status: task.status?.status || "Unknown",
459
465
  priority: task.priority,
460
- due_date: task.due_date,
466
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
467
+ due_date_raw: task.due_date, // Keep raw timestamp for reference if needed
461
468
  url: task.url,
462
469
  list: task.list.name,
463
470
  space: task.space.name,
@@ -543,21 +550,24 @@ export const getTasksTool = {
543
550
  };
544
551
  // Get tasks with filters
545
552
  const tasks = await taskService.getTasks(targetListId, filters);
553
+ // Format the tasks data to be more API friendly
554
+ const formattedTasks = tasks.map(task => ({
555
+ id: task.id,
556
+ name: task.name,
557
+ status: task.status?.status || 'Unknown',
558
+ url: task.url,
559
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
560
+ due_date_raw: task.due_date,
561
+ priority: task.priority?.priority,
562
+ assignees: task.assignees?.map(a => a.username) || []
563
+ }));
546
564
  // Format response
547
565
  return {
548
566
  content: [{
549
567
  type: "text",
550
568
  text: JSON.stringify({
551
- list_id: targetListId,
552
- task_count: tasks.length,
553
- tasks: tasks.map((task) => ({
554
- id: task.id,
555
- name: task.name,
556
- status: task.status?.status || "Unknown",
557
- priority: task.priority,
558
- due_date: task.due_date,
559
- url: task.url
560
- }))
569
+ total: tasks.length,
570
+ tasks: formattedTasks
561
571
  }, null, 2)
562
572
  }]
563
573
  };
@@ -974,6 +984,7 @@ export async function handleCreateTask(parameters) {
974
984
  name: task.name,
975
985
  url: task.url,
976
986
  status: task.status?.status || "New",
987
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
977
988
  list: task.list.name,
978
989
  space: task.space.name,
979
990
  folder: task.folder?.name
@@ -1041,8 +1052,8 @@ export async function handleUpdateTask(parameters) {
1041
1052
  url: task.url,
1042
1053
  status: task.status?.status || "Unknown",
1043
1054
  updated: true,
1055
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1044
1056
  list: task.list.name,
1045
- space: task.space.name,
1046
1057
  folder: task.folder?.name
1047
1058
  }, null, 2)
1048
1059
  }]
@@ -1108,6 +1119,7 @@ export async function handleMoveTask(parameters) {
1108
1119
  name: task.name,
1109
1120
  url: task.url,
1110
1121
  moved: true,
1122
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1111
1123
  list: task.list.name,
1112
1124
  space: task.space.name,
1113
1125
  folder: task.folder?.name
@@ -1171,6 +1183,7 @@ export async function handleDuplicateTask(parameters) {
1171
1183
  name: task.name,
1172
1184
  url: task.url,
1173
1185
  duplicated: true,
1186
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1174
1187
  list: task.list.name,
1175
1188
  space: task.space.name,
1176
1189
  folder: task.folder?.name
@@ -1206,21 +1219,24 @@ export async function handleGetTasks(parameters) {
1206
1219
  };
1207
1220
  // Get tasks with filters
1208
1221
  const tasks = await taskService.getTasks(targetListId, filters);
1222
+ // Format the tasks data to be more API friendly
1223
+ const formattedTasks = tasks.map(task => ({
1224
+ id: task.id,
1225
+ name: task.name,
1226
+ status: task.status?.status || 'Unknown',
1227
+ url: task.url,
1228
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1229
+ due_date_raw: task.due_date,
1230
+ priority: task.priority?.priority,
1231
+ assignees: task.assignees?.map(a => a.username) || []
1232
+ }));
1209
1233
  // Format response
1210
1234
  return {
1211
1235
  content: [{
1212
1236
  type: "text",
1213
1237
  text: JSON.stringify({
1214
- list_id: targetListId,
1215
- task_count: tasks.length,
1216
- tasks: tasks.map((task) => ({
1217
- id: task.id,
1218
- name: task.name,
1219
- status: task.status?.status || "Unknown",
1220
- priority: task.priority,
1221
- due_date: task.due_date,
1222
- url: task.url
1223
- }))
1238
+ total: tasks.length,
1239
+ tasks: formattedTasks
1224
1240
  }, null, 2)
1225
1241
  }]
1226
1242
  };
@@ -1260,7 +1276,24 @@ export async function handleGetTask(parameters) {
1260
1276
  return {
1261
1277
  content: [{
1262
1278
  type: "text",
1263
- text: JSON.stringify(task, null, 2)
1279
+ text: JSON.stringify({
1280
+ id: task.id,
1281
+ name: task.name,
1282
+ description: task.description,
1283
+ status: task.status?.status || "Unknown",
1284
+ priority: task.priority,
1285
+ due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1286
+ due_date_raw: task.due_date, // Keep raw timestamp for reference if needed
1287
+ url: task.url,
1288
+ list: task.list.name,
1289
+ space: task.space.name,
1290
+ folder: task.folder?.name,
1291
+ creator: task.creator,
1292
+ assignees: task.assignees,
1293
+ tags: task.tags,
1294
+ time_estimate: task.time_estimate,
1295
+ time_spent: task.time_spent,
1296
+ }, null, 2)
1264
1297
  }]
1265
1298
  };
1266
1299
  }
@@ -41,25 +41,52 @@ export function parseDueDate(dateString) {
41
41
  today.setHours(23, 59, 59, 999);
42
42
  return today.getTime();
43
43
  }
44
- if (lowerDate === 'tomorrow') {
45
- const tomorrow = new Date();
46
- tomorrow.setDate(tomorrow.getDate() + 1);
47
- tomorrow.setHours(23, 59, 59, 999);
48
- return tomorrow.getTime();
49
- }
50
- if (lowerDate.includes('next week')) {
51
- const nextWeek = new Date();
52
- nextWeek.setDate(nextWeek.getDate() + 7);
53
- nextWeek.setHours(23, 59, 59, 999);
54
- return nextWeek.getTime();
55
- }
56
- if (lowerDate.includes('next month')) {
57
- const nextMonth = new Date();
58
- nextMonth.setMonth(nextMonth.getMonth() + 1);
59
- nextMonth.setHours(23, 59, 59, 999);
60
- return nextMonth.getTime();
44
+ // Handle relative dates with specific times
45
+ const relativeTimeRegex = /(?:(\d+)\s*(days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
46
+ const match = lowerDate.match(relativeTimeRegex);
47
+ if (match) {
48
+ const date = new Date();
49
+ const [_, amount, unit, hours, minutes, meridian] = match;
50
+ // Calculate the future date
51
+ if (amount && unit) {
52
+ const value = parseInt(amount);
53
+ if (unit.startsWith('day')) {
54
+ date.setDate(date.getDate() + value);
55
+ }
56
+ else if (unit.startsWith('week')) {
57
+ date.setDate(date.getDate() + (value * 7));
58
+ }
59
+ else if (unit.startsWith('month')) {
60
+ date.setMonth(date.getMonth() + value);
61
+ }
62
+ }
63
+ else if (lowerDate.startsWith('tomorrow')) {
64
+ date.setDate(date.getDate() + 1);
65
+ }
66
+ else if (lowerDate.includes('next week')) {
67
+ date.setDate(date.getDate() + 7);
68
+ }
69
+ else if (lowerDate.includes('next month')) {
70
+ date.setMonth(date.getMonth() + 1);
71
+ }
72
+ // Set the time if specified
73
+ if (hours) {
74
+ let parsedHours = parseInt(hours);
75
+ const parsedMinutes = minutes ? parseInt(minutes) : 0;
76
+ // Convert to 24-hour format if meridian is specified
77
+ if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
78
+ parsedHours += 12;
79
+ if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
80
+ parsedHours = 0;
81
+ date.setHours(parsedHours, parsedMinutes, 0, 0);
82
+ }
83
+ else {
84
+ // Default to end of day if no time specified
85
+ date.setHours(23, 59, 59, 999);
86
+ }
87
+ return date.getTime();
61
88
  }
62
- // Handle hours/days/weeks/months from now
89
+ // Handle hours from now
63
90
  const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
64
91
  const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
65
92
  const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
@@ -93,3 +120,31 @@ export function parseDueDate(dateString) {
93
120
  return undefined;
94
121
  }
95
122
  }
123
+ /**
124
+ * Format a due date timestamp into a human-readable string
125
+ *
126
+ * @param timestamp Unix timestamp in milliseconds
127
+ * @returns Formatted date string or undefined if timestamp is invalid
128
+ */
129
+ export function formatDueDate(timestamp) {
130
+ if (!timestamp)
131
+ return undefined;
132
+ try {
133
+ const date = new Date(timestamp);
134
+ if (isNaN(date.getTime()))
135
+ return undefined;
136
+ // Format: "March 10, 2025 at 10:56 PM"
137
+ return date.toLocaleString('en-US', {
138
+ year: 'numeric',
139
+ month: 'long',
140
+ day: 'numeric',
141
+ hour: 'numeric',
142
+ minute: '2-digit',
143
+ hour12: true
144
+ }).replace(' at', ',');
145
+ }
146
+ catch (error) {
147
+ console.warn(`Failed to format due date: ${timestamp}`, error);
148
+ return undefined;
149
+ }
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.4.62",
3
+ "version": "0.4.64",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -18,9 +18,7 @@
18
18
  "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
19
19
  "start": "node build/index.js",
20
20
  "dev": "tsc -w",
21
- "prepare": "npm run build",
22
- "prepublishOnly": "npm test",
23
- "test": "echo \"No tests specified\" && exit 0"
21
+ "prepare": "npm run build"
24
22
  },
25
23
  "keywords": [
26
24
  "clickup",
@@ -1,28 +0,0 @@
1
- import { WorkspaceService } from "./workspace.js";
2
- /**
3
- * Service responsible for initializing the server state
4
- * Handles preloading workspace data
5
- */
6
- export class InitializationService {
7
- constructor(config) {
8
- // Create workspace service
9
- this.workspaceService = new WorkspaceService(config.apiKey, config.teamId, config.baseUrl);
10
- }
11
- /**
12
- * Preload workspace hierarchy data on server startup
13
- * This loads the entire workspace tree for faster initial access
14
- */
15
- async preloadWorkspaceData() {
16
- try {
17
- console.log("Preloading workspace data...");
18
- const startTime = Date.now();
19
- // Force refresh to get the latest data
20
- await this.workspaceService.getWorkspaceHierarchy(true);
21
- const duration = (Date.now() - startTime) / 1000;
22
- console.log(`Workspace data preloaded successfully in ${duration.toFixed(2)}s`);
23
- }
24
- catch (error) {
25
- console.error("Failed to preload workspace data:", error);
26
- }
27
- }
28
- }