@hauptsache.net/clickup-mcp 1.2.0 → 1.3.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.
@@ -12,15 +12,12 @@ exports.generateTaskUrl = generateTaskUrl;
12
12
  exports.generateListUrl = generateListUrl;
13
13
  exports.generateSpaceUrl = generateSpaceUrl;
14
14
  exports.generateFolderUrl = generateFolderUrl;
15
- exports.formatTaskLink = formatTaskLink;
16
- exports.formatListLink = formatListLink;
17
- exports.formatSpaceLink = formatSpaceLink;
18
- exports.extractTaskIdFromUrl = extractTaskIdFromUrl;
19
- exports.isClickUpUrl = isClickUpUrl;
20
- exports.formatLinksSection = formatLinksSection;
15
+ exports.generateDocumentUrl = generateDocumentUrl;
21
16
  exports.getSpaceSearchIndex = getSpaceSearchIndex;
22
17
  exports.getSpaceContent = getSpaceContent;
23
18
  exports.getAllTeamMembers = getAllTeamMembers;
19
+ exports.getDocumentSearchIndex = getDocumentSearchIndex;
20
+ exports.performMultiTermSearch = performMultiTermSearch;
24
21
  const config_1 = require("./config");
25
22
  const fuse_js_1 = __importDefault(require("fuse.js"));
26
23
  const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
@@ -172,9 +169,10 @@ function createFuseIndex(tasks) {
172
169
  { name: 'folder.name', weight: 0.2 },
173
170
  { name: 'space.name', weight: 0.1 }
174
171
  ],
172
+ findAllMatches: true,
175
173
  includeScore: true,
176
- threshold: 0.4,
177
174
  minMatchCharLength: 2,
175
+ threshold: 0.4,
178
176
  });
179
177
  }
180
178
  // ===== LINK UTILITIES =====
@@ -188,65 +186,28 @@ function generateTaskUrl(taskId) {
188
186
  * Generate a ClickUp list URL from a list ID
189
187
  */
190
188
  function generateListUrl(listId) {
191
- return `https://app.clickup.com/v/l/${listId}`;
189
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/li/${listId}`;
192
190
  }
193
191
  /**
194
192
  * Generate a ClickUp space URL from a space ID
195
193
  */
196
194
  function generateSpaceUrl(spaceId) {
197
- return `https://app.clickup.com/v/s/${spaceId}`;
195
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/s/${spaceId}`;
198
196
  }
199
197
  /**
200
198
  * Generate a ClickUp folder URL from a folder ID
201
199
  */
202
200
  function generateFolderUrl(folderId) {
203
- return `https://app.clickup.com/v/f/${folderId}`;
204
- }
205
- /**
206
- * Format a ClickUp task link as markdown
207
- */
208
- function formatTaskLink(taskId, taskName) {
209
- const url = generateTaskUrl(taskId);
210
- const displayText = taskName ? `${taskName} (${taskId})` : taskId;
211
- return `[${displayText}](${url})`;
212
- }
213
- /**
214
- * Format a ClickUp list link as markdown
215
- */
216
- function formatListLink(listId, listName) {
217
- const url = generateListUrl(listId);
218
- const displayText = listName ? `${listName} (${listId})` : listId;
219
- return `[${displayText}](${url})`;
220
- }
221
- /**
222
- * Format a ClickUp space link as markdown
223
- */
224
- function formatSpaceLink(spaceId, spaceName) {
225
- const url = generateSpaceUrl(spaceId);
226
- const displayText = spaceName ? `${spaceName} (${spaceId})` : spaceId;
227
- return `[${displayText}](${url})`;
228
- }
229
- /**
230
- * Extract task ID from a ClickUp URL
231
- */
232
- function extractTaskIdFromUrl(url) {
233
- const match = url.match(/https?:\/\/app\.clickup\.com\/t\/([a-z0-9]{6,9})/i);
234
- return match ? match[1] : null;
235
- }
236
- /**
237
- * Validate if a string is a valid ClickUp URL
238
- */
239
- function isClickUpUrl(url) {
240
- return /^https?:\/\/app\.clickup\.com\//.test(url);
201
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/f/${folderId}`;
241
202
  }
242
203
  /**
243
- * Format a prominent link section for responses
204
+ * Generate a ClickUp document URL from a document ID and optional page ID
244
205
  */
245
- function formatLinksSection(links) {
246
- if (links.length === 0)
247
- return '';
248
- const linkLines = links.map(link => `🔗 [${link.text}](${link.url})`);
249
- return `\n\n**📌 Quick Links:**\n${linkLines.join('\n')}`;
206
+ function generateDocumentUrl(docId, pageId) {
207
+ if (pageId) {
208
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}/${pageId}`;
209
+ }
210
+ return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}`;
250
211
  }
251
212
  // Space search index cache - cache promise to prevent race conditions
252
213
  let spaceSearchIndexPromise = null;
@@ -298,7 +259,7 @@ async function getSpaceSearchIndex() {
298
259
  }
299
260
  const listCache = new Map(); // Cache for space lists/folders
300
261
  /**
301
- * Get lists and folders for a specific space with caching
262
+ * Get lists, folders, and documents for a specific space with caching
302
263
  */
303
264
  async function getSpaceContent(spaceId) {
304
265
  const cacheKey = `space-content-${spaceId}`;
@@ -310,18 +271,35 @@ async function getSpaceContent(spaceId) {
310
271
  // Fetch content with parallel requests
311
272
  const fetchPromise = (async () => {
312
273
  try {
313
- const [foldersResponse, listsResponse] = await Promise.all([
274
+ const [folders, lists, documents] = await Promise.all([
314
275
  fetch(`https://api.clickup.com/api/v2/space/${spaceId}/folder`, {
