@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
JavaScript
;
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'}`,
},
],
};
}
});
}