UNPKG

@hauptsache.net/clickup-mcp

Version:

Search, create, and retrieve tasks, add comments, and track time through natural language commands.

162 lines (161 loc) 8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.registerSearchTools = registerSearchTools; const zod_1 = require("zod"); const config_1 = require("../shared/config"); const utils_1 = require("../shared/utils"); const task_tools_1 = require("./task-tools"); const MAX_SEARCH_RESULTS = 50; function registerSearchTools(server, userData) { // Dynamically construct the searchTasks description const searchTasksDescriptionBase = [ "Searches tasks (sometimes called Tickets or Cards) by name, content, assignees, and ID with fuzzy matching and support for multiple search terms (OR logic).", "Can filter by multiple list_ids, space_ids, todo status, or tasks assigned to the current user. If no search terms provided, returns most recently updated tasks.", "Can also be used to find tasks for the current user by providing the assigned_to_me flag." ]; if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') { searchTasksDescriptionBase.push(`For optimal results, as your ClickUp tasks may be primarily in '${config_1.CONFIG.primaryLanguageHint}', consider providing search terms in English and '${config_1.CONFIG.primaryLanguageHint}'.`); } searchTasksDescriptionBase.push("Always reference tasks by their URLs when discussing search results or suggesting actions."); searchTasksDescriptionBase.push("You'll get a rough overview of the tasks that match the search terms, sorted by relevance."); searchTasksDescriptionBase.push("Always use getTaskById to get more specific information if a task is relevant, and always share the task URL."); server.tool("searchTasks", searchTasksDescriptionBase.join("\n"), { terms: zod_1.z .array(zod_1.z.string()) .optional() .describe("Array of search terms (OR logic). Can include task IDs. Optional - if not provided, returns most recent tasks."), list_ids: zod_1.z .array(zod_1.z.string()) .optional() .describe("Filter tasks to specific list IDs"), space_ids: zod_1.z .array(zod_1.z.string()) .optional() .describe("Filter tasks to specific space IDs"), only_todo: zod_1.z .boolean() .optional() .describe("Filter for open/todo tasks only (exclude done and closed tasks)"), status: zod_1.z .array(zod_1.z.string()) .optional() .describe("Filter for tasks with specific status names (overrides only_todo if provided)"), assigned_to_me: zod_1.z .boolean() .optional() .describe(`Filter for tasks assigned to the current user (${userData.user.username} (${userData.user.id}))`), }, { readOnlyHint: true }, async ({ terms, list_ids, space_ids, only_todo, status, assigned_to_me }) => { // Get current user ID if filtering by assigned_to_me const assignees = assigned_to_me ? [userData.user.id] : []; const searchIndex = await (0, utils_1.getTaskSearchIndex)(space_ids, list_ids, assignees); if (!searchIndex) { return { content: [ { type: "text", text: "No tasks available or index could not be built.", }, ], }; } // Early return for no search terms if (!terms || terms.length === 0) { let allTasks = searchIndex._docs || []; // Apply status filtering if (status && status.length > 0) { const statusLower = status.map(s => s.toLowerCase()); allTasks = allTasks.filter((task) => statusLower.includes(task.status.status.toLowerCase())); } else if (only_todo) { allTasks = allTasks.filter((task) => task.status.type !== "done" && task.status.type !== "closed"); } // Sort by updated date (most recent first) and limit const resultTasks = allTasks .sort((a, b) => { const dateA = parseInt(a.date_updated || "0"); const dateB = parseInt(b.date_updated || "0"); return dateB - dateA; }) .slice(0, MAX_SEARCH_RESULTS); if (resultTasks.length === 0) { return { content: [ { type: "text", text: "No tasks found.", }, ], }; } return { content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))), }; } // Create a results map to track unique tasks with scores const uniqueResults = new Map(); // Perform multi-term search with aggressive boosting const searchResults = await (0, utils_1.performMultiTermSearch)(searchIndex, terms); searchResults.forEach(task => { uniqueResults.set(task.id, { item: task, score: 0.1 }); // Give search results a good score }); // Task ID Fallback Logic const potentialTaskIds = terms.filter(utils_1.isTaskId); const foundTaskIdsByFuse = new Set(Array.from(uniqueResults.keys()).map(id => id.toLowerCase())); const taskIdsToFetchDirectly = potentialTaskIds.filter(id => { const lowerId = id.toLowerCase(); return !foundTaskIdsByFuse.has(lowerId); }); if (taskIdsToFetchDirectly.length > 0) { console.error(`Attempting direct fetch for task IDs: ${taskIdsToFetchDirectly.join(', ')}`); const directFetchPromises = taskIdsToFetchDirectly.map(async (id) => { try { const response = await fetch(`https://api.clickup.com/api/v2/task/${id}`, { headers: { Authorization: config_1.CONFIG.apiKey } }); if (response.ok) { const task = await response.json(); if (task && typeof task.id === 'string') { const existing = uniqueResults.get(task.id); if (!existing || 0 < existing.score) { uniqueResults.set(task.id, { item: task, score: 0 }); } } return task; } return null; } catch (error) { console.error(`Error directly fetching task ${id}:`, error); return null; } }); await Promise.all(directFetchPromises); } let resultTasks = Array.from(uniqueResults.values()) .sort((a, b) => a.score - b.score) .map(entry => entry.item); // Apply status filtering if (status && status.length > 0) { const statusLower = status.map(s => s.toLowerCase()); resultTasks = resultTasks.filter((task) => statusLower.includes(task.status.status.toLowerCase())); } else if (only_todo) { resultTasks = resultTasks.filter((task) => task.status.type !== "done" && task.status.type !== "closed"); } // Apply result limit resultTasks = resultTasks.slice(0, MAX_SEARCH_RESULTS); if (resultTasks.length === 0) { return { content: [ { type: "text", text: "No tasks found matching the search criteria.", }, ], }; } return { content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))), }; }); }