@taazkareem/clickup-mcp-server 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -16,18 +16,24 @@ A Model Context Protocol (MCP) server for integrating ClickUp tasks with AI appl
16
16
  - Flexible identification using IDs or names
17
17
 
18
18
  - ✨ **Task Operations**
19
- - Create and update tasks
19
+ - Create single or bulk tasks
20
20
  - Move tasks between lists
21
21
  - Duplicate tasks
22
22
  - Set priorities and due dates
23
23
  - Assign team members
24
24
 
25
25
  - 📊 **Information Retrieval**
26
- - Get spaces and lists with their IDs
26
+ - Get complete hierarchy of spaces, folders, and lists with IDs
27
27
  - List available statuses
28
28
  - Find items by name (case-insensitive)
29
29
  - View task relationships
30
30
 
31
+ - 🔍 **Smart Name Resolution**
32
+ - Use names instead of IDs for lists and folders
33
+ - Global search across all spaces
34
+ - Case-insensitive matching
35
+ - Automatic location of items
36
+
31
37
  - 📝 **AI Integration**
32
38
  - Generate task descriptions with AI
33
39
  - Summarize tasks and analyze priorities
@@ -81,17 +87,20 @@ You can get these values from:
81
87
 
82
88
  ### Available Tools
83
89
 
84
- 1. **list_spaces**
85
- - Lists all spaces and their lists with IDs
90
+ 1. **workspace_hierarchy**
91
+ - Lists complete hierarchy of the ClickUp workspace
92
+ - Shows spaces, folders, and lists with their IDs
86
93
  - Shows available statuses for each list
94
+ - Provides a tree view of your workspace organization
87
95
  - No parameters required
88
96
 
89
97
  2. **create_task**
90
98
  - Creates a new task in ClickUp
91
99
  - Required parameters:
92
- - `listId`: ID of the list to create the task in
93
100
  - `name`: Name of the task
94
101
  - Optional parameters:
102
+ - `listId`: ID of the list (optional if listName provided)
103
+ - `listName`: Name of the list (optional if listId provided)
95
104
  - `description`: Task description
96
105
  - `status`: Task status
97
106
  - `priority`: Priority level (1-4)
@@ -101,7 +110,6 @@ You can get these values from:
101
110
  3. **create_bulk_tasks**
102
111
  - Creates multiple tasks simultaneously in a list
103
112
  - Required parameters:
104
- - `listId`: ID of the list to create the tasks in
105
113
  - `tasks`: Array of task objects, each containing:
106
114
  - `name`: Name of the task (required)
107
115
  - `description`: Task description (optional)
@@ -109,6 +117,9 @@ You can get these values from:
109
117
  - `priority`: Priority level 1-4 (optional)
110
118
  - `dueDate`: Due date ISO string (optional)
111
119
  - `assignees`: Array of user IDs (optional)
120
+ - Optional parameters:
121
+ - `listId`: ID of the list (optional if listName provided)
122
+ - `listName`: Name of the list (optional if listId provided)
112
123
 
113
124
  4. **create_list**
114
125
  - Creates a new list in a space
@@ -147,13 +158,17 @@ You can get these values from:
147
158
  - Moves a task to a different list
148
159
  - Required parameters:
149
160
  - `taskId`: ID of the task to move
150
- - `listId`: ID of the destination list
161
+ - Optional parameters:
162
+ - `listId`: ID of destination list (optional if listName provided)
163
+ - `listName`: Name of destination list (optional if listId provided)
151
164
 
152
165
  8. **duplicate_task**
153
166
  - Creates a copy of a task in a specified list
154
167
  - Required parameters:
155
168
  - `taskId`: ID of the task to duplicate
156
- - `listId`: ID of the destination list
169
+ - Optional parameters:
170
+ - `listId`: ID of destination list (optional if listName provided)
171
+ - `listName`: Name of destination list (optional if listId provided)
157
172
 
158
173
  9. **update_task**
159
174
  - Updates an existing task
@@ -231,4 +246,4 @@ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md)
231
246
 
232
247
  ## License
233
248
 
234
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
249
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
package/build/config.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import dotenv from 'dotenv';
2
2
  // Load environment variables from .env file
3
3
  dotenv.config();
4
+ console.log('Environment variables received:', {
5
+ CLICKUP_API_KEY: process.env.CLICKUP_API_KEY,
6
+ TEAM_ID: process.env.TEAM_ID
7
+ });
4
8
  // Parse command line arguments for --env flags
5
9
  const args = process.argv.slice(2);
10
+ console.log('Command line arguments:', args);
6
11
  const envArgs = {};
