@hauptsache.net/clickup-mcp 1.0.4 → 1.1.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.
@@ -0,0 +1,251 @@
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
+ /**
8
+ * Converts ISO date string to Unix timestamp in milliseconds
9
+ */
10
+ function isoToTimestamp(isoString) {
11
+ return new Date(isoString).getTime();
12
+ }
13
+ /**
14
+ * Formats timestamp to ISO string with local timezone (not UTC)
15
+ */
16
+ function timestampToIso(timestamp) {
17
+ const date = new Date(timestamp);
18
+ const year = date.getFullYear();
19
+ const month = String(date.getMonth() + 1).padStart(2, '0');
20
+ const day = String(date.getDate()).padStart(2, '0');
21
+ const hours = String(date.getHours()).padStart(2, '0');
22
+ const minutes = String(date.getMinutes()).padStart(2, '0');
23
+ const seconds = String(date.getSeconds()).padStart(2, '0');
24
+ // Calculate timezone offset
25
+ const offset = date.getTimezoneOffset();
26
+ const offsetHours = Math.floor(Math.abs(offset) / 60);
27
+ const offsetMinutes = Math.abs(offset) % 60;
28
+ const sign = offset <= 0 ? '+' : '-';
29
+ const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
30
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${timezoneOffset}`;
31
+ }
32
+ function registerTimeToolsRead(server) {
33
+ 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
+ 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
+ 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 }) => {
38
+ try {
39
+ // Build query parameters
40
+ const params = new URLSearchParams();
41
+ if (task_id) {
42
+ params.append('task_id', task_id);
43
+ }
44
+ if (start_date) {
45
+ params.append('start_date', isoToTimestamp(start_date).toString());
46
+ }
47
+ if (end_date) {
48
+ params.append('end_date', isoToTimestamp(end_date).toString());
49
+ }
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}`);
55
+ }
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
+ };
64
+ }
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, []);
78
+ }
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
+ }
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;
119
+ }
120
+ }
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
+ });
153
+ });
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
+ };
166
+ }
167
+ catch (error) {
168
+ console.error('Error fetching time entries:', error);
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: `Error fetching time entries: ${error instanceof Error ? error.message : 'Unknown error'}`,
174
+ },
175
+ ],
176
+ };
177
+ }
178
+ });
179
+ }
180
+ function registerTimeToolsWrite(server) {
181
+ server.tool("createTimeEntry", [
182
+ "Creates a time entry (books time) on a task for the current user.",
183
+ "Use decimal hours (e.g., 0.25 for 15 minutes, 0.5 for 30 minutes, 2.5 for 2.5 hours).",
184
+ "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.",
185
+ "Suggest moving the task to an active status like 'in progress' first."
186
+ ].join("\n"), {
187
+ task_id: zod_1.z.string().min(6).max(9).describe("The 6-9 character task ID to book time against"),
188
+ 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)"),
189
+ description: zod_1.z.string().optional().describe("Optional description for the time entry"),
190
+ 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)")
191
+ }, async ({ task_id, hours, description, start_time }) => {
192
+ try {
193
+ // Convert hours to milliseconds (ClickUp API uses milliseconds)
194
+ const durationMs = Math.round(hours * 60 * 60 * 1000);
195
+ // Convert ISO date to timestamp if provided, otherwise use current time
196
+ const startTimeMs = start_time ? isoToTimestamp(start_time) : Date.now();
197
+ const requestBody = {
198
+ tid: task_id,
199
+ start: startTimeMs,
200
+ duration: durationMs,
201
+ ...(description && { description })
202
+ };
203
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries`, {
204
+ method: 'POST',
205
+ headers: {
206
+ Authorization: config_1.CONFIG.apiKey,
207
+ 'Content-Type': 'application/json'
208
+ },
209
+ body: JSON.stringify(requestBody)
210
+ });
211
+ if (!response.ok) {
212
+ const errorData = await response.json().catch(() => ({}));
213
+ throw new Error(`Error creating time entry: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
214
+ }
215
+ const timeEntry = await response.json();
216
+ // Format duration for display
217
+ const displayHours = Math.floor(hours);
218
+ const displayMinutes = Math.round((hours - displayHours) * 60);
219
+ const durationDisplay = displayHours > 0 ?
220
+ `${displayHours}h ${displayMinutes}m` :
221
+ `${displayMinutes}m`;
222
+ return {
223
+ content: [
224
+ {
225
+ type: "text",
226
+ text: [
227
+ `Time entry created successfully!`,
228
+ `entry_id: ${timeEntry.data?.id || 'N/A'}`,
229
+ `task_id: ${task_id}`,
230
+ `duration: ${durationDisplay}`,
231
+ `start_time: ${timestampToIso(startTimeMs)}`,
232
+ ...(description ? [`description: ${description}`] : []),
233
+ `user: ${timeEntry.data?.user?.username || 'Current user'}`
234
+ ].join('\n')
235
+ }
236
+ ],
237
+ };
238
+ }
239
+ catch (error) {
240
+ console.error('Error creating time entry:', error);
241
+ return {
242
+ content: [
243
+ {
244
+ type: "text",
245
+ text: `Error creating time entry: ${error instanceof Error ? error.message : 'Unknown error'}`,
246
+ },
247
+ ],
248
+ };
249
+ }
250
+ });
251
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",