@hauptsache.net/clickup-mcp 1.1.0 → 1.2.0

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.
@@ -4,6 +4,7 @@ exports.registerTimeToolsRead = registerTimeToolsRead;
4
4
  exports.registerTimeToolsWrite = registerTimeToolsWrite;
5
5
  const zod_1 = require("zod");
6
6
  const config_1 = require("../shared/config");
7
+ const utils_1 = require("../shared/utils");
7
8
  /**
8
9
  * Converts ISO date string to Unix timestamp in milliseconds
9
10
  */
@@ -29,12 +30,35 @@ function timestampToIso(timestamp) {
29
30
  const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
30
31
  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${timezoneOffset}`;
31
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
+ }
32
53
  function registerTimeToolsRead(server) {
33
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.", {
34
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."),
35
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."),
36
- 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.")
37
- }, async ({ task_id, start_date, end_date }) => {
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
+ }, async ({ task_id, start_date, end_date, list_id, space_id, include_all_users }) => {
38
62
  try {
39
63
  // Build query parameters
40
64
  const params = new URLSearchParams();
@@ -47,122 +71,37 @@ function registerTimeToolsRead(server) {
47
71
  if (end_date) {
48
72
  params.append('end_date', isoToTimestamp(end_date).toString());
49
73
  }
50
- const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
51
- headers: { Authorization: config_1.CONFIG.apiKey },
52
- });
53
- if (!response.ok) {
54
- throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
74
+ // Add single list_id or space_id filter (not both)
75
+ if (list_id) {
76
+ params.append('list_id', list_id);
55
77
  }
56
- const data = await response.json();
57
- if (!data.data || !Array.isArray(data.data)) {
58
- const noEntriesMsg = task_id ?
59
- `No time entries found for task ${task_id}.` :
60
- 'No time entries found for current user.';
61
- return {
62
- content: [{ type: "text", text: noEntriesMsg }],
63
- };
78
+ else if (space_id) {
79
+ params.append('space_id', space_id);
64
80
  }
65
- let summaryLines = [];
66
- let totalTimeMs = 0;
67
- if (task_id) {
68
- // Task-specific view: Group by user
69
- const timeByUser = new Map();
70
- const entriesByUser = new Map();
71
- data.data.forEach((entry) => {
72
- const username = entry.user?.username || 'Unknown User';
73
- const currentTime = timeByUser.get(username) || 0;
74
- const entryDurationMs = parseInt(entry.duration) || 0;
75
- timeByUser.set(username, currentTime + entryDurationMs);
76
- if (!entriesByUser.has(username)) {
77
- entriesByUser.set(username, []);
81
+ // Always include location names to get list information
82
+ params.append('include_location_names', 'true');
83
+ // Handle include_all_users by fetching all team members and adding them as assignees filter
84
+ // Note: This only works for Workspace Owners/Admins
85
+ if (include_all_users) {
86
+ try {
87
+ const teamMembers = await (0, utils_1.getAllTeamMembers)();
88
+ if (teamMembers.length > 0) {
89
+ params.append('assignee', teamMembers.join(','));
78
90
  }
79
- entriesByUser.get(username).push(entry);
80
- });
81
- summaryLines = [`Time entries for task ${task_id}:`];
82
- for (const [username, totalMs] of timeByUser.entries()) {
83
- const hours = totalMs / (1000 * 60 * 60);
84
- const displayHours = Math.floor(hours);
85
- const displayMinutes = Math.round((hours - displayHours) * 60);
86
- const timeDisplay = displayHours > 0 ?
87
- `${displayHours}h ${displayMinutes}m` :
88
- `${displayMinutes}m`;
89
- summaryLines.push(` ${username}: ${timeDisplay}`);
90
- totalTimeMs += totalMs;
91
91
  }
92
- }
93
- else {
94
- // All user entries: Group by task
95
- const timeByTask = new Map();
96
- const entriesByTask = new Map();
97
- data.data.forEach((entry) => {
98
- const taskInfo = entry.task ?
99
- `${entry.task.name} (${entry.task.id})` :
100
- 'No task';
101
- const currentTime = timeByTask.get(taskInfo) || 0;
102
- const entryDurationMs = parseInt(entry.duration) || 0;
103
- timeByTask.set(taskInfo, currentTime + entryDurationMs);
104
- if (!entriesByTask.has(taskInfo)) {
105
- entriesByTask.set(taskInfo, []);
106
- }
107
- entriesByTask.get(taskInfo).push(entry);
108
- });
109
- summaryLines = ['Time entries for current user:'];
110
- for (const [taskInfo, totalMs] of timeByTask.entries()) {
111
- const hours = totalMs / (1000 * 60 * 60);
112
- const displayHours = Math.floor(hours);
113
- const displayMinutes = Math.round((hours - displayHours) * 60);
114
- const timeDisplay = displayHours > 0 ?
115
- `${displayHours}h ${displayMinutes}m` :
116
- `${displayMinutes}m`;
117
- summaryLines.push(` ${taskInfo}: ${timeDisplay}`);
118
- totalTimeMs += totalMs;
92
+ catch (error) {
93
+ console.error('Warning: Could not fetch all team members. This feature requires Workspace Owner/Admin permissions.');
94
+ // Continue without all users - will only show current user's entries
119
95
  }
120
96
  }
121
- const totalHours = totalTimeMs / (1000 * 60 * 60);
122
- const totalDisplayHours = Math.floor(totalHours);
123
- const totalDisplayMinutes = Math.round((totalHours - totalDisplayHours) * 60);
124
- const totalDisplay = totalDisplayHours > 0 ?
125
- `${totalDisplayHours}h ${totalDisplayMinutes}m` :
126
- `${totalDisplayMinutes}m`;
127
- summaryLines.push(`Total: ${totalDisplay}`);
128
- // Create detailed entries
129
- const detailBlocks = [];
130
- data.data.forEach((entry) => {
131
- const entryHours = (parseInt(entry.duration) || 0) / (1000 * 60 * 60);
132
- const entryDisplayHours = Math.floor(entryHours);
133
- const entryDisplayMinutes = Math.round((entryHours - entryDisplayHours) * 60);
134
- const entryTimeDisplay = entryDisplayHours > 0 ?
135
- `${entryDisplayHours}h ${entryDisplayMinutes}m` :
136
- `${entryDisplayMinutes}m`;
137
- const entryInfo = [
138
- `entry_id: ${entry.id}`,
139
- `user: ${entry.user?.username || 'Unknown User'}`,
140
- `duration: ${entryTimeDisplay}`,
141
- `start: ${timestampToIso(parseInt(entry.start))}`,
142
- ...(entry.description ? [`description: ${entry.description}`] : []),
143
- `billable: ${entry.billable || false}`
144
- ];
145
- // Add task info only when showing all user entries (no task filter)
146
- if (!task_id && entry.task) {
147
- entryInfo.splice(2, 0, `task: ${entry.task.name} (${entry.task.id})`);
148
- }
149
- detailBlocks.push({
150
- type: "text",
151
- text: entryInfo.join('\n')
152
- });
97
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
98
+ headers: { Authorization: config_1.CONFIG.apiKey },
153
99
  });
154
- return {
155
- content: [
156
- {
157
- type: "text",
158
- text: summaryLines.join('\n')
159
- },
160
- ...(detailBlocks.length > 0 ? [
161
- { type: "text", text: "\nDetailed entries:" },
162
- ...detailBlocks
163
- ] : [])
164
- ],
165
- };
100
+ if (!response.ok) {
101
+ throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
102
+ }
103
+ const data = await response.json();
104
+ return processTimeEntriesData(data, task_id, start_date, end_date, include_all_users);
166
105
  }
167
106
  catch (error) {
168
107
  console.error('Error fetching time entries:', error);
@@ -177,6 +116,147 @@ function registerTimeToolsRead(server) {
177
116
  }
178
117
  });
179
118
  }
119
+ /**
120
+ * Process the time entries data and return formatted hierarchical output
121
+ */
122
+ function processTimeEntriesData(data, task_id, start_date, end_date, include_all_users) {
123
+ if (!data.data || !Array.isArray(data.data)) {
124
+ const noEntriesMsg = task_id ?
125
+ `No time entries found for task ${task_id}.` :
126
+ 'No time entries found.';
127
+ return {
128
+ content: [{ type: "text", text: noEntriesMsg }],
129
+ };
130
+ }
131
+ const filteredEntries = data.data;
132
+ // Create hierarchical structure: List → Task → User → Individual entries
133
+ const hierarchy = new Map();
134
+ let totalTimeMs = 0;
135
+ filteredEntries.forEach((entry) => {
136
+ const taskId = entry.task?.id || 'no-task';
137
+ // Use location names from include_location_names parameter
138
+ const listId = entry.task_location?.list_id || 'no-list';
139
+ const listName = entry.task_location?.list_name || 'No List';
140
+ const taskName = entry.task?.name || 'No Task';
141
+ const userId = entry.user?.id || 'no-user';
142
+ const userName = entry.user?.username || 'Unknown User';
143
+ // Handle running timers (negative duration)
144
+ let entryDurationMs = parseInt(entry.duration) || 0;
145
+ const isRunningTimer = entryDurationMs < 0;
146
+ if (isRunningTimer) {
147
+ // For running timers, calculate current duration from start time
148
+ entryDurationMs = Date.now() - parseInt(entry.start);
149
+ }
150
+ totalTimeMs += entryDurationMs;
151
+ // Initialize list level
152
+ if (!hierarchy.has(listId)) {
153
+ hierarchy.set(listId, {
154
+ name: listName,
155
+ id: listId,
156
+ totalTime: 0,
157
+ tasks: new Map()
158
+ });
159
+ }
160
+ const listData = hierarchy.get(listId);
161
+ listData.totalTime += entryDurationMs;
162
+ // Initialize task level
163
+ if (!listData.tasks.has(taskId)) {
164
+ listData.tasks.set(taskId, {
165
+ name: taskName,
166
+ id: taskId,
167
+ totalTime: 0,
168
+ users: new Map()
169
+ });
170
+ }
171
+ const taskData = listData.tasks.get(taskId);
172
+ taskData.totalTime += entryDurationMs;
173
+ // Initialize user level
174
+ if (!taskData.users.has(userId)) {
175
+ taskData.users.set(userId, {
176
+ name: userName,
177
+ id: userId,
178
+ totalTime: 0,
179
+ entries: []
180
+ });
181
+ }
182
+ const userData = taskData.users.get(userId);
183
+ userData.totalTime += entryDurationMs;
184
+ userData.entries.push(entry);
185
+ });
186
+ // Count total tasks across all lists
187
+ let totalTasks = 0;
188
+ for (const [listId, listData] of hierarchy.entries()) {
189
+ totalTasks += listData.tasks.size;
190
+ }
191
+ // Format the hierarchical output
192
+ const outputLines = [];
193
+ // Header with date range and total
194
+ const dateRange = start_date && end_date ?
195
+ ` (${start_date.split('T')[0]} to ${end_date.split('T')[0]})` :
196
+ start_date ? ` (from ${start_date.split('T')[0]})` :
197
+ end_date ? ` (until ${end_date.split('T')[0]})` : '';
198
+ outputLines.push(`Time Entries Summary${dateRange}`);
199
+ outputLines.push(`Total: ${formatDuration(totalTimeMs)}`);
200
+ outputLines.push('');
201
+ // Check if result is too large (>100 tasks)
202
+ const TASK_LIMIT = 100;
203
+ const isTruncated = totalTasks > TASK_LIMIT;
204
+ if (isTruncated) {
205
+ // Show only list-level summary
206
+ outputLines.push(`⚠️ Large result detected (${totalTasks} tasks). Showing summary only.`);
207
+ outputLines.push(`💡 Use list_id, space_id, or date filters for detailed view.`);
208
+ outputLines.push('');
209
+ for (const [listId, listData] of hierarchy.entries()) {
210
+ const taskCount = listData.tasks.size;
211
+ outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)} across ${taskCount} task${taskCount === 1 ? '' : 's'}`);
212
+ }
213
+ }
214
+ else {
215
+ // Show full hierarchical display
216
+ for (const [listId, listData] of hierarchy.entries()) {
217
+ outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)}`);
218
+ for (const [taskId, taskData] of listData.tasks.entries()) {
219
+ outputLines.push(` ├─ 🎯 ${taskData.name} (Task: ${taskId}) - ${formatDuration(taskData.totalTime)}`);
220
+ const userEntries = Array.from(taskData.users.entries());
221
+ for (let userIndex = 0; userIndex < userEntries.length; userIndex++) {
222
+ const [userId, userData] = userEntries[userIndex];
223
+ const isLastUser = userIndex === userEntries.length - 1;
224
+ const userPrefix = isLastUser ? ' └─' : ' ├─';
225
+ outputLines.push(`${userPrefix} ${userData.name}: ${formatDuration(userData.totalTime)}`);
226
+ // Add individual entries
227
+ userData.entries.forEach((entry, entryIndex) => {
228
+ const isLastEntry = entryIndex === userData.entries.length - 1;
229
+ const entryPrefix = isLastUser ?
230
+ (isLastEntry ? ' └─' : ' ├─') :
231
+ (isLastEntry ? ' │ └─' : ' │ ├─');
232
+ const entryStart = formatEntryTime(parseInt(entry.start));
233
+ // Handle running timers
234
+ const rawDuration = parseInt(entry.duration) || 0;
235
+ const isRunningTimer = rawDuration < 0;
236
+ let entryDuration;
237
+ if (isRunningTimer) {
238
+ const currentDuration = Date.now() - parseInt(entry.start);
239
+ entryDuration = `${formatDuration(currentDuration)} (running)`;
240
+ }
241
+ else {
242
+ entryDuration = formatDuration(rawDuration);
243
+ }
244
+ outputLines.push(`${entryPrefix} ${entryStart} - ${entryDuration}`);
245
+ });
246
+ }
247
+ }
248
+ outputLines.push('');
249
+ }
250
+ }
251
+ return {
252
+ content: [
253
+ {
254
+ type: "text",
255
+ text: outputLines.join('\n')
256
+ }
257
+ ],
258
+ };
259
+ }
180
260
  function registerTimeToolsWrite(server) {
181
261
  server.tool("createTimeEntry", [
182
262
  "Creates a time entry (books time) on a task for the current user.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.1.0",
4
- "description": "A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration",
3
+ "version": "1.2.0",
4
+ "description": "Transform your AI assistant into a powerful ClickUp integration for both agentic coding and productivity management. Enables seamless task context sharing, intelligent search, time tracking, and complete project management workflows.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "bin": "dist/index.js",
@@ -23,9 +23,19 @@
23
23
  "mcp",
24
24
  "model-context-protocol",
25
25
  "llm",
26
- "ai"
26
+ "ai",
27
+ "ticket",
28
+ "time-tracking",
29
+ "productivity",
30
+ "project-management",
31
+ "task-management",
32
+ "agentic-coding",
33
+ "automation"
27
34
  ],
28
- "author": "",
35
+ "author": {
36
+ "name": "Marco Pfeiffer",
37
+ "email": "marco@hauptsache.net"
38
+ },
29
39
  "license": "ISC",
30
40
  "dependencies": {
31
41
  "@modelcontextprotocol/sdk": "^1.9.0",