@taazkareem/clickup-mcp-server 0.6.9 → 0.7.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.
@@ -0,0 +1,684 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Task time tracking tools
6
+ *
7
+ * This module provides tools for time tracking operations on ClickUp tasks:
8
+ * - Get time entries for a task
9
+ * - Start time tracking on a task
10
+ * - Stop time tracking
11
+ * - Add a manual time entry
12
+ * - Delete a time entry
13
+ */
14
+ import { timeTrackingService } from "../../services/shared.js";
15
+ import { getTaskId } from "./utilities.js";
16
+ import { Logger } from "../../logger.js";
17
+ import { parseDueDate } from "../../utils/date-utils.js";
18
+ // Logger instance
19
+ const logger = new Logger('TimeTrackingTools');
20
+ /**
21
+ * Tool definition for getting time entries
22
+ */
23
+ export const getTaskTimeEntriesTool = {
24
+ name: "get_task_time_entries",
25
+ description: "Gets all time entries for a task with filtering options. Use taskId (preferred) or taskName + optional listName. Returns all tracked time with user info, descriptions, tags, start/end times, and durations.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ taskId: {
30
+ type: "string",
31
+ description: "ID of the task to get time entries for. Works with both regular task IDs and custom IDs."
32
+ },
33
+ taskName: {
34
+ type: "string",
35
+ description: "Name of the task to get time entries for. When using this parameter, it's recommended to also provide listName."
36
+ },
37
+ listName: {
38
+ type: "string",
39
+ description: "Name of the list containing the task. Helps find the right task when using taskName."
40
+ },
41
+ startDate: {
42
+ type: "string",
43
+ description: "Optional start date filter. Supports Unix timestamps (in milliseconds) and natural language expressions like 'yesterday', 'last week', etc."
44
+ },
45
+ endDate: {
46
+ type: "string",
47
+ description: "Optional end date filter. Supports Unix timestamps (in milliseconds) and natural language expressions."
48
+ }
49
+ }
50
+ }
51
+ };
52
+ /**
53
+ * Tool definition for starting time tracking
54
+ */
55
+ export const startTimeTrackingTool = {
56
+ name: "start_time_tracking",
57
+ description: "Starts time tracking on a task. Use taskId (preferred) or taskName + optional listName. Optional fields: description, billable status, and tags. Only one timer can be running at a time.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ taskId: {
62
+ type: "string",
63
+ description: "ID of the task to start tracking time on. Works with both regular task IDs and custom IDs."
64
+ },
65
+ taskName: {
66
+ type: "string",
67
+ description: "Name of the task to start tracking time on. When using this parameter, it's recommended to also provide listName."
68
+ },
69
+ listName: {
70
+ type: "string",
71
+ description: "Name of the list containing the task. Helps find the right task when using taskName."
72
+ },
73
+ description: {
74
+ type: "string",
75
+ description: "Optional description for the time entry."
76
+ },
77
+ billable: {
78
+ type: "boolean",
79
+ description: "Whether this time is billable. Default is workspace setting."
80
+ },
81
+ tags: {
82
+ type: "array",
83
+ items: {
84
+ type: "string"
85
+ },
86
+ description: "Optional array of tag names to assign to the time entry."
87
+ }
88
+ }
89
+ }
90
+ };
91
+ /**
92
+ * Tool definition for stopping time tracking
93
+ */
94
+ export const stopTimeTrackingTool = {
95
+ name: "stop_time_tracking",
96
+ description: "Stops the currently running time tracker. Optional fields: description and tags. Returns the completed time entry details.",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ description: {
101
+ type: "string",
102
+ description: "Optional description to update or add to the time entry."
103
+ },
104
+ tags: {
105
+ type: "array",
106
+ items: {
107
+ type: "string"
108
+ },
109
+ description: "Optional array of tag names to assign to the time entry."
110
+ }
111
+ }
112
+ }
113
+ };
114
+ /**
115
+ * Tool definition for adding a manual time entry
116
+ */
117
+ export const addTimeEntryTool = {
118
+ name: "add_time_entry",
119
+ description: "Adds a manual time entry to a task. Use taskId (preferred) or taskName + optional listName. Required: start time, duration. Optional: description, billable, tags.",
120
+ inputSchema: {
121
+ type: "object",
122
+ properties: {
123
+ taskId: {
124
+ type: "string",
125
+ description: "ID of the task to add time entry to. Works with both regular task IDs and custom IDs."
126
+ },
127
+ taskName: {
128
+ type: "string",
129
+ description: "Name of the task to add time entry to. When using this parameter, it's recommended to also provide listName."
130
+ },
131
+ listName: {
132
+ type: "string",
133
+ description: "Name of the list containing the task. Helps find the right task when using taskName."
134
+ },
135
+ start: {
136
+ type: "string",
137
+ description: "Start time for the entry. Supports Unix timestamps (in milliseconds) and natural language expressions like '2 hours ago', 'yesterday 9am', etc."
138
+ },
139
+ duration: {
140
+ type: "string",
141
+ description: "Duration of the time entry. Format as 'Xh Ym' (e.g., '1h 30m') or just minutes (e.g., '90m')."
142
+ },
143
+ description: {
144
+ type: "string",
145
+ description: "Optional description for the time entry."
146
+ },
147
+ billable: {
148
+ type: "boolean",
149
+ description: "Whether this time is billable. Default is workspace setting."
150
+ },
151
+ tags: {
152
+ type: "array",
153
+ items: {
154
+ type: "string"
155
+ },
156
+ description: "Optional array of tag names to assign to the time entry."
157
+ }
158
+ },
159
+ required: ["start", "duration"]
160
+ }
161
+ };
162
+ /**
163
+ * Tool definition for deleting a time entry
164
+ */
165
+ export const deleteTimeEntryTool = {
166
+ name: "delete_time_entry",
167
+ description: "Deletes a time entry. Required: time entry ID.",
168
+ inputSchema: {
169
+ type: "object",
170
+ properties: {
171
+ timeEntryId: {
172
+ type: "string",
173
+ description: "ID of the time entry to delete."
174
+ }
175
+ },
176
+ required: ["timeEntryId"]
177
+ }
178
+ };
179
+ /**
180
+ * Tool definition for getting current time entry
181
+ */
182
+ export const getCurrentTimeEntryTool = {
183
+ name: "get_current_time_entry",
184
+ description: "Gets the currently running time entry, if any. No parameters needed.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {}
188
+ }
189
+ };
190
+ /**
191
+ * Handle get task time entries tool
192
+ */
193
+ export async function handleGetTaskTimeEntries(params) {
194
+ logger.info("Handling request to get task time entries", params);
195
+ try {
196
+ // Resolve task ID
197
+ const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
198
+ 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
+ };
205
+ }
206
+ // Parse date filters
207
+ let startDate;
208
+ let endDate;
209
+ if (params.startDate) {
210
+ startDate = parseDueDate(params.startDate);
211
+ }
212
+ if (params.endDate) {
213
+ endDate = parseDueDate(params.endDate);
214
+ }
215
+ // Get time entries
216
+ const result = await timeTrackingService.getTimeEntries(taskId, startDate, endDate);
217
+ if (!result.success) {
218
+ return {
219
+ success: false,
220
+ error: {
221
+ message: result.error?.message || "Failed to get time entries"
222
+ }
223
+ };
224
+ }
225
+ const timeEntries = result.data || [];
226
+ // Format the response
227
+ return {
228
+ success: true,
229
+ time_entries: timeEntries.map(entry => ({
230
+ id: entry.id,
231
+ description: entry.description,
232
+ start: entry.start,
233
+ end: entry.end,
234
+ duration: formatDuration(entry.duration),
235
+ duration_ms: entry.duration,
236
+ billable: entry.billable,
237
+ tags: entry.tags,
238
+ user: {
239
+ id: entry.user.id,
240
+ username: entry.user.username
241
+ },
242
+ task: {
243
+ id: entry.task.id,
244
+ name: entry.task.name,
245
+ status: entry.task.status.status
246
+ }
247
+ }))
248
+ };
249
+ }
250
+ catch (error) {
251
+ 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
+ };
258
+ }
259
+ }
260
+ /**
261
+ * Handle start time tracking tool
262
+ */
263
+ export async function handleStartTimeTracking(params) {
264
+ logger.info("Handling request to start time tracking", params);
265
+ try {
266
+ // Resolve task ID
267
+ const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
268
+ 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
+ };
275
+ }
276
+ // Check for currently running timer
277
+ const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
278
+ 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
+ }
292
+ }
293
+ };
294
+ }
295
+ // Prepare request data
296
+ const requestData = {
297
+ tid: taskId,
298
+ description: params.description,
299
+ billable: params.billable,
300
+ tags: params.tags
301
+ };
302
+ // Start time tracking
303
+ const result = await timeTrackingService.startTimeTracking(requestData);
304
+ if (!result.success) {
305
+ return {
306
+ success: false,
307
+ error: {
308
+ message: result.error?.message || "Failed to start time tracking"
309
+ }
310
+ };
311
+ }
312
+ const timeEntry = result.data;
313
+ if (!timeEntry) {
314
+ return {
315
+ success: false,
316
+ error: {
317
+ message: "No time entry data returned from API"
318
+ }
319
+ };
320
+ }
321
+ // Format the response
322
+ return {
323
+ success: true,
324
+ time_entry: {
325
+ id: timeEntry.id,
326
+ description: timeEntry.description,
327
+ start: timeEntry.start,
328
+ end: timeEntry.end,
329
+ task: {
330
+ id: timeEntry.task.id,
331
+ name: timeEntry.task.name
332
+ },
333
+ billable: timeEntry.billable,
334
+ tags: timeEntry.tags
335
+ }
336
+ };
337
+ }
338
+ catch (error) {
339
+ logger.error("Error starting time tracking", error);
340
+ return {
341
+ success: false,
342
+ error: {
343
+ message: error.message || "An unknown error occurred"
344
+ }
345
+ };
346
+ }
347
+ }
348
+ /**
349
+ * Handle stop time tracking tool
350
+ */
351
+ export async function handleStopTimeTracking(params) {
352
+ logger.info("Handling request to stop time tracking", params);
353
+ try {
354
+ // Check for currently running timer
355
+ const currentTimerResult = await timeTrackingService.getCurrentTimeEntry();
356
+ 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
+ };
363
+ }
364
+ // Prepare request data
365
+ const requestData = {
366
+ description: params.description,
367
+ tags: params.tags
368
+ };
369
+ // Stop time tracking
370
+ const result = await timeTrackingService.stopTimeTracking(requestData);
371
+ if (!result.success) {
372
+ return {
373
+ success: false,
374
+ error: {
375
+ message: result.error?.message || "Failed to stop time tracking"
376
+ }
377
+ };
378
+ }
379
+ const timeEntry = result.data;
380
+ if (!timeEntry) {
381
+ return {
382
+ success: false,
383
+ error: {
384
+ message: "No time entry data returned from API"
385
+ }
386
+ };
387
+ }
388
+ // Format the response
389
+ return {
390
+ success: true,
391
+ time_entry: {
392
+ id: timeEntry.id,
393
+ description: timeEntry.description,
394
+ start: timeEntry.start,
395
+ end: timeEntry.end,
396
+ duration: formatDuration(timeEntry.duration),
397
+ duration_ms: timeEntry.duration,
398
+ task: {
399
+ id: timeEntry.task.id,
400
+ name: timeEntry.task.name
401
+ },
402
+ billable: timeEntry.billable,
403
+ tags: timeEntry.tags
404
+ }
405
+ };
406
+ }
407
+ catch (error) {
408
+ logger.error("Error stopping time tracking", error);
409
+ return {
410
+ success: false,
411
+ error: {
412
+ message: error.message || "An unknown error occurred"
413
+ }
414
+ };
415
+ }
416
+ }
417
+ /**
418
+ * Handle add time entry tool
419
+ */
420
+ export async function handleAddTimeEntry(params) {
421
+ logger.info("Handling request to add time entry", params);
422
+ try {
423
+ // Resolve task ID
424
+ const taskId = await getTaskId(params.taskId, params.taskName, params.listName);
425
+ 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
+ };
432
+ }
433
+ // Parse start time
434
+ const startTime = parseDueDate(params.start);
435
+ 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
+ };
442
+ }
443
+ // Parse duration
444
+ const durationMs = parseDuration(params.duration);
445
+ 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
+ };
452
+ }
453
+ // Prepare request data
454
+ const requestData = {
455
+ tid: taskId,
456
+ start: startTime,
457
+ duration: durationMs,
458
+ description: params.description,
459
+ billable: params.billable,
460
+ tags: params.tags
461
+ };
462
+ // Add time entry
463
+ const result = await timeTrackingService.addTimeEntry(requestData);
464
+ if (!result.success) {
465
+ return {
466
+ success: false,
467
+ error: {
468
+ message: result.error?.message || "Failed to add time entry"
469
+ }
470
+ };
471
+ }
472
+ const timeEntry = result.data;
473
+ if (!timeEntry) {
474
+ return {
475
+ success: false,
476
+ error: {
477
+ message: "No time entry data returned from API"
478
+ }
479
+ };
480
+ }
481
+ // Format the response
482
+ return {
483
+ success: true,
484
+ time_entry: {
485
+ id: timeEntry.id,
486
+ description: timeEntry.description,
487
+ start: timeEntry.start,
488
+ end: timeEntry.end,
489
+ duration: formatDuration(timeEntry.duration),
490
+ duration_ms: timeEntry.duration,
491
+ task: {
492
+ id: timeEntry.task.id,
493
+ name: timeEntry.task.name
494
+ },
495
+ billable: timeEntry.billable,
496
+ tags: timeEntry.tags
497
+ }
498
+ };
499
+ }
500
+ catch (error) {
501
+ logger.error("Error adding time entry", error);
502
+ return {
503
+ success: false,
504
+ error: {
505
+ message: error.message || "An unknown error occurred"
506
+ }
507
+ };
508
+ }
509
+ }
510
+ /**
511
+ * Handle delete time entry tool
512
+ */
513
+ export async function handleDeleteTimeEntry(params) {
514
+ logger.info("Handling request to delete time entry", params);
515
+ try {
516
+ const { timeEntryId } = params;
517
+ if (!timeEntryId) {
518
+ return {
519
+ success: false,
520
+ error: {
521
+ message: "Time entry ID is required."
522
+ }
523
+ };
524
+ }
525
+ // Delete time entry
526
+ const result = await timeTrackingService.deleteTimeEntry(timeEntryId);
527
+ if (!result.success) {
528
+ return {
529
+ success: false,
530
+ error: {
531
+ message: result.error?.message || "Failed to delete time entry"
532
+ }
533
+ };
534
+ }
535
+ // Format the response
536
+ return {
537
+ success: true,
538
+ message: "Time entry deleted successfully."
539
+ };
540
+ }
541
+ catch (error) {
542
+ logger.error("Error deleting time entry", error);
543
+ return {
544
+ success: false,
545
+ error: {
546
+ message: error.message || "An unknown error occurred"
547
+ }
548
+ };
549
+ }
550
+ }
551
+ /**
552
+ * Handle get current time entry tool
553
+ */
554
+ export async function handleGetCurrentTimeEntry(params) {
555
+ logger.info("Handling request to get current time entry");
556
+ try {
557
+ // Get current time entry
558
+ const result = await timeTrackingService.getCurrentTimeEntry();
559
+ if (!result.success) {
560
+ return {
561
+ success: false,
562
+ error: {
563
+ message: result.error?.message || "Failed to get current time entry"
564
+ }
565
+ };
566
+ }
567
+ const timeEntry = result.data;
568
+ // If no timer is running
569
+ if (!timeEntry) {
570
+ return {
571
+ success: true,
572
+ timer_running: false,
573
+ message: "No timer is currently running."
574
+ };
575
+ }
576
+ // Format the response
577
+ const elapsedTime = calculateElapsedTime(timeEntry.start);
578
+ return {
579
+ success: true,
580
+ timer_running: true,
581
+ time_entry: {
582
+ id: timeEntry.id,
583
+ description: timeEntry.description,
584
+ start: timeEntry.start,
585
+ elapsed: formatDuration(elapsedTime),
586
+ elapsed_ms: elapsedTime,
587
+ task: {
588
+ id: timeEntry.task.id,
589
+ name: timeEntry.task.name
590
+ },
591
+ billable: timeEntry.billable,
592
+ tags: timeEntry.tags
593
+ }
594
+ };
595
+ }
596
+ catch (error) {
597
+ 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
+ };
604
+ }
605
+ }
606
+ /**
607
+ * Calculate elapsed time in milliseconds from a start time string to now
608
+ */
609
+ function calculateElapsedTime(startTimeString) {
610
+ const startTime = new Date(startTimeString).getTime();
611
+ const now = Date.now();
612
+ return Math.max(0, now - startTime);
613
+ }
614
+ /**
615
+ * Format duration in milliseconds to a human-readable string
616
+ */
617
+ function formatDuration(durationMs) {
618
+ if (!durationMs)
619
+ return "0m";
620
+ const seconds = Math.floor(durationMs / 1000);
621
+ const minutes = Math.floor(seconds / 60);
622
+ const hours = Math.floor(minutes / 60);
623
+ const remainingMinutes = minutes % 60;
624
+ if (hours === 0) {
625
+ return `${remainingMinutes}m`;
626
+ }
627
+ else if (remainingMinutes === 0) {
628
+ return `${hours}h`;
629
+ }
630
+ else {
631
+ return `${hours}h ${remainingMinutes}m`;
632
+ }
633
+ }
634
+ /**
635
+ * Parse duration string to milliseconds
636
+ */
637
+ function parseDuration(durationString) {
638
+ if (!durationString)
639
+ return 0;
640
+ // Clean the input and handle potential space issues
641
+ const cleanDuration = durationString.trim().toLowerCase().replace(/\s+/g, ' ');
642
+ // Handle simple minute format like "90m"
643
+ if (/^\d+m$/.test(cleanDuration)) {
644
+ const minutes = parseInt(cleanDuration.replace('m', ''), 10);
645
+ return minutes * 60 * 1000;
646
+ }
647
+ // Handle simple hour format like "2h"
648
+ if (/^\d+h$/.test(cleanDuration)) {
649
+ const hours = parseInt(cleanDuration.replace('h', ''), 10);
650
+ return hours * 60 * 60 * 1000;
651
+ }
652
+ // Handle combined format like "1h 30m"
653
+ const combinedPattern = /^(\d+)h\s*(?:(\d+)m)?$|^(?:(\d+)h\s*)?(\d+)m$/;
654
+ const match = cleanDuration.match(combinedPattern);
655
+ if (match) {
656
+ const hours = parseInt(match[1] || match[3] || '0', 10);
657
+ const minutes = parseInt(match[2] || match[4] || '0', 10);
658
+ return (hours * 60 * 60 + minutes * 60) * 1000;
659
+ }
660
+ // Try to parse as just a number of minutes
661
+ const justMinutes = parseInt(cleanDuration, 10);
662
+ if (!isNaN(justMinutes)) {
663
+ return justMinutes * 60 * 1000;
664
+ }
665
+ return 0;
666
+ }
667
+ // Export all time tracking tools
668
+ export const timeTrackingTools = [
669
+ getTaskTimeEntriesTool,
670
+ startTimeTrackingTool,
671
+ stopTimeTrackingTool,
672
+ addTimeEntryTool,
673
+ deleteTimeEntryTool,
674
+ getCurrentTimeEntryTool
675
+ ];
676
+ // Export all time tracking handlers
677
+ export const timeTrackingHandlers = {
678
+ get_task_time_entries: handleGetTaskTimeEntries,
679
+ start_time_tracking: handleStartTimeTracking,
680
+ stop_time_tracking: handleStopTimeTracking,
681
+ add_time_entry: handleAddTimeEntry,
682
+ delete_time_entry: handleDeleteTimeEntry,
683
+ get_current_time_entry: handleGetCurrentTimeEntry
684
+ };
@@ -59,7 +59,7 @@ export class SponsorService {
59
59
  if (this.isEnabled && includeSponsorMessage) {
60
60
  content.push({
61
61
  type: "text",
62
- text: `♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
62
+ text: `\n♥ Support this project by sponsoring the developer at ${this.sponsorUrl}`
63
63
  });
64
64
  }
65
65
  return { content };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
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",