@taazkareem/clickup-mcp-server 0.8.4 → 0.9.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 (36) hide show
  1. package/LICENSE +9 -17
  2. package/README.md +33 -38
  3. package/build/enhanced_server.js +262 -0
  4. package/build/index.js +9 -3
  5. package/build/license.js +172 -0
  6. package/build/middleware/auth.js +211 -0
  7. package/build/routes/auth.js +306 -0
  8. package/build/schemas/member.js +13 -0
  9. package/build/server.js +15 -1
  10. package/build/server.log +15 -0
  11. package/build/services/auth/oauth2.js +236 -0
  12. package/build/services/auth/session.js +337 -0
  13. package/build/services/clickup/adapter.js +281 -0
  14. package/build/services/clickup/factory.js +339 -0
  15. package/build/services/clickup/task/task-attachments.js +20 -12
  16. package/build/services/clickup/task/task-comments.js +19 -9
  17. package/build/services/clickup/task/task-core.js +68 -4
  18. package/build/services/clickup/task/task-custom-fields.js +23 -13
  19. package/build/services/clickup/task/task-search.js +79 -71
  20. package/build/services/clickup/task/task-service.js +88 -9
  21. package/build/services/clickup/task/task-tags.js +25 -13
  22. package/build/sse_server.js +4 -4
  23. package/build/tools/documents.js +11 -4
  24. package/build/tools/health.js +23 -0
  25. package/build/tools/member.js +2 -4
  26. package/build/tools/task/bulk-operations.js +5 -5
  27. package/build/tools/task/handlers.js +62 -12
  28. package/build/tools/task/single-operations.js +9 -9
  29. package/build/tools/task/time-tracking.js +61 -170
  30. package/build/tools/task/utilities.js +56 -22
  31. package/build/utils/date-utils.js +341 -141
  32. package/build/utils/schema-compatibility.js +222 -0
  33. package/build/utils/universal-schema-compatibility.js +171 -0
  34. package/build/virtual-sdk/generator.js +53 -0
  35. package/build/virtual-sdk/registry.js +45 -0
  36. package/package.json +2 -2
@@ -15,6 +15,7 @@ import { timeTrackingService } from "../../services/shared.js";
15
15
  import { getTaskId } from "./utilities.js";
16
16
  import { Logger } from "../../logger.js";
17
17
  import { parseDueDate } from "../../utils/date-utils.js";
18
+ import { sponsorService } from "../../utils/sponsor-service.js";
18
19
  // Logger instance
19
20
  const logger = new Logger('TimeTrackingTools');
