@hauptsache.net/clickup-mcp 1.0.0 → 1.0.2
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 +51 -102
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -43
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,124 +1,73 @@
|
|
|
1
|
-
# ClickUp MCP
|
|
1
|
+
# ClickUp MCP for AI Assistants
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
This MCP excels at providing AI assistants with rich access to your ClickUp tasks:
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
|
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
|
-
##
|
|
17
|
+
## Limitations
|
|
19
18
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
-
##
|
|
23
|
+
## Setup for Claude Desktop, Windsurf, or Cursor
|
|
25
24
|
|
|
26
|
-
1.
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
29
|
+
2. **Configuration**:
|
|
30
|
+
Add the following to your MCP configuration file:
|
|
32
31
|
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
npm run build
|
|
37
|
-
```
|
|
50
|
+
Replace `your_api_key` and `your_team_id` with your actual ClickUp credentials.
|
|
38
51
|
|
|
39
|
-
|
|
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
|
-
|
|
57
|
+
## Using with Your AI Assistant
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
- `CLICKUP_TEAM_ID`: Your ClickUp team ID
|
|
59
|
+
Once connected, your AI assistant can:
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
1. **View Task Details**:
|
|
62
|
+
Ask: "Show me details for task CU-123456"
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
2. **Search Tasks**:
|
|
65
|
+
Ask: "Find tasks related to login functionality"
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
3. **Check Your Todo List**:
|
|
68
|
+
Ask: "What tasks are assigned to me?"
|
|
51
69
|
|
|
52
|
-
|
|
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
|
-
### CLI Usage
|
|
61
|
-
|
|
62
|
-
For testing or manual usage, you can use the CLI interface:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
# List all available tools
|
|
66
|
-
npm run cli
|
|
67
|
-
|
|
68
|
-
# Get a task by ID
|
|
69
|
-
npm run cli getTaskById id=abc1234
|
|
70
|
-
|
|
71
|
-
# Search for tasks
|
|
72
|
-
npm run cli searchTask terms="feature|bug|enhancement"
|
|
73
|
-
|
|
74
|
-
# List open tasks for the current user
|
|
75
|
-
npm run cli listTodo
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Available Tools
|
|
79
|
-
|
|
80
|
-
### getTaskById
|
|
81
|
-
|
|
82
|
-
Retrieves a complete ClickUp task with its description, comments, and embedded images.
|
|
83
|
-
|
|
84
|
-
Parameters:
|
|
85
|
-
- `id`: The 7-9 character ClickUp task ID (without "#" or "CU-" prefix)
|
|
86
|
-
|
|
87
|
-
### searchTask
|
|
88
|
-
|
|
89
|
-
Searches for tasks by name with support for multiple search terms (using OR logic).
|
|
90
|
-
|
|
91
|
-
Parameters:
|
|
92
|
-
- `terms`: Search terms separated by '|' (e.g., 'term1|term2|term3')
|
|
93
|
-
|
|
94
|
-
### listTodo
|
|
95
|
-
|
|
96
|
-
Lists all open tasks assigned to the current user.
|
|
97
|
-
|
|
98
|
-
Parameters: None
|
|
99
|
-
|
|
100
|
-
## Development
|
|
101
|
-
|
|
102
|
-
For development with hot reloading:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npm run dev
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Format code with Prettier:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
npm run prettier
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
## How It Works
|
|
115
|
-
|
|
116
|
-
This MCP implementation:
|
|
117
|
-
|
|
118
|
-
1. Connects to the ClickUp API using your API key
|
|
119
|
-
2. Provides tools for LLMs to query and process ClickUp data
|
|
120
|
-
3. Handles image processing in Markdown descriptions
|
|
121
|
-
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.
|
|
122
71
|
|
|
123
72
|
## License
|
|
124
73
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.server = void 0;
|
|
@@ -8,6 +9,7 @@ const clickup_text_1 = require("./clickup-text");
|
|
|
8
9
|
const CONFIG = {
|
|
9
10
|
apiKey: process.env.CLICKUP_API_KEY,
|
|
10
11
|
teamId: process.env.CLICKUP_TEAM_ID,
|
|
12
|
+
maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
|
|
11
13
|
};
|
|
12
14
|
if (!CONFIG.apiKey || !CONFIG.teamId) {
|
|
13
15
|
throw new Error("Missing Clickup API key or team ID");
|
|
@@ -22,40 +24,59 @@ exports.server.tool("getTaskById", "Get a Clickup task with images and comments
|
|
|
22
24
|
.string()
|
|
23
25
|
.min(7)
|
|
24
26
|
.max(9)
|
|
25
|
-
.describe(`The 7-9 character ID of the task to get without a prefix like "#"
|
|
27
|
+
.describe(`The 7-9 character ID of the task to get without a prefix like "#", "CU-" or "https://app.clickup.com/t/"`),
|
|
26
28
|
}, async ({ id }) => {
|
|
27
29
|
const [content, comments] = await Promise.all([
|
|
28
30
|
loadTaskContent(id),
|
|
29
31
|
loadTaskComments(id),
|
|
30
32
|
]);
|
|
33
|
+
// Combine all content and limit the number of images
|
|
34
|
+
const allContent = [...content, ...comments];
|
|
35
|
+
const limitedContent = limitImages(allContent, CONFIG.maxImages);
|
|
31
36
|
return {
|
|
32
|
-
content:
|
|
37
|
+
content: limitedContent,
|
|
33
38
|
};
|
|
34
39
|
});
|
|
40
|
+
/**
|
|
41
|
+
* Helper function to generate consistent task metadata
|
|
42
|
+
*/
|
|
43
|
+
function generateTaskMetadata(task) {
|
|
44
|
+
const metadataLines = [
|
|
45
|
+
`task_id: ${task.id}`,
|
|
46
|
+
`name: ${task.name}`,
|
|
47
|
+
`status: ${task.status.status}`,
|
|
48
|
+
`date_created: ${new Date(+task.date_created)}`,
|
|
49
|
+
`date_updated: ${new Date(+task.date_updated)}`,
|
|
50
|
+
`creator: ${task.creator.username}`,
|
|
51
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
52
|
+
];
|
|
53
|
+
// Add parent task information if it exists
|
|
54
|
+
if (typeof task.parent === "string") {
|
|
55
|
+
metadataLines.push(`parent_task_id: ${task.parent}`);
|
|
56
|
+
}
|
|
57
|
+
// Add child task information if it exists
|
|
58
|
+
if (task.subtasks && task.subtasks.length > 0) {
|
|
59
|
+
metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
type: "text",
|
|
63
|
+
text: metadataLines.join("\n"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
35
66
|
async function loadTaskContent(id) {
|
|
36
|
-
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}?include_markdown_description=true`, { headers: { Authorization: CONFIG.apiKey } });
|
|
67
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}?include_markdown_description=true&include_subtasks=true`, { headers: { Authorization: CONFIG.apiKey } });
|
|
37
68
|
const task = await response.json();
|
|
38
69
|
const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
|
|
39
|
-
// Create the task metadata block
|
|
40
|
-
const taskMetadata =
|
|
41
|
-
type: "text",
|
|
42
|
-
text: [
|
|
43
|
-
`task_id: ${task.id}`,
|
|
44
|
-
`name: ${task.name}`,
|
|
45
|
-
`status: ${task.status.status}`,
|
|
46
|
-
`date_created: ${new Date(+task.date_created)}`,
|
|
47
|
-
`date_updated: ${new Date(+task.date_updated)}`,
|
|
48
|
-
`creator: ${task.creator.username}`,
|
|
49
|
-
`list: ${task.list.name} (${task.list.id})`,
|
|
50
|
-
].join("\n"),
|
|
51
|
-
};
|
|
70
|
+
// Create the task metadata block using the helper function
|
|
71
|
+
const taskMetadata = generateTaskMetadata(task);
|
|
52
72
|
return [taskMetadata, ...content];
|
|
53
73
|
}
|
|
54
74
|
async function loadTaskComments(id) {
|
|
55
75
|
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment`, { headers: { Authorization: CONFIG.apiKey } });
|
|
56
76
|
const comments = await response.json();
|
|
57
77
|
return Promise.all(comments.comments
|
|
58
|
-
|
|
78
|
+
// Sort comments by date, newest first to prioritize recent images
|
|
79
|
+
.sort((a, b) => +b.date - +a.date)
|
|
59
80
|
.map(async (comment) => {
|
|
60
81
|
// Create a header for the comment
|
|
61
82
|
const commentHeader = {
|
|
@@ -79,6 +100,41 @@ async function loadTaskComments(id) {
|
|
|
79
100
|
}
|
|
80
101
|
}));
|
|
81
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Limits the number of images in the content array, replacing excess images with text placeholders
|
|
105
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
106
|
+
*
|
|
107
|
+
* @param content Array of content blocks that may contain images
|
|
108
|
+
* @param maxImages Maximum number of images to keep
|
|
109
|
+
* @returns Modified content array with limited images
|
|
110
|
+
*/
|
|
111
|
+
function limitImages(content, maxImages) {
|
|
112
|
+
// Count how many images we have
|
|
113
|
+
const imageIndices = [];
|
|
114
|
+
// Find all image blocks
|
|
115
|
+
content.forEach((block, index) => {
|
|
116
|
+
if (block.type === "image") {
|
|
117
|
+
imageIndices.push(index);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
// If we have fewer images than the limit, return the original content
|
|
121
|
+
if (imageIndices.length <= maxImages) {
|
|
122
|
+
return content;
|
|
123
|
+
}
|
|
124
|
+
// Determine which images to keep (the most recent ones)
|
|
125
|
+
// We want to keep the last 'maxImages' images
|
|
126
|
+
const imagesToRemove = imageIndices.slice(0, imageIndices.length - maxImages);
|
|
127
|
+
// Create a new content array with excess images replaced by text
|
|
128
|
+
return content.map((block, index) => {
|
|
129
|
+
if (block.type === "image" && imagesToRemove.includes(index)) {
|
|
130
|
+
return {
|
|
131
|
+
type: "text",
|
|
132
|
+
text: "[Image removed due to size limitations. Only the most recent images are shown.]",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return block;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
82
138
|
let cachedTasks = [];
|
|
83
139
|
let lastTaskCacheUpdate = 0;
|
|
84
140
|
exports.server.tool("searchTask", [
|
|
@@ -117,18 +173,7 @@ exports.server.tool("searchTask", [
|
|
|
117
173
|
};
|
|
118
174
|
}
|
|
119
175
|
return {
|
|
120
|
-
content: tasks.map((task) => (
|
|
121
|
-
type: "text",
|
|
122
|
-
text: [
|
|
123
|
-
`task_id: ${task.id}`,
|
|
124
|
-
`name: ${task.name}`,
|
|
125
|
-
`status: ${task.status.status}`,
|
|
126
|
-
`date_created: ${new Date(+task.date_created)}`,
|
|
127
|
-
`date_updated: ${new Date(+task.date_updated)}`,
|
|
128
|
-
`creator: ${task.creator.username}`,
|
|
129
|
-
`list: ${task.list.name} (${task.list.id})`,
|
|
130
|
-
].join("\n"),
|
|
131
|
-
})),
|
|
176
|
+
content: tasks.map((task) => generateTaskMetadata(task)),
|
|
132
177
|
};
|
|
133
178
|
});
|
|
134
179
|
exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}, async () => {
|
|
@@ -142,8 +187,8 @@ exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}
|
|
|
142
187
|
const tasks = taskLists.flatMap((tl) => tl.tasks);
|
|
143
188
|
// filter out closed tasks
|
|
144
189
|
const openTasks = tasks
|
|
145
|
-
.filter((task) => task.status.type !== "done")
|
|
146
|
-
.slice(0,
|
|
190
|
+
.filter((task) => task.status.type !== "done") // done is not closed but also not a todo
|
|
191
|
+
.slice(0, 50);
|
|
147
192
|
if (openTasks.length === 0) {
|
|
148
193
|
return {
|
|
149
194
|
content: [
|
|
@@ -152,18 +197,7 @@ exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}
|
|
|
152
197
|
};
|
|
153
198
|
}
|
|
154
199
|
return {
|
|
155
|
-
content: openTasks.map((task) => (
|
|
156
|
-
type: "text",
|
|
157
|
-
text: [
|
|
158
|
-
`task_id: ${task.id}`,
|
|
159
|
-
`name: ${task.name}`,
|
|
160
|
-
`status: ${task.status.status}`,
|
|
161
|
-
`date_created: ${new Date(+task.date_created)}`,
|
|
162
|
-
`date_updated: ${new Date(+task.date_updated)}`,
|
|
163
|
-
`creator: ${task.creator.username}`,
|
|
164
|
-
`list: ${task.list.name} (${task.list.id})`,
|
|
165
|
-
].join("\n"),
|
|
166
|
-
})),
|
|
200
|
+
content: openTasks.map((task) => generateTaskMetadata(task)),
|
|
167
201
|
};
|
|
168
202
|
});
|
|
169
203
|
// Only connect to the transport if this file is being run directly (not imported)
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hauptsache.net/clickup-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
7
|
+
"bin": "dist/index.js",
|
|
7
8
|
"files": [
|
|
8
9
|
"dist",
|
|
9
10
|
"README.md"
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
"dev": "tsc -w & nodemon dist/index.js",
|
|
15
16
|
"cli": "npx ts-node src/cli.ts",
|
|
16
17
|
"prettier": "prettier --write src/**/*.ts",
|
|
17
|
-
"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"
|
|
18
20
|
},
|
|
19
21
|
"keywords": [
|
|
20
22
|
"clickup",
|