@hauptsache.net/clickup-mcp 1.0.1 → 1.0.3

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
@@ -1,148 +1,73 @@
1
- # ClickUp MCP (Model Context Protocol)
1
+ # ClickUp MCP for AI Assistants
2
2
 
3
- A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration, designed to allow Large Language Models (LLMs) to read ClickUp tasks and data.
3
+ A Model Context Protocol (MCP) server that enables AI assistants like Claude, Windsurf, and Cursor to interact with your ClickUp tasks.
4
4
 
5
- The main focus is to represent tickets in a way that is easy for LLMs to process and understand.
6
- This especially includes images in the description and in comments of tickets,
7
- which are not that easy to understand by just passing the api responses.
5
+ ## Why This MCP Is Great
8
6
 
9
- ## Overview
7
+ This MCP excels at providing AI assistants with rich access to your ClickUp tasks:
10
8
 
11
- This MCP provides a lightweight interface for LLMs to:
12
- - Retrieve detailed task information by ID
13
- - Search for tasks using keywords
14
- - List open tasks assigned to the current user
9
+ - **Complete Task Information**: View detailed task data including status, creation date, and assignees
10
+ - **Full Comment History**: Access the entire conversation thread for context
11
+ - **Inline Images Support**: View images embedded in task descriptions and comments
12
+ - **Task Search**: Find relevant tasks using keywords
13
+ - **Todo Management**: List all your open tasks
15
14
 
16
- The implementation includes image handling for task descriptions, allowing LLMs to process both text content and visual information from ClickUp tasks.
15
+ The standout feature is the ability to retrieve individual tickets with their complete comment history and inline images, giving your AI assistant the full context of your work.
17
16
 
18
- ## Prerequisites
17
+ ## Limitations
19
18
 
20
- - Node.js (v16 or higher)
21
- - A ClickUp account with API access
22
- - ClickUp API key and Team ID
19
+ - **Todo Management**: The todo management feature is currently limited to 50 tasks since it would otherwise flood the context.
20
+ - **No List/Space Support**: This MCP does not currently support browsing ClickUp Lists or Spaces due to performance issues with large instances.
21
+ - **Image Limit**: The MCP processes only the 4 most recent images per task to prevent exceeding context limits. You can adjust this number by setting the `MAX_IMAGES` environment variable, though most AI tools have constraints that prevent using more than 4 images.
23
22
 
24
- ## Installation
23
+ ## Setup for Claude Desktop, Windsurf, or Cursor
25
24
 
26
- 1. Clone this repository
27
- 2. Install dependencies:
25
+ 1. **Prerequisites**:
26
+ - A ClickUp account with API access (Profile Icon > Settings > Apps > API Token ~ usually starts with pk_)
27
+ - Your ClickUp API key and Team ID (The ~7 digit number in the url when you are in the settings)
28
28
 
29
- ```bash
30
- npm install
31
- ```
29
+ 2. **Configuration**:
30
+ Add the following to your MCP configuration file:
32
31
 
