@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/LICENSE +22 -0
- package/README.md +173 -0
- package/dist/constants.d.ts +36 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +39 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1352 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +75 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +343 -0
- package/dist/utils.js.map +1 -0
- package/package.json +54 -0
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
|