7
12
  for (let i = 0; i < args.length; i++) {
8
13
  if (args[i] === '--env' && i + 1 < args.length) {
@@ -14,10 +19,12 @@ for (let i = 0; i < args.length; i++) {
14
19
  i++; // Skip the next argument since we used it
15
20
  }
16
21
  }
22
+ console.log('Parsed environment arguments:', envArgs);
17
23
  const configuration = {
18
24
  clickupApiKey: envArgs.clickupApiKey || process.env.CLICKUP_API_KEY || '',
19
- teamId: envArgs.teamId || process.env.TEAM_ID || '',
25
+ teamId: envArgs.teamId || process.env.TEAM_ID || ''
20
26
  };
27
+ console.log('Final configuration:', configuration);
21
28
  // Check for missing environment variables
22
29
  const missingEnvVars = Object.entries(configuration)
23
30
  .filter(([_, value]) => !value)
@@ -0,0 +1,25 @@
1
+ import { getAllTasks } from '../utils/resolvers.js';
2
+ export async function handleSummarizeTasks(clickup, teamId) {
3
+ const { tasks } = await getAllTasks(clickup, teamId);
4
+ let output = "Summarized Tasks:\n\n";
5
+ for (const task of tasks) {
6
+ output += `- ${task.name}: ${task.description}\n`;
7
+ }
8
+ return output;
9
+ }
10
+ export async function handleAnalyzeTaskPriorities(clickup, teamId) {
11
+ const { tasks } = await getAllTasks(clickup, teamId);
12
+ const priorities = tasks.map(task => task.priority?.priority);
13
+ const uniquePriorities = [...new Set(priorities.filter(p => p !== undefined))];
14
+ const priorityCounts = uniquePriorities.map(priority => ({
15
+ priority,
16
+ count: priorities.filter(p => p === priority).length
17
+ }));
18
+ let output = "Task Priorities Analysis:\n\n";
19
+ output += "Available Priorities: " + uniquePriorities.join(', ') + "\n\n";
20
+ output += "Priority Counts:\n";
21
+ for (const priority of priorityCounts) {
22
+ output += `- Priority ${priority.priority}: ${priority.count}\n`;
23
+ }
24
+ return output;
25
+ }
@@ -0,0 +1,38 @@
1
+ import { resolveListId } from '../utils/resolvers.js';
2
+ export async function handleWorkspaceHierarchy(clickup, teamId) {
3
+ const spaces = await clickup.getSpaces(teamId);
4
+ const allLists = await clickup.getAllLists(teamId);
5
+ let output = "ClickUp Workspace Hierarchy:\n\n";
6
+ for (const space of spaces) {
7
+ output += `Space: ${space.name} (ID: ${space.id})\n`;
8
+ const folders = await clickup.getFolders(space.id);
9
+ for (const folder of folders) {
10
+ output += ` ├─ Folder: ${folder.name} (ID: ${folder.id})\n`;
11
+ const folderLists = folder.lists || [];
12
+ for (const list of folderLists) {
13
+ const { statuses } = await clickup.getTasks(list.id);
14
+ output += ` │ └─ List: ${list.name} (ID: ${list.id})\n`;
15
+ output += ` │ Available Statuses: ${statuses.join(', ')}\n`;
16
+ }
17
+ }
18
+ const spaceLists = allLists.filter(list => list.space &&
19
+ list.space.id === space.id &&
20
+ !folders.some(folder => folder.lists?.some(fl => fl.id === list.id)));
21
+ if (spaceLists.length > 0) {
22
+ output += " ├─ Lists (not in folders):\n";
23
+ for (const list of spaceLists) {
24
+ const { statuses } = await clickup.getTasks(list.id);
25
+ output += ` │ └─ List: ${list.name} (ID: ${list.id})\n`;
26
+ output += ` │ Available Statuses: ${statuses.join(', ')}\n`;
27
+ }
28
+ }
29
+ output += "\n";
30
+ }
31
+ return output;
32
+ }
33
+ export async function handleCreateTask(clickup, teamId, args) {
34
+ const listId = await resolveListId(clickup, teamId, args.listId, args.listName);
35
+ const { listId: _, listName: __, ...taskData } = args;
36
+ return await clickup.createTask(listId, taskData);
37
+ }
38
+ // Add other handler functions for each tool...
package/build/index.js CHANGED
@@ -1,36 +1,31 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * This is a template MCP server that implements a simple ClickUp task management system.
4
- * It demonstrates core MCP concepts like resources, tools, and prompts by allowing:
5
- * - Listing ClickUp tasks as resources
6
- * - Reading individual ClickUp tasks
7
- * - Creating new ClickUp tasks via a tool
8
- * - Updating existing ClickUp tasks via a tool
9
- * - Summarizing all ClickUp tasks via a prompt
10
- * - Analyzing task priorities via a prompt
11
- * - Generating detailed descriptions for tasks via a prompt
12
- */
13
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
4
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
16
5
  import { ClickUpService } from "./services/clickup.js";
17
6
  import config from "./config.js";
18
- /**
19
- * Simple in-memory storage for notes.
20
- * In a real implementation, this would likely be backed by a database.
21
- */
22
- const notes = {
23
- "1": { title: "First Note", content: "This is note 1" },
24
- "2": { title: "Second Note", content: "This is note 2" }
25
- };
7
+ import { handleWorkspaceHierarchy, handleCreateTask } from "./handlers/tools.js";
8
+ import { handleSummarizeTasks, handleAnalyzeTaskPriorities } from "./handlers/prompts.js";
9
+ import { getAllTasks } from "./utils/resolvers.js";
10
+ console.log('Server starting up...');
11
+ console.log('Config loaded:', {
12
+ clickupApiKey: config.clickupApiKey ? '***' : 'missing',
13
+ teamId: config.teamId || 'missing'
14
+ });
26
15
  // Initialize ClickUp service
27
- const clickup = ClickUpService.initialize(config.clickupApiKey);
28
- /**
29
- * Create an MCP server with capabilities for resources (to list/read ClickUp tasks),
30
- * tools (to create/update ClickUp tasks), and prompts (to summarize/analyze ClickUp tasks).
31
- */
16
+ let clickup;
17
+ try {
18
+ console.log('Initializing ClickUp service...');
19
+ clickup = ClickUpService.initialize(config.clickupApiKey);
20
+ console.log('ClickUp service initialized successfully');
21
+ }
22
+ catch (error) {
23
+ console.error("Failed to initialize ClickUp service:", error);
24
+ process.exit(1);
25
+ }
26
+ console.log('Creating MCP server...');
32
27
  const server = new Server({
33
- name: "clickup-mcp-server",
28
+ name: "clickup",
34
29
  version: "0.1.0",
35
30
  }, {
36
31
  capabilities: {
@@ -39,40 +34,31 @@ const server = new Server({
39
34
  prompts: {},
40
35
  },
41
36
  });
37
+ console.log('MCP server created');
42
38
  /**
43
39
  * Handler for listing available ClickUp tasks as resources.
44
- * Each task is exposed as a resource with:
45
- * - A clickup:// URI scheme
46
- * - JSON MIME type
47
- * - Human readable name and description (including the task name and description)
48
40
  */
49
41
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
42
+ console.log('Handling ListResources request');
50
43
  try {
51
- const spaces = await clickup.getSpaces(config.teamId);
52
- const resources = [];
53
- for (const space of spaces) {
54
- const lists = await clickup.getLists(space.id);
55
- for (const list of lists) {
56
- const { tasks } = await clickup.getTasks(list.id);
57
- resources.push(...tasks.map((task) => ({
58
- uri: `clickup://task/${task.id}`,
59
- mimeType: "application/json",
60
- name: task.name,
61
- description: task.description || `Task in ${list.name} (${space.name})`,
62
- tags: []
63
- })));
64
- }
65
- }
66
- return { resources };
44
+ const { tasks, spaces } = await getAllTasks(clickup, config.teamId);
45
+ return {
46
+ resources: tasks.map(task => ({
47
+ uri: `clickup://task/${task.id}`,
48
+ mimeType: "application/json",
49
+ name: task.name,
50
+ description: task.description || `Task in ${task.list.name} (${task.space.name})`,
51
+ tags: []
52
+ }))
53
+ };
67
54
  }
68
55
  catch (error) {
69
- console.error('Error listing resources:', error);
56
+ console.error('Error in ListResources:', error);
70
57
  throw error;
71
58
  }
72
59
  });
73
60
  /**
74
61
  * Handler for reading the contents of a specific ClickUp task.
75
- * Takes a clickup:// URI and returns the task content as JSON.
76
62
  */
77
63
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
78
64
  try {
@@ -94,15 +80,15 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
94
80
  }
95
81
  });
