@griffinwork40/clickup-mcp-server 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.
package/dist/index.js ADDED
@@ -0,0 +1,1352 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ClickUp MCP Server
4
+ *
5
+ * A Model Context Protocol server that provides comprehensive integration with the ClickUp API.
6
+ * Enables LLMs to manage tasks, projects, teams, and workflows programmatically.
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { z } from "zod";
11
+ import { makeApiRequest, handleApiError, formatTaskMarkdown, formatTaskCompact, generateTaskSummary, formatListMarkdown, formatSpaceMarkdown, formatFolderMarkdown, formatCommentMarkdown, formatTimeEntryMarkdown, getPagination, truncateResponse, formatTruncationInfo, getApiToken } from "./utils.js";
12
+ import { ResponseFormat, ResponseMode, Priority, DEFAULT_LIMIT, MAX_LIMIT } from "./constants.js";
13
+ // ============================================================================
14
+ // Server Initialization
15
+ // ============================================================================
16
+ const server = new McpServer({
17
+ name: "clickup-mcp-server",
18
+ version: "1.0.0"
19
+ });
20
+ // ============================================================================
21
+ // Zod Schemas for Input Validation
22
+ // ============================================================================
23
+ const ResponseFormatSchema = z.nativeEnum(ResponseFormat)
24
+ .default(ResponseFormat.MARKDOWN)
25
+ .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable");
26
+ const ResponseModeSchema = z.nativeEnum(ResponseMode)
27
+ .default(ResponseMode.FULL)
28
+ .describe("Response detail level: 'full' for complete task details, 'compact' for essential fields only (id, name, status, assignees), 'summary' for statistical overview");
29
+ const PaginationSchema = z.object({
30
+ limit: z.number()
31
+ .int()
32
+ .min(1)
33
+ .max(MAX_LIMIT)
34
+ .default(DEFAULT_LIMIT)
35
+ .describe(`Maximum results to return (1-${MAX_LIMIT})`),
36
+ offset: z.number()
37
+ .int()
38
+ .min(0)
39
+ .default(0)
40
+ .describe("Number of results to skip for pagination")
41
+ });
42
+ // ============================================================================
43
+ // Tool 1: Get Teams/Workspaces
44
+ // ============================================================================
45
+ server.registerTool("clickup_get_teams", {
46
+ title: "Get ClickUp Teams",
47
+ description: `Get all teams/workspaces accessible to the authenticated user.
48
+
49
+ This tool retrieves the list of teams (also called workspaces) that the user has access to in ClickUp. Each team represents a top-level organizational unit.
50
+
51
+ Args:
52
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
53
+
54
+ Returns:
55
+ For JSON format:
56
+ {
57
+ "teams": [
58
+ {
59
+ "id": "string", // Team ID
60
+ "name": "string", // Team name
61
+ "color": "string", // Team color (optional)
62
+ "avatar": "string" // Team avatar URL (optional)
63
+ }
64
+ ]
65
+ }
66
+
67
+ Examples:
68
+ - Use when: "What teams do I have access to?"
69
+ - Use when: "List all my workspaces"
70
+ - Don't use when: You need to list spaces or folders (use clickup_get_spaces instead)
71
+
72
+ Error Handling:
73
+ - Returns "Error: Invalid or missing API token" if authentication fails (401)
74
+ - Returns "Error: Rate limit exceeded" if too many requests (429)`,
75
+ inputSchema: z.object({
76
+ response_format: ResponseFormatSchema
77
+ }).strict(),
78
+ annotations: {
79
+ readOnlyHint: true,
80
+ destructiveHint: false,
81
+ idempotentHint: true,
82
+ openWorldHint: true
83
+ }
84
+ }, async (params) => {
85
+ try {
86
+ const data = await makeApiRequest("team");
87
+ const teams = data.teams || [];
88
+ let result;
89
+ if (params.response_format === ResponseFormat.MARKDOWN) {
90
+ const lines = ["# ClickUp Teams", ""];
91
+ lines.push(`Found ${teams.length} team(s)`, "");
92
+ for (const team of teams) {
93
+ lines.push(`## ${team.name} (${team.id})`);
94
+ if (team.color) {
95
+ lines.push(`- Color: ${team.color}`);
96
+ }
97
+ lines.push("");
98
+ }
99
+ result = lines.join("\n");
100
+ }
101
+ else {
102
+ result = JSON.stringify({ teams }, null, 2);
103
+ }
104
+ return {
105
+ content: [{ type: "text", text: result }]
106
+ };
107
+ }
108
+ catch (error) {
109
+ return {
110
+ content: [{ type: "text", text: handleApiError(error) }]
111
+ };
112
+ }
113
+ });
114
+ // ============================================================================
115
+ // Tool 2: Get Spaces
116
+ // ============================================================================
117
+ server.registerTool("clickup_get_spaces", {
118
+ title: "Get ClickUp Spaces",
119
+ description: `Get all spaces in a team/workspace.
120
+
121
+ Spaces are the second level in the ClickUp hierarchy (Team → Space → Folder → List → Task). This tool retrieves all spaces within a specific team.
122
+
123
+ Args:
124
+ - team_id (string): The team/workspace ID
125
+ - archived (boolean): Include archived spaces (default: false)
126
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
127
+
128
+ Returns:
129
+ For JSON format:
130
+ {
131
+ "spaces": [
132
+ {
133
+ "id": "string",
134
+ "name": "string",
135
+ "private": boolean,
136
+ "multiple_assignees": boolean,
137
+ "features": { ... }
138
+ }
139
+ ]
140
+ }
141
+
142
+ Examples:
143
+ - Use when: "Show me all spaces in team 123456"
144
+ - Use when: "List the spaces in my workspace"
145
+ - Don't use when: You need to list teams (use clickup_get_teams)
146
+
147
+ Error Handling:
148
+ - Returns "Error: Resource not found" if team_id is invalid (404)`,
149
+ inputSchema: z.object({
150
+ team_id: z.string()
151
+ .min(1)
152
+ .describe("Team/workspace ID"),
153
+ archived: z.boolean()
154
+ .default(false)
155
+ .describe("Include archived spaces"),
156
+ response_format: ResponseFormatSchema
157
+ }).strict(),
158
+ annotations: {
159
+ readOnlyHint: true,
160
+ destructiveHint: false,
161
+ idempotentHint: true,
162
+ openWorldHint: true
163
+ }
164
+ }, async (params) => {
165
+ try {
166
+ const data = await makeApiRequest(`team/${params.team_id}/space`, "GET", undefined, { archived: params.archived });
167
+ const spaces = data.spaces || [];
168
+ let result;
169
+ if (params.response_format === ResponseFormat.MARKDOWN) {
170
+ const lines = [`# Spaces in Team ${params.team_id}`, ""];
171
+ lines.push(`Found ${spaces.length} space(s)`, "");
172
+ for (const space of spaces) {
173
+ lines.push(formatSpaceMarkdown(space));
174
+ lines.push("");
175
+ }
176
+ result = lines.join("\n");
177
+ }
178
+ else {
179
+ result = JSON.stringify({ spaces }, null, 2);
180
+ }
181
+ const { content: finalContent, truncation } = truncateResponse(result, spaces.length, "spaces");
182
+ result = finalContent + formatTruncationInfo(truncation);
183
+ return {
184
+ content: [{ type: "text", text: result }]
185
+ };
186
+ }
187
+ catch (error) {
188
+ return {
189
+ content: [{ type: "text", text: handleApiError(error) }]
190
+ };
191
+ }
192
+ });
193
+ // ============================================================================
194
+ // Tool 3: Get Folders
195
+ // ============================================================================
196
+ server.registerTool("clickup_get_folders", {
197
+ title: "Get ClickUp Folders",
198
+ description: `Get all folders in a space.
199
+
200
+ Folders are optional groupings within spaces (Team → Space → Folder → List → Task). This tool retrieves all folders in a specific space.
201
+
202
+ Args:
203
+ - space_id (string): The space ID
204
+ - archived (boolean): Include archived folders (default: false)
205
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
206
+
207
+ Returns:
208
+ For JSON format:
209
+ {
210
+ "folders": [
211
+ {
212
+ "id": "string",
213
+ "name": "string",
214
+ "hidden": boolean,
215
+ "task_count": "string",
216
+ "lists": [...]
217
+ }
218
+ ]
219
+ }
220
+
221
+ Examples:
222
+ - Use when: "Show me folders in space 123456"
223
+ - Use when: "List all folders in this space"
224
+
225
+ Error Handling:
226
+ - Returns "Error: Resource not found" if space_id is invalid (404)`,
227
+ inputSchema: z.object({
228
+ space_id: z.string()
229
+ .min(1)
230
+ .describe("Space ID"),
231
+ archived: z.boolean()
232
+ .default(false)
233
+ .describe("Include archived folders"),
234
+ response_format: ResponseFormatSchema
235
+ }).strict(),
236
+ annotations: {
237
+ readOnlyHint: true,
238
+ destructiveHint: false,
239
+ idempotentHint: true,
240
+ openWorldHint: true
241
+ }
242
+ }, async (params) => {
243
+ try {
244
+ const data = await makeApiRequest(`space/${params.space_id}/folder`, "GET", undefined, { archived: params.archived });
245
+ const folders = data.folders || [];
246
+ let result;
247
+ if (params.response_format === ResponseFormat.MARKDOWN) {
248
+ const lines = [`# Folders in Space ${params.space_id}`, ""];
249
+ lines.push(`Found ${folders.length} folder(s)`, "");
250
+ for (const folder of folders) {
251
+ lines.push(formatFolderMarkdown(folder));
252
+ lines.push("");
253
+ }
254
+ result = lines.join("\n");
255
+ }
256
+ else {
257
+ result = JSON.stringify({ folders }, null, 2);
258
+ }
259
+ const { content: finalContent, truncation } = truncateResponse(result, folders.length, "folders");
260
+ result = finalContent + formatTruncationInfo(truncation);
261
+ return {
262
+ content: [{ type: "text", text: result }]
263
+ };
264
+ }
265
+ catch (error) {
266
+ return {
267
+ content: [{ type: "text", text: handleApiError(error) }]
268
+ };
269
+ }
270
+ });
271
+ // ============================================================================
272
+ // Tool 4: Get Lists
273
+ // ============================================================================
274
+ server.registerTool("clickup_get_lists", {
275
+ title: "Get ClickUp Lists",
276
+ description: `Get all lists in a folder or space.
277
+
278
+ Lists are containers for tasks (Team → Space → Folder → List → Task). This tool retrieves lists from either a folder or directly from a space (folderless lists).
279
+
280
+ Args:
281
+ - folder_id (string, optional): Folder ID to get lists from
282
+ - space_id (string, optional): Space ID to get folderless lists from
283
+ - archived (boolean): Include archived lists (default: false)
284
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
285
+
286
+ Note: You must provide either folder_id OR space_id, but not both.
287
+
288
+ Returns:
289
+ For JSON format:
290
+ {
291
+ "lists": [
292
+ {
293
+ "id": "string",
294
+ "name": "string",
295
+ "task_count": number,
296
+ "statuses": [...]
297
+ }
298
+ ]
299
+ }
300
+
301
+ Examples:
302
+ - Use when: "Show me lists in folder 123456"
303
+ - Use when: "Get folderless lists in space 789"
304
+
305
+ Error Handling:
306
+ - Returns error if neither folder_id nor space_id provided
307
+ - Returns "Error: Resource not found" if ID is invalid (404)`,
308
+ inputSchema: z.object({
309
+ folder_id: z.string().optional().describe("Folder ID"),
310
+ space_id: z.string().optional().describe("Space ID"),
311
+ archived: z.boolean().default(false).describe("Include archived lists"),
312
+ response_format: ResponseFormatSchema
313
+ }).strict(),
314
+ annotations: {
315
+ readOnlyHint: true,
316
+ destructiveHint: false,
317
+ idempotentHint: true,
318
+ openWorldHint: true
319
+ }
320
+ }, async (params) => {
321
+ try {
322
+ if (!params.folder_id && !params.space_id) {
323
+ return {
324
+ content: [{
325
+ type: "text",
326
+ text: "Error: Must provide either folder_id or space_id"
327
+ }]
328
+ };
329
+ }
330
+ if (params.folder_id && params.space_id) {
331
+ return {
332
+ content: [{
333
+ type: "text",
334
+ text: "Error: Provide only one of folder_id or space_id, not both"
335
+ }]
336
+ };
337
+ }
338
+ const endpoint = params.folder_id
339
+ ? `folder/${params.folder_id}/list`
340
+ : `space/${params.space_id}/list`;
341
+ const data = await makeApiRequest(endpoint, "GET", undefined, { archived: params.archived });
342
+ const lists = data.lists || [];
343
+ let result;
344
+ if (params.response_format === ResponseFormat.MARKDOWN) {
345
+ const parent = params.folder_id ? `Folder ${params.folder_id}` : `Space ${params.space_id}`;
346
+ const lines = [`# Lists in ${parent}`, ""];
347
+ lines.push(`Found ${lists.length} list(s)`, "");
348
+ for (const list of lists) {
349
+ lines.push(formatListMarkdown(list));
350
+ lines.push("");
351
+ }
352
+ result = lines.join("\n");
353
+ }
354
+ else {
355
+ result = JSON.stringify({ lists }, null, 2);
356
+ }
357
+ const { content: finalContent, truncation } = truncateResponse(result, lists.length, "lists");
358
+ result = finalContent + formatTruncationInfo(truncation);
359
+ return {
360
+ content: [{ type: "text", text: result }]
361
+ };
362
+ }
363
+ catch (error) {
364
+ return {
365
+ content: [{ type: "text", text: handleApiError(error) }]
366
+ };
367
+ }
368
+ });
369
+ // ============================================================================
370
+ // Tool 5: Get List Details
371
+ // ============================================================================
372
+ server.registerTool("clickup_get_list_details", {
373
+ title: "Get ClickUp List Details",
374
+ description: `Get detailed information about a specific list, including available statuses and custom fields.
375
+
376
+ This tool is essential for understanding what statuses and custom fields are available before creating or updating tasks in a list.
377
+
378
+ Args:
379
+ - list_id (string): The list ID
380
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
381
+
382
+ Returns:
383
+ Detailed list information including:
384
+ - Available statuses (for setting task status)
385
+ - Custom fields (for setting custom field values)
386
+ - Task count and other metadata
387
+
388
+ Examples:
389
+ - Use when: "What statuses are available in list 123456?"
390
+ - Use when: "Show me custom fields for this list"
391
+ - Use before: Creating tasks to know valid statuses
392
+
393
+ Error Handling:
394
+ - Returns "Error: Resource not found" if list_id is invalid (404)`,
395
+ inputSchema: z.object({
396
+ list_id: z.string().min(1).describe("List ID"),
397
+ response_format: ResponseFormatSchema
398
+ }).strict(),
399
+ annotations: {
400
+ readOnlyHint: true,
401
+ destructiveHint: false,
402
+ idempotentHint: true,
403
+ openWorldHint: true
404
+ }
405
+ }, async (params) => {
406
+ try {
407
+ const list = await makeApiRequest(`list/${params.list_id}`);
408
+ let result;
409
+ if (params.response_format === ResponseFormat.MARKDOWN) {
410
+ result = formatListMarkdown(list);
411
+ }
412
+ else {
413
+ result = JSON.stringify(list, null, 2);
414
+ }
415
+ return {
416
+ content: [{ type: "text", text: result }]
417
+ };
418
+ }
419
+ catch (error) {
420
+ return {
421
+ content: [{ type: "text", text: handleApiError(error) }]
422
+ };
423
+ }
424
+ });
425
+ // ============================================================================
426
+ // Tool 6: Get Tasks
427
+ // ============================================================================
428
+ server.registerTool("clickup_get_tasks", {
429
+ title: "Get Tasks in List",
430
+ description: `Get tasks in a specific list with filtering and pagination.
431
+
432
+ This tool retrieves tasks from a list with support for filtering by status, assignee, and other criteria.
433
+
434
+ Args:
435
+ - list_id (string): The list ID
436
+ - archived (boolean): Include archived tasks (default: false)
437
+ - include_closed (boolean): Include closed tasks (default: false)
438
+ - statuses (string[], optional): Filter by status names. MUST be an array, e.g., ["to do", "in progress"]
439
+ - assignees (number[], optional): Filter by assignee IDs. MUST be an array, e.g., [123, 456]
440
+ - limit (number): Maximum results (1-100, default: 20)
441
+ - offset (number): Pagination offset (default: 0). MUST be a multiple of limit (0, 20, 40, 60, etc.)
442
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
443
+ - response_mode ('full' | 'compact' | 'summary'): Detail level (default: 'full')
444
+ * 'full': Complete task details with descriptions
445
+ * 'compact': Essential fields only (id, name, status, assignees) - use for large result sets
446
+ * 'summary': Statistical overview by status/assignee without individual task details
447
+
448
+ Pagination:
449
+ Use the next_offset value from the response to get the next page. Offset must be a multiple of limit.
450
+
451
+ Returns:
452
+ For JSON format:
453
+ {
454
+ "tasks": [...],
455
+ "pagination": {
456
+ "count": number,
457
+ "offset": number,
458
+ "has_more": boolean,
459
+ "next_offset": number
460
+ }
461
+ }
462
+
463
+ Examples:
464
+ - Use when: "Show me tasks in list 123456"
465
+ - Use when: "Get all 'to do' tasks assigned to user 789"
466
+ - Use with: response_mode='compact' for large lists (100+ tasks)
467
+ - Use with: response_mode='summary' for quick status overview
468
+
469
+ Error Handling:
470
+ - Returns "Error: Resource not found" if list_id is invalid (404)
471
+ - Returns helpful error if arrays not formatted correctly`,
472
+ inputSchema: z.object({
473
+ list_id: z.string().min(1).describe("List ID"),
474
+ archived: z.boolean().default(false).describe("Include archived tasks"),
475
+ include_closed: z.boolean().default(false).describe("Include closed tasks"),
476
+ statuses: z.array(z.string()).optional().describe("Filter by status names - MUST be array like [\"to do\", \"in progress\"]"),
477
+ assignees: z.array(z.number()).optional().describe("Filter by assignee IDs - MUST be array like [123, 456]"),
478
+ ...PaginationSchema.shape,
479
+ response_format: ResponseFormatSchema,
480
+ response_mode: ResponseModeSchema
481
+ }).strict(),
482
+ annotations: {
483
+ readOnlyHint: true,
484
+ destructiveHint: false,
485
+ idempotentHint: true,
486
+ openWorldHint: true
487
+ }
488
+ }, async (params) => {
489
+ try {
490
+ const limit = params.limit ?? DEFAULT_LIMIT;
491
+ const offset = params.offset ?? 0;
492
+ // Validate pagination alignment
493
+ if (offset % limit !== 0) {
494
+ return {
495
+ content: [{
496
+ type: "text",
497
+ text: `Error: offset (${offset}) must be a multiple of limit (${limit}) for proper pagination. Use the next_offset value from previous responses, or ensure offset is divisible by limit.`
498
+ }]
499
+ };
500
+ }
501
+ const queryParams = {
502
+ archived: params.archived,
503
+ include_closed: params.include_closed,
504
+ page: Math.floor(offset / limit)
505
+ };
506
+ if (params.statuses && params.statuses.length > 0) {
507
+ queryParams.statuses = JSON.stringify(params.statuses);
508
+ }
509
+ if (params.assignees && params.assignees.length > 0) {
510
+ queryParams.assignees = JSON.stringify(params.assignees);
511
+ }
512
+ const data = await makeApiRequest(`list/${params.list_id}/task`, "GET", undefined, queryParams);
513
+ const tasks = data.tasks || [];
514
+ const pagination = getPagination(undefined, tasks.length, offset, limit);
515
+ let result;
516
+ if (params.response_format === ResponseFormat.MARKDOWN) {
517
+ const lines = [`# Tasks in List ${params.list_id}`, ""];
518
+ // Handle summary mode
519
+ if (params.response_mode === ResponseMode.SUMMARY) {
520
+ result = generateTaskSummary(tasks);
521
+ }
522
+ else {
523
+ lines.push(`Found ${tasks.length} task(s) (offset: ${offset})`, "");
524
+ // Handle full vs compact mode
525
+ for (const task of tasks) {
526
+ if (params.response_mode === ResponseMode.COMPACT) {
527
+ lines.push(formatTaskCompact(task));
528
+ }
529
+ else {
530
+ lines.push(formatTaskMarkdown(task));
531
+ lines.push("");
532
+ lines.push("---");
533
+ lines.push("");
534
+ }
535
+ }
536
+ if (pagination.has_more) {
537
+ lines.push("");
538
+ lines.push(`More results available. Use offset=${pagination.next_offset} to get next page.`);
539
+ }
540
+ result = lines.join("\n");
541
+ }
542
+ }
543
+ else {
544
+ // JSON format always returns full data
545
+ result = JSON.stringify({ tasks, pagination }, null, 2);
546
+ }
547
+ const { content: finalContent, truncation } = truncateResponse(result, tasks.length, "tasks");
548
+ result = finalContent + formatTruncationInfo(truncation);
549
+ return {
550
+ content: [{ type: "text", text: result }]
551
+ };
552
+ }
553
+ catch (error) {
554
+ return {
555
+ content: [{ type: "text", text: handleApiError(error) }]
556
+ };
557
+ }
558
+ });
559
+ // ============================================================================
560
+ // Tool 7: Get Task
561
+ // ============================================================================
562
+ server.registerTool("clickup_get_task", {
563
+ title: "Get Task Details",
564
+ description: `Get detailed information about a specific task.
565
+
566
+ This tool retrieves complete information about a single task, including description, status, assignees, custom fields, checklists, and more.
567
+
568
+ Args:
569
+ - task_id (string): The task ID
570
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
571
+
572
+ Returns:
573
+ Complete task information including:
574
+ - Name, description, status, priority
575
+ - Assignees, watchers, creator
576
+ - Due date, time estimates, time spent
577
+ - Custom fields, checklists, tags
578
+ - Related tasks, dependencies
579
+ - URL for viewing in ClickUp
580
+
581
+ Examples:
582
+ - Use when: "Show me details for task abc123"
583
+ - Use when: "What's the status of task xyz?"
584
+
585
+ Error Handling:
586
+ - Returns "Error: Resource not found" if task_id is invalid (404)`,
587
+ inputSchema: z.object({
588
+ task_id: z.string().min(1).describe("Task ID"),
589
+ response_format: ResponseFormatSchema
590
+ }).strict(),
591
+ annotations: {
592
+ readOnlyHint: true,
593
+ destructiveHint: false,
594
+ idempotentHint: true,
595
+ openWorldHint: true
596
+ }
597
+ }, async (params) => {
598
+ try {
599
+ const task = await makeApiRequest(`task/${params.task_id}`);
600
+ let result;
601
+ if (params.response_format === ResponseFormat.MARKDOWN) {
602
+ result = formatTaskMarkdown(task);
603
+ }
604
+ else {
605
+ result = JSON.stringify(task, null, 2);
606
+ }
607
+ return {
608
+ content: [{ type: "text", text: result }]
609
+ };
610
+ }
611
+ catch (error) {
612
+ return {
613
+ content: [{ type: "text", text: handleApiError(error) }]
614
+ };
615
+ }
616
+ });
617
+ // ============================================================================
618
+ // Tool 8: Create Task
619
+ // ============================================================================
620
+ server.registerTool("clickup_create_task", {
621
+ title: "Create ClickUp Task",
622
+ description: `Create a new task in a list.
623
+
624
+ This tool creates a new task with specified properties. Use clickup_get_list_details first to see available statuses.
625
+
626
+ Args:
627
+ - list_id (string): The list ID where task will be created
628
+ - name (string): Task name (required)
629
+ - description (string, optional): Task description (supports markdown)
630
+ - status (string, optional): Task status (must match list statuses)
631
+ - priority (1-4, optional): Priority (1=Urgent, 2=High, 3=Normal, 4=Low)
632
+ - assignees (number[], optional): Array of assignee user IDs
633
+ - due_date (number, optional): Due date as Unix timestamp in milliseconds
634
+ - start_date (number, optional): Start date as Unix timestamp in milliseconds
635
+ - tags (string[], optional): Array of tag names
636
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
637
+
638
+ Returns:
639
+ The created task with all properties including the new task ID.
640
+
641
+ Examples:
642
+ - Use when: "Create a task called 'Fix bug' in list 123456"
643
+ - Use when: "Add a new task with priority high and assign to user 789"
644
+
645
+ Error Handling:
646
+ - Returns "Error: Bad request" if status doesn't match list statuses (400)
647
+ - Returns "Error: Resource not found" if list_id is invalid (404)`,
648
+ inputSchema: z.object({
649
+ list_id: z.string().min(1).describe("List ID"),
650
+ name: z.string().min(1).max(1000).describe("Task name"),
651
+ description: z.string().optional().describe("Task description (markdown supported)"),
652
+ status: z.string().optional().describe("Task status (must match list statuses)"),
653
+ priority: z.nativeEnum(Priority).optional().describe("Priority: 1=Urgent, 2=High, 3=Normal, 4=Low"),
654
+ assignees: z.array(z.number()).optional().describe("Assignee user IDs"),
655
+ due_date: z.number().optional().describe("Due date (Unix timestamp in milliseconds)"),
656
+ start_date: z.number().optional().describe("Start date (Unix timestamp in milliseconds)"),
657
+ tags: z.array(z.string()).optional().describe("Tag names"),
658
+ response_format: ResponseFormatSchema
659
+ }).strict(),
660
+ annotations: {
661
+ readOnlyHint: false,
662
+ destructiveHint: false,
663
+ idempotentHint: false,
664
+ openWorldHint: true
665
+ }
666
+ }, async (params) => {
667
+ try {
668
+ const taskData = {
669
+ name: params.name
670
+ };
671
+ if (params.description)
672
+ taskData.description = params.description;
673
+ if (params.status)
674
+ taskData.status = params.status;
675
+ if (params.priority)
676
+ taskData.priority = params.priority;
677
+ if (params.assignees)
678
+ taskData.assignees = params.assignees;
679
+ if (params.due_date)
680
+ taskData.due_date = params.due_date;
681
+ if (params.start_date)
682
+ taskData.start_date = params.start_date;
683
+ if (params.tags)
684
+ taskData.tags = params.tags;
685
+ const task = await makeApiRequest(`list/${params.list_id}/task`, "POST", taskData);
686
+ let result;
687
+ if (params.response_format === ResponseFormat.MARKDOWN) {
688
+ const lines = ["# Task Created Successfully", ""];
689
+ lines.push(formatTaskMarkdown(task));
690
+ result = lines.join("\n");
691
+ }
692
+ else {
693
+ result = JSON.stringify(task, null, 2);
694
+ }
695
+ return {
696
+ content: [{ type: "text", text: result }]
697
+ };
698
+ }
699
+ catch (error) {
700
+ return {
701
+ content: [{ type: "text", text: handleApiError(error) }]
702
+ };
703
+ }
704
+ });
705
+ // ============================================================================
706
+ // Tool 9: Update Task
707
+ // ============================================================================
708
+ server.registerTool("clickup_update_task", {
709
+ title: "Update ClickUp Task",
710
+ description: `Update an existing task's properties.
711
+
712
+ This tool updates one or more properties of an existing task. Only include the fields you want to change.
713
+
714
+ Args:
715
+ - task_id (string): The task ID
716
+ - name (string, optional): New task name
717
+ - description (string, optional): New description
718
+ - status (string, optional): New status
719
+ - priority (1-4, optional): New priority
720
+ - assignees_add (number[], optional): User IDs to add as assignees
721
+ - assignees_rem (number[], optional): User IDs to remove from assignees
722
+ - due_date (number, optional): New due date (Unix timestamp in milliseconds)
723
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
724
+
725
+ Returns:
726
+ The updated task with all properties.
727
+
728
+ Examples:
729
+ - Use when: "Update task abc123 status to 'complete'"
730
+ - Use when: "Change task priority to urgent and add assignee 789"
731
+
732
+ Error Handling:
733
+ - Returns "Error: Resource not found" if task_id is invalid (404)
734
+ - Returns "Error: Bad request" if status doesn't exist in list (400)`,
735
+ inputSchema: z.object({
736
+ task_id: z.string().min(1).describe("Task ID"),
737
+ name: z.string().optional().describe("New task name"),
738
+ description: z.string().optional().describe("New description"),
739
+ status: z.string().optional().describe("New status"),
740
+ priority: z.nativeEnum(Priority).optional().describe("New priority"),
741
+ assignees_add: z.array(z.number()).optional().describe("User IDs to add as assignees"),
742
+ assignees_rem: z.array(z.number()).optional().describe("User IDs to remove"),
743
+ due_date: z.number().optional().describe("New due date (Unix timestamp)"),
744
+ response_format: ResponseFormatSchema
745
+ }).strict(),
746
+ annotations: {
747
+ readOnlyHint: false,
748
+ destructiveHint: false,
749
+ idempotentHint: false,
750
+ openWorldHint: true
751
+ }
752
+ }, async (params) => {
753
+ try {
754
+ const updateData = {};
755
+ if (params.name)
756
+ updateData.name = params.name;
757
+ if (params.description !== undefined)
758
+ updateData.description = params.description;
759
+ if (params.status)
760
+ updateData.status = params.status;
761
+ if (params.priority)
762
+ updateData.priority = params.priority;
763
+ if (params.due_date !== undefined)
764
+ updateData.due_date = params.due_date;
765
+ // Handle assignee updates separately
766
+ if (params.assignees_add && params.assignees_add.length > 0) {
767
+ updateData.assignees = { add: params.assignees_add };
768
+ }
769
+ if (params.assignees_rem && params.assignees_rem.length > 0) {
770
+ if (!updateData.assignees)
771
+ updateData.assignees = {};
772
+ updateData.assignees.rem = params.assignees_rem;
773
+ }
774
+ const task = await makeApiRequest(`task/${params.task_id}`, "PUT", updateData);
775
+ let result;
776
+ if (params.response_format === ResponseFormat.MARKDOWN) {
777
+ const lines = ["# Task Updated Successfully", ""];
778
+ lines.push(formatTaskMarkdown(task));
779
+ result = lines.join("\n");
780
+ }
781
+ else {
782
+ result = JSON.stringify(task, null, 2);
783
+ }
784
+ return {
785
+ content: [{ type: "text", text: result }]
786
+ };
787
+ }
788
+ catch (error) {
789
+ return {
790
+ content: [{ type: "text", text: handleApiError(error) }]
791
+ };
792
+ }
793
+ });
794
+ // ============================================================================
795
+ // Tool 10: Delete Task
796
+ // ============================================================================
797
+ server.registerTool("clickup_delete_task", {
798
+ title: "Delete ClickUp Task",
799
+ description: `Delete a task permanently.
800
+
801
+ ⚠️ WARNING: This action is destructive and cannot be undone. The task will be permanently deleted from ClickUp.
802
+
803
+ Args:
804
+ - task_id (string): The task ID to delete
805
+
806
+ Returns:
807
+ Confirmation message of deletion.
808
+
809
+ Examples:
810
+ - Use when: "Delete task abc123"
811
+ - Don't use when: You want to archive (use update status to 'closed' instead)
812
+
813
+ Error Handling:
814
+ - Returns "Error: Resource not found" if task_id is invalid (404)
815
+ - Returns "Error: Permission denied" if no delete access (403)`,
816
+ inputSchema: z.object({
817
+ task_id: z.string().min(1).describe("Task ID to delete")
818
+ }).strict(),
819
+ annotations: {
820
+ readOnlyHint: false,
821
+ destructiveHint: true,
822
+ idempotentHint: true,
823
+ openWorldHint: true
824
+ }
825
+ }, async (params) => {
826
+ try {
827
+ await makeApiRequest(`task/${params.task_id}`, "DELETE");
828
+ return {
829
+ content: [{
830
+ type: "text",
831
+ text: `Task ${params.task_id} has been deleted successfully.`
832
+ }]
833
+ };
834
+ }
835
+ catch (error) {
836
+ return {
837
+ content: [{ type: "text", text: handleApiError(error) }]
838
+ };
839
+ }
840
+ });
841
+ // ============================================================================
842
+ // Tool 11: Search Tasks
843
+ // ============================================================================
844
+ server.registerTool("clickup_search_tasks", {
845
+ title: "Search ClickUp Tasks",
846
+ description: `Search for tasks across a team with advanced filtering.
847
+
848
+ This tool searches across all accessible tasks in a team with support for multiple filters.
849
+
850
+ Args:
851
+ - team_id (string): The team ID to search in
852
+ - query (string, optional): Search query string
853
+ - statuses (string[], optional): Filter by status names. MUST be an array, e.g., ["to do", "in progress"]
854
+ - assignees (number[], optional): Filter by assignee IDs. MUST be an array, e.g., [123, 456]
855
+ - tags (string[], optional): Filter by tag names. MUST be an array, e.g., ["bug", "feature"]
856
+ - date_created_gt (number, optional): Created after (Unix timestamp)
857
+ - date_updated_gt (number, optional): Updated after (Unix timestamp)
858
+ - limit (number): Maximum results (1-100, default: 20)
859
+ - offset (number): Pagination offset (default: 0). MUST be a multiple of limit (0, 20, 40, 60, etc.)
860
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
861
+ - response_mode ('full' | 'compact' | 'summary'): Detail level (default: 'full')
862
+ * 'full': Complete task details with descriptions
863
+ * 'compact': Essential fields only (id, name, status, assignees) - use for large result sets
864
+ * 'summary': Statistical overview by status/assignee without individual task details
865
+
866
+ Pagination:
867
+ Use the next_offset value from the response to get the next page. Offset must be a multiple of limit.
868
+
869
+ Returns:
870
+ Matching tasks with pagination information.
871
+
872
+ Examples:
873
+ - Use when: "Search for tasks containing 'bug' in team 123456"
874
+ - Use when: "Find all 'in progress' tasks assigned to user 789"
875
+ - Use with: response_mode='compact' for large result sets
876
+ - Use with: response_mode='summary' for quick overview
877
+
878
+ Error Handling:
879
+ - Returns "Error: Resource not found" if team_id is invalid (404)
880
+ - Returns helpful error if arrays not formatted correctly`,
881
+ inputSchema: z.object({
882
+ team_id: z.string().min(1).describe("Team ID"),
883
+ query: z.string().optional().describe("Search query string"),
884
+ statuses: z.array(z.string()).optional().describe("Filter by status names - MUST be array like [\"to do\", \"in progress\"]"),
885
+ assignees: z.array(z.number()).optional().describe("Filter by assignee IDs - MUST be array like [123, 456]"),
886
+ tags: z.array(z.string()).optional().describe("Filter by tag names - MUST be array like [\"bug\", \"feature\"]"),
887
+ date_created_gt: z.number().optional().describe("Created after (Unix timestamp)"),
888
+ date_updated_gt: z.number().optional().describe("Updated after (Unix timestamp)"),
889
+ ...PaginationSchema.shape,
890
+ response_format: ResponseFormatSchema,
891
+ response_mode: ResponseModeSchema
892
+ }).strict(),
893
+ annotations: {
894
+ readOnlyHint: true,
895
+ destructiveHint: false,
896
+ idempotentHint: true,
897
+ openWorldHint: true
898
+ }
899
+ }, async (params) => {
900
+ try {
901
+ const limit = params.limit ?? DEFAULT_LIMIT;
902
+ const offset = params.offset ?? 0;
903
+ // Validate pagination alignment
904
+ if (offset % limit !== 0) {
905
+ return {
906
+ content: [{
907
+ type: "text",
908
+ text: `Error: offset (${offset}) must be a multiple of limit (${limit}) for proper pagination. Use the next_offset value from previous responses, or ensure offset is divisible by limit.`
909
+ }]
910
+ };
911
+ }
912
+ const queryParams = {
913
+ page: Math.floor(offset / limit)
914
+ };
915
+ if (params.query)
916
+ queryParams.query = params.query;
917
+ if (params.statuses)
918
+ queryParams.statuses = JSON.stringify(params.statuses);
919
+ if (params.assignees)
920
+ queryParams.assignees = JSON.stringify(params.assignees);
921
+ if (params.tags)
922
+ queryParams.tags = JSON.stringify(params.tags);
923
+ if (params.date_created_gt)
924
+ queryParams.date_created_gt = params.date_created_gt;
925
+ if (params.date_updated_gt)
926
+ queryParams.date_updated_gt = params.date_updated_gt;
927
+ const data = await makeApiRequest(`team/${params.team_id}/task`, "GET", undefined, queryParams);
928
+ const tasks = data.tasks || [];
929
+ const pagination = getPagination(undefined, tasks.length, offset, limit);
930
+ let result;
931
+ if (params.response_format === ResponseFormat.MARKDOWN) {
932
+ const lines = ["# Task Search Results", ""];
933
+ // Handle summary mode
934
+ if (params.response_mode === ResponseMode.SUMMARY) {
935
+ result = generateTaskSummary(tasks);
936
+ }
937
+ else {
938
+ lines.push(`Found ${tasks.length} task(s)`, "");
939
+ // Handle full vs compact mode
940
+ for (const task of tasks) {
941
+ if (params.response_mode === ResponseMode.COMPACT) {
942
+ lines.push(formatTaskCompact(task));
943
+ }
944
+ else {
945
+ lines.push(formatTaskMarkdown(task));
946
+ lines.push("");
947
+ lines.push("---");
948
+ lines.push("");
949
+ }
950
+ }
951
+ if (pagination.has_more) {
952
+ lines.push("");
953
+ lines.push(`More results available. Use offset=${pagination.next_offset} to get next page.`);
954
+ }
955
+ result = lines.join("\n");
956
+ }
957
+ }
958
+ else {
959
+ // JSON format always returns full data
960
+ result = JSON.stringify({ tasks, pagination }, null, 2);
961
+ }
962
+ const { content: finalContent, truncation } = truncateResponse(result, tasks.length, "tasks");
963
+ result = finalContent + formatTruncationInfo(truncation);
964
+ return {
965
+ content: [{ type: "text", text: result }]
966
+ };
967
+ }
968
+ catch (error) {
969
+ return {
970
+ content: [{ type: "text", text: handleApiError(error) }]
971
+ };
972
+ }
973
+ });
974
+ // ============================================================================
975
+ // Tool 12: Add Comment
976
+ // ============================================================================
977
+ server.registerTool("clickup_add_comment", {
978
+ title: "Add Comment to Task",
979
+ description: `Add a comment to a task.
980
+
981
+ This tool posts a comment on a specific task. The comment will be attributed to the authenticated user.
982
+
983
+ Args:
984
+ - task_id (string): The task ID
985
+ - comment_text (string): The comment text (supports markdown)
986
+ - notify_all (boolean): Notify all task watchers (default: false)
987
+
988
+ Returns:
989
+ The created comment with metadata.
990
+
991
+ Examples:
992
+ - Use when: "Add comment 'Great work!' to task abc123"
993
+ - Use when: "Comment on task xyz with update"
994
+
995
+ Error Handling:
996
+ - Returns "Error: Resource not found" if task_id is invalid (404)`,
997
+ inputSchema: z.object({
998
+ task_id: z.string().min(1).describe("Task ID"),
999
+ comment_text: z.string().min(1).describe("Comment text (markdown supported)"),
1000
+ notify_all: z.boolean().default(false).describe("Notify all task watchers")
1001
+ }).strict(),
1002
+ annotations: {
1003
+ readOnlyHint: false,
1004
+ destructiveHint: false,
1005
+ idempotentHint: false,
1006
+ openWorldHint: true
1007
+ }
1008
+ }, async (params) => {
1009
+ try {
1010
+ const comment = await makeApiRequest(`task/${params.task_id}/comment`, "POST", {
1011
+ comment_text: params.comment_text,
1012
+ notify_all: params.notify_all
1013
+ });
1014
+ return {
1015
+ content: [{
1016
+ type: "text",
1017
+ text: `Comment added successfully to task ${params.task_id}\n\n${formatCommentMarkdown(comment)}`
1018
+ }]
1019
+ };
1020
+ }
1021
+ catch (error) {
1022
+ return {
1023
+ content: [{ type: "text", text: handleApiError(error) }]
1024
+ };
1025
+ }
1026
+ });
1027
+ // ============================================================================
1028
+ // Tool 13: Get Comments
1029
+ // ============================================================================
1030
+ server.registerTool("clickup_get_comments", {
1031
+ title: "Get Task Comments",
1032
+ description: `Get all comments on a task.
1033
+
1034
+ This tool retrieves all comments posted on a specific task.
1035
+
1036
+ Args:
1037
+ - task_id (string): The task ID
1038
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
1039
+
1040
+ Returns:
1041
+ List of comments with author, date, and text.
1042
+
1043
+ Examples:
1044
+ - Use when: "Show me comments on task abc123"
1045
+ - Use when: "Get all comments for this task"
1046
+
1047
+ Error Handling:
1048
+ - Returns "Error: Resource not found" if task_id is invalid (404)`,
1049
+ inputSchema: z.object({
1050
+ task_id: z.string().min(1).describe("Task ID"),
1051
+ response_format: ResponseFormatSchema
1052
+ }).strict(),
1053
+ annotations: {
1054
+ readOnlyHint: true,
1055
+ destructiveHint: false,
1056
+ idempotentHint: true,
1057
+ openWorldHint: true
1058
+ }
1059
+ }, async (params) => {
1060
+ try {
1061
+ const data = await makeApiRequest(`task/${params.task_id}/comment`);
1062
+ const comments = data.comments || [];
1063
+ let result;
1064
+ if (params.response_format === ResponseFormat.MARKDOWN) {
1065
+ const lines = [`# Comments on Task ${params.task_id}`, ""];
1066
+ lines.push(`Found ${comments.length} comment(s)`, "");
1067
+ lines.push("");
1068
+ for (const comment of comments) {
1069
+ lines.push(formatCommentMarkdown(comment));
1070
+ lines.push("");
1071
+ lines.push("---");
1072
+ lines.push("");
1073
+ }
1074
+ result = lines.join("\n");
1075
+ }
1076
+ else {
1077
+ result = JSON.stringify({ comments }, null, 2);
1078
+ }
1079
+ const { content: finalContent, truncation } = truncateResponse(result, comments.length, "comments");
1080
+ result = finalContent + formatTruncationInfo(truncation);
1081
+ return {
1082
+ content: [{ type: "text", text: result }]
1083
+ };
1084
+ }
1085
+ catch (error) {
1086
+ return {
1087
+ content: [{ type: "text", text: handleApiError(error) }]
1088
+ };
1089
+ }
1090
+ });
1091
+ // ============================================================================
1092
+ // Tool 14: Set Custom Field
1093
+ // ============================================================================
1094
+ server.registerTool("clickup_set_custom_field", {
1095
+ title: "Set Custom Field Value",
1096
+ description: `Set a custom field value on a task.
1097
+
1098
+ This tool updates a custom field value on a specific task. Use clickup_get_list_details first to see available custom fields.
1099
+
1100
+ Args:
1101
+ - task_id (string): The task ID
1102
+ - field_id (string): The custom field ID
1103
+ - value (any): The value to set (format depends on field type)
1104
+
1105
+ Note: Value format varies by field type:
1106
+ - Text/URL/Email: "string value"
1107
+ - Number/Currency: 123
1108
+ - Date: Unix timestamp in milliseconds
1109
+ - Dropdown: "option_uuid"
1110
+ - Checkbox: true or false
1111
+
1112
+ Returns:
1113
+ Confirmation of the update.
1114
+
1115
+ Examples:
1116
+ - Use when: "Set custom field abc to 'Complete' on task xyz"
1117
+ - Use after: Getting list details to know field IDs
1118
+
1119
+ Error Handling:
1120
+ - Returns "Error: Bad request" if value format is wrong (400)
1121
+ - Returns "Error: Resource not found" if IDs are invalid (404)`,
1122
+ inputSchema: z.object({
1123
+ task_id: z.string().min(1).describe("Task ID"),
1124
+ field_id: z.string().min(1).describe("Custom field ID"),
1125
+ value: z.any().describe("Value to set (format depends on field type)")
1126
+ }).strict(),
1127
+ annotations: {
1128
+ readOnlyHint: false,
1129
+ destructiveHint: false,
1130
+ idempotentHint: true,
1131
+ openWorldHint: true
1132
+ }
1133
+ }, async (params) => {
1134
+ try {
1135
+ await makeApiRequest(`task/${params.task_id}/field/${params.field_id}`, "POST", { value: params.value });
1136
+ return {
1137
+ content: [{
1138
+ type: "text",
1139
+ text: `Custom field ${params.field_id} updated successfully on task ${params.task_id}`
1140
+ }]
1141
+ };
1142
+ }
1143
+ catch (error) {
1144
+ return {
1145
+ content: [{ type: "text", text: handleApiError(error) }]
1146
+ };
1147
+ }
1148
+ });
1149
+ // ============================================================================
1150
+ // Tool 15: Start Time Entry
1151
+ // ============================================================================
1152
+ server.registerTool("clickup_start_time_entry", {
1153
+ title: "Start Time Tracking",
1154
+ description: `Start tracking time on a task.
1155
+
1156
+ This tool starts a new time tracking entry for the authenticated user on a specific task.
1157
+
1158
+ Args:
1159
+ - team_id (string): The team ID
1160
+ - task_id (string): The task ID to track time for
1161
+ - description (string, optional): Description of what you're working on
1162
+
1163
+ Returns:
1164
+ The started time entry with start time and ID.
1165
+
1166
+ Examples:
1167
+ - Use when: "Start tracking time on task abc123"
1168
+ - Use when: "Begin time entry for this task"
1169
+
1170
+ Error Handling:
1171
+ - Returns error if already tracking time
1172
+ - Returns "Error: Resource not found" if task_id is invalid (404)`,
1173
+ inputSchema: z.object({
1174
+ team_id: z.string().min(1).describe("Team ID"),
1175
+ task_id: z.string().min(1).describe("Task ID"),
1176
+ description: z.string().optional().describe("Description of work")
1177
+ }).strict(),
1178
+ annotations: {
1179
+ readOnlyHint: false,
1180
+ destructiveHint: false,
1181
+ idempotentHint: false,
1182
+ openWorldHint: true
1183
+ }
1184
+ }, async (params) => {
1185
+ try {
1186
+ const data = { tid: params.task_id };
1187
+ if (params.description)
1188
+ data.description = params.description;
1189
+ const entry = await makeApiRequest(`team/${params.team_id}/time_entries/start`, "POST", data);
1190
+ return {
1191
+ content: [{
1192
+ type: "text",
1193
+ text: `Time tracking started successfully\n\n${formatTimeEntryMarkdown(entry.data)}`
1194
+ }]
1195
+ };
1196
+ }
1197
+ catch (error) {
1198
+ return {
1199
+ content: [{ type: "text", text: handleApiError(error) }]
1200
+ };
1201
+ }
1202
+ });
1203
+ // ============================================================================
1204
+ // Tool 16: Stop Time Entry
1205
+ // ============================================================================
1206
+ server.registerTool("clickup_stop_time_entry", {
1207
+ title: "Stop Time Tracking",
1208
+ description: `Stop the currently running time entry.
1209
+
1210
+ This tool stops the active time tracking entry for the authenticated user.
1211
+
1212
+ Args:
1213
+ - team_id (string): The team ID
1214
+
1215
+ Returns:
1216
+ The completed time entry with duration.
1217
+
1218
+ Examples:
1219
+ - Use when: "Stop tracking time"
1220
+ - Use when: "End current time entry"
1221
+
1222
+ Error Handling:
1223
+ - Returns error if no active time tracking
1224
+ - Returns "Error: Resource not found" if team_id is invalid (404)`,
1225
+ inputSchema: z.object({
1226
+ team_id: z.string().min(1).describe("Team ID")
1227
+ }).strict(),
1228
+ annotations: {
1229
+ readOnlyHint: false,
1230
+ destructiveHint: false,
1231
+ idempotentHint: true,
1232
+ openWorldHint: true
1233
+ }
1234
+ }, async (params) => {
1235
+ try {
1236
+ const entry = await makeApiRequest(`team/${params.team_id}/time_entries/stop`, "POST");
1237
+ return {
1238
+ content: [{
1239
+ type: "text",
1240
+ text: `Time tracking stopped successfully\n\n${formatTimeEntryMarkdown(entry.data)}`
1241
+ }]
1242
+ };
1243
+ }
1244
+ catch (error) {
1245
+ return {
1246
+ content: [{ type: "text", text: handleApiError(error) }]
1247
+ };
1248
+ }
1249
+ });
1250
+ // ============================================================================
1251
+ // Tool 17: Get Time Entries
1252
+ // ============================================================================
1253
+ server.registerTool("clickup_get_time_entries", {
1254
+ title: "Get Time Entries",
1255
+ description: `Get time tracking entries for a team.
1256
+
1257
+ This tool retrieves time entries with optional filtering by assignee and date range.
1258
+
1259
+ Args:
1260
+ - team_id (string): The team ID
1261
+ - assignee (number, optional): Filter by assignee user ID
1262
+ - start_date (number, optional): Filter entries after this date (Unix timestamp)
1263
+ - end_date (number, optional): Filter entries before this date (Unix timestamp)
1264
+ - response_format ('markdown' | 'json'): Output format (default: 'markdown')
1265
+
1266
+ Returns:
1267
+ List of time entries with task, user, duration, and dates.
1268
+
1269
+ Examples:
1270
+ - Use when: "Show me time entries for team 123456"
1271
+ - Use when: "Get time tracked by user 789 this week"
1272
+
1273
+ Error Handling:
1274
+ - Returns "Error: Resource not found" if team_id is invalid (404)`,
1275
+ inputSchema: z.object({
1276
+ team_id: z.string().min(1).describe("Team ID"),
1277
+ assignee: z.number().optional().describe("Filter by assignee user ID"),
1278
+ start_date: z.number().optional().describe("Filter after date (Unix timestamp)"),
1279
+ end_date: z.number().optional().describe("Filter before date (Unix timestamp)"),
1280
+ response_format: ResponseFormatSchema
1281
+ }).strict(),
1282
+ annotations: {
1283
+ readOnlyHint: true,
1284
+ destructiveHint: false,
1285
+ idempotentHint: true,
1286
+ openWorldHint: true
1287
+ }
1288
+ }, async (params) => {
1289
+ try {
1290
+ const queryParams = {};
1291
+ if (params.assignee)
1292
+ queryParams.assignee = params.assignee;
1293
+ if (params.start_date)
1294
+ queryParams.start_date = params.start_date;
1295
+ if (params.end_date)
1296
+ queryParams.end_date = params.end_date;
1297
+ const data = await makeApiRequest(`team/${params.team_id}/time_entries`, "GET", undefined, queryParams);
1298
+ const entries = data.data || [];
1299
+ let result;
1300
+ if (params.response_format === ResponseFormat.MARKDOWN) {
1301
+ const lines = [`# Time Entries for Team ${params.team_id}`, ""];
1302
+ lines.push(`Found ${entries.length} time entr${entries.length === 1 ? "y" : "ies"}`, "");
1303
+ lines.push("");
1304
+ for (const entry of entries) {
1305
+ lines.push(formatTimeEntryMarkdown(entry));
1306
+ lines.push("");
1307
+ lines.push("---");
1308
+ lines.push("");
1309
+ }
1310
+ result = lines.join("\n");
1311
+ }
1312
+ else {
1313
+ result = JSON.stringify({ entries }, null, 2);
1314
+ }
1315
+ const { content: finalContent, truncation } = truncateResponse(result, entries.length, "entries");
1316
+ result = finalContent + formatTruncationInfo(truncation);
1317
+ return {
1318
+ content: [{ type: "text", text: result }]
1319
+ };
1320
+ }
1321
+ catch (error) {
1322
+ return {
1323
+ content: [{ type: "text", text: handleApiError(error) }]
1324
+ };
1325
+ }
1326
+ });
1327
+ // ============================================================================
1328
+ // Main Function
1329
+ // ============================================================================
1330
+ async function main() {
1331
+ // Verify API token is set
1332
+ try {
1333
+ getApiToken();
1334
+ }
1335
+ catch (error) {
1336
+ console.error("ERROR: CLICKUP_API_TOKEN environment variable is required");
1337
+ console.error("Get your token at: https://app.clickup.com/settings/apps");
1338
+ process.exit(1);
1339
+ }
1340
+ // Create stdio transport
1341
+ const transport = new StdioServerTransport();
1342
+ // Connect server to transport
1343
+ await server.connect(transport);
1344
+ // Log to stderr (stdout is used for MCP protocol)
1345
+ console.error("ClickUp MCP server running via stdio");
1346
+ }
1347
+ // Run the server
1348
+ main().catch((error) => {
1349
+ console.error("Server error:", error);
1350
+ process.exit(1);
1351
+ });
1352
+ //# sourceMappingURL=index.js.map