@taazkareem/clickup-mcp-server 0.4.68 → 0.4.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,9 @@
4
4
  * This module defines folder-related tools for creating, retrieving,
5
5
  * updating, and deleting folders in the ClickUp workspace hierarchy.
6
6
  */
7
- import { createClickUpServices } from '../services/clickup/index.js';
8
- import config from '../config.js';
9
- // Initialize ClickUp services using the factory function
10
- const services = createClickUpServices({
11
- apiKey: config.clickupApiKey,
12
- teamId: config.clickupTeamId
13
- });
14
- // Extract the services we need for folder operations
15
- const { folder: folderService, workspace: workspaceService } = services;
7
+ import { clickUpServices } from '../services/shared.js';
8
+ // Use shared services instance
9
+ const { folder: folderService, workspace: workspaceService } = clickUpServices;
16
10
  /**
17
11
  * Tool definition for creating a folder
18
12
  */
@@ -5,15 +5,10 @@
5
5
  * retrieving, and deleting lists. It supports creating lists both in spaces
6
6
  * and in folders.
7
7
  */
8
- import { createClickUpServices } from '../services/clickup/index.js';
8
+ import { clickUpServices } from '../services/shared.js';
9
9
  import config from '../config.js';
10
- // Initialize ClickUp services using the factory function
11
- const services = createClickUpServices({
12
- apiKey: config.clickupApiKey,
13
- teamId: config.clickupTeamId
14
- });
15
- // Extract the services we need for list operations
16
- const { list: listService, workspace: workspaceService } = services;
10
+ // Use shared services instance
11
+ const { list: listService, workspace: workspaceService } = clickUpServices;
17
12
  /**
18
13
  * Tool definition for creating a list directly in a space
19
14
  */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Simple Logs Tool
3
+ *
4
+ * Provides a basic tool for reading server logs.
5
+ */
6
+ import { promises as fs } from 'fs';
7
+ import { join, dirname } from 'path';
8
+ // Define the log file path - using __dirname to get absolute path
9
+ const LOG_FILE = join(dirname(__dirname), 'server.log');
10
+ // Tool definition
11
+ export const checkLogsTool = {
12
+ name: "check_logs",
13
+ description: "Check server logs with optional filtering by log level",
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ level: {
18
+ type: "string",
19
+ enum: ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"],
20
+ description: "Filter logs by level"
21
+ },
22
+ limit: {
23
+ type: "number",
24
+ description: "Maximum number of log entries to return (default: 50)"
25
+ }
26
+ }
27
+ }
28
+ };
29
+ // Simple handler implementation using async/await with promises
30
+ export async function handleCheckLogs(params) {
31
+ try {
32
+ // Read log file using promises
33
+ const content = await fs.readFile(LOG_FILE, 'utf-8');
34
+ // Split into lines and filter out empty lines
35
+ let lines = content.split('\n').filter(Boolean);
36
+ // Apply level filtering if specified
37
+ if (params?.level) {
38
+ lines = lines.filter(line => line.includes(`${params.level}:`));
39
+ }
40
+ // Apply limit (default to 50)
41
+ const limit = params?.limit || 50;
42
+ const result = lines.slice(-limit);
43
+ // Return results
44
+ return {
45
+ logs: result.length > 0 ? result : ['No matching logs found.']
46
+ };
47
+ }
48
+ catch (error) {
49
+ // Simple error handling
50
+ if (error.code === 'ENOENT') {
51
+ return { logs: ['Log file not found.'] };
52
+ }
53
+ return { logs: [`Error reading logs: ${error.message}`] };
54
+ }
55
+ }
@@ -5,17 +5,11 @@
5
5
  * moving, duplicating, and deleting tasks. It also provides tools for
6
6
  * retrieving task details.
7
7
  */
8
- import { createClickUpServices } from '../services/clickup/index.js';
9
- import config from '../config.js';
8
+ import { clickUpServices } from '../services/shared.js';
10
9
  import { findListIDByName } from './list.js';
