@taazkareem/clickup-mcp-server 0.8.0 → 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.0",
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)
@@ -134,8 +134,7 @@ export const listDocumentPagesTool = {
134
134
  },
135
135
  max_page_depth: {
136
136
  type: "number",
137
- description: "Maximum depth of pages to retrieve (-1 for unlimited)",
138
- optional: true
137
+ description: "Maximum depth of pages to retrieve (-1 for unlimited)"
139
138
  }
140
139
  },
141
140
  required: ["documentId"]
@@ -164,8 +163,7 @@ export const getDocumentPagesTool = {
164
163
  content_format: {
165
164
  type: "string",
166
165
  enum: ["text/md", "text/html"],
167
- description: "Format of the content to retrieve",
168
- optional: true
166
+ description: "Format of the content to retrieve"
169
167
  }
170
168
  },
171
169
  required: ["documentId", "pageIds"]
@@ -186,8 +184,7 @@ export const createDocumentPageTool = {
186
184
  },
187
185
  content: {
188
186
  type: "string",
189
- description: "Content of the page",
190
- optional: true
187
+ description: "Content of the page"
191
188
  },
192
189
  name: {
193
190
  type: "string",
@@ -195,13 +192,11 @@ export const createDocumentPageTool = {
195
192
  },
196
193
  sub_title: {
197
194
  type: "string",
198
- description: "Subtitle of the page",
199
- optional: true
195
+ description: "Subtitle of the page"
200
196
  },
201
197
  parent_page_id: {
202
198
  type: "string",
203
- description: "ID of the parent page (if this is a sub-page)",
204
- optional: true
199
+ description: "ID of the parent page (if this is a sub-page)"
205
200
  }
206
201
  },
207
202
  required: ["documentId", "name"]
@@ -226,30 +221,25 @@ export const updateDocumentPageTool = {
226
221
  },
227
222
  name: {
228
223
  type: "string",
229
- description: "New name for the page",
230
- optional: true
224
+ description: "New name for the page"
231
225
  },
232
226
  sub_title: {
233
227
  type: "string",
234
- description: "New subtitle for the page",
235
- optional: true
228
+ description: "New subtitle for the page"
236
229
  },
237
230
  content: {
238
231
  type: "string",
239
- description: "New content for the page",
240
- optional: true
232
+ description: "New content for the page"
241
233
  },
242
234
  content_edit_mode: {
243
235
  type: "string",
244
236
  enum: ["replace", "append", "prepend"],
245
- description: "How to update the content. Defaults to replace",
246
- optional: true
237
+ description: "How to update the content. Defaults to replace"
247
238
  },
248
239
  content_format: {
249
240
  type: "string",
250
241
  enum: ["text/md", "text/plain"],
251
- description: "Format of the content. Defaults to text/md",
252
- optional: true
242
+ description: "Format of the content. Defaults to text/md"
253
243
  },
254
244
  },
255
245
  required: ["documentId", "pageId"]
@@ -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.0",
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",