@sebastienheyd/clickup-mcp 1.6.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +184 -0
  6. package/dist/clickup-text.d.ts +83 -0
  7. package/dist/clickup-text.d.ts.map +1 -0
  8. package/dist/clickup-text.js +563 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +133 -0
  12. package/dist/resources/space-resources.d.ts +6 -0
  13. package/dist/resources/space-resources.d.ts.map +1 -0
  14. package/dist/resources/space-resources.js +95 -0
  15. package/dist/shared/config.d.ts +11 -0
  16. package/dist/shared/config.d.ts.map +1 -0
  17. package/dist/shared/config.js +61 -0
  18. package/dist/shared/data-uri.d.ts +14 -0
  19. package/dist/shared/data-uri.d.ts.map +1 -0
  20. package/dist/shared/data-uri.js +34 -0
  21. package/dist/shared/image-processing.d.ts +13 -0
  22. package/dist/shared/image-processing.d.ts.map +1 -0
  23. package/dist/shared/image-processing.js +199 -0
  24. package/dist/shared/types.d.ts +21 -0
  25. package/dist/shared/types.d.ts.map +1 -0
  26. package/dist/shared/types.js +2 -0
  27. package/dist/shared/utils.d.ts +95 -0
  28. package/dist/shared/utils.d.ts.map +1 -0
  29. package/dist/shared/utils.js +630 -0
  30. package/dist/test-utils.d.ts +23 -0
  31. package/dist/test-utils.d.ts.map +1 -0
  32. package/dist/test-utils.js +44 -0
  33. package/dist/tools/doc-tools.d.ts +4 -0
  34. package/dist/tools/doc-tools.d.ts.map +1 -0
  35. package/dist/tools/doc-tools.js +436 -0
  36. package/dist/tools/list-tools.d.ts +4 -0
  37. package/dist/tools/list-tools.d.ts.map +1 -0
  38. package/dist/tools/list-tools.js +175 -0
  39. package/dist/tools/search-tools.d.ts +3 -0
  40. package/dist/tools/search-tools.d.ts.map +1 -0
  41. package/dist/tools/search-tools.js +188 -0
  42. package/dist/tools/space-tools.d.ts +3 -0
  43. package/dist/tools/space-tools.d.ts.map +1 -0
  44. package/dist/tools/space-tools.js +144 -0
  45. package/dist/tools/task-tools.d.ts +8 -0
  46. package/dist/tools/task-tools.d.ts.map +1 -0
  47. package/dist/tools/task-tools.js +325 -0
  48. package/dist/tools/task-write-tools.d.ts +3 -0
  49. package/dist/tools/task-write-tools.d.ts.map +1 -0
  50. package/dist/tools/task-write-tools.js +575 -0
  51. package/dist/tools/time-tools.d.ts +4 -0
  52. package/dist/tools/time-tools.d.ts.map +1 -0
  53. package/dist/tools/time-tools.js +338 -0
  54. package/package.json +72 -0
