UNPKG

@hauptsache.net/clickup-mcp

Version:

Transform your AI assistant into a powerful ClickUp integration for both agentic coding and productivity management. Enables seamless task context sharing, intelligent search, time tracking, and complete project management workflows.

191 lines (190 loc) • 10.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerSpaceTools = registerSpaceTools; const zod_1 = require("zod"); const utils_1 = require("../shared/utils"); function registerSpaceTools(server) { server.tool("searchSpaces", [ "Searches spaces (sometimes called projects) by name or ID with fuzzy matching.", "If 5 or fewer spaces match, automatically fetches all lists (sometimes called boards) and folders within those spaces to provide a complete tree structure.", "If more than 5 spaces match, returns only space information with guidance to search more precisely.", "You can search by space name (fuzzy matching) or provide an exact space ID.", "Always reference spaces by their URLs when discussing projects or suggesting actions." ].join("\n"), { terms: zod_1.z .array(zod_1.z.string()) .optional() .describe("Array of search terms to match against space names or IDs. If not provided, returns all spaces."), archived: zod_1.z.boolean().optional().describe("Include archived spaces (default: false)") }, async ({ terms, archived = false }) => { try { const searchIndex = await (0, utils_1.getSpaceSearchIndex)(); if (!searchIndex) { return { content: [{ type: "text", text: "Error: Could not build space search index." }], }; } let matchingSpaces = []; if (!terms || terms.length === 0) { // Return all spaces if no search terms matchingSpaces = searchIndex._docs || []; } else { // Search with fuzzy matching const uniqueResults = new Map(); terms.forEach(term => { const trimmedTerm = term.trim(); if (trimmedTerm.length === 0) return; // Check if it's an exact space ID first const exactMatch = searchIndex._docs.find((space) => space.id === trimmedTerm); if (exactMatch) { uniqueResults.set(exactMatch.id, { item: exactMatch, score: 0 }); return; } // Fuzzy search const results = searchIndex.search(trimmedTerm); results.forEach(result => { if (result.item && typeof result.item.id === 'string') { const currentScore = result.score ?? 1; const existing = uniqueResults.get(result.item.id); if (!existing || currentScore < existing.score) { uniqueResults.set(result.item.id, { item: result.item, score: currentScore }); } } }); }); matchingSpaces = Array.from(uniqueResults.values()) .sort((a, b) => a.score - b.score) .map(entry => entry.item); } // Filter by archived status if (!archived) { matchingSpaces = matchingSpaces.filter((space) => !space.archived); } if (matchingSpaces.length === 0) { return { content: [{ type: "text", text: "No spaces found matching the search criteria." }], }; } // Conditionally fetch detailed content based on result count const spaceContentPromises = matchingSpaces.map(async (space) => { try { if (matchingSpaces.length <= 5) { // Detailed mode: fetch lists and folders for this space const { lists, folders } = await (0, utils_1.getSpaceContent)(space.id); return { space, lists, folders }; } else { // Summary mode: just return space without content return { space, lists: [], folders: [] }; } } catch (error) { console.error(`Error fetching content for space ${space.id}:`, error); return { space, lists: [], folders: [] }; } }); const spacesWithContent = await Promise.all(spaceContentPromises); const contentBlocks = []; spacesWithContent.forEach(({ space, lists, folders }, index) => { const spaceLines = []; const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0); // Space header spaceLines.push(`šŸ¢ SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''}) ${(0, utils_1.generateSpaceUrl)(space.id)}`, ` ${totalLists} lists, ${folders.length} folders`); // Create a tree structure const hasDirectLists = lists.length > 0; const hasFolders = folders.length > 0; // Direct lists (not in folders) if (hasDirectLists) { lists.forEach((list, listIndex) => { const isLastDirectList = listIndex === lists.length - 1; const isLastOverall = !hasFolders && isLastDirectList; const treeChar = isLastOverall ? '└──' : 'ā”œā”€ā”€'; const extraInfo = [ ...(list.task_count ? [`${list.task_count} tasks`] : []), ...(list.private ? ['private'] : []), ...(list.archived ? ['archived'] : []) ].join(', '); const listLine = `${treeChar} šŸ“ ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`; spaceLines.push(listLine); }); } // Folders and their lists if (hasFolders) { folders.forEach((folder, folderIndex) => { const isLastFolder = folderIndex === folders.length - 1; const folderTreeChar = isLastFolder ? '└──' : 'ā”œā”€ā”€'; const folderContinuation = isLastFolder ? ' ' : '│ '; const folderExtraInfo = [ ...(folder.lists?.length ? [`${folder.lists.length} lists`] : []), ...(folder.private ? ['private'] : []), ...(folder.archived ? ['archived'] : []) ].join(', '); const folderLine = `${folderTreeChar} šŸ“‚ ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${(0, utils_1.generateFolderUrl)(folder.id)}`; spaceLines.push(folderLine); // Lists within this folder if (folder.lists && folder.lists.length > 0) { folder.lists.forEach((list, listIndex) => { const isLastListInFolder = listIndex === folder.lists.length - 1; const listTreeChar = isLastListInFolder ? '└──' : 'ā”œā”€ā”€'; const listExtraInfo = [ ...(list.task_count ? [`${list.task_count} tasks`] : []), ...(list.private ? ['private'] : []), ...(list.archived ? ['archived'] : []) ].join(', '); const listLine = `${folderContinuation}${listTreeChar} šŸ“ ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`; spaceLines.push(listLine); }); } }); } // Add the complete space as a single content block contentBlocks.push({ type: "text", text: spaceLines.join('\n') }); // Add separator between spaces (except for the last one) if (index < spacesWithContent.length - 1) { contentBlocks.push({ type: "text", text: '─'.repeat(50) }); } }); const totalLists = spacesWithContent.reduce((sum, { lists, folders }) => sum + lists.length + folders.reduce((folderSum, f) => folderSum + (f.lists?.length || 0), 0), 0); // Add tip message for summary mode (when there are too many spaces) if (matchingSpaces.length > 5) { contentBlocks.push({ type: "text", text: `\nšŸ’” Tip: Use more specific search terms to get detailed list information (≤5 spaces will show complete structure)` }); } return { content: [ { type: "text", text: matchingSpaces.length <= 5 ? `Found ${matchingSpaces.length} space(s) with complete tree structure (${totalLists} total lists):` : `Found ${matchingSpaces.length} space(s) - too many to show detailed list information. Please search more precisely to get complete tree structure with lists and folders:` }, ...contentBlocks ], }; } catch (error) { console.error('Error searching spaces:', error); return { content: [ { type: "text", text: `Error searching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } }); }