96
82
  /**
97
- * Handler that lists available tools.
98
- * Exposes tools for listing spaces, creating tasks, and updating tasks.
83
+ * Handler for listing available tools.
99
84
  */
100
85
  server.setRequestHandler(ListToolsRequestSchema, async () => {
86
+ console.log('Handling ListTools request');
101
87
  return {
102
88
  tools: [
103
89
  {
104
- name: "list_spaces",
105
- description: "List all spaces and their lists with IDs",
90
+ name: "workspace_hierarchy",
91
+ description: "List complete hierarchy of the ClickUp workspace",
106
92
  inputSchema: {
107
93
  type: "object",
108
94
  properties: {},
@@ -117,7 +103,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
117
103
  properties: {
118
104
  listId: {
119
105
  type: "string",
120
- description: "ID of the list to create the task in"
106
+ description: "ID of the list to create the task in (optional if listName is provided)"
107
+ },
108
+ listName: {
109
+ type: "string",
110
+ description: "Name of the list to create the task in (optional if listId is provided)"
121
111
  },
122
112
  name: {
123
113
  type: "string",
@@ -140,7 +130,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
140
130
  description: "Due date of the task (ISO string)"
141
131
  }
142
132
  },
143
- required: ["listId", "name"]
133
+ required: ["name"]
144
134
  }
145
135
  },
146
136
  {
@@ -151,7 +141,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
151
141
  properties: {
152
142
  listId: {
153
143
  type: "string",
154
- description: "ID of the list to create the tasks in"
144
+ description: "ID of the list to create the tasks in (optional if listName is provided)"
145
+ },
146
+ listName: {
147
+ type: "string",
148
+ description: "Name of the list to create the tasks in (optional if listId is provided)"
155
149
  },
156
150
  tasks: {
157
151
  type: "array",
@@ -177,7 +171,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
177
171
  },
178
172
  dueDate: {
179
173
  type: "string",
180
- description: "Due date of the task (ISO string)"
174
+ description: "Due date (ISO string)"
181
175
  },
182
176
  assignees: {
183
177
  type: "array",
@@ -191,7 +185,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
191
185
  }
192
186
  }
193
187
  },
194
- required: ["listId", "tasks"]
188
+ required: ["tasks"]
195
189
  }
196
190
  },
197
191
  {
@@ -312,10 +306,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
312
306
  },
313
307
  listId: {
314
308
  type: "string",
315
- description: "ID of the destination list"
309
+ description: "ID of the destination list (optional if listName is provided)"
310
+ },
311
+ listName: {
312
+ type: "string",
313
+ description: "Name of the destination list (optional if listId is provided)"
316
314
  }
317
315
  },
318
- required: ["taskId", "listId"]
316
+ required: ["taskId"]
319
317
  }
