@taazkareem/clickup-mcp-server 0.8.1 → 0.8.2

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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI applications. This server allows AI agents to interact with ClickUp tasks, spaces, lists, and folders through a standardized protocol.
8
8
 
9
- > 🚀 **Status Update:** v0.8.0 is now available with HTTP Streamable transport support, Legacy SSE Support, Member Management tools, and more.
9
+ > 🚀 **Status Update:** v0.8.1 is now available with HTTP Streamable transport support, Legacy SSE Support, Member Management tools, and more. Next release will patch some bugs. See Unreleased section of [Changelog](changelog.md) for details.
10
10
 
11
11
  ## Setup
12
12
 
@@ -270,4 +270,4 @@ This software makes use of third-party APIs and may reference trademarks
270
270
  or brands owned by third parties. The use of such APIs or references does not imply
271
271
  any affiliation with or endorsement by the respective companies. All trademarks and
272
272
  brand names are the property of their respective owners. This project is an independent
273
- work and is not officially associated with or sponsored by any third-party company mentioned.
273
+ work and is not officially associated with or sponsored by any third-party company mentioned.
package/build/server.js CHANGED
@@ -22,7 +22,7 @@ const logger = new Logger('Server');
22
22
  const { workspace } = clickUpServices;
23
23
  export const server = new Server({
24
24
  name: "clickup-mcp-server",
25
- version: "0.8.1",
25
+ version: "0.8.2",
26
26
  }, {
27
27
  capabilities: {
28
28
  tools: {},
@@ -59,6 +59,8 @@ export class TaskServiceCore extends BaseClickUpService {
59
59
  params.append('include_closed', String(filters.include_closed));
60
60
  if (filters.subtasks)
61
61
  params.append('subtasks', String(filters.subtasks));
62
+ if (filters.include_subtasks)
63
+ params.append('include_subtasks', String(filters.include_subtasks));
62
64
  if (filters.page)
63
65
  params.append('page', String(filters.page));
64
66
  if (filters.order_by)
@@ -12,6 +12,7 @@ import { clickUpServices } from '../../services/shared.js';
12
12
  import { BulkService } from '../../services/clickup/bulk.js';
13
13
  import { parseDueDate } from '../utils.js';
14
14
  import { validateTaskIdentification, validateListIdentification, validateTaskUpdateData, validateBulkTasks, parseBulkOptions, resolveListIdWithValidation } from './utilities.js';
15
+ import { handleResolveAssignees } from '../member.js';
15
16
  import { workspaceService } from '../../services/shared.js';
16
17
  import { isNameMatch } from '../../utils/resolver-utils.js';
17
18
  import { Logger } from '../../logger.js';
@@ -79,10 +80,55 @@ function parseTimeEstimate(timeEstimate) {
79
80
  }
80
81
  return Math.round(totalMinutes); // Return minutes
81
82
  }
83
+ /**
84
+ * Resolve assignees from mixed input (user IDs, emails, usernames) to user IDs
85
+ */
86
+ async function resolveAssignees(assignees) {
87
+ if (!assignees || !Array.isArray(assignees) || assignees.length === 0) {
88
+ return [];
89
+ }
90
+ const resolved = [];
91
+ const toResolve = [];
92
+ // Separate numeric IDs from strings that need resolution
93
+ for (const assignee of assignees) {
94
+ if (typeof assignee === 'number') {
95
+ resolved.push(assignee);
96
+ }
97
+ else if (typeof assignee === 'string') {
98
+ // Check if it's a numeric string
99
+ const numericId = parseInt(assignee, 10);
100
+ if (!isNaN(numericId) && numericId.toString() === assignee) {
101
+ resolved.push(numericId);
102
+ }
103
+ else {
104
+ // It's an email or username that needs resolution
105
+ toResolve.push(assignee);
106
+ }
107
+ }
108
+ }
109
+ // Resolve emails/usernames to user IDs if any
110
+ if (toResolve.length > 0) {
111
+ try {
112
+ const result = await handleResolveAssignees({ assignees: toResolve });
113
+ if (result.userIds && Array.isArray(result.userIds)) {
114
+ for (const userId of result.userIds) {
115
+ if (userId !== null && typeof userId === 'number') {
116
+ resolved.push(userId);
117
+ }
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ console.warn('Failed to resolve some assignees:', error.message);
123
+ // Continue with the IDs we could resolve
124
+ }
125
+ }
126
+ return resolved;
127
+ }
82
128
  /**
83
129
  * Build task update data from parameters
84
130
  */
85
- function buildUpdateData(params) {
131
+ async function buildUpdateData(params) {
86
132
  const updateData = {};
87
133
  if (params.name !== undefined)
88
134
  updateData.name = params.name;
@@ -116,6 +162,10 @@ function buildUpdateData(params) {
116
162
  if (params.custom_fields !== undefined) {
117
163
  updateData.custom_fields = params.custom_fields;
118
164
  }
165
+ // Handle assignees if provided - resolve emails/usernames to user IDs
166
+ if (params.assignees !== undefined) {
167
+ updateData.assignees = await resolveAssignees(params.assignees);
168
+ }
119
169
  return updateData;
120
170
  }
121
171
  /**
@@ -377,6 +427,8 @@ export async function createTaskHandler(params) {
377
427
  // Use our helper function to validate and convert priority
378
428
  const priority = toTaskPriority(params.priority);
379
429
  const listId = await getListId(params.listId, params.listName);
430
+ // Resolve assignees if provided
431
+ const resolvedAssignees = assignees ? await resolveAssignees(assignees) : undefined;
380
432
  const taskData = {
381
433
  name,
382
434
  description,
@@ -387,7 +439,7 @@ export async function createTaskHandler(params) {
387
439
  tags,
388
440
  custom_fields,
389
441
  check_required_custom_fields,
390
- assignees
442
+ assignees: resolvedAssignees
391
443
  };
392
444
  // Add due date if specified
393
445
  if (dueDate) {
@@ -405,12 +457,14 @@ export async function createTaskHandler(params) {
405
457
  * Handler for updating a task
406
458
  */
407
459
  export async function updateTaskHandler(taskService, params) {
408
- const { taskId, taskName, listName, customTaskId, ...updateData } = params;
460
+ const { taskId, taskName, listName, customTaskId, ...rawUpdateData } = params;
409
461
  // Validate task identification with global lookup enabled
410
462
  const validationResult = validateTaskIdentification(params, { useGlobalLookup: true });
411
463
  if (!validationResult.isValid) {
412
464
  throw new Error(validationResult.errorMessage);
413
465
  }
466
+ // Build properly formatted update data from raw parameters (now async)
467
+ const updateData = await buildUpdateData(rawUpdateData);
414
468
  // Validate update data
415
469
  validateTaskUpdateData(updateData);
416
470
  try {
@@ -564,7 +618,11 @@ export async function getWorkspaceTasksHandler(taskService, params) {
564
618
  date_updated_lt: params.date_updated_lt,
565
619
  assignees: params.assignees,
566
620
  page: params.page,
567
- detail_level: params.detail_level || 'detailed'
621
+ detail_level: params.detail_level || 'detailed',
622
+ subtasks: params.subtasks,
623
+ include_subtasks: params.include_subtasks,
624
+ include_compact_time_entries: params.include_compact_time_entries,
625
+ custom_fields: params.custom_fields
568
626
  };
569
627
  // Get tasks with adaptive response format support
570
628
  const response = await taskService.getWorkspaceTasks(filters);
@@ -597,8 +655,10 @@ export async function createBulkTasksHandler(params) {
597
655
  validateBulkTasks(tasks, 'create');
598
656
  // Validate and resolve list ID
599
657
  const targetListId = await resolveListIdWithValidation(listId, listName);
600
- // Format tasks for creation
601
- const formattedTasks = tasks.map(task => {
658
+ // Format tasks for creation - resolve assignees for each task
659
+ const formattedTasks = await Promise.all(tasks.map(async (task) => {
660
+ // Resolve assignees if provided
661
+ const resolvedAssignees = task.assignees ? await resolveAssignees(task.assignees) : undefined;
602
662
  const taskData = {
603
663
  name: task.name,
604
664
  description: task.description,
@@ -607,7 +667,7 @@ export async function createBulkTasksHandler(params) {
607
667
  priority: toTaskPriority(task.priority),
608
668
  tags: task.tags,
609
669
  custom_fields: task.custom_fields,
610
- assignees: task.assignees
670
+ assignees: resolvedAssignees
611
671
  };
612
672
  // Add due date if specified
613
673
  if (task.dueDate) {
@@ -620,7 +680,7 @@ export async function createBulkTasksHandler(params) {
620
680
  taskData.start_date_time = true;
621
681
  }
622
682
  return taskData;
623
- });
683
+ }));
624
684
  // Parse bulk options
625
685
  const bulkOptions = parseBulkOptions(options);
626
686
  // Create tasks - pass arguments in correct order: listId, tasks, options
@@ -17,6 +17,7 @@ export const getWorkspaceTasksTool = {
17
17
  Valid Usage:
18
18
  1. Apply any combination of filters (tags, lists, folders, spaces, statuses, etc.)
19
19
  2. Use pagination to manage large result sets
20
+ 3. Include subtasks by setting subtasks=true
20
21
 
21
22
  Requirements:
22
23
  - At least one filter parameter is REQUIRED (tags, list_ids, folder_ids, space_ids, statuses, assignees, or date filters)
@@ -28,6 +29,8 @@ Notes:
28
29
  - Tag filtering is especially useful for cross-list organization (e.g., "project-x", "blocker", "needs-review")
29
30
  - Combine multiple filters to narrow down your search scope
30
31
  - Use pagination for large result sets
32
+ - Set subtasks=true to include subtask details in the response
33
+ IMPORTANT: subtasks=true enables subtasks to appear in results, but subtasks must still match your other filter criteria (tags, lists, etc.) to be returned. To see all subtasks of a specific task regardless of filters, use the get_task tool with subtasks=true instead.
31
34
  - Use the detail_level parameter to control the amount of data returned:
32
35
  - "summary": Returns lightweight task data (name, status, list, tags)
33
36
  - "detailed": Returns complete task data with all fields (DEFAULT if not specified)
@@ -123,6 +126,22 @@ Notes:
123
126
  type: 'string',
124
127
  enum: ['summary', 'detailed'],
125
128
  description: 'Level of detail to return. Use summary for lightweight responses or detailed for full task data. If not specified, defaults to "detailed".'
129
+ },
130
+ subtasks: {
131
+ type: 'boolean',
132
+ description: 'Include subtasks in the response. Set to true to retrieve subtask details for all returned tasks. Note: subtasks must still match your other filter criteria to appear in results.'
133
+ },
134
+ include_subtasks: {
135
+ type: 'boolean',
136
+ description: 'Alternative parameter for including subtasks (legacy support).'
137
+ },
138
+ include_compact_time_entries: {
139
+ type: 'boolean',
140
+ description: 'Include compact time entry data in the response.'
141
+ },
142
+ custom_fields: {
143
+ type: 'object',
144
+ description: 'Filter by custom field values. Provide as key-value pairs where keys are custom field IDs.'
126
145
  }
127
146
  }
128
147
  },
@@ -216,6 +235,22 @@ Notes:
216
235
  type: 'string',
217
236
  enum: ['summary', 'detailed'],
218
237
  description: 'Level of detail to return. Use summary for lightweight responses or detailed for full task data. If not specified, defaults to "detailed".'
238
+ },
239
+ subtasks: {
240
+ type: 'boolean',
241
+ description: 'Include subtasks in the response. Set to true to retrieve subtask details for all returned tasks. Note: subtasks must still match your other filter criteria to appear in results.'
242
+ },
243
+ include_subtasks: {
244
+ type: 'boolean',
245
+ description: 'Alternative parameter for including subtasks (legacy support).'
246
+ },
247
+ include_compact_time_entries: {
248
+ type: 'boolean',
249
+ description: 'Include compact time entry data in the response.'
250
+ },
251
+ custom_fields: {
252
+ type: 'object',
253
+ description: 'Filter by custom field values. Provide as key-value pairs where keys are custom field IDs.'
219
254
  }
220
255
  }
221
256
  }
@@ -69,6 +69,14 @@ export function parseDueDate(dateString) {
69
69
  if (!dateString)
70
70
  return undefined;
71
71
  try {
72
+ // First, try to parse as a direct timestamp
73
+ const numericValue = Number(dateString);
74
+ if (!isNaN(numericValue) && numericValue > 0) {
75
+ // If it's a reasonable timestamp (after year 2000), use it
76
+ if (numericValue > 946684800000) { // Jan 1, 2000
77
+ return numericValue;
78
+ }
79
+ }
72
80
  // Handle natural language dates
73
81
  const lowerDate = dateString.toLowerCase().trim();
74
82
  // Handle "now" specifically
@@ -98,6 +106,44 @@ export function parseDueDate(dateString) {
98
106
  tomorrow.setHours(23, 59, 59, 999);
99
107
  return tomorrow.getTime();
100
108
  }
109
+ // Handle day names (Monday, Tuesday, etc.) - find next occurrence
110
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
111
+ const dayMatch = lowerDate.match(/\b(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\b/);
112
+ if (dayMatch) {
113
+ const targetDayName = dayMatch[1];
114
+ const targetDayIndex = dayNames.indexOf(targetDayName);
115
+ const today = new Date();
116
+ const currentDayIndex = today.getDay();
117
+ // Calculate days until target day
118
+ let daysUntilTarget = targetDayIndex - currentDayIndex;
119
+ if (daysUntilTarget <= 0) {
120
+ daysUntilTarget += 7; // Next week
121
+ }
122
+ // Handle "next" prefix explicitly
123
+ if (lowerDate.includes('next ')) {
124
+ daysUntilTarget += 7;
125
+ }
126
+ const targetDate = new Date(today);
127
+ targetDate.setDate(today.getDate() + daysUntilTarget);
128
+ // Extract time if specified (e.g., "Friday at 3pm", "Saturday 2:30pm")
129
+ const timeMatch = lowerDate.match(/(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i);
130
+ if (timeMatch) {
131
+ let hours = parseInt(timeMatch[1]);
132
+ const minutes = timeMatch[2] ? parseInt(timeMatch[2]) : 0;
133
+ const meridian = timeMatch[3]?.toLowerCase();
134
+ // Convert to 24-hour format
135
+ if (meridian === 'pm' && hours < 12)
136
+ hours += 12;
137
+ if (meridian === 'am' && hours === 12)
138
+ hours = 0;
139
+ targetDate.setHours(hours, minutes, 0, 0);
140
+ }
141
+ else {
142
+ // Default to end of day if no time specified
143
+ targetDate.setHours(23, 59, 59, 999);
144
+ }
145
+ return targetDate.getTime();
146
+ }
101
147
  // Handle relative dates with specific times
102
148
  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;
103
149
  const match = lowerDate.match(relativeTimeRegex);
@@ -191,10 +237,42 @@ export function parseDueDate(dateString) {
191
237
  }
192
238
  return date.getTime();
193
239
  }
194
- // Try to parse as a date string
195
- const date = new Date(dateString);
196
- if (!isNaN(date.getTime())) {
197
- return date.getTime();
240
+ // Enhanced fallback: Try JavaScript's native Date constructor with various formats
241
+ // This handles many natural language formats like "Saturday at 3pm EST", "next Friday", etc.
242
+ const nativeDate = new Date(dateString);
243
+ if (!isNaN(nativeDate.getTime())) {
244
+ // Check if the parsed date is reasonable (not too far in the past or future)
245
+ const now = Date.now();
246
+ const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
247
+ const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
248
+ if (nativeDate.getTime() > oneYearAgo && nativeDate.getTime() < tenYearsFromNow) {
249
+ return nativeDate.getTime();
250
+ }
251
+ }
252
+ // Try some common variations and transformations
253
+ const variations = [
254
+ dateString.replace(/\s+at\s+/i, ' '), // "Saturday at 3pm" -> "Saturday 3pm"
255
+ dateString.replace(/\s+EST|EDT|PST|PDT|CST|CDT|MST|MDT/i, ''), // Remove timezone
256
+ dateString.replace(/next\s+/i, ''), // "next Friday" -> "Friday"
257
+ dateString.replace(/this\s+/i, ''), // "this Friday" -> "Friday"
258
+ ];
259
+ for (const variation of variations) {
260
+ const varDate = new Date(variation);
261
+ if (!isNaN(varDate.getTime())) {
262
+ const now = Date.now();
263
+ const oneYearAgo = now - (365 * 24 * 60 * 60 * 1000);
264
+ const tenYearsFromNow = now + (10 * 365 * 24 * 60 * 60 * 1000);
265
+ if (varDate.getTime() > oneYearAgo && varDate.getTime() < tenYearsFromNow) {
266
+ // If the parsed date is in the past, assume they meant next occurrence
267
+ if (varDate.getTime() < now) {
268
+ // Add 7 days if it's a day of the week
269
+ if (dateString.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) {
270
+ varDate.setDate(varDate.getDate() + 7);
271
+ }
272
+ }
273
+ return varDate.getTime();
274
+ }
275
+ }
198
276
  }
199
277
  // If all parsing fails, return undefined
200
278
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",
package/build/server.log DELETED
@@ -1 +0,0 @@
1
- Logging initialized to /Volumes/Code/Projects/MCP/clickup-mcp-server/build/server.log