315
276
  headers: { Authorization: config_1.CONFIG.apiKey },
277
+ })
278
+ .then(response => response.json())
279
+ .then(json => json.folders || [])
280
+ .catch(e => {
281
+ console.error(e);
282
+ return [];
316
283
  }),
317
284
  fetch(`https://api.clickup.com/api/v2/space/${spaceId}/list`, {
318
285
  headers: { Authorization: config_1.CONFIG.apiKey },
319
286
  })
287
+ .then(response => response.json())
288
+ .then(json => json.lists || [])
289
+ .catch(e => {
290
+ console.error(e);
291
+ return [];
292
+ }),
293
+ fetch(`https://api.clickup.com/api/v3/workspaces/${config_1.CONFIG.teamId}/docs?parent_id=${spaceId}`, {
294
+ headers: { Authorization: config_1.CONFIG.apiKey },
295
+ })
296
+ .then(response => response.json())
297
+ .then(json => json.docs || [])
298
+ .catch(e => {
299
+ console.error(e);
300
+ return [];
301
+ })
320
302
  ]);
321
- const folders = foldersResponse.ok ?
322
- (await foldersResponse.json()).folders || [] : [];
323
- const lists = listsResponse.ok ?
324
- (await listsResponse.json()).lists || [] : [];
325
303
  // For each folder, also fetch its lists
326
304
  const folderListPromises = folders.map(async (folder) => {
327
305
  try {
@@ -339,11 +317,11 @@ async function getSpaceContent(spaceId) {
339
317
  }
340
318
  });
341
319
  const foldersWithLists = await Promise.all(folderListPromises);
342
- return { lists, folders: foldersWithLists };
320
+ return { lists, folders: foldersWithLists, documents };
343
321
  }
344
322
  catch (error) {
345
323
  console.error(`Error fetching space content for ${spaceId}:`, error);
346
- return { lists: [], folders: [] };
324
+ return { lists: [], folders: [], documents: [] };
347
325
  }
348
326
  })();
349
327
  // Cache the promise
@@ -401,3 +379,211 @@ async function getAllTeamMembers() {
401
379
  }, GLOBAL_REFRESH_INTERVAL);
402
380
  return fetchPromise;
403
381
  }
