@taazkareem/clickup-mcp-server 0.6.2 → 0.6.3

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 (41) hide show
  1. package/README.md +6 -6
  2. package/build/logger.js +26 -1
  3. package/build/server.js +1 -1
  4. package/build/services/clickup/base.js +22 -1
  5. package/build/services/clickup/bulk.js +76 -45
  6. package/build/services/clickup/index.js +2 -2
  7. package/build/services/clickup/task/index.js +32 -0
  8. package/build/services/clickup/task/task-attachments.js +97 -0
  9. package/build/services/clickup/task/task-comments.js +104 -0
  10. package/build/services/clickup/task/task-core.js +477 -0
  11. package/build/services/clickup/task/task-custom-fields.js +97 -0
  12. package/build/services/clickup/task/task-search.js +462 -0
  13. package/build/services/clickup/task/task-service.js +25 -0
  14. package/build/services/clickup/task/task-tags.js +101 -0
  15. package/build/services/clickup/workspace.js +81 -36
  16. package/build/tools/folder.js +1 -1
  17. package/build/tools/list.js +2 -4
  18. package/build/tools/task/attachments.js +18 -5
  19. package/build/tools/task/attachments.types.js +9 -0
  20. package/build/tools/task/bulk-operations.js +111 -15
  21. package/build/tools/task/handlers.js +169 -24
  22. package/build/tools/task/index.js +1 -1
  23. package/build/tools/task/main.js +36 -1
  24. package/build/tools/task/single-operations.js +51 -4
  25. package/build/tools/task/utilities.js +24 -71
  26. package/build/tools/utils.js +2 -2
  27. package/build/utils/date-utils.js +149 -30
  28. package/build/utils/resolver-utils.js +33 -40
  29. package/build/utils/sponsor-service.js +1 -1
  30. package/package.json +1 -1
  31. package/build/mcp-tools.js +0 -64
  32. package/build/server-state.js +0 -93
  33. package/build/server.log +0 -0
  34. package/build/services/clickup/task.js +0 -701
  35. package/build/tools/bulk-tasks.js +0 -36
  36. package/build/tools/debug.js +0 -76
  37. package/build/tools/logs.js +0 -55
  38. package/build/tools/task.js +0 -1554
  39. package/build/utils/params-utils.js +0 -39
  40. package/build/utils/sponsor-analytics.js +0 -100
  41. package/build/utils/sponsor-utils.js +0 -57
@@ -30,6 +30,31 @@ export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0
30
30
  now.setMonth(now.getMonth() + months);
31
31
  return now.getTime();
32
32
  }
