@pripanggalih/clickup-mcp 1.6.1

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 (57) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +295 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +184 -0
  6. package/dist/clickup-text.d.ts +83 -0
  7. package/dist/clickup-text.d.ts.map +1 -0
  8. package/dist/clickup-text.js +563 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +135 -0
  12. package/dist/resources/space-resources.d.ts +6 -0
  13. package/dist/resources/space-resources.d.ts.map +1 -0
  14. package/dist/resources/space-resources.js +95 -0
  15. package/dist/shared/config.d.ts +11 -0
  16. package/dist/shared/config.d.ts.map +1 -0
  17. package/dist/shared/config.js +61 -0
  18. package/dist/shared/data-uri.d.ts +14 -0
  19. package/dist/shared/data-uri.d.ts.map +1 -0
  20. package/dist/shared/data-uri.js +34 -0
  21. package/dist/shared/image-processing.d.ts +13 -0
  22. package/dist/shared/image-processing.d.ts.map +1 -0
  23. package/dist/shared/image-processing.js +199 -0
  24. package/dist/shared/types.d.ts +21 -0
  25. package/dist/shared/types.d.ts.map +1 -0
  26. package/dist/shared/types.js +2 -0
  27. package/dist/shared/utils.d.ts +71 -0
  28. package/dist/shared/utils.d.ts.map +1 -0
  29. package/dist/shared/utils.js +508 -0
  30. package/dist/test-utils.d.ts +23 -0
  31. package/dist/test-utils.d.ts.map +1 -0
  32. package/dist/test-utils.js +44 -0
  33. package/dist/tools/admin-tools.d.ts +3 -0
  34. package/dist/tools/admin-tools.d.ts.map +1 -0
  35. package/dist/tools/admin-tools.js +288 -0
  36. package/dist/tools/doc-tools.d.ts +4 -0
  37. package/dist/tools/doc-tools.d.ts.map +1 -0
  38. package/dist/tools/doc-tools.js +436 -0
  39. package/dist/tools/list-tools.d.ts +4 -0
  40. package/dist/tools/list-tools.d.ts.map +1 -0
  41. package/dist/tools/list-tools.js +175 -0
  42. package/dist/tools/search-tools.d.ts +3 -0
  43. package/dist/tools/search-tools.d.ts.map +1 -0
  44. package/dist/tools/search-tools.js +161 -0
  45. package/dist/tools/space-tools.d.ts +3 -0
  46. package/dist/tools/space-tools.d.ts.map +1 -0
  47. package/dist/tools/space-tools.js +128 -0
  48. package/dist/tools/task-tools.d.ts +8 -0
  49. package/dist/tools/task-tools.d.ts.map +1 -0
  50. package/dist/tools/task-tools.js +329 -0
  51. package/dist/tools/task-write-tools.d.ts +3 -0
  52. package/dist/tools/task-write-tools.d.ts.map +1 -0
  53. package/dist/tools/task-write-tools.js +567 -0
  54. package/dist/tools/time-tools.d.ts +4 -0
  55. package/dist/tools/time-tools.d.ts.map +1 -0
  56. package/dist/tools/time-tools.js +338 -0
  57. package/package.json +74 -0
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerTimeToolsRead = registerTimeToolsRead;
4
+ exports.registerTimeToolsWrite = registerTimeToolsWrite;
5
+ const zod_1 = require("zod");
6
+ const config_1 = require("../shared/config");
7
+ const utils_1 = require("../shared/utils");
8
+ /**
9
+ * Converts ISO date string to Unix timestamp in milliseconds
10
+ */
11
+ function isoToTimestamp(isoString) {
12
+ return new Date(isoString).getTime();
13
+ }
14
+ /**
15
+ * Formats timestamp to ISO string with local timezone (not UTC)
16
+ */
17
+ function timestampToIso(timestamp) {
18
+ const date = new Date(timestamp);
19
+ const year = date.getFullYear();
20
+ const month = String(date.getMonth() + 1).padStart(2, '0');
21
+ const day = String(date.getDate()).padStart(2, '0');
22
+ const hours = String(date.getHours()).padStart(2, '0');
23
+ const minutes = String(date.getMinutes()).padStart(2, '0');
24
+ const seconds = String(date.getSeconds()).padStart(2, '0');
25
+ // Calculate timezone offset
26
+ const offset = date.getTimezoneOffset();
27
+ const offsetHours = Math.floor(Math.abs(offset) / 60);
28
+ const offsetMinutes = Math.abs(offset) % 60;
29
+ const sign = offset <= 0 ? '+' : '-';
30
+ const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
31
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${timezoneOffset}`;
32
+ }
33
+ /**
34
+ * Formats duration in milliseconds to human readable format
35
+ */
36
+ function formatDuration(durationMs) {
37
+ const hours = durationMs / (1000 * 60 * 60);
38
+ const displayHours = Math.floor(hours);
39
+ const displayMinutes = Math.round((hours - displayHours) * 60);
40
+ return displayHours > 0 ? `${displayHours}h ${displayMinutes}m` : `${displayMinutes}m`;
41
+ }
42
+ /**
43
+ * Formats timestamp to simple date and time for entry display
44
+ */
45
+ function formatEntryTime(timestamp) {
46
+ const date = new Date(timestamp);
47
+ const month = String(date.getMonth() + 1).padStart(2, '0');
48
+ const day = String(date.getDate()).padStart(2, '0');
49
+ const hours = String(date.getHours()).padStart(2, '0');
50
+ const minutes = String(date.getMinutes()).padStart(2, '0');
51
+ return `${date.getFullYear()}-${month}-${day} ${hours}:${minutes}`;
52
+ }
53
+ function registerTimeToolsRead(server) {
54
+ server.tool("getTimeEntries", "Gets time entries for a specific task or all user's time entries. Returns last 30 days by default if no dates specified.", {
55
+ task_id: zod_1.z.string().min(6).max(9).optional().describe("Optional 6-9 character task ID to filter entries. If not provided, returns all user's time entries."),
56
+ start_date: zod_1.z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00'). Defaults to 30 days ago."),
57
+ end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date."),
58
+ list_id: zod_1.z.string().optional().describe("Optional single list ID to filter time entries by a specific list"),
59
+ space_id: zod_1.z.string().optional().describe("Optional single space ID to filter time entries by a specific space"),
60
+ include_all_users: zod_1.z.boolean().optional().describe("Optional flag to include time entries from all team members (default: false, only current user)")
61
+ }, {
62
+ readOnlyHint: true
63
+ }, async ({ task_id, start_date, end_date, list_id, space_id, include_all_users }) => {
64
+ try {
65
+ // Build query parameters
66
+ const params = new URLSearchParams();
67
+ if (task_id) {
68
+ params.append('task_id', task_id);
69
+ }
70
+ if (start_date) {
71
+ params.append('start_date', isoToTimestamp(start_date).toString());
72
+ }
73
+ if (end_date) {
74
+ params.append('end_date', isoToTimestamp(end_date).toString());
75
+ }
76
+ // Add single list_id or space_id filter (not both)
77
+ if (list_id) {
78
+ params.append('list_id', list_id);
79
+ }
80
+ else if (space_id) {
81
+ params.append('space_id', space_id);
82
+ }
83
+ // Always include location names to get list information
84
+ params.append('include_location_names', 'true');
85
+ // Handle include_all_users by fetching all team members and adding them as assignees filter
86
+ // Note: This only works for Workspace Owners/Admins
87
+ if (include_all_users) {
88
+ try {
89
+ const teamMembers = await (0, utils_1.getAllTeamMembers)();
90
+ if (teamMembers.length > 0) {
91
+ params.append('assignee', teamMembers.join(','));
92
+ }
93
+ }
94
+ catch (error) {
95
+ console.error('Warning: Could not fetch all team members. This feature requires Workspace Owner/Admin permissions.');
96
+ // Continue without all users - will only show current user's entries
97
+ }
98
+ }
99
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
100
+ headers: { Authorization: config_1.CONFIG.apiKey },
101
+ });
102
+ if (!response.ok) {
103
+ throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
104
+ }
105
+ const data = await response.json();
106
+ return processTimeEntriesData(data, task_id, start_date, end_date, include_all_users);
107
+ }
108
+ catch (error) {
109
+ console.error('Error fetching time entries:', error);
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: `Error fetching time entries: ${error instanceof Error ? error.message : 'Unknown error'}`,
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ });
120
+ }
121
+ /**
122
+ * Process the time entries data and return formatted hierarchical output
123
+ */
124
+ function processTimeEntriesData(data, task_id, start_date, end_date, include_all_users) {
125
+ if (!data.data || !Array.isArray(data.data)) {
126
+ const noEntriesMsg = task_id ?
127
+ `No time entries found for task ${task_id}.` :
128
+ 'No time entries found.';
129
+ return {
130
+ content: [{ type: "text", text: noEntriesMsg }],
131
+ };
132
+ }
133
+ const filteredEntries = data.data;
134
+ // Create hierarchical structure: List → Task → User → Individual entries
135
+ const hierarchy = new Map();
136
+ let totalTimeMs = 0;
137
+ filteredEntries.forEach((entry) => {
138
+ const taskId = entry.task?.id || 'no-task';
139
+ // Use location names from include_location_names parameter
140
+ const listId = entry.task_location?.list_id || 'no-list';
141
+ const listName = entry.task_location?.list_name || 'No List';
142
+ const taskName = entry.task?.name || 'No Task';
143
+ const userId = entry.user?.id || 'no-user';
144
+ const userName = entry.user?.username || 'Unknown User';
145
+ // Handle running timers (negative duration)
146
+ let entryDurationMs = parseInt(entry.duration) || 0;
147
+ const isRunningTimer = entryDurationMs < 0;
148
+ if (isRunningTimer) {
149
+ // For running timers, calculate current duration from start time
150
+ entryDurationMs = Date.now() - parseInt(entry.start);
151
+ }
152
+ totalTimeMs += entryDurationMs;
153
+ // Initialize list level
154
+ if (!hierarchy.has(listId)) {
155
+ hierarchy.set(listId, {
156
+ name: listName,
157
+ id: listId,
158
+ totalTime: 0,
159
+ tasks: new Map()
160
+ });
161
+ }
162
+ const listData = hierarchy.get(listId);
163
+ listData.totalTime += entryDurationMs;
164
+ // Initialize task level
165
+ if (!listData.tasks.has(taskId)) {
166
+ listData.tasks.set(taskId, {
167
+ name: taskName,
168
+ id: taskId,
169
+ totalTime: 0,
170
+ users: new Map()
171
+ });
172
+ }
173
+ const taskData = listData.tasks.get(taskId);
174
+ taskData.totalTime += entryDurationMs;
175
+ // Initialize user level
176
+ if (!taskData.users.has(userId)) {
177
+ taskData.users.set(userId, {
178
+ name: userName,
179
+ id: userId,
180
+ totalTime: 0,
181
+ entries: []
182
+ });
183
+ }
184
+ const userData = taskData.users.get(userId);
185
+ userData.totalTime += entryDurationMs;
186
+ userData.entries.push(entry);
187
+ });
188
+ // Count total tasks across all lists
189
+ let totalTasks = 0;
190
+ for (const [listId, listData] of hierarchy.entries()) {
191
+ totalTasks += listData.tasks.size;
192
+ }
193
+ // Format the hierarchical output
194
+ const outputLines = [];
195
+ // Header with date range and total
196
+ const dateRange = start_date && end_date ?
197
+ ` (${start_date.split('T')[0]} to ${end_date.split('T')[0]})` :
198
+ start_date ? ` (from ${start_date.split('T')[0]})` :
199
+ end_date ? ` (until ${end_date.split('T')[0]})` : '';
200
+ outputLines.push(`Time Entries Summary${dateRange}`);
201
+ outputLines.push(`Total: ${formatDuration(totalTimeMs)}`);
202
+ outputLines.push('');
203
+ // Check if result is too large (>100 tasks)
204
+ const TASK_LIMIT = 100;
205
+ const isTruncated = totalTasks > TASK_LIMIT;
206
+ if (isTruncated) {
207
+ // Show only list-level summary
208
+ outputLines.push(`⚠️ Large result detected (${totalTasks} tasks). Showing summary only.`);
209
+ outputLines.push(`💡 Use list_id, space_id, or date filters for detailed view.`);
210
+ outputLines.push('');
211
+ for (const [listId, listData] of hierarchy.entries()) {
212
+ const taskCount = listData.tasks.size;
213
+ outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)} across ${taskCount} task${taskCount === 1 ? '' : 's'}`);
214
+ }
215
+ }
216
+ else {
217
+ // Show full hierarchical display
218
+ for (const [listId, listData] of hierarchy.entries()) {
219
+ outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)}`);
220
+ for (const [taskId, taskData] of listData.tasks.entries()) {
221
+ outputLines.push(` ├─ 🎯 ${taskData.name} (Task: ${taskId}) - ${formatDuration(taskData.totalTime)}`);
222
+ const userEntries = Array.from(taskData.users.entries());
223
+ for (let userIndex = 0; userIndex < userEntries.length; userIndex++) {
224
+ const [userId, userData] = userEntries[userIndex];
225
+ const isLastUser = userIndex === userEntries.length - 1;
226
+ const userPrefix = isLastUser ? ' └─' : ' ├─';
227
+ outputLines.push(`${userPrefix} ${userData.name}: ${formatDuration(userData.totalTime)}`);
228
+ // Add individual entries
229
+ userData.entries.forEach((entry, entryIndex) => {
230
+ const isLastEntry = entryIndex === userData.entries.length - 1;
231
+ const entryPrefix = isLastUser ?
232
+ (isLastEntry ? ' └─' : ' ├─') :
233
+ (isLastEntry ? ' │ └─' : ' │ ├─');
234
+ const entryStart = formatEntryTime(parseInt(entry.start));
235
+ // Handle running timers
236
+ const rawDuration = parseInt(entry.duration) || 0;
237
+ const isRunningTimer = rawDuration < 0;
238
+ let entryDuration;
239
+ if (isRunningTimer) {
240
+ const currentDuration = Date.now() - parseInt(entry.start);
241
+ entryDuration = `${formatDuration(currentDuration)} (running)`;
242
+ }
243
+ else {
244
+ entryDuration = formatDuration(rawDuration);
245
+ }
246
+ const entryDescription = entry.description ? ` | ${entry.description}` : '';
247
+ outputLines.push(`${entryPrefix} ${entryStart} - ${entryDuration}${entryDescription}`);
248
+ });
249
+ }
250
+ }
251
+ outputLines.push('');
252
+ }
253
+ }
254
+ return {
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: outputLines.join('\n')
259
+ }
260
+ ],
261
+ };
262
+ }
263
+ function registerTimeToolsWrite(server) {
264
+ server.tool("createTimeEntry", [
265
+ "Creates a time entry (books time) on a task for the current user.",
266
+ "Use decimal hours (e.g., 0.25 for 15 minutes, 0.5 for 30 minutes, 2.5 for 2.5 hours).",
267
+ "IMPORTANT: Before booking time, check the task's status - booking time on tasks in 'backlog', 'closed', or similar inactive states usually doesn't make sense.",
268
+ "Suggest moving the task to an active status like 'in progress' first."
269
+ ].join("\n"), {
270
+ task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to book time against"),
271
+ hours: zod_1.z.number().min(0.01).max(24).describe("Hours to book (decimal format, e.g., 0.25 = 15min, 1.5 = 1h 30min)"),
272
+ description: zod_1.z.string().optional().describe("Optional description for the time entry"),
273
+ start_time: zod_1.z.string().optional().describe("Optional start time as ISO date string (e.g., '2024-10-06T09:00:00+02:00', defaults to current time)")
274
+ }, {
275
+ readOnlyHint: false,
276
+ destructiveHint: false,
277
+ idempotentHint: false,
278
+ }, async ({ task_id, hours, description, start_time }) => {
279
+ try {
280
+ // Convert hours to milliseconds (ClickUp API uses milliseconds)
281
+ const durationMs = Math.round(hours * 60 * 60 * 1000);
282
+ // Convert ISO date to timestamp if provided, otherwise use current time
283
+ const startTimeMs = start_time ? isoToTimestamp(start_time) : Date.now();
284
+ const requestBody = {
285
+ tid: task_id,
286
+ start: startTimeMs,
287
+ duration: durationMs,
288
+ ...(description && { description })
289
+ };
290
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries`, {
291
+ method: 'POST',
292
+ headers: {
293
+ Authorization: config_1.CONFIG.apiKey,
294
+ 'Content-Type': 'application/json'
295
+ },
296
+ body: JSON.stringify(requestBody)
297
+ });
298
+ if (!response.ok) {
299
+ const errorData = await response.json().catch(() => ({}));
300
+ throw new Error(`Error creating time entry: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
301
+ }
302
+ const timeEntry = await response.json();
303
+ // Format duration for display
304
+ const displayHours = Math.floor(hours);
305
+ const displayMinutes = Math.round((hours - displayHours) * 60);
306
+ const durationDisplay = displayHours > 0 ?
307
+ `${displayHours}h ${displayMinutes}m` :
308
+ `${displayMinutes}m`;
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: [
314
+ `Time entry created successfully!`,
315
+ `entry_id: ${timeEntry.data?.id || 'N/A'}`,
316
+ `task_id: ${task_id}`,
317
+ `duration: ${durationDisplay}`,
318
+ `start_time: ${timestampToIso(startTimeMs)}`,
319
+ ...(description ? [`description: ${description}`] : []),
320
+ `user: ${timeEntry.data?.user?.username || 'Current user'}`
321
+ ].join('\n')
322
+ }
323
+ ],
324
+ };
325
+ }
326
+ catch (error) {
327
+ console.error('Error creating time entry:', error);
328
+ return {
329
+ content: [
330
+ {
331
+ type: "text",
332
+ text: `Error creating time entry: ${error instanceof Error ? error.message : 'Unknown error'}`,
333
+ },
334
+ ],
335
+ };
336
+ }
337
+ });
338
+ }
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@pripanggalih/clickup-mcp",
3
+ "version": "1.6.1",
4
+ "description": "Personal ClickUp MCP server for task context, documents, time tracking, and workspace administration.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "clickup-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "start": "node dist/index.js",
17
+ "dev": "npx tsc -w & nodemon dist/index.js",
18
+ "cli": "npx ts-node src/cli.ts",
19
+ "prettier": "prettier --write src/**/*.ts",
20
+ "prepublishOnly": "rm -r dist && npm run build",
21
+ "release": "npm run build && npm publish --access public && git add . && git commit -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git tag -a v$(node -p 'require(\"./package.json\").version') -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git push && git push --tags",
22
+ "mcpb": "npm run build && mcpb pack . ClickUp.mcpb",
23
+ "test": "node --test -r ts-node/register src/**/*.test.ts"
24
+ },
25
+ "keywords": [
26
+ "clickup",
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "llm",
30
+ "ai",
31
+ "ticket",
32
+ "time-tracking",
33
+ "productivity",
34
+ "project-management",
35
+ "task-management",
36
+ "agentic-coding",
37
+ "automation"
38
+ ],
39
+ "author": {
40
+ "name": "MW Pripanggalih",
41
+ "email": "galihcrew07@gmail.com"
42
+ },
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "1.15.1",
46
+ "fuse.js": "^7.1.0",
47
+ "remark-gfm": "^4.0.1",
48
+ "remark-parse": "^11.0.0",
49
+ "unified": "^11.0.5",
50
+ "zod": "^3.24.2"
51
+ },
52
+ "devDependencies": {
53
+ "@anthropic-ai/mcpb": "^1.1.1",
54
+ "@types/mdast": "^4.0.4",
55
+ "@types/node": "^22.14.1",
56
+ "dotenv": "^16.5.0",
57
+ "nodemon": "^3.1.9",
58
+ "prettier": "^3.5.3",
59
+ "ts-node": "^10.9.2",
60
+ "typescript": "^5.8.3",
61
+ "undici": "^7.16.0"
62
+ },
63
+ "engines": {
64
+ "node": ">=16.0.0"
65
+ },
66
+ "repository": {
67
+ "type": "git",
68
+ "url": "git+https://github.com/pripanggalih/clickup-mcp.git"
69
+ },
70
+ "bugs": {
71
+ "url": "https://github.com/pripanggalih/clickup-mcp/issues"
72
+ },
73
+ "homepage": "https://github.com/pripanggalih/clickup-mcp#readme"
74
+ }