20
21
  /**
@@ -196,12 +197,7 @@ export async function handleGetTaskTimeEntries(params) {
196
197
  // Resolve task ID
197
198
  const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
198
199
  if (!taskId) {
199
- return {
200
- success: false,
201
- error: {
202
- message: "Task not found. Please provide a valid taskId or taskName + listName combination."
203
- }
204
- };
200
+ return sponsorService.createErrorResponse("Task not found. Please provide a valid taskId or taskName + listName combination.");
205
201
  }
206
202
  // Parse date filters
207
203
  let startDate;
@@ -215,46 +211,37 @@ export async function handleGetTaskTimeEntries(params) {
215
211
  // Get time entries
216
212
  const result = await timeTrackingService.getTimeEntries(taskId, startDate, endDate);
217
213
  if (!result.success) {
218
- return {
219
- success: false,
220
- error: {
221
- message: result.error?.message || "Failed to get time entries"
222
- }
223
- };
214
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to get time entries");
224
215
  }
225
216
  const timeEntries = result.data || [];
226
217
  // Format the response
227
- return {
218
+ return sponsorService.createResponse({
228
219
  success: true,
220
+ count: timeEntries.length,
229
221
  time_entries: timeEntries.map(entry => ({
230
222
  id: entry.id,
231
- description: entry.description,
223
+ description: entry.description || "",
232
224
  start: entry.start,
233
225
  end: entry.end,
234
- duration: formatDuration(entry.duration),
235
- duration_ms: entry.duration,
236
- billable: entry.billable,
237
- tags: entry.tags,
238
- user: {
226
+ duration: formatDuration(entry.duration || 0),
227
+ duration_ms: entry.duration || 0,
228
+ billable: entry.billable || false,
229
+ tags: entry.tags || [],
230
+ user: entry.user ? {
239
231
  id: entry.user.id,
240
232
  username: entry.user.username
241
- },
242
- task: {
233
+ } : null,
234
+ task: entry.task ? {
243
235
  id: entry.task.id,
244
236
  name: entry.task.name,
245
- status: entry.task.status.status
246
- }
237
+ status: entry.task.status?.status || "Unknown"
238
+ } : null
247
239
  }))
248
- };
240
+ }, true);
249
241
  }
250
242
  catch (error) {
251
243
  logger.error("Error getting task time entries", error);
252
- return {
253
- success: false,
254
- error: {
255
- message: error.message || "An unknown error occurred"
256
- }
257
- };
244
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
258
245
  }
259
246
  }
260
247
  /**
@@ -266,31 +253,22 @@ export async function handleStartTimeTracking(params) {
266
253
  // Resolve task ID
267
254
  const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
268
255
  if (!taskId) {
269
- return {
270
- success: false,
271
- error: {
272
- message: "Task not found. Please provide a valid taskId or taskName + listName combination."
273
- }
274
- };
256
+ return sponsorService.createErrorResponse("Task not found. Please provide a valid taskId or taskName + listName combination.");
275
257
  }
276
258
  // Check for currently running timer
277
259
  const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
278
260
  if (currentTimerResult.success && currentTimerResult.data) {
279
- return {
280
- success: false,
281
- error: {
282
- message: "A timer is already running. Please stop the current timer before starting a new one.",
283
- timer: {
284
- id: currentTimerResult.data.id,
285
- task: {
286
- id: currentTimerResult.data.task.id,
287
- name: currentTimerResult.data.task.name
288
- },
289
- start: currentTimerResult.data.start,
290
- description: currentTimerResult.data.description
291
- }
261
+ return sponsorService.createErrorResponse("A timer is already running. Please stop the current timer before starting a new one.", {
262
+ timer: {
263
+ id: currentTimerResult.data.id,
264
+ task: {
265
+ id: currentTimerResult.data.task.id,
266
+ name: currentTimerResult.data.task.name
267
+ },
268
+ start: currentTimerResult.data.start,
269
+ description: currentTimerResult.data.description
292
270
  }
293
- };
271
+ });
294
272
  }
295
273
  // Prepare request data
296
274
  const requestData = {
@@ -302,25 +280,16 @@ export async function handleStartTimeTracking(params) {
302
280
  // Start time tracking
303
281
  const result = await timeTrackingService.startTimeTracking(requestData);
304
282
  if (!result.success) {
305
- return {
306
- success: false,
307
- error: {
308
- message: result.error?.message || "Failed to start time tracking"
309
- }
310
- };
283
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to start time tracking");
311
284
  }
312
285
  const timeEntry = result.data;
313
286
  if (!timeEntry) {
314
- return {
315
- success: false,
316
- error: {
317
- message: "No time entry data returned from API"
318
- }
319
- };
287
+ return sponsorService.createErrorResponse("No time entry data returned from API");
320
288
  }
321
289
  // Format the response
322
- return {
290
+ return sponsorService.createResponse({
323
291
  success: true,
292
+ message: "Time tracking started successfully",
324
293
  time_entry: {
325
294
  id: timeEntry.id,
326
295
  description: timeEntry.description,
@@ -333,16 +302,11 @@ export async function handleStartTimeTracking(params) {
333
302
  billable: timeEntry.billable,
334
303
  tags: timeEntry.tags
335
304
  }
336
- };
305
+ }, true);
337
306
  }
338
307
  catch (error) {
339
308
  logger.error("Error starting time tracking", error);
340
- return {
341
- success: false,
342
- error: {
343
- message: error.message || "An unknown error occurred"
344
- }
345
- };
309
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
346
310
  }
347
311
  }
348
312
  /**
@@ -354,12 +318,7 @@ export async function handleStopTimeTracking(params) {
354
318
  // Check for currently running timer
355
319
  const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
356
320
  if (currentTimerResult.success && !currentTimerResult.data) {
357
- return {
358
- success: false,
359
- error: {
360
- message: "No timer is currently running. Start a timer before trying to stop it."
361
- }
362
- };
321
+ return sponsorService.createErrorResponse("No timer is currently running. Start a timer before trying to stop it.");
363
322
  }
364
323
  // Prepare request data
365
324
  const requestData = {
@@ -369,25 +328,16 @@ export async function handleStopTimeTracking(params) {
369
328
  // Stop time tracking
370
329
  const result = await timeTrackingService.stopTimeTracking(requestData);
371
330
  if (!result.success) {
372
- return {
373
- success: false,
374
- error: {
375
- message: result.error?.message || "Failed to stop time tracking"
376
- }
377
- };
331
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to stop time tracking");
378
332
  }
379
333
  const timeEntry = result.data;
380
334
  if (!timeEntry) {
381
- return {
382
- success: false,
383
- error: {
384
- message: "No time entry data returned from API"
385
- }
386
- };
335
+ return sponsorService.createErrorResponse("No time entry data returned from API");
387
336
  }
388
337
  // Format the response
389
- return {
338
+ return sponsorService.createResponse({
390
339
  success: true,
340
+ message: "Time tracking stopped successfully",
391
341
  time_entry: {
392
342
  id: timeEntry.id,
393
343
  description: timeEntry.description,
@@ -402,16 +352,11 @@ export async function handleStopTimeTracking(params) {
402
352
  billable: timeEntry.billable,
403
353
  tags: timeEntry.tags
404
354
  }
405
- };
355
+ }, true);
406
356
  }
407
357
  catch (error) {
408
358
  logger.error("Error stopping time tracking", error);
409
- return {
410
- success: false,
411
- error: {
412
- message: error.message || "An unknown error occurred"
413
- }
414
- };
359
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
415
360
  }
416
361
  }
417
362
  /**
@@ -423,32 +368,17 @@ export async function handleAddTimeEntry(params) {
423
368
  // Resolve task ID
424
369
  const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
425
370
  if (!taskId) {
426
- return {
427
- success: false,
428
- error: {
429
- message: "Task not found. Please provide a valid taskId or taskName + listName combination."
430
- }
431
- };
371
+ return sponsorService.createErrorResponse("Task not found. Please provide a valid taskId or taskName + listName combination.");
432
372
  }
433
373
  // Parse start time
434
374
  const startTime = parseDueDate(params.start);
435
375
  if (!startTime) {
436
- return {
437
- success: false,
438
- error: {
439
- message: "Invalid start time format. Use a Unix timestamp (in milliseconds) or a natural language date string."
440
- }
441
- };
376
+ return sponsorService.createErrorResponse("Invalid start time format. Use a Unix timestamp (in milliseconds) or a natural language date string.");
442
377
  }
443
378
  // Parse duration
444
379
  const durationMs = parseDuration(params.duration);
445
380
  if (durationMs === 0) {
446
- return {
447
- success: false,
448
- error: {
449
- message: "Invalid duration format. Use 'Xh Ym' format (e.g., '1h 30m') or just minutes (e.g., '90m')."
450
- }
451
- };
381
+ return sponsorService.createErrorResponse("Invalid duration format. Use 'Xh Ym' format (e.g., '1h 30m') or just minutes (e.g., '90m').");
452
382
  }
453
383
  // Prepare request data
454
384
  const requestData = {
@@ -462,25 +392,16 @@ export async function handleAddTimeEntry(params) {
462
392
  // Add time entry
463
393
  const result = await timeTrackingService.addTimeEntry(requestData);
464
394
  if (!result.success) {
465
- return {
466
- success: false,
467
- error: {
468
- message: result.error?.message || "Failed to add time entry"
469
- }
470
- };
395
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to add time entry");
471
396
  }
472
397
  const timeEntry = result.data;
473
398
  if (!timeEntry) {
474
- return {
475
- success: false,
476
- error: {
477
- message: "No time entry data returned from API"
478
- }
479
- };
399
+ return sponsorService.createErrorResponse("No time entry data returned from API");
480
400
  }
481
401
  // Format the response
482
- return {
402
+ return sponsorService.createResponse({
483
403
  success: true,
404
+ message: "Time entry added successfully",
484
405
  time_entry: {
485
406
  id: timeEntry.id,
486
407
  description: timeEntry.description,
@@ -495,16 +416,11 @@ export async function handleAddTimeEntry(params) {
495
416
  billable: timeEntry.billable,
496
417
  tags: timeEntry.tags
497
418
  }
498
- };
419
+ }, true);
499
420
  }
500
421
  catch (error) {
501
422
  logger.error("Error adding time entry", error);
502
- return {
503
- success: false,
504
- error: {
505
- message: error.message || "An unknown error occurred"
506
- }
507
- };
423
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
508
424
  }
509
425
  }
510
426
  /**
@@ -515,37 +431,22 @@ export async function handleDeleteTimeEntry(params) {
515
431
  try {
516
432
  const { timeEntryId } = params;
517
433
  if (!timeEntryId) {
518
- return {
519
- success: false,
520
- error: {
521
- message: "Time entry ID is required."
522
- }
523
- };
434
+ return sponsorService.createErrorResponse("Time entry ID is required.");
524
435
  }
525
436
  // Delete time entry
526
437
  const result = await timeTrackingService.deleteTimeEntry(timeEntryId);
527
438
  if (!result.success) {
528
- return {
529
- success: false,
530
- error: {
531
- message: result.error?.message || "Failed to delete time entry"
532
- }
533
- };
439
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to delete time entry");
534
440
  }
535
441
  // Format the response
536
- return {
442
+ return sponsorService.createResponse({
537
443
  success: true,
538
444
  message: "Time entry deleted successfully."
539
- };
445
+ }, true);
540
446
  }
541
447
  catch (error) {
542
448
  logger.error("Error deleting time entry", error);
543
- return {
544
- success: false,
545
- error: {
546
- message: error.message || "An unknown error occurred"
547
- }
548
- };
449
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
549
450
  }
550
451
  }
551
452
  /**
@@ -557,25 +458,20 @@ export async function handleGetCurrentTimeEntry(params) {
557
458
  // Get current time entry
558
459
  const result = await timeTrackingService.getCurrentTimeEntry();
559
460
  if (!result.success) {
560
- return {
561
- success: false,
562
- error: {
563
- message: result.error?.message || "Failed to get current time entry"
564
- }
565
- };
461
+ return sponsorService.createErrorResponse(result.error?.message || "Failed to get current time entry");
566
462
  }
567
463
  const timeEntry = result.data;
568
464
  // If no timer is running
569
465
  if (!timeEntry) {
570
- return {
466
+ return sponsorService.createResponse({
571
467
  success: true,
572
468
  timer_running: false,
573
469
  message: "No timer is currently running."
574
- };
470
+ }, true);
575
471
  }
576
472
  // Format the response
577
473
  const elapsedTime = calculateElapsedTime(timeEntry.start);
578
- return {
474
+ return sponsorService.createResponse({
579
475
  success: true,
580
476
  timer_running: true,
581
477
  time_entry: {
@@ -591,16 +487,11 @@ export async function handleGetCurrentTimeEntry(params) {
591
487
  billable: timeEntry.billable,
592
488
  tags: timeEntry.tags
593
489
  }
594
- };
490
+ }, true);
595
491
  }
596
492
  catch (error) {
597
493
  logger.error("Error getting current time entry", error);
598
- return {
599
- success: false,
600
- error: {
601
- message: error.message || "An unknown error occurred"
602
- }
603
- };
494
+ return sponsorService.createErrorResponse(error.message || "An unknown error occurred");
604
495
  }
605
496
  }
606
497
  /**
@@ -65,6 +65,62 @@ export function formatTaskData(task, additional = {}) {
65
65
  ...additional
66
66
  };
67
67
  }
68
+ //=============================================================================
69
+ // TASK ID DETECTION UTILITIES
70
+ //=============================================================================
71
+ /**
72
+ * Detects if a task ID is a custom task ID based on common patterns
73
+ * Custom task IDs typically:
74
+ * - Contain hyphens (e.g., "DEV-1234", "PROJ-456")
75
+ * - Have uppercase prefixes followed by numbers
76
+ * - Are not 9-character alphanumeric strings (regular ClickUp task IDs)
77
+ *
78
+ * @param taskId The task ID to check
79
+ * @returns true if the ID appears to be a custom task ID
80
+ */
81
+ export function isCustomTaskId(taskId) {
82
+ if (!taskId || typeof taskId !== 'string') {
83
+ return false;
84
+ }
85
+ // Trim whitespace
86
+ taskId = taskId.trim();
87
+ // Regular ClickUp task IDs are typically 9 characters, alphanumeric
88
+ // Custom task IDs usually have different patterns
89
+ // Check if it's a standard 9-character ClickUp ID (letters and numbers only)
90
+ const standardIdPattern = /^[a-zA-Z0-9]{9}$/;
91
+ if (standardIdPattern.test(taskId)) {
92
+ return false;
93
+ }
94
+ // Check for common custom task ID patterns:
95
+ // 1. Contains hyphens (most common pattern: PREFIX-NUMBER)
96
+ if (taskId.includes('-')) {
97
+ // Additional validation: should have letters before hyphen and numbers after
98
+ const hyphenPattern = /^[A-Za-z]+[-][0-9]+$/;
99
+ return hyphenPattern.test(taskId);
100
+ }
101
+ // 2. Contains underscores (another common pattern: PREFIX_NUMBER)
102
+ if (taskId.includes('_')) {
103
+ const underscorePattern = /^[A-Za-z]+[_][0-9]+$/;
104
+ return underscorePattern.test(taskId);
105
+ }
106
+ // 3. Contains uppercase letters followed by numbers (without separators)
107
+ const customIdPattern = /^[A-Z]+\d+$/;
108
+ if (customIdPattern.test(taskId)) {
109
+ return true;
110
+ }
111
+ // 4. Mixed case with numbers but not 9 characters (less common)
112
+ const mixedCasePattern = /^[A-Za-z]+\d+$/;
113
+ if (mixedCasePattern.test(taskId) && taskId.length !== 9) {
114
+ return true;
115
+ }
116
+ // 5. Contains dots (some organizations use PROJECT.TASK format)
117
+ if (taskId.includes('.')) {
118
+ const dotPattern = /^[A-Za-z]+[.][0-9]+$/;
119
+ return dotPattern.test(taskId);
120
+ }
121
+ // If none of the patterns match, assume it's a regular task ID
122
+ return false;
123
+ }
68
124
  /**
69
125
  * Validates task identification parameters
70
126
  *
@@ -169,28 +225,6 @@ export function parseBulkOptions(rawOptions) {
169
225
  }
170
226
  return rawOptions;
171
227
  }
172
- //=============================================================================
173
- // ID DETECTION UTILITIES
174
- //=============================================================================
175
- /**
176
- * Determines if an ID is a custom ID based on its format
177
- * Custom IDs typically have an uppercase prefix followed by a hyphen and number (e.g., DEV-1234)
178
- * Regular task IDs are always 9 characters long
179
- *
180
- * @param id The task ID to check
181
- * @returns True if the ID appears to be a custom ID
182
- */
183
- export function isCustomTaskId(id) {
184
- if (!id)
185
- return false;
186
- // Regular task IDs are exactly 9 characters
187
- if (id.length === 9) {
188
- return false;
189
- }
190
- // Custom IDs have an uppercase prefix followed by a hyphen and numbers
191
- const customIdPattern = /^[A-Z]+-\d+$/;
192
- return customIdPattern.test(id);
193
- }
194
228
  /**
195
229
  * Resolves a list ID from either direct ID or name
196
230
  * Handles validation and throws appropriate errors