33
+ /**
34
+ * Get the start of today (midnight) in Unix milliseconds
35
+ * @returns Timestamp in milliseconds for start of current day
36
+ */
37
+ export function getStartOfDay() {
38
+ const now = new Date();
39
+ now.setHours(0, 0, 0, 0);
40
+ return now.getTime();
41
+ }
42
+ /**
43
+ * Get the end of today (23:59:59.999) in Unix milliseconds
44
+ * @returns Timestamp in milliseconds for end of current day
45
+ */
46
+ export function getEndOfDay() {
47
+ const now = new Date();
48
+ now.setHours(23, 59, 59, 999);
49
+ return now.getTime();
50
+ }
51
+ /**
52
+ * Get the current time in Unix milliseconds
53
+ * @returns Current timestamp in milliseconds
54
+ */
55
+ export function getCurrentTimestamp() {
56
+ return new Date().getTime();
57
+ }
33
58
  /**
34
59
  * Parse a due date string into a timestamp
35
60
  * Supports ISO 8601 format or natural language like "tomorrow"
@@ -42,15 +67,36 @@ export function parseDueDate(dateString) {
42
67
  return undefined;
43
68
  try {
44
69
  // Handle natural language dates
45
- const lowerDate = dateString.toLowerCase();
46
- const now = new Date();
70
+ const lowerDate = dateString.toLowerCase().trim();
71
+ // Handle "now" specifically
72
+ if (lowerDate === 'now') {
73
+ return getCurrentTimestamp();
74
+ }
75
+ // Handle "today" with different options
47
76
  if (lowerDate === 'today') {
48
- const today = new Date();
49
- today.setHours(23, 59, 59, 999);
50
- return today.getTime();
77
+ return getEndOfDay();
78
+ }
79
+ if (lowerDate === 'today start' || lowerDate === 'start of today') {
80
+ return getStartOfDay();
81
+ }
82
+ if (lowerDate === 'today end' || lowerDate === 'end of today') {
83
+ return getEndOfDay();
84
+ }
85
+ // Handle "yesterday" and "tomorrow"
86
+ if (lowerDate === 'yesterday') {
87
+ const yesterday = new Date();
88
+ yesterday.setDate(yesterday.getDate() - 1);
89
+ yesterday.setHours(23, 59, 59, 999);
90
+ return yesterday.getTime();
91
+ }
92
+ if (lowerDate === 'tomorrow') {
93
+ const tomorrow = new Date();
94
+ tomorrow.setDate(tomorrow.getDate() + 1);
95
+ tomorrow.setHours(23, 59, 59, 999);
96
+ return tomorrow.getTime();
51
97
  }
52
98
  // Handle relative dates with specific times
53
- const relativeTimeRegex = /(?:(\d+)\s*(minutes?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
99
+ const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
54
100
  const match = lowerDate.match(relativeTimeRegex);
55
101
  if (match) {
56
102
  const date = new Date();
@@ -61,6 +107,9 @@ export function parseDueDate(dateString) {
61
107
  if (unit.startsWith('minute')) {
62
108
  date.setMinutes(date.getMinutes() + value);
63
109
  }
110
+ else if (unit.startsWith('hour')) {
111
+ date.setHours(date.getHours() + value);
112
+ }
64
113
  else if (unit.startsWith('day')) {
65
114
  date.setDate(date.getDate() + value);
66
115
  }
@@ -80,6 +129,9 @@ export function parseDueDate(dateString) {
80
129
  else if (lowerDate.includes('next month')) {
81
130
  date.setMonth(date.getMonth() + 1);
82
131
  }
132
+ else if (lowerDate.includes('next year')) {
133
+ date.setFullYear(date.getFullYear() + 1);
134
+ }
83
135
  // Set the time if specified
84
136
  if (hours) {
85
137
  let parsedHours = parseInt(hours);
@@ -97,31 +149,44 @@ export function parseDueDate(dateString) {
97
149
  }
98
150
  return date.getTime();
99
151
  }
100
- // Handle hours from now
101
- const minutesRegex = /(\d+)\s*minutes?\s*from\s*now/i;
102
- const hoursRegex = /(\d+)\s*hours?\s*from\s*now/i;
103
- const daysRegex = /(\d+)\s*days?\s*from\s*now/i;
104
- const weeksRegex = /(\d+)\s*weeks?\s*from\s*now/i;
105
- const monthsRegex = /(\d+)\s*months?\s*from\s*now/i;
106
- if (minutesRegex.test(lowerDate)) {
107
- const minutes = parseInt(lowerDate.match(minutesRegex)[1]);
108
- return getRelativeTimestamp(minutes);
109
- }
110
- if (hoursRegex.test(lowerDate)) {
111
- const hours = parseInt(lowerDate.match(hoursRegex)[1]);
112
- return getRelativeTimestamp(0, hours);
113
- }
114
- if (daysRegex.test(lowerDate)) {
115
- const days = parseInt(lowerDate.match(daysRegex)[1]);
116
- return getRelativeTimestamp(0, 0, days);
117
- }
118
- if (weeksRegex.test(lowerDate)) {
119
- const weeks = parseInt(lowerDate.match(weeksRegex)[1]);
120
- return getRelativeTimestamp(0, 0, 0, weeks);
152
+ // Handle various relative formats
153
+ const relativeFormats = [
154
+ { regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
155
+ { regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
156
+ { regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
157
+ { regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
158
+ { regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
159
+ ];
160
+ for (const format of relativeFormats) {
161
+ if (format.regex.test(lowerDate)) {
162
+ const value = parseInt(lowerDate.match(format.regex)[1]);
163
+ return format.handler(value);
164
+ }
121
165
  }
122
- if (monthsRegex.test(lowerDate)) {
123
- const months = parseInt(lowerDate.match(monthsRegex)[1]);
124
- return getRelativeTimestamp(0, 0, 0, 0, months);
166
+ // Handle specific date formats
167
+ // Format: MM/DD/YYYY
168
+ const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
169
+ const usDateMatch = lowerDate.match(usDateRegex);
170
+ if (usDateMatch) {
171
+ const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
172
+ const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
173
+ parseInt(day));
174
+ // Add time if specified
175
+ if (hours) {
176
+ let parsedHours = parseInt(hours);
177
+ const parsedMinutes = minutes ? parseInt(minutes) : 0;
178
+ // Convert to 24-hour format if meridian is specified
179
+ if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
180
+ parsedHours += 12;
181
+ if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
182
+ parsedHours = 0;
183
+ date.setHours(parsedHours, parsedMinutes, 0, 0);
184
+ }
185
+ else {
186
+ // Default to end of day if no time specified
187
+ date.setHours(23, 59, 59, 999);
188
+ }
189
+ return date.getTime();
125
190
  }
126
191
  // Try to parse as a date string
127
192
  const date = new Date(dateString);
@@ -164,3 +229,57 @@ export function formatDueDate(timestamp) {
164
229
  return undefined;
165
230
  }
166
231
  }
232
+ /**
233
+ * Checks if a timestamp is for today
234
+ *
235
+ * @param timestamp Unix timestamp in milliseconds
236
+ * @returns Boolean indicating if the timestamp is for today
237
+ */
238
+ export function isToday(timestamp) {
239
+ const date = new Date(timestamp);
240
+ const today = new Date();
241
+ return date.getDate() === today.getDate() &&
242
+ date.getMonth() === today.getMonth() &&
243
+ date.getFullYear() === today.getFullYear();
244
+ }
245
+ /**
246
+ * Get timestamp range for today (start to end)
247
+ *
248
+ * @returns Object with start and end timestamps for today
249
+ */
250
+ export function getTodayRange() {
251
+ return {
252
+ start: getStartOfDay(),
253
+ end: getEndOfDay()
254
+ };
255
+ }
256
+ /**
257
+ * Format a date for display in errors and messages
258
+ * @param timestamp The timestamp to format
259
+ * @returns A human-readable relative time (e.g., "2 hours ago")
260
+ */
261
+ export function formatRelativeTime(timestamp) {
262
+ if (!timestamp)
263
+ return 'Unknown';
264
+ const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
265
+ const now = Date.now();
266
+ const diffMs = now - timestampNum;
267
+ // Convert to appropriate time unit
268
+ const diffSec = Math.floor(diffMs / 1000);
269
+ if (diffSec < 60)
270
+ return `${diffSec} seconds ago`;
271
+ const diffMin = Math.floor(diffSec / 60);
272
+ if (diffMin < 60)
273
+ return `${diffMin} minutes ago`;
274
+ const diffHour = Math.floor(diffMin / 60);
275
+ if (diffHour < 24)
276
+ return `${diffHour} hours ago`;
277
+ const diffDays = Math.floor(diffHour / 24);
278
+ if (diffDays < 30)
279
+ return `${diffDays} days ago`;
280
+ const diffMonths = Math.floor(diffDays / 30);
281
+ if (diffMonths < 12)
282
+ return `${diffMonths} months ago`;
283
+ const diffYears = Math.floor(diffMonths / 12);
284
+ return `${diffYears} years ago`;
285
+ }
@@ -9,13 +9,40 @@
9
9
  import { clickUpServices } from '../services/shared.js';
