@hauptsache.net/clickup-mcp 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -31
- package/dist/cli.js +6 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +51 -408
- package/dist/shared/config.d.ts +10 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +31 -0
- package/dist/shared/types.d.ts +13 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +30 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +185 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +278 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +182 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +61 -0
- package/dist/tools/task-tools.d.ts +9 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +610 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +251 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,421 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
-
};
|
|
6
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.server = void 0;
|
|
4
|
+
exports.serverPromise = exports.server = void 0;
|
|
8
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
6
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// and convert to lowercase.
|
|
19
|
-
const langPart = rawPrimaryLang.match(/^[a-zA-Z]{2,3}/);
|
|
20
|
-
if (langPart) {
|
|
21
|
-
detectedLanguageHint = langPart[0].toLowerCase();
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const CONFIG = {
|
|
25
|
-
apiKey: process.env.CLICKUP_API_KEY,
|
|
26
|
-
teamId: process.env.CLICKUP_TEAM_ID,
|
|
27
|
-
maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
|
|
28
|
-
primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
|
|
29
|
-
};
|
|
30
|
-
if (!CONFIG.apiKey || !CONFIG.teamId) {
|
|
31
|
-
throw new Error("Missing Clickup API key or team ID");
|
|
32
|
-
}
|
|
7
|
+
const config_1 = require("./shared/config");
|
|
8
|
+
const utils_1 = require("./shared/utils");
|
|
9
|
+
// Import tool registration functions
|
|
10
|
+
const task_tools_1 = require("./tools/task-tools");
|
|
11
|
+
const search_tools_1 = require("./tools/search-tools");
|
|
12
|
+
const space_tools_1 = require("./tools/space-tools");
|
|
13
|
+
const list_tools_1 = require("./tools/list-tools");
|
|
14
|
+
const time_tools_1 = require("./tools/time-tools");
|
|
33
15
|
// Create an MCP server
|
|
34
|
-
|
|
16
|
+
const server = new mcp_js_1.McpServer({
|
|
35
17
|
name: "Clickup MCP",
|
|
36
18
|
version: "1.0.0",
|
|
37
19
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let processedEventBlocks = [];
|
|
72
|
-
for (const event of allDatedEvents) {
|
|
73
|
-
processedEventBlocks.push(...event.contentBlocks);
|
|
74
|
-
}
|
|
75
|
-
// 5. Combine task details with processed event blocks
|
|
76
|
-
const allContentBlocks = [...taskDetailContentBlocks, ...processedEventBlocks];
|
|
77
|
-
// 6. Limit images
|
|
78
|
-
const limitedContent = limitImages(allContentBlocks, CONFIG.maxImages);
|
|
79
|
-
return {
|
|
80
|
-
content: limitedContent,
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
async function loadTaskContent(taskId) {
|
|
84
|
-
const response = await fetch(`https://api.clickup.com/api/v2/task/${taskId}?include_markdown_description=true&include_subtasks=true`, { headers: { Authorization: CONFIG.apiKey } });
|
|
85
|
-
const task = await response.json();
|
|
86
|
-
const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
|
|
87
|
-
// Create the task metadata block using the helper function
|
|
88
|
-
const taskMetadata = await generateTaskMetadata(task);
|
|
89
|
-
return [taskMetadata, ...content];
|
|
90
|
-
}
|
|
91
|
-
async function loadTaskComments(id) {
|
|
92
|
-
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment?start_date=0`, // Ensure all comments are fetched
|
|
93
|
-
{ headers: { Authorization: CONFIG.apiKey } });
|
|
94
|
-
if (!response.ok) {
|
|
95
|
-
console.error(`Error fetching comments for task ${id}: ${response.status} ${response.statusText}`);
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
const commentsData = await response.json();
|
|
99
|
-
if (!commentsData.comments || !Array.isArray(commentsData.comments)) {
|
|
100
|
-
console.error(`Unexpected comment data structure for task ${id}`);
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
const commentEvents = await Promise.all(commentsData.comments.map(async (comment) => {
|
|
104
|
-
const headerBlock = {
|
|
105
|
-
type: "text",
|
|
106
|
-
text: `Comment by ${comment.user.username} on ${new Date(+comment.date)}:`,
|
|
107
|
-
};
|
|
108
|
-
const commentBodyBlocks = await (0, clickup_text_1.processClickUpText)(comment.comment);
|
|
109
|
-
return {
|
|
110
|
-
date: comment.date, // String timestamp from ClickUp for sorting
|
|
111
|
-
contentBlocks: [headerBlock, ...commentBodyBlocks],
|
|
112
|
-
};
|
|
113
|
-
}));
|
|
114
|
-
return commentEvents;
|
|
115
|
-
}
|
|
116
|
-
async function loadTimeInStatusHistory(taskId) {
|
|
117
|
-
const url = `https://api.clickup.com/api/v2/task/${taskId}/time_in_status`;
|
|
118
|
-
try {
|
|
119
|
-
const response = await fetch(url, { headers: { Authorization: CONFIG.apiKey } });
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
console.error(`Error fetching time in status for task ${taskId}: ${response.status} ${response.statusText}`);
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
// Using 'any' for less strict typing as per user preference, but keeping structure for clarity
|
|
125
|
-
const data = await response.json();
|
|
126
|
-
const events = [];
|
|
127
|
-
const processStatusEntry = (entry) => {
|
|
128
|
-
if (!entry || !entry.total_time || !entry.total_time.since || !entry.status)
|
|
129
|
-
return null;
|
|
130
|
-
return {
|
|
131
|
-
date: entry.total_time.since,
|
|
132
|
-
contentBlocks: [{
|
|
133
|
-
type: "text",
|
|
134
|
-
text: `Status set to '${entry.status}' on ${new Date(+entry.total_time.since)}`,
|
|
135
|
-
}],
|
|
136
|
-
};
|
|
137
|
-
};
|
|
138
|
-
if (data.status_history && Array.isArray(data.status_history)) {
|
|
139
|
-
data.status_history.forEach((historyEntry) => {
|
|
140
|
-
const event = processStatusEntry(historyEntry);
|
|
141
|
-
if (event)
|
|
142
|
-
events.push(event);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (data.current_status) {
|
|
146
|
-
const event = processStatusEntry(data.current_status);
|
|
147
|
-
// Ensure current_status is only added if it's distinct or more recent than the last history item.
|
|
148
|
-
// The deduplication logic below handles if it's the same as the last history entry.
|
|
149
|
-
if (event)
|
|
150
|
-
events.push(event);
|
|
151
|
-
}
|
|
152
|
-
// Deduplicate events based on date and status name to avoid adding current_status if it's identical to the last history entry
|
|
153
|
-
const uniqueEvents = Array.from(new Map(events.map(event => [`${event.date}-${event.contentBlocks[0]?.text}`, event] // Keying by date and text content of first block
|
|
154
|
-
)).values());
|
|
155
|
-
return uniqueEvents;
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
console.error(`Exception fetching time in status for task ${taskId}:`, error);
|
|
159
|
-
return [];
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Helper function to generate consistent task metadata
|
|
164
|
-
*/
|
|
165
|
-
async function generateTaskMetadata(task) {
|
|
166
|
-
let spaceName = task.space?.name || 'Unknown Space';
|
|
167
|
-
let spaceIdForDisplay = task.space?.id || 'N/A';
|
|
168
|
-
if (spaceName === 'Unknown Space' && task.space?.id) {
|
|
169
|
-
const spaceDetails = await getSpaceDetails(task.space.id);
|
|
170
|
-
if (spaceDetails && spaceDetails.name) {
|
|
171
|
-
spaceName = spaceDetails.name;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const metadataLines = [
|
|
175
|
-
`task_id: ${task.id}`,
|
|
176
|
-
`name: ${task.name}`,
|
|
177
|
-
`status: ${task.status.status}`,
|
|
178
|
-
`date_created: ${new Date(+task.date_created)}`,
|
|
179
|
-
`date_updated: ${new Date(+task.date_updated)}`,
|
|
180
|
-
`creator: ${task.creator.username}`,
|
|
181
|
-
`assignee: ${task.assignees.map((a) => a.username).join(', ')}`,
|
|
182
|
-
`list: ${task.list.name} (${task.list.id})`,
|
|
183
|
-
`space: ${spaceName} (${spaceIdForDisplay})`,
|
|
184
|
-
];
|
|
185
|
-
// Add parent task information if it exists
|
|
186
|
-
if (typeof task.parent === "string") {
|
|
187
|
-
metadataLines.push(`parent_task_id: ${task.parent}`);
|
|
188
|
-
}
|
|
189
|
-
// Add child task information if it exists
|
|
190
|
-
if (task.subtasks && task.subtasks.length > 0) {
|
|
191
|
-
metadataLines.push(`child_task_ids: ${task.subtasks.map((st) => st.id).join(', ')}`);
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
type: "text",
|
|
195
|
-
text: metadataLines.join("\n"),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
const spaceCache = new Map(); // Global cache for space details promises
|
|
199
|
-
/**
|
|
200
|
-
* Function to get space details, using a cache to avoid redundant fetches
|
|
201
|
-
*/
|
|
202
|
-
async function getSpaceDetails(spaceId) {
|
|
203
|
-
if (!spaceId) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
if (!spaceCache.has(spaceId)) {
|
|
207
|
-
const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, {
|
|
208
|
-
headers: { Authorization: CONFIG.apiKey },
|
|
209
|
-
})
|
|
210
|
-
.then(res => {
|
|
211
|
-
if (!res.ok) {
|
|
212
|
-
// Don't cache failed requests, or handle errors more gracefully
|
|
213
|
-
console.error(`Error fetching space ${spaceId}: ${res.status}`);
|
|
214
|
-
spaceCache.delete(spaceId); // Allow retry on next call
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
return res.json();
|
|
218
|
-
})
|
|
219
|
-
.catch(error => {
|
|
220
|
-
console.error(`Network error fetching space ${spaceId}:`, error);
|
|
221
|
-
spaceCache.delete(spaceId); // Allow retry on next call
|
|
222
|
-
return null;
|
|
223
|
-
});
|
|
224
|
-
spaceCache.set(spaceId, fetchPromise);
|
|
225
|
-
}
|
|
226
|
-
return spaceCache.get(spaceId);
|
|
20
|
+
exports.server = server;
|
|
21
|
+
// Register tools based on mode with user data for enhanced documentation
|
|
22
|
+
async function initializeServer() {
|
|
23
|
+
console.error(`Starting ClickUp MCP in ${config_1.CONFIG.mode} mode`);
|
|
24
|
+
// Fetch current user for enhanced tool documentation and API health check
|
|
25
|
+
const userData = await (0, utils_1.getCurrentUser)();
|
|
26
|
+
console.error(`Connected as: ${userData.user.username} (${userData.user.email})`);
|
|
27
|
+
if (config_1.CONFIG.mode === 'read-minimal') {
|
|
28
|
+
// Core task context tools for AI coding assistance
|
|
29
|
+
// Only getTaskById and searchTasks
|
|
30
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
31
|
+
(0, search_tools_1.registerSearchTools)(server);
|
|
32
|
+
}
|
|
33
|
+
else if (config_1.CONFIG.mode === 'read') {
|
|
34
|
+
// All read-only tools
|
|
35
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
36
|
+
(0, search_tools_1.registerSearchTools)(server);
|
|
37
|
+
(0, space_tools_1.registerSpaceTools)(server);
|
|
38
|
+
(0, list_tools_1.registerListToolsRead)(server);
|
|
39
|
+
(0, time_tools_1.registerTimeToolsRead)(server);
|
|
40
|
+
}
|
|
41
|
+
else if (config_1.CONFIG.mode === 'write') {
|
|
42
|
+
// All tools (full functionality)
|
|
43
|
+
(0, task_tools_1.registerTaskToolsRead)(server, userData);
|
|
44
|
+
(0, task_tools_1.registerTaskToolsWrite)(server, userData);
|
|
45
|
+
(0, search_tools_1.registerSearchTools)(server);
|
|
46
|
+
(0, space_tools_1.registerSpaceTools)(server);
|
|
47
|
+
(0, list_tools_1.registerListToolsRead)(server);
|
|
48
|
+
(0, list_tools_1.registerListToolsWrite)(server);
|
|
49
|
+
(0, time_tools_1.registerTimeToolsRead)(server);
|
|
50
|
+
(0, time_tools_1.registerTimeToolsWrite)(server);
|
|
51
|
+
}
|
|
52
|
+
return server;
|
|
227
53
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*
|
|
232
|
-
* @param content Array of content blocks that may contain images
|
|
233
|
-
* @param maxImages Maximum number of images to keep
|
|
234
|
-
* @returns Modified content array with limited images
|
|
235
|
-
*/
|
|
236
|
-
function limitImages(content, maxImages) {
|
|
237
|
-
// Count how many images we have
|
|
238
|
-
const imageIndices = [];
|
|
239
|
-
// Find all image blocks
|
|
240
|
-
content.forEach((block, index) => {
|
|
241
|
-
if (block.type === "image") {
|
|
242
|
-
imageIndices.push(index);
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
// If we have fewer images than the limit, return the original content
|
|
246
|
-
if (imageIndices.length <= maxImages) {
|
|
247
|
-
return content;
|
|
248
|
-
}
|
|
249
|
-
// Determine which images to keep (the most recent ones)
|
|
250
|
-
// We want to keep the last 'maxImages' images
|
|
251
|
-
const imagesToRemove = imageIndices.slice(0, imageIndices.length - maxImages);
|
|
252
|
-
// Create a new content array with excess images replaced by text
|
|
253
|
-
return content.map((block, index) => {
|
|
254
|
-
if (block.type === "image" && imagesToRemove.includes(index)) {
|
|
255
|
-
return {
|
|
256
|
-
type: "text",
|
|
257
|
-
text: "[Image removed due to size limitations. Only the most recent images are shown.]",
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
return block;
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
let taskSearchIndex = null;
|
|
264
|
-
let lastIndexUpdateTime = 0;
|
|
265
|
-
const INDEX_REFRESH_INTERVAL = 60000; // 60 seconds
|
|
266
|
-
const MAX_SEARCH_RESULTS = 50;
|
|
267
|
-
// Dynamically construct the searchTask description
|
|
268
|
-
const searchTaskDescriptionBase = [
|
|
269
|
-
"Searches tasks by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
|
|
270
|
-
// Placeholder for language-specific guidance
|
|
271
|
-
"You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
|
|
272
|
-
"Always use getTaskById to get more specific information if a task is relevant.",
|
|
273
|
-
];
|
|
274
|
-
if (CONFIG.primaryLanguageHint && CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
275
|
-
searchTaskDescriptionBase.splice(1, 0, `For optimal results, as your ClickUp tasks may be primarily in '${CONFIG.primaryLanguageHint}', consider providing search terms in English and '${CONFIG.primaryLanguageHint}'.`);
|
|
276
|
-
}
|
|
277
|
-
exports.server.tool("searchTask", searchTaskDescriptionBase.join("\n"), {
|
|
278
|
-
terms: zod_1.z
|
|
279
|
-
.string()
|
|
280
|
-
.min(3)
|
|
281
|
-
.describe("Search terms separated by '|' for OR logic (e.g., 'term1|term2|term3') or a direct task ID"),
|
|
282
|
-
}, async ({ terms }) => {
|
|
283
|
-
const now = Date.now();
|
|
284
|
-
if (!taskSearchIndex || (now - lastIndexUpdateTime > INDEX_REFRESH_INTERVAL)) {
|
|
285
|
-
console.error('Refreshing ClickUp task index...');
|
|
286
|
-
const taskListsPromises = [...Array(30)].map((_, i) => {
|
|
287
|
-
return fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}&subtasks=true`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json()).catch(e => {
|
|
288
|
-
console.error(`Error fetching page ${i} for index:`, e);
|
|
289
|
-
return { tasks: [] };
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
const taskLists = await Promise.all(taskListsPromises);
|
|
293
|
-
const allFetchedTasks = taskLists.flatMap(taskList => taskList.tasks);
|
|
294
|
-
if (allFetchedTasks.length > 0) {
|
|
295
|
-
taskSearchIndex = new fuse_js_1.default(allFetchedTasks, {
|
|
296
|
-
keys: [
|
|
297
|
-
{ name: 'name', weight: 0.7 },
|
|
298
|
-
{ name: 'id', weight: 0.6 },
|
|
299
|
-
{ name: 'text_content', weight: 0.5 }, // Task description/content
|
|
300
|
-
{ name: 'tags.name', weight: 0.4 }, // Task Tags
|
|
301
|
-
{ name: 'assignees.username', weight: 0.4 }, // Task Assignees
|
|
302
|
-
{ name: 'list.name', weight: 0.3 }, // Name of the List the task is in
|
|
303
|
-
{ name: 'folder.name', weight: 0.2 }, // Name of the Folder the task is in
|
|
304
|
-
{ name: 'space.name', weight: 0.1 } // Name of the Space the task is in
|
|
305
|
-
],
|
|
306
|
-
includeScore: true,
|
|
307
|
-
threshold: 0.4,
|
|
308
|
-
minMatchCharLength: 2,
|
|
309
|
-
});
|
|
310
|
-
lastIndexUpdateTime = now;
|
|
311
|
-
console.error(`Task index refreshed with ${allFetchedTasks.length} tasks.`);
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
console.error('No tasks fetched to build search index.');
|
|
315
|
-
// Potentially keep the old index if fetching failed, or clear it
|
|
316
|
-
// For now, if fetching fails to get any tasks, the index won't update.
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
const searchTermsArray = terms
|
|
320
|
-
.split("|")
|
|
321
|
-
.map((term) => term.trim())
|
|
322
|
-
.filter(term => term.length > 0);
|
|
323
|
-
if (searchTermsArray.length === 0) {
|
|
324
|
-
return { content: [{ type: "text", text: "No search terms provided." }] };
|
|
325
|
-
}
|
|
326
|
-
const uniqueResults = new Map();
|
|
327
|
-
if (taskSearchIndex) {
|
|
328
|
-
searchTermsArray.forEach(term => {
|
|
329
|
-
const results = taskSearchIndex.search(term.toLowerCase());
|
|
330
|
-
results.forEach(result => {
|
|
331
|
-
if (result.item && typeof result.item.id === 'string') {
|
|
332
|
-
const currentScore = result.score ?? 1; // Default to 1 if undefined
|
|
333
|
-
const existing = uniqueResults.get(result.item.id);
|
|
334
|
-
if (!existing || currentScore < existing.score) {
|
|
335
|
-
uniqueResults.set(result.item.id, { item: result.item, score: currentScore });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
// Task ID Fallback Logic
|
|
342
|
-
const potentialTaskIds = searchTermsArray.filter(isTaskId);
|
|
343
|
-
const foundTaskIdsByFuse = new Set(Array.from(uniqueResults.keys()).map(id => id.toLowerCase())); // Store lowercase for comparison
|
|
344
|
-
// Filter task IDs that were not found by Fuse, comparing case-insensitively
|
|
345
|
-
const taskIdsToFetchDirectly = potentialTaskIds.filter(id => {
|
|
346
|
-
const lowerId = id.toLowerCase();
|
|
347
|
-
return !foundTaskIdsByFuse.has(lowerId);
|
|
348
|
-
});
|
|
349
|
-
if (taskIdsToFetchDirectly.length > 0) {
|
|
350
|
-
console.error(`Attempting direct fetch for task IDs: ${taskIdsToFetchDirectly.join(', ')}`);
|
|
351
|
-
const directFetchPromises = taskIdsToFetchDirectly.map(async (id) => {
|
|
352
|
-
try {
|
|
353
|
-
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}`, { headers: { Authorization: CONFIG.apiKey } });
|
|
354
|
-
if (response.ok) {
|
|
355
|
-
const task = await response.json();
|
|
356
|
-
if (task && typeof task.id === 'string') {
|
|
357
|
-
// Add/update with a perfect score if fetched directly, unless a better Fuse score already exists
|
|
358
|
-
const existing = uniqueResults.get(task.id);
|
|
359
|
-
if (!existing || 0 < existing.score) { // 0 is a perfect score
|
|
360
|
-
uniqueResults.set(task.id, { item: task, score: 0 });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return task;
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
console.error(`Error directly fetching task ${id}:`, error);
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
await Promise.all(directFetchPromises);
|
|
373
|
-
}
|
|
374
|
-
const sortedResults = Array.from(uniqueResults.values())
|
|
375
|
-
.sort((a, b) => a.score - b.score) // Sort by score, ascending (lower is better)
|
|
376
|
-
.map(entry => entry.item)
|
|
377
|
-
.slice(0, MAX_SEARCH_RESULTS); // Limit the number of results
|
|
378
|
-
if (sortedResults.length === 0) {
|
|
379
|
-
return {
|
|
380
|
-
content: [
|
|
381
|
-
{
|
|
382
|
-
type: "text",
|
|
383
|
-
text: "No tasks found matching the search terms.",
|
|
384
|
-
},
|
|
385
|
-
],
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
return {
|
|
389
|
-
content: await Promise.all(sortedResults.map((task) => generateTaskMetadata(task))),
|
|
390
|
-
};
|
|
391
|
-
});
|
|
392
|
-
exports.server.tool("listTodo", "Lists all open tasks for the current user.", async () => {
|
|
393
|
-
// fetch current user ID
|
|
394
|
-
const userResp = await fetch("https://api.clickup.com/api/v2/user", {
|
|
395
|
-
headers: { Authorization: CONFIG.apiKey },
|
|
396
|
-
}).then((res) => res.json());
|
|
397
|
-
const userId = userResp.user.id;
|
|
398
|
-
// page through team tasks assigned to this user
|
|
399
|
-
const taskLists = await Promise.all([...Array(10)].map((_, i) => fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}&assignees[]=${userId}`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json())));
|
|
400
|
-
const tasks = taskLists.flatMap((tl) => tl.tasks);
|
|
401
|
-
// filter out closed tasks
|
|
402
|
-
const openTasks = tasks
|
|
403
|
-
.filter((task) => task.status.type !== "done") // done is not closed but also not a todo
|
|
404
|
-
.slice(0, 50);
|
|
405
|
-
if (openTasks.length === 0) {
|
|
406
|
-
return {
|
|
407
|
-
content: [
|
|
408
|
-
{ type: "text", text: "No open tasks found for the current user." },
|
|
409
|
-
],
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
return {
|
|
413
|
-
content: await Promise.all(openTasks.map((task) => generateTaskMetadata(task))),
|
|
414
|
-
};
|
|
415
|
-
});
|
|
54
|
+
// Initialize server with enhanced documentation and export
|
|
55
|
+
const serverPromise = initializeServer();
|
|
56
|
+
exports.serverPromise = serverPromise;
|
|
416
57
|
// Only connect to the transport if this file is being run directly (not imported)
|
|
417
58
|
if (require.main === module) {
|
|
418
|
-
// Start receiving messages on stdin and sending messages on stdout
|
|
419
|
-
|
|
420
|
-
|
|
59
|
+
// Start receiving messages on stdin and sending messages on stdout after initialization
|
|
60
|
+
serverPromise.then(() => {
|
|
61
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
62
|
+
server.connect(transport);
|
|
63
|
+
}).catch(console.error);
|
|
421
64
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const rawPrimaryLang: string | undefined;
|
|
2
|
+
export type McpMode = 'read-minimal' | 'read' | 'write';
|
|
3
|
+
export declare const CONFIG: {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
teamId: string;
|
|
6
|
+
maxImages: number;
|
|
7
|
+
primaryLanguageHint: string | undefined;
|
|
8
|
+
mode: McpMode;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,oBAA2D,CAAC;AAavF,MAAM,MAAM,OAAO,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAUxD,eAAO,MAAM,MAAM;;;;;;CAMlB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIG = exports.rawPrimaryLang = void 0;
|
|
4
|
+
exports.rawPrimaryLang = process.env.CLICKUP_PRIMARY_LANGUAGE || process.env.LANG;
|
|
5
|
+
let detectedLanguageHint = undefined;
|
|
6
|
+
if (exports.rawPrimaryLang) {
|
|
7
|
+
// Extract the primary language part (e.g., 'en' from 'en_US.UTF-8' or 'en-GB')
|
|
8
|
+
// and convert to lowercase.
|
|
9
|
+
const langPart = exports.rawPrimaryLang.match(/^[a-zA-Z]{2,3}/);
|
|
10
|
+
if (langPart) {
|
|
11
|
+
detectedLanguageHint = langPart[0].toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const rawMode = process.env.CLICKUP_MCP_MODE?.toLowerCase();
|
|
15
|
+
let mcpMode = 'write'; // Default to write (full functionality)
|
|
16
|
+
if (rawMode === 'read-minimal' || rawMode === 'read') {
|
|
17
|
+
mcpMode = rawMode;
|
|
18
|
+
}
|
|
19
|
+
else if (rawMode && rawMode !== 'write') {
|
|
20
|
+
console.error(`Invalid CLICKUP_MCP_MODE "${rawMode}". Using default "write". Valid options: read-minimal, read, write`);
|
|
21
|
+
}
|
|
22
|
+
exports.CONFIG = {
|
|
23
|
+
apiKey: process.env.CLICKUP_API_KEY,
|
|
24
|
+
teamId: process.env.CLICKUP_TEAM_ID,
|
|
25
|
+
maxImages: process.env.MAX_IMAGES ? parseInt(process.env.MAX_IMAGES) : 4,
|
|
26
|
+
primaryLanguageHint: detectedLanguageHint, // Store the cleaned code directly
|
|
27
|
+
mode: mcpMode,
|
|
28
|
+
};
|
|
29
|
+
if (!exports.CONFIG.apiKey || !exports.CONFIG.teamId) {
|
|
30
|
+
throw new Error("Missing Clickup API key or team ID");
|
|
31
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
|
+
export type ContentBlock = CallToolResult['content'][number];
|
|
3
|
+
export interface DatedContentEvent {
|
|
4
|
+
date: string;
|
|
5
|
+
contentBlocks: ContentBlock[];
|
|
6
|
+
}
|
|
7
|
+
export interface Config {
|
|
8
|
+
apiKey: string;
|
|
9
|
+
teamId: string;
|
|
10
|
+
maxImages: number;
|
|
11
|
+
primaryLanguageHint: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC;AAG7D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B;AAGD,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ContentBlock } from "./types";
|
|
2
|
+
import Fuse from 'fuse.js';
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a string looks like a valid ClickUp task ID
|
|
5
|
+
* Valid task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
6
|
+
*/
|
|
7
|
+
export declare function isTaskId(str: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Get current authenticated user information from ClickUp API
|
|
10
|
+
* Caches the result after first successful fetch to avoid repeated API calls during the session
|
|
11
|
+
*/
|
|
12
|
+
export declare function getCurrentUser(): Promise<any>;
|
|
13
|
+
/**
|
|
14
|
+
* Limits the number of images in the content array, replacing excess images with text placeholders
|
|
15
|
+
* Prioritizes keeping the most recent images (assumes content is ordered with newest items last)
|
|
16
|
+
*
|
|
17
|
+
* @param content Array of content blocks that may contain images
|
|
18
|
+
* @param maxImages Maximum number of images to keep
|
|
19
|
+
* @returns Modified content array with limited images
|
|
20
|
+
*/
|
|
21
|
+
export declare function limitImages(content: ContentBlock[], maxImages: number): ContentBlock[];
|
|
22
|
+
/**
|
|
23
|
+
* Function to get space details, using a cache to avoid redundant fetches
|
|
24
|
+
*/
|
|
25
|
+
export declare function getSpaceDetails(spaceId: string): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* Get or create a task search index with specified filters
|
|
28
|
+
*/
|
|
29
|
+
export declare function getTaskSearchIndex(space_ids?: string[], list_ids?: string[], assignees?: string[]): Promise<Fuse<any> | null>;
|
|
30
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AAErC,OAAO,IAAI,MAAM,SAAS,CAAC;AAI3B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7C;AAKD;;;GAGG;AACH,wBAAsB,cAAc,iBAiBnC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,EAAE,CA8BtF;AAID;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B7D;AAKD;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA4B3B"}
|