@@ -0,0 +1,630 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.downloadImages = void 0;
7
+ exports.isTaskId = isTaskId;
8
+ exports.isCustomTaskId = isCustomTaskId;
9
+ exports.resolveCustomTaskId = resolveCustomTaskId;
10
+ exports.resolveTaskId = resolveTaskId;
11
+ exports.getCurrentUser = getCurrentUser;
12
+ exports.getFolderDetails = getFolderDetails;
13
+ exports.formatFolderTree = formatFolderTree;
14
+ exports.getSpaceDetails = getSpaceDetails;
15
+ exports.getTaskSearchIndex = getTaskSearchIndex;
16
+ exports.generateTaskUrl = generateTaskUrl;
17
+ exports.generateListUrl = generateListUrl;
18
+ exports.generateSpaceUrl = generateSpaceUrl;
19
+ exports.generateFolderUrl = generateFolderUrl;
20
+ exports.generateDocumentUrl = generateDocumentUrl;
21
+ exports.formatSpaceTree = formatSpaceTree;
22
+ exports.getSpaceSearchIndex = getSpaceSearchIndex;
23
+ exports.getSpaceContent = getSpaceContent;
24
+ exports.getAllTeamMembers = getAllTeamMembers;
25
+ exports.performMultiTermSearch = performMultiTermSearch;
26
+ const config_1 = require("./config");
27
+ const fuse_js_1 = __importDefault(require("fuse.js"));
28
+ const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
29
+ /**
30
+ * Checks if a string looks like a valid ClickUp task ID
31
+ * Valid task IDs are 6-9 characters long and contain only alphanumeric characters
32
+ */
33
+ function isTaskId(str) {
34
+ // Task IDs are 6-9 characters long and contain only alphanumeric characters
35
+ return /^[a-z0-9]{6,9}$/i.test(str);
36
+ }
37
+ /**
38
+ * Checks if a string looks like a ClickUp custom task ID (e.g. "SOI-4422", "PQP-123")
39
+ */
40
+ function isCustomTaskId(str) {
41
+ return /^[A-Z]{1,10}-\d+$/i.test(str);
42
+ }
43
+ /**
44
+ * Resolves a custom task ID to an internal task ID via the ClickUp API
45
+ */
46
+ async function resolveCustomTaskId(customId) {
47
+ const response = await fetch(`https://api.clickup.com/api/v2/task/${customId}?custom_task_ids=true&team_id=${config_1.CONFIG.teamId}`, { headers: { Authorization: config_1.CONFIG.apiKey } });
48
+ if (!response.ok) {
49
+ throw new Error(`Task not found for custom ID "${customId}": ${response.status} ${response.statusText}`);
50
+ }
51
+ const task = await response.json();
52
+ if (!task || !task.id) {
53
+ throw new Error(`Task not found for custom ID "${customId}"`);
54
+ }
55
+ console.error(`Resolved custom task ID "${customId}" to internal ID "${task.id}"`);
56
+ return task.id;
57
+ }
58
+ /**
59
+ * Resolves a task ID (internal or custom) to an internal task ID.
60
+ * If the ID is already an internal ID, returns it as-is.
61
+ * If the ID is a custom ID (e.g. "SOI-4422"), resolves it via the API.
62
+ */
63
+ async function resolveTaskId(id) {
64
+ if (isTaskId(id)) {
65
+ return id;
66
+ }
67
+ if (isCustomTaskId(id)) {
68
+ return resolveCustomTaskId(id);
69
+ }
70
+ throw new Error(`Invalid task ID format: "${id}". Expected an internal ID (6-9 alphanumeric characters) or a custom ID (e.g. "SOI-4422").`);
71
+ }
72
+ // Cache for current user info to avoid repeated API calls and race conditions
73
+ let cachedUserPromise = null;
74
+ /**
75
+ * Get current authenticated user information from ClickUp API
76
+ * Caches the promise to prevent race conditions on concurrent calls
77
+ */
78
+ async function getCurrentUser() {
79
+ // Return cached promise if available
80
+ if (cachedUserPromise) {
81
+ return cachedUserPromise;
82
+ }
83
+ // Create the fetch promise
84
+ const fetchPromise = (async () => {
85
+ const userResponse = await fetch("https://api.clickup.com/api/v2/user", {
86
+ headers: { Authorization: config_1.CONFIG.apiKey },
87
+ });
88
+ if (!userResponse.ok) {
89
+ throw new Error(`Error fetching user info: ${userResponse.status} ${userResponse.statusText}`);
90
+ }
91
+ return await userResponse.json();
92
+ })();
93
+ // Cache the promise
94
+ cachedUserPromise = fetchPromise;
95
+ // Auto-cleanup after 60 seconds
96
+ setTimeout(() => {
97
+ cachedUserPromise = null;
98
+ console.error(`Auto-cleaned user data cache`);
99
+ }, GLOBAL_REFRESH_INTERVAL);
100
+ return fetchPromise;
101
+ }
102
+ // Re-export image processing functions for backward compatibility
103
+ var image_processing_1 = require("./image-processing");
104
+ Object.defineProperty(exports, "downloadImages", { enumerable: true, get: function () { return image_processing_1.downloadImages; } });
105
+ const folderCache = new Map(); // Global cache for folder details promises
106
+ /**
107
+ * Function to get folder details, using a cache to avoid redundant fetches.
108
+ * Fetches both the folder metadata and its lists (via the dedicated /list endpoint)
109
+ * to ensure lists are always available regardless of what GET /folder/{id} returns.
110
+ */
111
+ function getFolderDetails(folderId) {
112
+ if (!folderId) {
113
+ return Promise.reject(new Error('Invalid folder ID'));
114
+ }
115
+ const cached = folderCache.get(folderId);
116
+ if (cached)
117
+ return cached;
118
+ const fetchPromise = (async () => {
119
+ const [folderRes, listsRes] = await Promise.all([
120
+ fetch(`https://api.clickup.com/api/v2/folder/${folderId}`, { headers: { Authorization: config_1.CONFIG.apiKey } }),
121
+ fetch(`https://api.clickup.com/api/v2/folder/${folderId}/list`, { headers: { Authorization: config_1.CONFIG.apiKey } }),
122
+ ]);
123
+ if (!folderRes.ok)
124
+ throw new Error(`Error fetching folder ${folderId}: ${folderRes.status}`);
125
+ const folder = await folderRes.json();
126
+ // Merge lists from the dedicated endpoint if the folder payload lacks them
127
+ if (listsRes.ok) {
128
+ const listsData = await listsRes.json();
129
+ if (listsData.lists && listsData.lists.length > 0) {
130
+ folder.lists = listsData.lists;
131
+ }
132
+ }
133
+ return folder;
134
+ })().catch(error => {
135
+ console.error(`Network error fetching folder ${folderId}:`, error);
136
+ folderCache.delete(folderId);
137
+ throw new Error(`Error fetching folder ${folderId}: ${error}`);
138
+ });
139
+ folderCache.set(folderId, fetchPromise);
140
+ setTimeout(() => {
141
+ folderCache.delete(folderId);
142
+ console.error(`Auto-cleaned folder cache for ${folderId}`);
143
+ }, GLOBAL_REFRESH_INTERVAL);
144
+ return fetchPromise;
145
+ }
146
+ /**
147
+ * Format folder content as tree structure
148
+ */
149
+ function formatFolderTree(folder) {
150
+ const lines = [];
151
+ // Folder header with parent space info
152
+ const folderExtraInfo = [
153
+ ...(folder.private ? ['private'] : []),
154
+ ...(folder.archived ? ['archived'] : []),
155
+ ].join(', ');
156
+ lines.push(`📂 FOLDER: ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${generateFolderUrl(folder.id)}`);
157
+ // Parent space
158
+ if (folder.space) {
159
+ lines.push(` Parent space: ${folder.space.name || 'Unknown'} (space_id: ${folder.space.id}) ${generateSpaceUrl(folder.space.id)}`);
160
+ }
161
+ // Lists in folder
162
+ const lists = folder.lists || [];
163
+ if (lists.length > 0) {
164
+ lines.push(` ${lists.length} list(s):`);
165
+ lists.forEach((list, i) => {
166
+ const isLast = i === lists.length - 1;
167
+ const treeChar = isLast ? '└──' : '├──';
168
+ const listExtraInfo = [
169
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
170
+ ...(list.private ? ['private'] : []),
171
+ ...(list.archived ? ['archived'] : []),
172
+ ].join(', ');
173
+ lines.push(` ${treeChar} 📝 ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${generateListUrl(list.id)}`);
174
+ // Statuses for each list
175
+ if (list.statuses && list.statuses.length > 0) {
176
+ const continuation = isLast ? ' ' : '│ ';
177
+ const statusNames = list.statuses.map((s) => s.status).join(', ');
178
+ lines.push(` ${continuation} Statuses: ${statusNames}`);
179
+ }
180
+ });
181
+ }
182
+ else {
183
+ lines.push(' No lists in this folder.');
184
+ }
185
+ return lines.join('\n');
186
+ }
187
+ const spaceCache = new Map(); // Global cache for space details promises
188
+ /**
189
+ * Function to get space details, using a cache to avoid redundant fetches
190
+ */
191
+ function getSpaceDetails(spaceId) {
192
+ if (!spaceId) {
193
+ return Promise.reject(new Error('Invalid space ID'));
194
+ }
195
+ const cachedSpace = spaceCache.get(spaceId);
196
+ if (cachedSpace) {
197
+ return cachedSpace;
198
+ }
199
+ const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, { headers: { Authorization: config_1.CONFIG.apiKey } })
200
+ .then(res => {
201
+ if (!res.ok) {
202
+ throw new Error(`Error fetching space ${spaceId}: ${res.status}`);
203
+ }
204
+ return res.json();
205
+ })
206
+ .catch(error => {
207
+ console.error(`Network error fetching space ${spaceId}:`, error);
208
+ throw new Error(`Error fetching space ${spaceId}: ${error}`);
209
+ });
210
+ spaceCache.set(spaceId, fetchPromise);
211
+ return fetchPromise;
212
+ }
213
+ // Task search index management - cache promises to prevent race conditions
214
+ const taskIndices = new Map();
215
+ /**
216
+ * Get or create a task search index with specified filters
217
+ * Caches promises to prevent race conditions on concurrent calls
218
+ */
219
+ async function getTaskSearchIndex(space_ids, list_ids, assignees) {
220
+ // Create cache key from sorted filter arrays
221
+ const key = JSON.stringify({
222
+ space_ids: space_ids?.sort(),
223
+ list_ids: list_ids?.sort(),
224
+ assignees: assignees?.sort()
225
+ });
226
+ // Check for existing valid index promise
227
+ const cachedPromise = taskIndices.get(key);
228
+ if (cachedPromise) {
229
+ return cachedPromise;
230
+ }
231
+ // Create the fetch promise
232
+ const fetchPromise = (async () => {
233
+ console.error(`Refreshing task index for filters: ${key}`);
234
+ const tasks = await fetchTasks(space_ids, list_ids, assignees);
235
+ const index = createFuseIndex(tasks);
236
+ console.error(`Task index created with ${tasks.length} tasks`);
237
+ return index;
238
+ })();
239
+ // Store promise with auto-cleanup
240
+ taskIndices.set(key, fetchPromise);
241
+ setTimeout(() => {
242
+ taskIndices.delete(key);
243
+ console.error(`Auto-cleaned index for filters: ${key}`);
244
+ }, GLOBAL_REFRESH_INTERVAL);
245
+ return fetchPromise;
246
+ }
247
+ /**
248
+ * Fetch tasks using team endpoint with dynamic filters
249
+ */
250
+ async function fetchTasks(space_ids, list_ids, assignees) {
251
+ const queryParams = ['order_by=updated', 'subtasks=true'];
252
+ // Add filter parameters
253
+ if (space_ids?.length) {
254
+ space_ids.forEach(id => queryParams.push(`space_ids[]=${id}`));
255
+ }
256
+ if (list_ids?.length) {
257
+ list_ids.forEach(id => queryParams.push(`list_ids[]=${id}`));
258
+ }
259
+ if (assignees?.length) {
260
+ assignees.forEach(id => queryParams.push(`assignees[]=${id}`));
261
+ }
262
+ const queryString = queryParams.join('&');
263
+ // Fetch multiple pages in parallel
264
+ const maxPages = space_ids?.length || list_ids?.length || assignees?.length ? 10 : 30; // Fewer pages for filtered searches
265
+ const taskListsPromises = [...Array(maxPages)].map(async (_, i) => {
266
+ const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/task?${queryString}&page=${i}`;
267
+ try {
268
+ const res = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } });
269
+ return await res.json();
270
+ }
271
+ catch (e) {
272
+ console.error(`Error fetching page ${i}:`, e);
273
+ return { tasks: [] };
274
+ }
275
+ });
276
+ const taskLists = await Promise.all(taskListsPromises);
277
+ return taskLists.flatMap(taskList => taskList.tasks || []);
278
+ }
279
+ /**
280
+ * Create a Fuse index from tasks array
281
+ */
282
+ function createFuseIndex(tasks) {
283
+ return new fuse_js_1.default(tasks, {
284
+ keys: [
285
+ { name: 'name', weight: 0.7 },
286
+ { name: 'id', weight: 0.6 },
287
+ { name: 'text_content', weight: 0.5 },
288
+ { name: 'tags.name', weight: 0.4 },
289
+ { name: 'assignees.username', weight: 0.4 },
290
+ { name: 'list.name', weight: 0.3 },
291
+ { name: 'folder.name', weight: 0.2 },
292
+ { name: 'space.name', weight: 0.1 }
293
+ ],
294
+ findAllMatches: true,
295
+ includeScore: true,
296
+ minMatchCharLength: 2,
297
+ threshold: 0.4,
298
+ });
299
+ }
300
+ // ===== LINK UTILITIES =====
301
+ /**
302
+ * Generate a ClickUp task URL from a task ID
303
+ */
304
+ function generateTaskUrl(taskId) {
305
+ return `https://app.clickup.com/t/${taskId}`;
306
+ }
307
+ /**
308
+ * Generate a ClickUp list URL from a list ID
309
+ */
310
+ function generateListUrl(listId) {
311
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/li/${listId}`;
312
+ }
313
+ /**
314
+ * Generate a ClickUp space URL from a space ID
315
+ */
316
+ function generateSpaceUrl(spaceId) {
317
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/s/${spaceId}`;
318
+ }
319
+ /**
320
+ * Generate a ClickUp folder URL from a folder ID
321
+ */
322
+ function generateFolderUrl(folderId) {
323
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/f/${folderId}`;
324
+ }
325
+ /**
326
+ * Generate a ClickUp document URL from a document ID and optional page ID
327
+ */
328
+ function generateDocumentUrl(docId, pageId) {
329
+ if (pageId) {
330
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}/${pageId}`;
331
+ }
332
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}`;
333
+ }
334
+ /**
335
+ * Format space content as tree structure
336
+ * Shared function used by both searchSpaces tool and space resources
337
+ */
338
+ function formatSpaceTree(space, lists, folders, documents) {
339
+ const spaceLines = [];
340
+ const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0);
341
+ // Space header
342
+ spaceLines.push(`🏢 SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''}) ${generateSpaceUrl(space.id)}`, ` ${totalLists} lists, ${folders.length} folders, ${documents.length} documents`);
343
+ // Create a tree structure
344
+ const hasDirectLists = lists.length > 0;
345
+ const hasFolders = folders.length > 0;
346
+ const hasDocuments = documents.length > 0;
347
+ // Direct lists (not in folders)
348
+ if (hasDirectLists) {
349
+ lists.forEach((list, listIndex) => {
350
+ const isLastDirectList = listIndex === lists.length - 1;
351
+ const isLastOverall = !hasFolders && !hasDocuments && isLastDirectList;
352
+ const treeChar = isLastOverall ? '└──' : '├──';
353
+ const extraInfo = [
354
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
355
+ ...(list.private ? ['private'] : []),
356
+ ...(list.archived ? ['archived'] : [])
357
+ ].join(', ');
358
+ const listLine = `${treeChar} 📝 ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${generateListUrl(list.id)}`;
359
+ spaceLines.push(listLine);
360
+ });
361
+ }
362
+ // Folders and their lists
363
+ if (hasFolders) {
364
+ folders.forEach((folder, folderIndex) => {
365
+ const isLastFolder = folderIndex === folders.length - 1;
366
+ const isLastOverall = !hasDocuments && isLastFolder;
367
+ const folderTreeChar = isLastOverall ? '└──' : '├──';
368
+ const folderContinuation = isLastOverall ? ' ' : '│ ';
369
+ const folderExtraInfo = [
370
+ ...(folder.lists?.length ? [`${folder.lists.length} lists`] : []),
371
+ ...(folder.private ? ['private'] : []),
372
+ ...(folder.archived ? ['archived'] : [])
373
+ ].join(', ');
374
+ const folderLine = `${folderTreeChar} 📂 ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${generateFolderUrl(folder.id)}`;
375
+ spaceLines.push(folderLine);
376
+ // Lists within this folder
377
+ if (folder.lists && folder.lists.length > 0) {
378
+ folder.lists.forEach((list, listIndex) => {
379
+ const isLastListInFolder = listIndex === folder.lists.length - 1;
380
+ const listTreeChar = isLastListInFolder ? '└──' : '├──';
381
+ const listExtraInfo = [
382
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
383
+ ...(list.private ? ['private'] : []),
384
+ ...(list.archived ? ['archived'] : [])
385
+ ].join(', ');
386
+ const listLine = `${folderContinuation}${listTreeChar} 📝 ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${generateListUrl(list.id)}`;
387
+ spaceLines.push(listLine);
388
+ });
389
+ }
390
+ });
391
+ }
392
+ // Documents attached to this space
393
+ if (hasDocuments) {
394
+ documents.forEach((document, docIndex) => {
395
+ const isLastDocument = docIndex === documents.length - 1;
396
+ const docTreeChar = isLastDocument ? '└──' : '├──';
397
+ const docLine = `${docTreeChar} 📄 ${document.name} (doc_id: ${document.id}) ${generateDocumentUrl(document.id)}`;
398
+ spaceLines.push(docLine);
399
+ });
400
+ }
401
+ return spaceLines.join('\n');
402
+ }
403
+ // Space search index cache - cache promise to prevent race conditions
404
+ let spaceSearchIndexPromise = null;
405
+ /**
406
+ * Get or refresh the space search index
407
+ * Caches promise to prevent race conditions on concurrent calls
408
+ */
409
+ async function getSpaceSearchIndex() {
410
+ // Return cached promise if available
411
+ if (spaceSearchIndexPromise) {
412
+ return spaceSearchIndexPromise;
413
+ }
414
+ // Create the fetch promise
415
+ const fetchPromise = (async () => {
416
+ try {
417
+ const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/space`;
418
+ const response = await fetch(url, {
419
+ headers: { Authorization: config_1.CONFIG.apiKey },
420
+ });
421
+ if (!response.ok) {
422
+ throw new Error(`Error fetching spaces: ${response.status} ${response.statusText}`);
423
+ }
424
+ const data = await response.json();
425
+ const spacesData = data.spaces || [];
426
+ // Create Fuse search index
427
+ return new fuse_js_1.default(spacesData, {
428
+ keys: [
429
+ { name: 'name', weight: 0.7 },
430
+ { name: 'id', weight: 0.6 }
431
+ ],
432
+ includeScore: true,
433
+ threshold: 0.4,
434
+ minMatchCharLength: 1,
435
+ });
436
+ }
437
+ catch (error) {
438
+ console.error('Error creating space search index:', error);
439
+ return null;
440
+ }
441
+ })();
442
+ // Cache the promise
443
+ spaceSearchIndexPromise = fetchPromise;
444
+ // Auto-cleanup after 60 seconds
445
+ setTimeout(() => {
446
+ spaceSearchIndexPromise = null;
447
+ console.error('Auto-cleaned space search index');
448
+ }, GLOBAL_REFRESH_INTERVAL);
449
+ return fetchPromise;
450
+ }
451
+ const listCache = new Map(); // Cache for space lists/folders
452
+ /**
453
+ * Get lists, folders, and documents for a specific space with caching
454
+ */
455
+ async function getSpaceContent(spaceId) {
456
+ const cacheKey = `space-content-${spaceId}`;
457
+ // Check cache first
458
+ const cachedContent = listCache.get(cacheKey);
459
+ if (cachedContent) {
460
+ return cachedContent;
461
+ }
462
+ // Fetch content with parallel requests
463
+ const fetchPromise = (async () => {
464
+ try {
465
+ const [folders, lists, documents] = await Promise.all([
466
+ fetch(`https://api.clickup.com/api/v2/space/${spaceId}/folder`, {
467
+ headers: { Authorization: config_1.CONFIG.apiKey },
468
+ })
469
+ .then(response => response.json())
470
+ .then(json => json.folders || [])
471
+ .catch(e => {
472
+ console.error(e);
473
+ return [];
474
+ }),
475
+ fetch(`https://api.clickup.com/api/v2/space/${spaceId}/list`, {
476
+ headers: { Authorization: config_1.CONFIG.apiKey },
477
+ })
478
+ .then(response => response.json())
479
+ .then(json => json.lists || [])
480
+ .catch(e => {
481
+ console.error(e);
482
+ return [];
483
+ }),
484
+ fetch(`https://api.clickup.com/api/v3/workspaces/${config_1.CONFIG.teamId}/docs?parent_id=${spaceId}`, {
485
+ headers: { Authorization: config_1.CONFIG.apiKey },
486
+ })
487
+ .then(response => response.json())
488
+ .then(json => json.docs || [])
489
+ .catch(e => {
490
+ console.error(e);
491
+ return [];
492
+ })
493
+ ]);
494
+ // For each folder, also fetch its lists
495
+ const folderListPromises = folders.map(async (folder) => {
496
+ try {
497
+ const folderListResponse = await fetch(`https://api.clickup.com/api/v2/folder/${folder.id}/list`, { headers: { Authorization: config_1.CONFIG.apiKey } });
498
+ if (folderListResponse.ok) {
499
+ const folderListData = await folderListResponse.json();
500
+ folder.lists = folderListData.lists || [];
501
+ }
502
+ return folder;
503
+ }
504
+ catch (error) {
505
+ console.error(`Error fetching lists for folder ${folder.id}:`, error);
506
+ folder.lists = [];
507
+ return folder;
508
+ }
509
+ });
510
+ const foldersWithLists = await Promise.all(folderListPromises);
511
+ return { lists, folders: foldersWithLists, documents };
512
+ }
513
+ catch (error) {
514
+ console.error(`Error fetching space content for ${spaceId}:`, error);
515
+ return { lists: [], folders: [], documents: [] };
516
+ }
517
+ })();
518
+ // Cache the promise
519
+ listCache.set(cacheKey, fetchPromise);
520
+ // Auto-cleanup after 60 seconds
521
+ setTimeout(() => {
522
+ listCache.delete(cacheKey);
523
+ console.error(`Auto-cleaned space content cache for ${spaceId}`);
524
+ }, GLOBAL_REFRESH_INTERVAL);
525
+ return fetchPromise;
526
+ }
527
+ // Cache for team members to avoid repeated API calls and race conditions
528
+ let cachedTeamMembersPromise = null;
529
+ /**
530
+ * Gets all team members from ClickUp API with caching
531
+ */
532
+ async function getAllTeamMembers() {
533
+ // Return cached promise if available
534
+ if (cachedTeamMembersPromise) {
535
+ return cachedTeamMembersPromise;
536
+ }
537
+ // Create the fetch promise
538
+ const fetchPromise = (async () => {
539
+ try {
540
+ const response = await fetch(`https://api.clickup.com/api/v2/team`, {
541
+ headers: { Authorization: config_1.CONFIG.apiKey },
542
+ });
543
+ if (!response.ok) {
544
+ console.error(`Error fetching teams: ${response.status} ${response.statusText}`);
545
+ return [];
546
+ }
547
+ const data = await response.json();
548
+ if (!data.teams || !Array.isArray(data.teams)) {
549
+ return [];
550
+ }
551
+ // Find the team that matches our configured team ID and extract all user IDs
552
+ const currentTeam = data.teams.find((team) => team.id === config_1.CONFIG.teamId);
553
+ if (!currentTeam || !currentTeam.members || !Array.isArray(currentTeam.members)) {
554
+ console.error(`Team ${config_1.CONFIG.teamId} not found or has no members`);
555
+ return [];
556
+ }
557
+ return currentTeam.members.map((member) => member.user?.id).filter(Boolean);
558
+ }
559
+ catch (error) {
560
+ console.error('Error fetching team members:', error);
561
+ return [];
562
+ }
563
+ })();
564
+ // Cache the promise
565
+ cachedTeamMembersPromise = fetchPromise;
566
+ // Auto-cleanup after 60 seconds
567
+ setTimeout(() => {
568
+ cachedTeamMembersPromise = null;
569
+ console.error(`Auto-cleaned team members cache`);
570
+ }, GLOBAL_REFRESH_INTERVAL);
571
+ return fetchPromise;
572
+ }
573
+ /**
574
+ * Performs multi-term search with aggressive boosting for items matching multiple terms
575
+ * @param searchIndex Fuse search index to search within
576
+ * @param terms Array of search terms
577
+ * @returns Array of items sorted by relevance (multi-term matches ranked higher)
578
+ */
579
+ async function performMultiTermSearch(searchIndex, terms) {
580
+ // Filter valid search terms
581
+ const validTerms = terms.filter(term => term && term.trim().length > 0);
582
+ if (validTerms.length === 0) {
583
+ return [];
584
+ }
585
+ // Track multiple matches per item for aggressive boosting
586
+ const itemMatches = new Map();
587
+ // Collect all matches for each term
588
+ validTerms.forEach(term => {
589
+ const results = searchIndex.search(term);
590
+ results.forEach(result => {
591
+ if (result.item && typeof result.item.id === 'string') {
592
+ const itemId = result.item.id;
593
+ const currentScore = result.score ?? 1;
594
+ const existing = itemMatches.get(itemId);
595
+ if (!existing) {
596
+ itemMatches.set(itemId, {
597
+ item: result.item,
598
+ scores: [currentScore],
599
+ matchedTerms: [term]
600
+ });
601
+ }
602
+ else {
603
+ existing.scores.push(currentScore);
604
+ existing.matchedTerms.push(term);
605
+ }
606
+ }
607
+ });
608
+ });
609
+ // Calculate aggressively boosted scores for multi-term matches
610
+ const uniqueResults = new Map();
611
+ itemMatches.forEach((match, itemId) => {
612
+ const bestScore = Math.min(...match.scores);
613
+ const matchCount = match.scores.length;
614
+ const totalTerms = validTerms.length;
615
+ // Aggressive multi-term boost: exponential improvement for multiple matches
616
+ // 1 match: base score
617
+ // 2+ matches: exponentially better score based on match ratio
618
+ const matchRatio = matchCount / totalTerms;
619
+ const boostFactor = Math.pow(0.1, matchRatio * 4); // Very aggressive boost
620
+ const finalScore = bestScore * boostFactor;
621
+ uniqueResults.set(itemId, {
622
+ item: match.item,
623
+ score: finalScore
624
+ });
625
+ });
626
+ // Return sorted results (best scores first)
627
+ return Array.from(uniqueResults.values())
628
+ .sort((a, b) => a.score - b.score)
629
+ .map(entry => entry.item);
630
+ }
@@ -0,0 +1,23 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types";
3
+ /**
4
+ * A simple utility to test MCP tools directly without using the StdioServerTransport
5
+ */
6
+ export declare class ToolTester {
7
+ private server;
8
+ constructor(server: McpServer);
9
+ /**
10
+ * Test a tool by name with the provided parameters
11
+ * @param toolName The name of the tool to test
12
+ * @param params The parameters to pass to the tool
13
+ * @returns A promise that resolves to the tool's result
14
+ */
15
+ testTool(toolName: string, params: Record<string, any>): Promise<CallToolResult>;
16
+ }
17
+ /**
18
+ * Create a tool tester for the provided server
19
+ * @param server The MCP server instance
20
+ * @returns A ToolTester instance
21
+ */
22
+ export declare function createToolTester(server: McpServer): ToolTester;
23
+ //# sourceMappingURL=test-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAY;gBAEd,MAAM,EAAE,SAAS;IAI7B;;;;;OAKG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC;CAwBvF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAE9D"}