10
10
  import { findListIDByName } from '../tools/list.js';
11
11
  /**
12
- * Resolve a list ID from either a direct ID or list name
12
+ * Check if a task name matches search criteria
13
13
  *
14
- * @param listId Optional direct list ID
15
- * @param listName Optional list name to resolve
16
- * @param workspaceService Workspace service to use for lookup
17
- * @returns Resolved list ID
18
- * @throws Error if neither listId nor listName is provided, or if list name can't be resolved
14
+ * Performs flexible case-insensitive and emoji-aware text matching
15
+ * Used by multiple components for consistent name matching behavior
16
+ */
17
+ export function isNameMatch(taskName, searchTerm) {
18
+ const normalizedTask = taskName.toLowerCase().trim();
19
+ const normalizedSearch = searchTerm.toLowerCase().trim();
20
+ // Handle empty strings - don't match empty task names
21
+ if (normalizedTask === '')
22
+ return false;
23
+ if (normalizedSearch === '')
24
+ return false;
25
+ // Exact match check
26
+ if (normalizedTask === normalizedSearch)
27
+ return true;
28
+ // Substring match check
29
+ if (normalizedTask.includes(normalizedSearch) || normalizedSearch.includes(normalizedTask))
30
+ return true;
31
+ // Handle emoji characters in names
32
+ if (/[\p{Emoji}]/u.test(normalizedSearch) || /[\p{Emoji}]/u.test(normalizedTask)) {
33
+ const taskWithoutEmoji = normalizedTask.replace(/[\p{Emoji}]/gu, '').trim();
34
+ const searchWithoutEmoji = normalizedSearch.replace(/[\p{Emoji}]/gu, '').trim();
35
+ // Don't match if either becomes empty after emoji removal
36
+ if (taskWithoutEmoji === '' || searchWithoutEmoji === '')
37
+ return false;
38
+ return taskWithoutEmoji === searchWithoutEmoji ||
39
+ taskWithoutEmoji.includes(searchWithoutEmoji) ||
40
+ searchWithoutEmoji.includes(taskWithoutEmoji);
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * Resolve a list ID from either a direct ID or list name
19
46
  */
20
47
  export async function resolveListId(listId, listName, workspaceService = clickUpServices.workspace) {
21
48
  // If list ID is directly provided, use it
@@ -33,37 +60,3 @@ export async function resolveListId(listId, listName, workspaceService = clickUp
33
60
  // If neither is provided, throw an error
34
61
  throw new Error("Either listId or listName must be provided");
35
62
  }
36
- /**
37
- * Resolve a task ID from either a direct ID or task name + list info
38
- *
39
- * @param taskId Optional direct task ID
40
- * @param taskName Optional task name to resolve
41
- * @param listId Optional list ID for task lookup
42
- * @param listName Optional list name for task lookup
43
- * @param taskService Task service to use for lookup
44
- * @returns Resolved task ID
45
- * @throws Error if parameters are insufficient or task can't be found
46
- */
47
- export async function resolveTaskId(taskId, taskName, listId, listName, taskService = clickUpServices.task) {
48
- // If task ID is directly provided, use it
49
- if (taskId) {
50
- return taskId;
51
- }
52
- // If task name is provided, we need list info to find it
53
- if (taskName) {
54
- // We need either listId or listName to find a task by name
55
- if (!listId && !listName) {
56
- throw new Error(`List name or ID is required when using task name for task "${taskName}"`);
57
- }
58
- // Get list ID
59
- const targetListId = await resolveListId(listId, listName);
60
- // Find the task in the list
61
- const foundTask = await taskService.findTaskByName(targetListId, taskName);
62
- if (!foundTask) {
63
- throw new Error(`Task "${taskName}" not found in list`);
64
- }
65
- return foundTask.id;
66
- }
67
- // If neither is provided, throw an error
68
- throw new Error("Either taskId or taskName must be provided");
69
- }
@@ -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: `♥ 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.2",
3
+ "version": "0.6.3",
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",
@@ -1,64 +0,0 @@
1
- import { Logger } from "./logger.js";
2
- // Create a logger instance
3
- const logger = new Logger('MCPTools');
4
- /**
5
- * Register a handler for a tool that may receive JSON string parameters
6
- * This wrapper ensures that array and object parameters are properly parsed
7
- *
8
- * @param server MCP Server instance
9
- * @param name Tool name
10
- * @param handler Handler function
11
- */
12
- export function registerToolHandler(server, name, handler) {
13
- // Create a wrapper handler that pre-processes parameters
14
- const wrappedHandler = async (params) => {
15
- logger.debug(`Processing parameters for tool ${name}`, { params });
16
- try {
17
- // Process the parameters before passing them to the actual handler
18
- const processedParams = {};
19
- // Process each parameter - try to parse strings that might be JSON
20
- for (const [key, value] of Object.entries(params)) {
21
- if (typeof value === 'string') {
22
- try {
23
- // Check if this might be a JSON array or object
24
- if ((value.startsWith('[') && value.endsWith(']')) ||
25
- (value.startsWith('{') && value.endsWith('}'))) {
26
- try {
27
- processedParams[key] = JSON.parse(value);
28
- logger.debug(`Parsed JSON parameter: ${key}`, { original: value, parsed: processedParams[key] });
29
- }
30
- catch (parseError) {
31
- // If parsing fails, use the original string
32
- processedParams[key] = value;
33
- logger.debug(`Failed to parse JSON for parameter: ${key}, using original`, { error: parseError.message });
34
- }
35
- }
36
- else {
37
- processedParams[key] = value;
38
- }
39
- }
40
- catch (error) {
41
- // If there's any error, use the original value
42
- processedParams[key] = value;
43
- logger.debug(`Error processing parameter: ${key}`, { error: error.message });
44
- }
45
- }
46
- else {
47
- // Non-string values are used as-is
48
- processedParams[key] = value;
49
- }
50
- }
51
- logger.debug(`Processed parameters for tool ${name}`, { processedParams });
52
- // Call the original handler with processed parameters
53
- return handler(processedParams);
54
- }
55
- catch (error) {
56
- logger.error(`Error in wrapped handler for tool ${name}:`, { error: error.stack || error.message });
57
- throw error;
58
- }
59
- };
60
- // Use setRequestHandler to register the wrapped handler
61
- logger.info(`Registering wrapped handler for tool: ${name}`);
62
- // Override the tool's handler in the CallTool switch statement
63
- // The server.ts file will use the switch case to call this handler
64
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Server State Management Module
3
- *
4
- * This module provides shared state management for the MCP server,
5
- * particularly for controlling shutdown behavior and tracking busy states.
6
- */
7
- // State variables
8
- let serverBusyState = false; // Tracks if server is doing critical work
9
- let gracePeriodActive = false; // Tracks if we're in post-initialization grace period
10
- let gracePeriodTimer = null;
11
- export const GRACE_PERIOD_MS = 10000; // 10 second grace period after startup
12
- /**
13
- * Logging helper that avoids circular dependency
14
- */
15
- function safeLog(level, message, data) {
16
- // Use console as a fallback to avoid circular dependency with logger
17
- const timestamp = new Date().toISOString();
18
- if (level === 'error') {
19
- console.error(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
20
- }
21
- else if (level === 'debug' && process.env.DEBUG) {
22
- console.debug(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
23
- }
24
- else if (level !== 'debug') {
25
- console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
26
- }
27
- }
28
- /**
29
- * Set the server as busy doing critical work that shouldn't be interrupted
30
- * @param busy Whether the server is currently busy with critical operations
31
- */
32
- export function setServerBusy(busy) {
33
- serverBusyState = busy;
34
- safeLog('debug', `Server busy state set to: ${busy}`);
35
- }
36
- /**
37
- * Start grace period after initialization to prevent immediate shutdown
38
- */
39
- export function startGracePeriod() {
40
- gracePeriodActive = true;
41
- safeLog('debug', `Starting ${GRACE_PERIOD_MS}ms grace period to prevent premature shutdown`);
42
- if (gracePeriodTimer) {
43
- clearTimeout(gracePeriodTimer);
44
- }
45
- gracePeriodTimer = setTimeout(() => {
46
- gracePeriodActive = false;
47
- safeLog('debug', 'Grace period ended, server will now respond to shutdown signals');
48
- gracePeriodTimer = null;
49
- }, GRACE_PERIOD_MS);
50
- }
51
- /**
52
- * Cancel the grace period if needed
53
- */
54
- export function cancelGracePeriod() {
55
- if (gracePeriodTimer) {
56
- clearTimeout(gracePeriodTimer);
57
- gracePeriodTimer = null;
58
- }
59
- gracePeriodActive = false;
60
- safeLog('debug', 'Grace period canceled, server will now respond to shutdown signals');
61
- }
62
- /**
63
- * Check if the server should ignore shutdown signals
64
- * @returns true if shutdown signals should be ignored
65
- */
66
- export function shouldIgnoreShutdown() {
67
- // Ignore shutdown if explicitly configured via environment variable
68
- if (process.env.FORCE_KEEP_ALIVE === 'true') {
69
- return true;
70
- }
71
- // Ignore shutdown during the grace period after startup
72
- if (gracePeriodActive) {
73
- return true;
74
- }
75
- // Ignore shutdown if the server is doing critical work
76
- if (serverBusyState) {
77
- return true;
78
- }
79
- // Otherwise, allow normal shutdown
80
- return false;
81
- }
82
- /**
83
- * Check if grace period is currently active
84
- */
85
- export function isGracePeriodActive() {
86
- return gracePeriodActive;
87
- }
88
- /**
89
- * Check if server is currently in busy state
90
- */
91
- export function isServerBusy() {
92
- return serverBusyState;
93
- }
package/build/server.log DELETED
File without changes