382
+ // Document search index management - cache promises to prevent race conditions
383
+ const documentIndices = new Map();
384
+ /**
385
+ * Get or create a document search index with space name resolution
386
+ * Caches promises to prevent race conditions on concurrent calls
387
+ */
388
+ async function getDocumentSearchIndex(space_ids) {
389
+ // Create cache key from sorted filter arrays
390
+ const key = JSON.stringify({
391
+ space_ids: space_ids?.sort()
392
+ });
393
+ // Check for existing valid index promise
394
+ const cachedPromise = documentIndices.get(key);
395
+ if (cachedPromise) {
396
+ return cachedPromise;
397
+ }
398
+ // Create the fetch promise
399
+ const fetchPromise = (async () => {
400
+ console.error(`Refreshing document index for filters: ${key}`);
401
+ const documents = await fetchDocuments(space_ids);
402
+ const index = createDocumentFuseIndex(documents);
403
+ console.error(`Document index created with ${documents.length} documents`);
404
+ return index;
405
+ })();
406
+ // Store promise with auto-cleanup
407
+ documentIndices.set(key, fetchPromise);
408
+ setTimeout(() => {
409
+ documentIndices.delete(key);
410
+ console.error(`Auto-cleaned document index for filters: ${key}`);
411
+ }, GLOBAL_REFRESH_INTERVAL);
412
+ return fetchPromise;
413
+ }
414
+ /**
415
+ * Fetch documents and resolve space names
416
+ */
417
+ async function fetchDocuments(space_ids) {
418
+ try {
419
+ // Fetch spaces first
420
+ const spacesResponse = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/space`, {
421
+ headers: { Authorization: config_1.CONFIG.apiKey }
422
+ });
423
+ if (!spacesResponse.ok) {
424
+ console.error('Error fetching spaces:', spacesResponse.status);
425
+ return [];
426
+ }
427
+ const spacesData = await spacesResponse.json();
428
+ // Fetch documents with pagination
429
+ const allDocuments = [];
430
+ let nextCursor = null;
431
+ let pageCount = 0;
432
+ const maxPages = 10; // Limit to 10 pages to avoid excessive API calls
433
+ do {
434
+ const url = new URL(`https://api.clickup.com/api/v3/workspaces/${config_1.CONFIG.teamId}/docs`);
435
+ url.searchParams.set('limit', '100');
436
+ if (nextCursor) {
437
+ url.searchParams.set('next_cursor', nextCursor);
438
+ }
439
+ const documentsResponse = await fetch(url.toString(), {
440
+ headers: { Authorization: config_1.CONFIG.apiKey }
441
+ });
442
+ if (!documentsResponse.ok) {
443
+ console.error('Error fetching documents:', documentsResponse.status);
444
+ break;
445
+ }
446
+ const documentsData = await documentsResponse.json();
447
+ const pageDocs = documentsData.docs || [];
448
+ allDocuments.push(...pageDocs);
449
+ nextCursor = documentsData.next_cursor || null;
450
+ pageCount++;
451
+ console.error(`Fetched page ${pageCount} with ${pageDocs.length} documents (total: ${allDocuments.length})`);
452
+ } while (nextCursor && pageCount < maxPages);
453
+ const documents = allDocuments;
454
+ const spaces = spacesData.spaces || [];
455
+ // Create space lookup map
456
+ const spaceMap = new Map(spaces.map((space) => [space.id, space]));
457
+ // Enhance documents with parent information
458
+ const enhancedDocuments = documents.map((doc) => {
459
+ // Parent types: 4=Space, 5=Folder, 6=List, 7=Workspace
460
+ if (doc.parent?.type === 4) {
461
+ // Space parent - we can resolve the name
462
+ const space = spaceMap.get(doc.parent.id);
463
+ return {
464
+ ...doc,
465
+ space_name: space?.name || 'Unknown',
466
+ space_id: doc.parent.id,
467
+ parent_info: space?.name ? `Space: ${space.name} (${doc.parent.id})` : `Space: ${doc.parent.id}`
468
+ };
469
+ }
470
+ else if (doc.parent?.type === 6) {
471
+ // List parent - just show ID
472
+ return {
473
+ ...doc,
474
+ space_name: 'N/A',
475
+ space_id: null,
476
+ parent_info: `List: ${doc.parent.id}`
477
+ };
478
+ }
479
+ else if (doc.parent?.type === 5) {
480
+ // Folder parent - just show ID
481
+ return {
482
+ ...doc,
483
+ space_name: 'N/A',
484
+ space_id: null,
485
+ parent_info: `Folder: ${doc.parent.id}`
486
+ };
487
+ }
488
+ else if (doc.parent?.type === 7) {
489
+ // Workspace parent
490
+ return {
491
+ ...doc,
492
+ space_name: 'N/A',
493
+ space_id: null,
494
+ parent_info: `Workspace`
495
+ };
496
+ }
497
+ // Unknown parent type
498
+ return {
499
+ ...doc,
500
+ space_name: 'Unknown',
501
+ space_id: doc.parent?.id,
502
+ parent_info: doc.parent ? `Unknown (type ${doc.parent.type})` : 'Unknown'
503
+ };
504
+ });
505
+ // Filter by space_ids if provided
506
+ if (space_ids?.length) {
507
+ return enhancedDocuments.filter((doc) => space_ids.includes(doc.space_id));
508
+ }
509
+ return enhancedDocuments;
510
+ }
511
+ catch (error) {
512
+ console.error('Error fetching documents:', error);
513
+ return [];
514
+ }
515
+ }
516
+ /**
517
+ * Create a Fuse index from documents array
518
+ */
519
+ function createDocumentFuseIndex(documents) {
520
+ return new fuse_js_1.default(documents, {
521
+ keys: [
522
+ { name: 'name', weight: 0.8 },
523
+ { name: 'space_name', weight: 0.6 },
524
+ { name: 'id', weight: 0.4 }
525
+ ],
526
+ findAllMatches: true,
527
+ includeScore: true,
528
+ minMatchCharLength: 2,
529
+ threshold: 0.4,
530
+ });
531
+ }
532
+ /**
533
+ * Performs multi-term search with aggressive boosting for items matching multiple terms
534
+ * @param searchIndex Fuse search index to search within
535
+ * @param terms Array of search terms
536
+ * @returns Array of items sorted by relevance (multi-term matches ranked higher)
537
+ */
538
+ async function performMultiTermSearch(searchIndex, terms) {
539
+ // Filter valid search terms
540
+ const validTerms = terms.filter(term => term && term.trim().length > 0);
541
+ if (validTerms.length === 0) {
542
+ return [];
543
+ }
544
+ // Track multiple matches per item for aggressive boosting
545
+ const itemMatches = new Map();
546
+ // Collect all matches for each term
547
+ validTerms.forEach(term => {
548
+ const results = searchIndex.search(term);
549
+ results.forEach(result => {
550
+ if (result.item && typeof result.item.id === 'string') {
551
+ const itemId = result.item.id;
552
+ const currentScore = result.score ?? 1;
553
+ const existing = itemMatches.get(itemId);
554
+ if (!existing) {
555
+ itemMatches.set(itemId, {
556
+ item: result.item,
557
+ scores: [currentScore],
558
+ matchedTerms: [term]
559
+ });
560
+ }
561
+ else {
562
+ existing.scores.push(currentScore);
563
+ existing.matchedTerms.push(term);
564
+ }
565
+ }
566
+ });
567
+ });
568
+ // Calculate aggressively boosted scores for multi-term matches
569
+ const uniqueResults = new Map();
570
+ itemMatches.forEach((match, itemId) => {
571
+ const bestScore = Math.min(...match.scores);
572
+ const matchCount = match.scores.length;
573
+ const totalTerms = validTerms.length;
574
+ // Aggressive multi-term boost: exponential improvement for multiple matches
575
+ // 1 match: base score
576
+ // 2+ matches: exponentially better score based on match ratio
577
+ const matchRatio = matchCount / totalTerms;
578
+ const boostFactor = Math.pow(0.1, matchRatio * 4); // Very aggressive boost
579
+ const finalScore = bestScore * boostFactor;
580
+ uniqueResults.set(itemId, {
581
+ item: match.item,
582
+ score: finalScore
583
+ });
584
+ });
585
+ // Return sorted results (best scores first)
586
+ return Array.from(uniqueResults.values())
587
+ .sort((a, b) => a.score - b.score)
588
+ .map(entry => entry.item);
589
+ }
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDocumentToolsRead(server: McpServer): void;
3
+ export declare function registerDocumentToolsWrite(server: McpServer): void;
4
+ //# sourceMappingURL=doc-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc-tools.d.ts","sourceRoot":"","sources":["../../src/tools/doc-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoDpE,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS,QAiR1D;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,QA8O3D"}