11
10
  import { parseDueDate, formatDueDate } from './utils.js';
12
- // Initialize ClickUp services using the factory function
13
- const services = createClickUpServices({
14
- apiKey: config.clickupApiKey,
15
- teamId: config.clickupTeamId
16
- });
17
- // Extract the services we need for task operations
18
- const { task: taskService, workspace: workspaceService } = services;
11
+ // Use shared services instance
12
+ const { task: taskService, workspace: workspaceService } = clickUpServices;
19
13
  /**
20
14
  * Tool definition for creating a single task
21
15
  */
@@ -113,7 +107,7 @@ export const createTaskTool = {
113
107
  */
114
108
  export const updateTaskTool = {
115
109
  name: "update_task",
116
- description: "Modify an existing task's properties. Valid parameter combinations:\n1. Use taskId alone (preferred if you have it)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)\n\nAt least one update field (name, description, status, priority) must be provided. Only specified fields will be updated.",
110
+ description: "Modify an existing task's properties. Valid parameter combinations:\n1. Use taskId alone (preferred if you have it)\n2. Use taskName + listName (listName is REQUIRED when using taskName, not optional)\n\nAt least one update field (name, description, status, priority) must be provided. Only specified fields will be updated.",
117
111
  inputSchema: {
118
112
  type: "object",
119
113
  properties: {
@@ -123,11 +117,11 @@ export const updateTaskTool = {
123
117
  },
124
118
  taskName: {
125
119
  type: "string",
126
- description: "Name of the task to update. Only use this if you don't have taskId. Warning: Task names may not be unique."
120
+ description: "Name of the task to update. Only use this if you don't have taskId. When using this parameter, you MUST also provide listName."
127
121
  },
128
122
  listName: {
129
123
  type: "string",
130
- description: "Name of the list containing the task. Required when using taskName if multiple tasks have the same name."
124
+ description: "Name of the list containing the task. REQUIRED when using taskName."
131
125
  },
132
126
  name: {
133
127
  type: "string",
@@ -171,9 +165,8 @@ export const updateTaskTool = {
171
165
  }
172
166
  listId = listInfo.id;
173
167
  }
174
- // Now find the task
175
- const tasks = await taskService.getTasks(listId || '');
176
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
168
+ // Use the improved findTaskByName method
169
+ const foundTask = await taskService.findTaskByName(listId || '', taskName);
177
170
  if (!foundTask) {
178
171
  throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
179
172
  }
@@ -226,7 +219,7 @@ export const updateTaskTool = {
226
219
  */
227
220
  export const moveTaskTool = {
228
221
  name: "move_task",
229
- description: "Move a task to a different list. Valid parameter combinations:\n1. Use taskId + (listId or listName) - preferred\n2. Use taskName + sourceListName + (listId or listName)\n\nWARNING: Task statuses may reset if destination list has different status options.",
222
+ description: "Move a task to a different list. Valid parameter combinations:\n1. Use taskId + (listId or listName) - preferred\n2. Use taskName + sourceListName + (listId or listName)\n\nWARNING: When using taskName, sourceListName is ABSOLUTELY REQUIRED - the system cannot find a task by name without knowing which list to search in. Task statuses may reset if destination list has different status options.",
230
223
  inputSchema: {
231
224
  type: "object",
232
225
  properties: {
@@ -268,9 +261,8 @@ export const moveTaskTool = {
268
261
  }
269
262
  sourceListId = listInfo.id;
270
263
  }
271
- // Now find the task
272
- const tasks = await taskService.getTasks(sourceListId || '');
273
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
264
+ // Use the improved findTaskByName method
265
+ const foundTask = await taskService.findTaskByName(sourceListId || '', taskName);
274
266
  if (!foundTask) {
275
267
  throw new Error(`Task "${taskName}" not found${sourceListName ? ` in list "${sourceListName}"` : ""}`);
276
268
  }
@@ -315,7 +307,7 @@ export const moveTaskTool = {
315
307
  */
316
308
  export const duplicateTaskTool = {
317
309
  name: "duplicate_task",
318
- description: "Create a copy of a task in the same or different list. Valid parameter combinations:\n1. Use taskId + optional (listId or listName) - preferred\n2. Use taskName + sourceListName + optional (listId or listName)\n\nThe duplicate preserves the original task's properties.",
310
+ description: "Create a copy of a task in the same or different list. Valid parameter combinations:\n1. Use taskId + optional (listId or listName) - preferred\n2. Use taskName + sourceListName + optional (listId or listName)\n\nWARNING: When using taskName, sourceListName is ABSOLUTELY REQUIRED - the system cannot find a task by name without knowing which list to search in. The duplicate preserves the original task's properties.",
319
311
  inputSchema: {
320
312
  type: "object",
321
313
  properties: {
@@ -358,8 +350,8 @@ export const duplicateTaskTool = {
358
350
  if (!targetTaskId && taskName) {
359
351
  // Find the task in the source list if specified, otherwise search all tasks
360
352
  if (sourceListId) {
361
- const tasks = await taskService.getTasks(sourceListId);
362
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
353
+ // Use the improved findTaskByName method
354
+ const foundTask = await taskService.findTaskByName(sourceListId, taskName);
363
355
  if (!foundTask) {
364
356
  throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
365
357
  }
@@ -408,7 +400,7 @@ export const duplicateTaskTool = {
408
400
  */
409
401
  export const getTaskTool = {
410
402
  name: "get_task",
411
- description: "Retrieve detailed information about a specific task. Valid parameter combinations:\n1. Use taskId alone (preferred)\n2. Use taskName + optional listName (to disambiguate if multiple tasks have the same name)",
403
+ description: "Retrieve detailed information about a specific task. Valid parameter combinations:\n1. Use taskId alone (preferred)\n2. Use taskName + listName (listName is REQUIRED when using taskName). Task names are only unique within a list, so the system needs to know which list to search in.",
412
404
  inputSchema: {
413
405
  type: "object",
414
406
  properties: {
@@ -418,11 +410,11 @@ export const getTaskTool = {
418
410
  },
419
411
  taskName: {
420
412
  type: "string",
421
- description: "Name of task to retrieve. Warning: Task names may not be unique."
413
+ description: "Name of task to retrieve. When using this parameter, you MUST also provide listName."
422
414
  },
423
415
  listName: {
424
416
  type: "string",
425
- description: "Name of list containing the task. Helps find the right task when using taskName."
417
+ description: "Name of list containing the task. REQUIRED when using taskName."
426
418
  }
427
419
  },
428
420
  required: []
@@ -440,9 +432,8 @@ export const getTaskTool = {
440
432
  }
441
433
  listId = listInfo.id;
442
434
  }
443
- // Now find the task
444
- const tasks = await taskService.getTasks(listId || '');
445
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
435
+ // Use the improved findTaskByName method
436
+ const foundTask = await taskService.findTaskByName(listId || '', taskName);
446
437
  if (!foundTask) {
447
438
  throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
448
439
  }
@@ -809,6 +800,113 @@ export const moveBulkTasksTool = {
809
800
  required: ["tasks"]
810
801
  }
811
802
  };
803
+ /**
804
+ * Tool definition for getting task comments
805
+ */
806
+ export const getTaskCommentsTool = {
807
+ name: "get_task_comments",
808
+ description: "Retrieve comments for a ClickUp task. You can identify the task by either taskId or taskName. If using taskName, you can optionally provide listName to help locate the correct task if multiple tasks have the same name.",
809
+ inputSchema: {
810
+ type: "object",
811
+ properties: {
812
+ taskId: {
813
+ type: "string",
814
+ description: "ID of task to retrieve comments for (preferred). Use this instead of taskName if you have it."
815
+ },
816
+ taskName: {
817
+ type: "string",
818
+ description: "Name of task to retrieve comments for. Warning: Task names may not be unique."
819
+ },
820
+ listName: {
821
+ type: "string",
822
+ description: "Name of list containing the task. Helps find the right task when using taskName."
823
+ },
824
+ start: {
825
+ type: "number",
826
+ description: "Timestamp (in milliseconds) to start retrieving comments from. Used for pagination."
827
+ },
828
+ startId: {
829
+ type: "string",
830
+ description: "Comment ID to start from. Used together with start for pagination."
831
+ }
832
+ }
833
+ },
834
+ async handler({ taskId, taskName, listName, start, startId }) {
835
+ let targetTaskId = taskId;
836
+ // If no taskId but taskName is provided, look up the task ID
837
+ if (!targetTaskId && taskName) {
838
+ // First, we need to find the list if list name is provided
839
+ let listId;
840
+ if (listName) {
841
+ // Use workspace service to find list by name
842
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
843
+ const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
844
+ if (!listInfo) {
845
+ throw new Error(`List "${listName}" not found`);
846
+ }
847
+ listId = listInfo.id;
848
+ }
849
+ // Find task by name in the specific list or across workspace
850
+ if (listId) {
851
+ const task = await taskService.findTaskByName(listId, taskName);
852
+ if (!task) {
853
+ throw new Error(`Task "${taskName}" not found in list "${listName}"`);
854
+ }
855
+ targetTaskId = task.id;
856
+ }
857
+ else {
858
+ // Search across all accessible tasks (slower, less reliable)
859
+ const hierarchy = await workspaceService.getWorkspaceHierarchy();
860
+ // Collect all lists from all spaces and folders
861
+ const lists = [];
862
+ // Recursively extract lists from hierarchy nodes
863
+ const extractLists = (node) => {
864
+ if (node.type === 'list') {
865
+ lists.push({ id: node.id, name: node.name });
866
+ }
867
+ if (node.children && Array.isArray(node.children)) {
868
+ node.children.forEach(extractLists);
869
+ }
870
+ };
871
+ // Start extraction from root's children (spaces)
872
+ if (hierarchy.root && hierarchy.root.children) {
873
+ hierarchy.root.children.forEach(extractLists);
874
+ }
875
+ // Try to find the task in each list
876
+ for (const list of lists) {
877
+ try {
878
+ const task = await taskService.findTaskByName(list.id, taskName);
879
+ if (task) {
880
+ targetTaskId = task.id;
881
+ break;
882
+ }
883
+ }
884
+ catch (error) {
885
+ // Continue searching in other lists
886
+ }
887
+ }
888
+ if (!targetTaskId) {
889
+ throw new Error(`Task "${taskName}" not found in any list`);
890
+ }
891
+ }
892
+ }
893
+ if (!targetTaskId) {
894
+ throw new Error("Either taskId or taskName must be provided");
895
+ }
896
+ // Retrieve comments for the task
897
+ const comments = await taskService.getTaskComments(targetTaskId, start, startId);
898
+ return {
899
+ taskId: targetTaskId,
900
+ comments: comments,
901
+ totalComments: comments.length,
902
+ pagination: {
903
+ hasMore: comments.length === 25, // API returns max 25 comments at a time
904
+ nextStart: comments.length > 0 ? new Date(comments[comments.length - 1].date).getTime() : undefined,
905
+ nextStartId: comments.length > 0 ? comments[comments.length - 1].id : undefined
906
+ }
907
+ };
908
+ }
909
+ };
812
910
  /**
813
911
  * Handler for bulk task updates
814
912
  */
@@ -1010,9 +1108,8 @@ export async function handleUpdateTask(parameters) {
1010
1108
  }
1011
1109
  listId = listInfo.id;
1012
1110
  }
1013
- // Now find the task
1014
- const tasks = await taskService.getTasks(listId || '');
1015
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1111
+ // Use the improved findTaskByName method
1112
+ const foundTask = await taskService.findTaskByName(listId || '', taskName);
1016
1113
  if (!foundTask) {
1017
1114
  throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
1018
1115
  }
@@ -1079,8 +1176,8 @@ export async function handleMoveTask(parameters) {
1079
1176
  if (!targetTaskId && taskName) {
1080
1177
  // Find the task in the source list if specified, otherwise search all tasks
1081
1178
  if (sourceListId) {
1082
- const tasks = await taskService.getTasks(sourceListId);
1083
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1179
+ // Use the improved findTaskByName method
1180
+ const foundTask = await taskService.findTaskByName(sourceListId, taskName);
1084
1181
  if (!foundTask) {
1085
1182
  throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
1086
1183
  }
@@ -1088,7 +1185,6 @@ export async function handleMoveTask(parameters) {
1088
1185
  }
1089
1186
  else {
1090
1187
  // Without a source list, we need to search more broadly
1091
- // This is less efficient but necessary if source list is unknown
1092
1188
  throw new Error("When using taskName, sourceListName must be provided to find the task");
1093
1189
  }
1094
1190
  }
@@ -1105,9 +1201,6 @@ export async function handleMoveTask(parameters) {
1105
1201
  }
1106
1202
  targetListId = listInfo.id;
1107
1203
  }
1108
- if (!targetListId) {
1109
- throw new Error("Either listId or listName must be provided for the target list");
1110
- }
1111
1204
  // Move the task
1112
1205
  const task = await taskService.moveTask(targetTaskId, targetListId);
1113
1206
  // Format response
@@ -1147,8 +1240,8 @@ export async function handleDuplicateTask(parameters) {
1147
1240
  if (!targetTaskId && taskName) {
1148
1241
  // Find the task in the source list if specified, otherwise search all tasks
1149
1242
  if (sourceListId) {
1150
- const tasks = await taskService.getTasks(sourceListId);
1151
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1243
+ // Use the improved findTaskByName method
1244
+ const foundTask = await taskService.findTaskByName(sourceListId, taskName);
1152
1245
  if (!foundTask) {
1153
1246
  throw new Error(`Task "${taskName}" not found in list "${sourceListName}"`);
1154
1247
  }
@@ -1241,62 +1334,6 @@ export async function handleGetTasks(parameters) {
1241
1334
  }]
1242
1335
  };
1243
1336
  }
1244
- /**
1245
- * Handler for the get_task tool
1246
- */
1247
- export async function handleGetTask(parameters) {
1248
- const { taskId, taskName, listName } = parameters;
1249
- let targetTaskId = taskId;
1250
- // If no taskId but taskName is provided, look up the task ID
1251
- if (!targetTaskId && taskName) {
1252
- let listId;
1253
- // If listName is provided, find the list ID first
1254
- if (listName) {
1255
- const hierarchy = await workspaceService.getWorkspaceHierarchy();
1256
- const listInfo = workspaceService.findIDByNameInHierarchy(hierarchy, listName, 'list');
1257
- if (!listInfo) {
1258
- throw new Error(`List "${listName}" not found`);
1259
- }
1260
- listId = listInfo.id;
1261
- }
1262
- // Now find the task
1263
- const tasks = await taskService.getTasks(listId || '');
1264
- const foundTask = tasks.find(t => t.name.toLowerCase() === taskName.toLowerCase());
1265
- if (!foundTask) {
1266
- throw new Error(`Task "${taskName}" not found${listName ? ` in list "${listName}"` : ""}`);
1267
- }
1268
- targetTaskId = foundTask.id;
1269
- }
1270
- if (!targetTaskId) {
1271
- throw new Error("Either taskId or taskName must be provided");
1272
- }
1273
- // Get the task
1274
- const task = await taskService.getTask(targetTaskId);
1275
- // Format response
1276
- return {
1277
- content: [{
1278
- type: "text",
1279
- text: JSON.stringify({
1280
- id: task.id,
1281
- name: task.name,
1282
- description: task.description,
1283
- status: task.status?.status || "Unknown",
1284
- priority: task.priority,
1285
- due_date: task.due_date ? formatDueDate(Number(task.due_date)) : undefined,
1286
- due_date_raw: task.due_date, // Keep raw timestamp for reference if needed
1287
- url: task.url,
1288
- list: task.list.name,
1289
- space: task.space.name,
1290
- folder: task.folder?.name,
1291
- creator: task.creator,
1292
- assignees: task.assignees,
1293
- tags: task.tags,
1294
- time_estimate: task.time_estimate,
1295
- time_spent: task.time_spent,
1296
- }, null, 2)
1297
- }]
1298
- };
1299
- }
1300
1337
  /**
1301
1338
  * Handler for the delete_task tool
1302
1339
  */
@@ -1380,8 +1417,8 @@ export async function handleDeleteBulkTasks({ tasks }) {
1380
1417
  if (!listInfo) {
1381
1418
  throw new Error(`List "${task.listName}" not found`);
1382
1419
  }
1383
- const taskList = await taskService.getTasks(listInfo.id);
1384
- const foundTask = taskList.find(t => t.name.toLowerCase() === task.taskName.toLowerCase());
1420
+ // Use the improved findTaskByName method
1421
+ const foundTask = await taskService.findTaskByName(listInfo.id, task.taskName);
1385
1422
  if (!foundTask) {
1386
1423
  throw new Error(`Task "${task.taskName}" not found in list "${task.listName}"`);
1387
1424
  }
@@ -1517,3 +1554,40 @@ export async function handleMoveBulkTasks(parameters) {
1517
1554
  }]
1518
1555
  };
1519
1556
  }
1557
+ /**
1558
+ * Handler for getting task comments
1559
+ */
1560
+ export async function handleGetTaskComments(parameters) {
1561
+ const { taskId, taskName, listName, start, startId } = parameters;
1562
+ try {
1563
+ // Call the handler with validation
1564
+ const result = await getTaskCommentsTool.handler({
1565
+ taskId,
1566
+ taskName,
1567
+ listName,
1568
+ start: start ? Number(start) : undefined,
1569
+ startId
1570
+ });
1571
+ return {
1572
+ content: [{
1573
+ type: "text",
1574
+ text: JSON.stringify(result, null, 2)
1575
+ }]
1576
+ };
1577
+ }
1578
+ catch (error) {
1579
+ // Handle and format error response with proper content array
1580
+ console.error('Error getting task comments:', error);
1581
+ return {
1582
+ content: [{
1583
+ type: "text",
1584
+ text: JSON.stringify({
1585
+ error: error.message || 'Failed to get task comments',
1586
+ taskId: taskId || null,
1587
+ taskName: taskName || null,
1588
+ listName: listName || null
1589
+ }, null, 2)
1590
+ }]
1591
+ };
1592
+ }
1593
+ }
@@ -4,6 +4,9 @@
4
4
  * This module defines workspace-related tools like retrieving workspace hierarchy.
5
5
  * It handles the workspace tool definitions and the implementation of their handlers.
6
6
  */
7
+ import { Logger } from '../logger.js';
8
+ // Create a logger for workspace tools
9
+ const logger = new Logger('WorkspaceTool');
7
10
  // Use the workspace service imported from the server
8
11
  // This is defined when server.ts imports this module
9
12
  let workspaceService;
@@ -22,7 +25,15 @@ export const workspaceHierarchyTool = {
22
25
  * Initialize the tool with services
23
26
  */
24
27
  export function initializeWorkspaceTool(services) {
28
+ logger.info('Initializing workspace tool');
29
+ if (!services || !services.workspace) {
30
+ logger.error('Failed to initialize workspace tool: services not provided');
31
+ throw new Error('Workspace service not available');
32
+ }
25
33
  workspaceService = services.workspace;
34
+ logger.info('Workspace tool initialized successfully', {
35
+ serviceType: workspaceService.constructor.name
36
+ });
26
37
  }
27
38
  /**
28
39
  * Handler for the get_workspace_hierarchy tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.4.68",
3
+ "version": "0.4.70",
4
4
  "description": "ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol",
5
5
  "type": "module",
6
6
  "main": "build/index.js",