33
- 3. Build the TypeScript code:
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "clickup": {
36
+ "command": "npx",
37
+ "args": [
38
+ "-y",
39
+ "@hauptsache.net/clickup-mcp"
40
+ ],
41
+ "env": {
42
+ "CLICKUP_API_KEY": "your_api_key",
43
+ "CLICKUP_TEAM_ID": "your_team_id"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
34
49
 
35
- ```bash
36
- npm run build
37
- ```
50
+ Replace `your_api_key` and `your_team_id` with your actual ClickUp credentials.
38
51
 
39
- ## Configuration
52
+ 3. **Connect Your AI Assistant**:
53
+ - **Claude Desktop**: Add this configuration in Settings > MCPs
54
+ - **Windsurf**: Add to your MCP configuration file
55
+ - **Cursor**: Configure through the MCP settings panel
40
56
 
41
- Set the following environment variables:
57
+ ## Using with Your AI Assistant
42
58
 
43
- - `CLICKUP_API_KEY`: Your ClickUp API key
44
- - `CLICKUP_TEAM_ID`: Your ClickUp team ID
59
+ Once connected, your AI assistant can:
45
60
 
46
- You can set these variables in your environment or create a `.env` file at the root of the project.
61
+ 1. **View Task Details**:
62
+ Ask: "Show me details for task CU-123456"
47
63
 
48
- ## Usage
64
+ 2. **Search Tasks**:
65
+ Ask: "Find tasks related to login functionality"
49
66
 
50
- ### As an MCP Server
67
+ 3. **Check Your Todo List**:
68
+ Ask: "What tasks are assigned to me?"
51
69
 
52
- To use this as an MCP server with an LLM:
53
-
54
- ```bash
55
- npm start
56
- ```
57
-
58
- This will start the server using the standard input/output for communication, following the Model Context Protocol.
59
-
60
- ### Using with Claude Desktop or Similar LLM Apps
61
-
62
- You can use this MCP server with Claude Desktop or similar applications by adding it to your MCP configuration:
63
-
64
- ```json
65
- {
66
- "mcpServers": {
67
- "clickup": {
68
- "command": "npx",
69
- "args": [
70
- "-y",
71
- "@hauptsache.net/clickup-mcp"
72
- ],
73
- "env": {
74
- "CLICKUP_API_KEY": "your_api_key",
75
- "CLICKUP_TEAM_ID": "your_team_id"
76
- }
77
- }
78
- }
79
- }
80
- ```
81
-
82
- Replace `your_api_key` and `your_team_id` with your actual ClickUp credentials. This configuration allows Claude or other LLMs to directly access your ClickUp tasks.
83
-
84
- ### CLI Usage
85
-
86
- For testing or manual usage, you can use the CLI interface:
87
-
88
- ```bash
89
- # List all available tools
90
- npm run cli
91
-
92
- # Get a task by ID
93
- npm run cli getTaskById id=abc1234
94
-
95
- # Search for tasks
96
- npm run cli searchTask terms="feature|bug|enhancement"
97
-
98
- # List open tasks for the current user
99
- npm run cli listTodo
100
- ```
101
-
102
- ## Available Tools
103
-
104
- ### getTaskById
105
-
106
- Retrieves a complete ClickUp task with its description, comments, and embedded images.
107
-
108
- Parameters:
109
- - `id`: The 7-9 character ClickUp task ID (without "#" or "CU-" prefix)
110
-
111
- ### searchTask
112
-
113
- Searches for tasks by name with support for multiple search terms (using OR logic).
114
-
115
- Parameters:
116
- - `terms`: Search terms separated by '|' (e.g., 'term1|term2|term3')
117
-
118
- ### listTodo
119
-
120
- Lists all open tasks assigned to the current user.
121
-
122
- Parameters: None
123
-
124
- ## Development
125
-
126
- For development with hot reloading:
127
-
128
- ```bash
129
- npm run dev
130
- ```
131
-
132
- Format code with Prettier:
133
-
134
- ```bash
135
- npm run prettier
136
- ```
137
-
138
- ## How It Works
139
-
140
- This MCP implementation:
141
-
142
- 1. Connects to the ClickUp API using your API key
143
- 2. Provides tools for LLMs to query and process ClickUp data
144
- 3. Handles image processing in Markdown descriptions
145
- 4. Returns structured data in a format optimized for LLM consumption
70
+ The AI will retrieve the information directly from ClickUp, including all text content, comments, and images, providing you with comprehensive assistance on your tasks.
146
71
 
147
72
  ## License
148
73
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgBpE,eAAO,MAAM,MAAM,WAGjB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiBpE,eAAO,MAAM,MAAM,WAGjB,CAAC"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const clickup_text_1 = require("./clickup-text");
9
9
  const CONFIG = {
10
10
  apiKey: process.env.CLICKUP_API_KEY,
11
11
  teamId: process.env.CLICKUP_TEAM_ID,
12
+ maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
12
13
  };
13
14
  if (!CONFIG.apiKey || !CONFIG.teamId) {
14
15
  throw new Error("Missing Clickup API key or team ID");
@@ -18,45 +19,75 @@ exports.server = new mcp_js_1.McpServer({
18
19
  name: "Clickup MCP",
19
20
  version: "1.0.0",
20
21
  });
22
+ /**
23
+ * Checks if a string looks like a valid ClickUp task ID
24
+ * Valid task IDs are 7-9 characters long and contain only alphanumeric characters
25
+ */
26
+ function isTaskId(str) {
27
+ // Task IDs are 7-9 characters long and contain only alphanumeric characters
28
+ return /^[a-z0-9]{7,9}$/i.test(str);
29
+ }
21
30
  exports.server.tool("getTaskById", "Get a Clickup task with images and comments by ID", {
22
31
  id: zod_1.z
23
32
  .string()
24
33
  .min(7)
25
34
  .max(9)
26
- .describe(`The 7-9 character ID of the task to get without a prefix like "#" or "CU-"`),
35
+ .refine(val => isTaskId(val), {
36
+ message: "Task ID must be 7-9 alphanumeric characters only"
37
+ })
38
+ .describe(`The 7-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
27
39
  }, async ({ id }) => {
28
40
  const [content, comments] = await Promise.all([
29
41
  loadTaskContent(id),
30
42
  loadTaskComments(id),
31
43
  ]);
44
+ // Combine all content and limit the number of images
45
+ const allContent = [...content, ...comments];
46
+ const limitedContent = limitImages(allContent, CONFIG.maxImages);
32
47
  return {
33
- content: [...content, ...comments],
48
+ content: limitedContent,
34
49
  };
35
50
  });
51
+ /**
52
+ * Helper function to generate consistent task metadata
53
+ */
54
+ function generateTaskMetadata(task) {
55
+ const metadataLines = [
56
+ `task_id: ${task.id}`,
57
+ `name: ${task.name}`,
58
+ `status: ${task.status.status}`,
59
+ `date_created: ${new Date(+task.date_created)}`,
60
+ `date_updated: ${new Date(+task.date_updated)}`,
61
+ `creator: ${task.creator.username}`,
62
+ `list: ${task.list.name} (${task.list.id})`,
63
+ ];
64
+ // Add parent task information if it exists
65
+ if (typeof task.parent === "string") {
66
+ metadataLines.push(`parent_task_id: ${task.parent}`);
67
+ }
68
+ // Add child task information if it exists
69
+ if (task.subtasks && task.subtasks.length > 0) {
70
+ metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
71
+ }
72
+ return {
73
+ type: "text",
74
+ text: metadataLines.join("\n"),
75
+ };
76
+ }
36
77
  async function loadTaskContent(id) {
37
- const response = await fetch(`https://api.clickup.com/api/v2/task/${id}?include_markdown_description=true`, { headers: { Authorization: CONFIG.apiKey } });
78
+ const response = await fetch(`https://api.clickup.com/api/v2/task/${id}?include_markdown_description=true&include_subtasks=true`, { headers: { Authorization: CONFIG.apiKey } });
38
79
  const task = await response.json();
39
80
  const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
40
- // Create the task metadata block
41
- const taskMetadata = {
42
- type: "text",
43
- text: [
44
- `task_id: ${task.id}`,
45
- `name: ${task.name}`,
46
- `status: ${task.status.status}`,
47
- `date_created: ${new Date(+task.date_created)}`,
48
- `date_updated: ${new Date(+task.date_updated)}`,
49
- `creator: ${task.creator.username}`,
50
- `list: ${task.list.name} (${task.list.id})`,
51
- ].join("\n"),
52
- };
81
+ // Create the task metadata block using the helper function
82
+ const taskMetadata = generateTaskMetadata(task);
53
83
  return [taskMetadata, ...content];
54
84
  }
55
85
  async function loadTaskComments(id) {
56
86
  const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment`, { headers: { Authorization: CONFIG.apiKey } });
57
87
  const comments = await response.json();
58
88
  return Promise.all(comments.comments
59
- .sort((a, b) => +a.date - +b.date)
89
+ // Sort comments by date, newest first to prioritize recent images
90
+ .sort((a, b) => +b.date - +a.date)
60
91
  .map(async (comment) => {
61
92
  // Create a header for the comment
62
93
  const commentHeader = {
@@ -80,6 +111,41 @@ async function loadTaskComments(id) {
80
111
  }
81
112
  }));
82
113
  }
114
+ /**
115
+ * Limits the number of images in the content array, replacing excess images with text placeholders
116
+ * Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
117
+ *
118
+ * @param content Array of content blocks that may contain images
119
+ * @param maxImages Maximum number of images to keep
120
+ * @returns Modified content array with limited images
121
+ */
122
+ function limitImages(content, maxImages) {
123
+ // Count how many images we have
124
+ const imageIndices = [];
125
+ // Find all image blocks
126
+ content.forEach((block, index) => {
127
+ if (block.type === "image") {
128
+ imageIndices.push(index);
129
+ }
130
+ });
131
+ // If we have fewer images than the limit, return the original content
132
+ if (imageIndices.length <= maxImages) {
133
+ return content;
134
+ }
135
+ // Determine which images to keep (the most recent ones)
136
+ // We want to keep the last 'maxImages' images
137
+ const imagesToRemove = imageIndices.slice(0, imageIndices.length - maxImages);
138
+ // Create a new content array with excess images replaced by text
139
+ return content.map((block, index) => {
140
+ if (block.type === "image" && imagesToRemove.includes(index)) {
141
+ return {
142
+ type: "text",
143
+ text: "[Image removed due to size limitations. Only the most recent images are shown.]",
144
+ };
145
+ }
146
+ return block;
147
+ });
148
+ }
83
149
  let cachedTasks = [];
84
150
  let lastTaskCacheUpdate = 0;
85
151
  exports.server.tool("searchTask", [
@@ -103,10 +169,41 @@ exports.server.tool("searchTask", [
103
169
  const searchTerms = terms
104
170
  .split("|")
105
171
  .map((term) => term.trim().toLowerCase());
106
- const tasks = cachedTasks.filter((task) => {
172
+ // Check if any search term looks like a task ID
173
+ const potentialTaskIds = searchTerms.filter(isTaskId);
174
+ // Fetch tasks from cache that match search terms
175
+ const tasksFromCache = cachedTasks.filter((task) => {
107
176
  const taskNameLower = task.name.toLowerCase();
108
- return searchTerms.some((term) => taskNameLower.includes(term));
177
+ const taskId = task.id.toLowerCase();
178
+ return searchTerms.some((term) => taskNameLower.includes(term) || taskId.includes(term));
179
+ });
180
+ // Fetch tasks by ID directly if they look like task IDs and they're not already in the cache
181
+ const tasksToFetch = potentialTaskIds.filter(id => {
182
+ // Check if this ID is already in the tasksFromCache
183
+ return !tasksFromCache.some(task => task.id.toLowerCase() === id.toLowerCase());
184
+ });
185
+ const taskPromises = tasksToFetch.map(async (id) => {
186
+ try {
187
+ // Fetch task directly from API
188
+ const response = await fetch(`https://api.clickup.com/api/v2/task/${id}`, { headers: { Authorization: CONFIG.apiKey } });
189
+ if (!response.ok)
190
+ return null;
191
+ const task = await response.json();
192
+ return task;
193
+ }
194
+ catch (error) {
195
+ console.error(`Error fetching task ${id}:`, error);
196
+ return null;
197
+ }
109
198
  });
199
+ // Wait for all task fetches to complete
200
+ const directlyFetchedTasks = await Promise.all(taskPromises);
201
+ // Combine tasks from cache and directly fetched tasks, removing nulls
202
+ const allTasks = [
203
+ ...tasksFromCache,
204
+ ...directlyFetchedTasks.filter(Boolean)
205
+ ];
206
+ const tasks = allTasks;
110
207
  if (tasks.length === 0) {
111
208
  return {
112
209
  content: [
@@ -118,18 +215,7 @@ exports.server.tool("searchTask", [
118
215
  };
119
216
  }
120
217
  return {
121
- content: tasks.map((task) => ({
122
- type: "text",
123
- text: [
124
- `task_id: ${task.id}`,
125
- `name: ${task.name}`,
126
- `status: ${task.status.status}`,
127
- `date_created: ${new Date(+task.date_created)}`,
128
- `date_updated: ${new Date(+task.date_updated)}`,
129
- `creator: ${task.creator.username}`,
130
- `list: ${task.list.name} (${task.list.id})`,
131
- ].join("\n"),
132
- })),
218
+ content: tasks.map((task) => generateTaskMetadata(task)),
133
219
  };
134
220
  });
135
221
  exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}, async () => {
@@ -143,8 +229,8 @@ exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}
143
229
  const tasks = taskLists.flatMap((tl) => tl.tasks);
144
230
  // filter out closed tasks
145
231
  const openTasks = tasks
146
- .filter((task) => task.status.type !== "done")
147
- .slice(0, 100);
232
+ .filter((task) => task.status.type !== "done") // done is not closed but also not a todo
233
+ .slice(0, 50);
148
234
  if (openTasks.length === 0) {
149
235
  return {
150
236
  content: [
@@ -153,18 +239,7 @@ exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}
153
239
  };
154
240
  }
155
241
  return {
156
- content: openTasks.map((task) => ({
157
- type: "text",
158
- text: [
159
- `task_id: ${task.id}`,
160
- `name: ${task.name}`,
161
- `status: ${task.status.status}`,
162
- `date_created: ${new Date(+task.date_created)}`,
163
- `date_updated: ${new Date(+task.date_updated)}`,
164
- `creator: ${task.creator.username}`,
165
- `list: ${task.list.name} (${task.list.id})`,
166
- ].join("\n"),
167
- })),
242
+ content: openTasks.map((task) => generateTaskMetadata(task)),
168
243
  };
169
244
  });
170
245
  // Only connect to the transport if this file is being run directly (not imported)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hauptsache.net/clickup-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,8 @@
15
15
  "dev": "tsc -w & nodemon dist/index.js",
16
16
  "cli": "npx ts-node src/cli.ts",
17
17
  "prettier": "prettier --write src/**/*.ts",
18
- "prepublishOnly": "npm run build"
18
+ "prepublishOnly": "rm -r dist && npm run build",
19
+ "release": "npm run build && npm publish --access public && git add . && git commit -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git tag -a v$(node -p 'require(\"./package.json\").version') -m \"Release v$(node -p 'require(\"./package.json\").version')\" && git push && git push --tags"
19
20
  },
20
21
  "keywords": [
21
22
  "clickup",