320
318
  },
321
319
  {
@@ -330,10 +328,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
330
328
  },
331
329
  listId: {
332
330
  type: "string",
333
- description: "ID of the list to create the duplicate in"
331
+ description: "ID of the destination list (optional if listName is provided)"
332
+ },
333
+ listName: {
334
+ type: "string",
335
+ description: "Name of the destination list (optional if listId is provided)"
334
336
  }
335
337
  },
336
- required: ["taskId", "listId"]
338
+ required: ["taskId"]
337
339
  }
338
340
  },
339
341
  {
@@ -374,36 +376,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
374
376
  };
375
377
  });
376
378
  /**
377
- * Handler for the CallToolRequestSchema.
378
- * Handles the execution of tools like listing spaces, creating tasks, and updating tasks.
379
+ * Handler for executing tools.
379
380
  */
380
381
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
381
382
  try {
382
383
  switch (request.params.name) {
383
- case "list_spaces": {
384
- const spaces = await clickup.getSpaces(config.teamId);
385
- const allLists = await clickup.getAllLists(config.teamId);
386
- let output = "Available Spaces and Lists:\n\n";
387
- for (const space of spaces) {
388
- output += `Space: ${space.name} (ID: ${space.id})\n`;
389
- const spaceLists = allLists.filter(list => list.space.id === space.id);
390
- for (const list of spaceLists) {
391
- const { statuses } = await clickup.getTasks(list.id);
392
- output += ` └─ List: ${list.name} (ID: ${list.id})\n`;
393
- output += ` Available Statuses: ${statuses.join(', ')}\n`;
394
- }
395
- output += "\n";
396
- }
397
- // Add lists without spaces at the end
398
- const listsWithoutSpace = allLists.filter(list => !list.space);
399
- if (listsWithoutSpace.length > 0) {
400
- output += "Lists without assigned spaces:\n";
401
- for (const list of listsWithoutSpace) {
402
- const { statuses } = await clickup.getTasks(list.id);
403
- output += ` └─ List: ${list.name} (ID: ${list.id})\n`;
404
- output += ` Available Statuses: ${statuses.join(', ')}\n`;
405
- }
406
- }
384
+ case "workspace_hierarchy": {
385
+ const output = await handleWorkspaceHierarchy(clickup, config.teamId);
407
386
  return {
408
387
  content: [{
409
388
  type: "text",
@@ -413,11 +392,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
413
392
  }
414
393
  case "create_task": {
415
394
  const args = request.params.arguments;
416
- if (!args.listId || !args.name) {
417
- throw new Error("listId and name are required");
418
- }
419
- const { listId, ...taskData } = args;
420
- const task = await clickup.createTask(listId, taskData);
395
+ const task = await handleCreateTask(clickup, config.teamId, args);
421
396
  return {
422
397
  content: [{
423
398
  type: "text",
@@ -427,10 +402,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
427
402
  }
428
403
  case "create_bulk_tasks": {
429
404
  const args = request.params.arguments;
430
- if (!args.listId || !args.tasks || !args.tasks.length) {
431
- throw new Error("listId and at least one task are required");
405
+ let listId = args.listId;
406
+ if (!listId && args.listName) {
407
+ const result = await clickup.findListByNameGlobally(config.teamId, args.listName);
408
+ if (!result) {
409
+ throw new Error(`List with name "${args.listName}" not found`);
410
+ }
411
+ listId = result.list.id;
432
412
  }
433
- const { listId, tasks } = args;
413
+ if (!listId) {
414
+ throw new Error("Either listId or listName is required");
415
+ }
416
+ if (!args.tasks || !args.tasks.length) {
417
+ throw new Error("At least one task is required");
418
+ }
419
+ const { listId: _, listName: __, tasks } = args;
434
420
  const createdTasks = await clickup.createBulkTasks(listId, { tasks });
435
421
  return {
436
422
  content: [{
@@ -444,6 +430,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
444
430
  if (!args.name) {
445
431
  throw new Error("name is required");
446
432
  }
433
+ // If folder is specified, create list in folder
434
+ if (args.folderName || args.folderId) {
435
+ let folderId = args.folderId;
436
+ if (!folderId && args.folderName) {
437
+ const result = await clickup.findFolderByNameGlobally(config.teamId, args.folderName);
438
+ if (!result) {
439
+ throw new Error(`Folder with name "${args.folderName}" not found`);
440
+ }
441
+ folderId = result.folder.id;
442
+ }
443
+ if (!folderId) {
444
+ throw new Error("Either folderId or folderName must be provided");
445
+ }
446
+ const { spaceId: _, spaceName: __, folderName: ___, folderId: ____, ...listData } = args;
447
+ const list = await clickup.createListInFolder(folderId, listData);
448
+ return {
449
+ content: [{
450
+ type: "text",
451
+ text: `Created list ${list.id}: ${list.name} in folder`
452
+ }]
453
+ };
454
+ }
455
+ // Otherwise, create list in space
447
456
  let spaceId = args.spaceId;
448
457
  if (!spaceId && args.spaceName) {
449
458
  const space = await clickup.findSpaceByName(config.teamId, args.spaceName);
@@ -455,7 +464,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
455
464
  if (!spaceId) {
456
465
  throw new Error("Either spaceId or spaceName must be provided");
457
466
  }
458
- const { spaceId: _, spaceName: __, ...listData } = args;
467
+ const { spaceId: _, spaceName: __, folderName: ___, folderId: ____, ...listData } = args;
459
468
  const list = await clickup.createList(spaceId, listData);
460
469
  return {
461
470
  content: [{
@@ -496,58 +505,78 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
496
505
  }
497
506
  let folderId = args.folderId;
498
507
  if (!folderId && args.folderName) {
499
- let spaceId = args.spaceId;
500
- if (!spaceId && args.spaceName) {
501
- const space = await clickup.findSpaceByName(config.teamId, args.spaceName);
502
- if (!space) {
503
- throw new Error(`Space with name "${args.spaceName}" not found`);
504
- }
505
- spaceId = space.id;
506
- }
507
- if (!spaceId) {
508
- throw new Error("Either spaceId or spaceName must be provided when using folderName");
509
- }
510
- const folder = await clickup.findFolderByName(spaceId, args.folderName);
511
- if (!folder) {
508
+ const result = await clickup.findFolderByNameGlobally(config.teamId, args.folderName);
509
+ if (!result) {
512
510
  throw new Error(`Folder with name "${args.folderName}" not found`);
513
511
  }
514
- folderId = folder.id;
512
+ folderId = result.folder.id;
515
513
  }
516
514
  if (!folderId) {
517
- throw new Error("Either folderId or folderName (with space information) must be provided");
515
+ throw new Error("Either folderId or folderName is required");
518
516
  }
519
- const { folderId: _, folderName: __, spaceId: ___, spaceName: ____, ...listData } = args;
520
- const list = await clickup.createListInFolder(folderId, listData);
521
- return {
522
- content: [{
523
- type: "text",
524
- text: `Created list ${list.id}: ${list.name} in folder`
525
- }]
517
+ const listData = {
518
+ name: args.name,
519
+ content: args.content,
520
+ status: args.status
526
521
  };
522
+ try {
523
+ const list = await clickup.createListInFolder(folderId, listData);
524
+ return {
525
+ content: [{
526
+ type: "text",
527
+ text: `Created list ${list.id}: ${list.name} in folder`
528
+ }]
529
+ };
530
+ }
531
+ catch (error) {
532
+ throw new Error(`Failed to create list: ${error.message}`);
533
+ }
527
534
  }
528
535
  case "move_task": {
529
536
  const args = request.params.arguments;
530
- if (!args.taskId || !args.listId) {
531
- throw new Error("taskId and listId are required");
537
+ if (!args.taskId) {
538
+ throw new Error("taskId is required");
539
+ }
540
+ let listId = args.listId;
541
+ if (!listId && args.listName) {
542
+ const result = await clickup.findListByNameGlobally(config.teamId, args.listName);
543
+ if (!result) {
544
+ throw new Error(`List with name "${args.listName}" not found`);
545
+ }
546
+ listId = result.list.id;
547
+ }
548
+ if (!listId) {
549
+ throw new Error("Either listId or listName is required");
532
550
  }
533
- const task = await clickup.moveTask(args.taskId, args.listId);
551
+ const task = await clickup.moveTask(args.taskId, listId);
534
552
  return {
535
553
  content: [{
536
554
  type: "text",
537
- text: `Moved task ${task.id} to list ${args.listId}`
555
+ text: `Moved task ${task.id} to list ${listId}`
538
556
  }]
539
557
  };
540
558
  }
541
559
  case "duplicate_task": {
542
560
  const args = request.params.arguments;
543
- if (!args.taskId || !args.listId) {
544
- throw new Error("taskId and listId are required");
561
+ if (!args.taskId) {
562
+ throw new Error("taskId is required");
545
563
  }
546
- const task = await clickup.duplicateTask(args.taskId, args.listId);
564
+ let listId = args.listId;
565
+ if (!listId && args.listName) {
566
+ const result = await clickup.findListByNameGlobally(config.teamId, args.listName);
567
+ if (!result) {
568
+ throw new Error(`List with name "${args.listName}" not found`);
569
+ }
570
+ listId = result.list.id;
571
+ }
572
+ if (!listId) {
573
+ throw new Error("Either listId or listName is required");
574
+ }
575
+ const task = await clickup.duplicateTask(args.taskId, listId);
547
576
  return {
548
577
  content: [{
549
578
  type: "text",
550
- text: `Duplicated task ${args.taskId} to new task ${task.id} in list ${args.listId}`
579
+ text: `Duplicated task ${args.taskId} to new task ${task.id} in list ${listId}`
551
580
  }]
552
581
  };
553
582
  }
@@ -556,8 +585,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
556
585
  if (!args.taskId) {
557
586
  throw new Error("taskId is required");
558
587
  }
559
- const { taskId, ...updateData } = args;
560
- const task = await clickup.updateTask(taskId, updateData);
588
+ const dueDate = args.due_date ? new Date(args.due_date).getTime() : undefined;
589
+ const task = await clickup.updateTask(args.taskId, {
590
+ name: args.name,
591
+ description: args.description,
592
+ status: args.status,
593
+ priority: args.priority,
594
+ due_date: dueDate
595
+ });
561
596
  return {
562
597
  content: [{
563
598
  type: "text",
@@ -570,147 +605,79 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
570
605
  }
571
606
  }
572
607
  catch (error) {
573
- console.error('Error handling tool call:', error);
608
+ console.error('Error executing tool:', error);
574
609
  throw error;
575
610
  }
576
611
  });
577
612
  /**
578
- * Add handlers for listing and getting prompts.
579
- * Prompts include summarizing tasks, analyzing priorities, and generating task descriptions.
613
+ * Handler for listing available prompts.
580
614
  */
581
615
  server.setRequestHandler(ListPromptsRequestSchema, async () => {
582
616
  return {
583
617
  prompts: [
584
618
  {
585
619
  name: "summarize_tasks",
586
- description: "Summarize all tasks in a list",
587
- },
588
- {
589
- name: "analyze_priorities",
590
- description: "Analyze task priorities and suggest optimizations",
620
+ description: "Summarize all ClickUp tasks"
591
621
  },
592
622
  {
593
- name: "generate_description",
594
- description: "Generate a detailed description for a task",
623
+ name: "analyze_task_priorities",
624
+ description: "Analyze task priorities"
595
625
  }
596
626
  ]
597
627
  };
598
628
  });
629
+ /**
630
+ * Handler for getting a specific prompt.
631
+ */
599
632
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
600
633
  try {
601
634
  switch (request.params.name) {
602
635
  case "summarize_tasks": {
603
- const spaces = await clickup.getSpaces(config.teamId);
604
- const tasks = [];
605
- // Gather all tasks
606
- for (const space of spaces) {
607
- const lists = await clickup.getLists(space.id);
608
- for (const list of lists) {
609
- const { tasks: listTasks } = await clickup.getTasks(list.id);
610
- tasks.push(...listTasks.map((task) => ({
611
- type: "resource",
612
- resource: {
613
- uri: `clickup://task/${task.id}`,
614
- mimeType: "application/json",
615
- text: JSON.stringify(task, null, 2)
616
- }
617
- })));
618
- }
619
- }
636
+ const output = await handleSummarizeTasks(clickup, config.teamId);
620
637
  return {
621
- messages: [
622
- {
623
- role: "user",
624
- content: {
625
- type: "text",
626
- text: "Please provide a summary of the following ClickUp tasks:"
627
- }
628
- },
629
- ...tasks.map(task => ({
630
- role: "user",
631
- content: task
632
- })),
633
- {
634
- role: "user",
635
- content: {
636
- type: "text",
637
- text: "Please provide:\n1. A high-level overview of all tasks\n2. Group them by status\n3. Highlight any urgent or high-priority items\n4. Suggest any task dependencies or relationships"
638
- }
639
- }
640
- ]
641
- };
642
- }
643
- case "analyze_priorities": {
644
- const spaces = await clickup.getSpaces(config.teamId);
645
- const tasks = [];
646
- for (const space of spaces) {
647
- const lists = await clickup.getLists(space.id);
648
- for (const list of lists) {
649
- const { tasks: listTasks } = await clickup.getTasks(list.id);
650
- tasks.push(...listTasks.map((task) => ({
651
- type: "resource",
652
- resource: {
653
- uri: `clickup://task/${task.id}`,
654
- mimeType: "application/json",
655
- text: JSON.stringify(task, null, 2)
656
- }
657
- })));
658
- }
659
- }
660
- return {
661
- messages: [
662
- {
663
- role: "user",
664
- content: {
665
- type: "text",
666
- text: "Please analyze the priorities of the following ClickUp tasks:"
667
- }
668
- },
669
- ...tasks.map(task => ({
670
- role: "user",
671
- content: task
672
- })),
673
- {
674
- role: "user",
675
- content: {
676
- type: "text",
677
- text: "Please provide:\n1. Analysis of current priority distribution\n2. Identify any misaligned priorities\n3. Suggest priority adjustments\n4. Recommend task sequencing based on priorities"
678
- }
679
- }
680
- ]
638
+ content: [{
639
+ type: "text",
640
+ text: output
641
+ }]
681
642
  };
682
643
  }
683
- case "generate_description": {
644
+ case "analyze_task_priorities": {
645
+ const output = await handleAnalyzeTaskPriorities(clickup, config.teamId);
684
646
  return {
685
- messages: [
686
- {
687
- role: "user",
688
- content: {
689
- type: "text",
690
- text: "Please help me generate a detailed description for a ClickUp task. The description should include:\n1. Clear objective\n2. Success criteria\n3. Required resources\n4. Dependencies\n5. Potential risks\n\nPlease ask me about the task details."
691
- }
692
- }
693
- ]
647
+ content: [{
648
+ type: "text",
649
+ text: output
650
+ }]
694
651
  };
695
652
  }
696
653
  default:
697
- throw new Error("Unknown prompt");
654
+ throw new Error("Prompt not found");
698
655
  }
699
656
  }
700
657
  catch (error) {
701
- console.error('Error handling prompt:', error);
658
+ console.error('Error getting prompt:', error);
702
659
  throw error;
703
660
  }
704
661
  });
705
- /**
706
- * Start the server using stdio transport.
707
- * This allows the server to communicate via standard input/output streams.
708
- */
709
- async function main() {
710
- const transport = new StdioServerTransport();
711
- await server.connect(transport);
712
- }
713
- main().catch((error) => {
714
- console.error("Server error:", error);
662
+ // Start the server
663
+ console.log('Setting up transport...');
664
+ const transport = new StdioServerTransport();
665
+ // Connect the server to the transport
666
+ console.log('Connecting server to transport...');
667
+ server.connect(transport).catch(error => {
668
+ console.error('Error connecting server to transport:', error);
715
669
  process.exit(1);
716
670
  });
671
+ // Handle process signals
672
+ process.on('SIGINT', () => {
673
+ console.log('Received SIGINT. Shutting down...');
674
+ transport.close();
675
+ });
676
+ process.on('SIGTERM', () => {
677
+ console.log('Received SIGTERM. Shutting down...');
678
+ transport.close();
679
+ });
680
+ // Prevent unhandled promise rejections from crashing the server
681
+ process.on('unhandledRejection', (error) => {
682
+ console.error('Unhandled promise rejection:', error);
683
+ });
@@ -110,6 +110,43 @@ export class ClickUpService {
110
110
  });
111
111
  return response.data;
112
112
  }
113
+ async findListByNameGlobally(teamId, listName) {
114
+ const spaces = await this.getSpaces(teamId);
115
+ for (const space of spaces) {
116
+ // Check lists in folders
117
+ const folders = await this.getFolders(space.id);
118
+ for (const folder of folders) {
119
+ const folderList = folder.lists?.find(list => list.name.toLowerCase() === listName.toLowerCase());
120
+ if (folderList) {
121
+ return { list: folderList, space, folder };
122
+ }
123
+ }
124
+ // Check lists directly in space
125
+ const spaceLists = await this.getLists(space.id);
126
+ const spaceList = spaceLists.find(list => list.name.toLowerCase() === listName.toLowerCase());
127
+ if (spaceList) {
128
+ return { list: spaceList, space };
129
+ }
130
+ }
131
+ // Check lists without spaces
132
+ const allLists = await this.getAllLists(teamId);
133
+ const list = allLists.find(list => list.name.toLowerCase() === listName.toLowerCase());
134
+ if (list) {
135
+ return { list };
136
+ }
137
+ return null;
138
+ }
139
+ async findFolderByNameGlobally(teamId, folderName) {
140
+ const spaces = await this.getSpaces(teamId);
141
+ for (const space of spaces) {
142
+ const folders = await this.getFolders(space.id);
143
+ const folder = folders.find(folder => folder.name.toLowerCase() === folderName.toLowerCase());
144
+ if (folder) {
145
+ return { folder, space };
146
+ }
147
+ }
148
+ return null;
149
+ }
113
150
  async duplicateTask(taskId, listId) {
114
151
  const response = await this.client.post(`/task/${taskId}/duplicate`, {
115
152
  list: listId
@@ -123,8 +160,4 @@ export class ClickUpService {
123
160
  const response = await this.client.put(`/list/${listId}`, data);
124
161
  return response.data;
125
162
  }
126
- async findListByName(spaceId, listName) {
127
- const lists = await this.getLists(spaceId);
128
- return lists.find(list => list.name.toLowerCase() === listName.toLowerCase()) || null;
129
- }
130
163
  }
@@ -0,0 +1,48 @@
1
+ export async function resolveListId(clickup, teamId, listId, listName) {
2
+ if (listId)
3
+ return listId;
4
+ if (!listName) {
5
+ throw new Error("Either listId or listName is required");
6
+ }
7
+ const result = await clickup.findListByNameGlobally(teamId, listName);
8
+ if (!result) {
9
+ throw new Error(`List with name "${listName}" not found`);
10
+ }
11
+ return result.list.id;
12
+ }
13
+ export async function resolveSpaceId(clickup, teamId, spaceId, spaceName) {
14
+ if (spaceId)
15
+ return spaceId;
16
+ if (!spaceName) {
17
+ throw new Error("Either spaceId or spaceName is required");
18
+ }
19
+ const space = await clickup.findSpaceByName(teamId, spaceName);
20
+ if (!space) {
21
+ throw new Error(`Space with name "${spaceName}" not found`);
22
+ }
23
+ return space.id;
24
+ }
25
+ export async function resolveFolderId(clickup, teamId, folderId, folderName) {
26
+ if (folderId)
27
+ return folderId;
28
+ if (!folderName) {
29
+ throw new Error("Either folderId or folderName is required");
30
+ }
31
+ const result = await clickup.findFolderByNameGlobally(teamId, folderName);
32
+ if (!result) {
33
+ throw new Error(`Folder with name "${folderName}" not found`);
34
+ }
35
+ return result.folder.id;
36
+ }
37
+ export async function getAllTasks(clickup, teamId) {
38
+ const spaces = await clickup.getSpaces(teamId);
39
+ const spacePromises = spaces.map(async (space) => {
40
+ const lists = await clickup.getLists(space.id);
41
+ const listPromises = lists.map(list => clickup.getTasks(list.id));
42
+ const listResults = await Promise.all(listPromises);
43
+ return listResults.flatMap(result => result.tasks);
44
+ });
45
+ const tasksPerSpace = await Promise.all(spacePromises);
46
+ const allTasks = tasksPerSpace.flat();
47
+ return { tasks: allTasks, spaces };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taazkareem/clickup-mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "homepage": "https://github.com/taazkareem/clickup-mcp-server#readme",
43
43
  "dependencies": {
44
- "@modelcontextprotocol/sdk": "0.6.0",
44
+ "@modelcontextprotocol/sdk": "^1.4.1",
45
45
  "axios": "^1.6.7",
46
46
  "dotenv": "^16.4.1"
47
47
  },