@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 +2 -2
- package/build/server.js +1 -1
- package/build/services/clickup/task/task-core.js +2 -0
- package/build/tools/task/handlers.js +68 -8
- package/build/tools/task/workspace-operations.js +35 -0
- package/build/utils/date-utils.js +82 -4
- package/package.json +1 -1
- package/build/server.log +0 -1
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.
|
|
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
|
@@ -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, ...
|
|
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:
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
package/build/server.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Logging initialized to /Volumes/Code/Projects/MCP/clickup-mcp-server/build/server.log
|