search_tasks
Find tasks using full-text search with relevance ranking. Apply filters like dartboard, status, and exclusion terms to narrow results.
Instructions
Full-text search across tasks with relevance ranking. Alternative to list_tasks for text-based discovery. Supports quoted phrases, exclusions (-term), and inline filters (dartboard:Name, status:Done, assignee:Name).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query. Supports quoted phrases ("exact match"), exclusions (-term), inline filters (dartboard:Name, status:Done), and regular terms. | |
| dartboard | No | Optional dartboard filter (dart_id or name) | |
| include_completed | No | Include completed tasks in results (default: false) | |
| limit | No | Max results to return (default: 50, max: 500) |
Implementation Reference
- src/tools/search_tasks.ts:32-113 (handler)Main handler for the search_tasks tool. Validates input, parses the query string, fetches tasks from the Dart API with optional filters, performs client-side relevance scoring, sorts by score, applies limit, and returns results with progressive detail levels.
export async function handleSearchTasks(input: SearchTasksInput): Promise<SearchTasksOutput> { // Defensive input handling const safeInput = input || {}; // Validate required fields if (!safeInput.query || typeof safeInput.query !== 'string') { throw new ValidationError('query is required and must be a string', 'query'); } const query = safeInput.query.trim(); if (query.length === 0) { throw new ValidationError('query cannot be empty or whitespace-only', 'query'); } const DART_TOKEN = process.env.DART_TOKEN; if (!DART_TOKEN) { throw new DartAPIError( 'DART_TOKEN environment variable is required. Get your token from: https://app.dartai.com/?settings=account', 401 ); } // Initialize Dart API client const client = new DartClient({ token: DART_TOKEN }); // Parse query into structured search terms const queryParsed = parseQuery(query); // Validate that we have at least some search terms (not just exclusions and filters) if (queryParsed.terms.length === 0 && queryParsed.phrases.length === 0) { // If only filters were provided (e.g. "dartboard:X"), that's a list operation not search if (Object.keys(queryParsed.filters).length > 0) { throw new ValidationError( 'query must contain search terms in addition to filters. Use list_tasks for filter-only queries.', 'query' ); } throw new ValidationError( 'query must contain at least one search term or phrase (not just exclusions)', 'query' ); } // Validate limit const limit = validateLimit(safeInput.limit); // Resolve dartboard: explicit parameter takes precedence over inline filter let dartboardId: string | undefined; const dartboardName = safeInput.dartboard || queryParsed.filters.dartboard; if (dartboardName) { dartboardId = await resolveDartboard(dartboardName, client); } // Resolve other inline filters const statusFilter = queryParsed.filters.status; const assigneeFilter = queryParsed.filters.assignee; // Client-side search: fetch tasks matching filters, then score locally const searchMethod = 'client_side'; // Fetch tasks for client-side search const tasks = await fetchAllTasks(client, dartboardId, safeInput.include_completed, statusFilter, assigneeFilter); // Perform client-side search with relevance scoring const searchResults = performClientSideSearch(tasks, queryParsed); // Sort by relevance descending searchResults.sort((a, b) => b.relevance_score - a.relevance_score); // Apply limit const limitedResults = searchResults.slice(0, limit); // Apply progressive detail levels based on relevance const resultsWithDetail = applyProgressiveDetail(limitedResults); return { tasks: resultsWithDetail, total_results: searchResults.length, query_parsed: queryParsed, search_method: searchMethod, }; } - src/tools/search_tasks.ts:134-179 (helper)Parses the search query into structured terms, phrases, exclusions, and inline filters. Supports quoted phrases, -term exclusions, and key:value filters for dartboard, status, assignee, priority.
function parseQuery(query: string): QueryParsed { const phrases: string[] = []; const exclusions: string[] = []; const terms: string[] = []; const filters: Record<string, string> = {}; let remaining = query; let match: RegExpExecArray | null; // Extract inline filters first (key:value where key is a known filter) // Supports quoted values: dartboard:"My Board" or dartboard:'My Board' // and unquoted values: dartboard:Personal/agnt const filterRegex = /(\w+):(?:"([^"]+)"|'([^']+)'|(\S+))/g; while ((match = filterRegex.exec(remaining)) !== null) { const key = match[1].toLowerCase(); const value = match[2] || match[3] || match[4]; if (INLINE_FILTER_KEYS.has(key)) { filters[key] = value; remaining = remaining.replace(match[0], ' '); } } // Extract quoted phrases (both " and ') const phraseRegex = /["']([^"']+)["']/g; while ((match = phraseRegex.exec(remaining)) !== null) { phrases.push(match[1].toLowerCase()); remaining = remaining.replace(match[0], ' '); } // Extract exclusions (words starting with -) const exclusionRegex = /-(\w+)/g; while ((match = exclusionRegex.exec(remaining)) !== null) { exclusions.push(match[1].toLowerCase()); remaining = remaining.replace(match[0], ' '); } // Extract remaining terms (split by whitespace, filter empty) const remainingTerms = remaining .split(/\s+/) .map(t => t.trim().toLowerCase()) .filter(t => t.length > 0); terms.push(...remainingTerms); return { terms, phrases, exclusions, filters }; } - src/tools/search_tasks.ts:184-202 (helper)Validates the limit parameter. Default is 50, max is 500, must be a positive integer.
function validateLimit(limit?: number): number { if (limit === undefined || limit === null) { return 50; // Default limit } if (typeof limit !== 'number' || !Number.isInteger(limit)) { throw new ValidationError('limit must be an integer', 'limit'); } if (limit < 1) { throw new ValidationError('limit must be at least 1', 'limit'); } if (limit > 500) { throw new ValidationError('limit must not exceed 500 (max allowed)', 'limit'); } return limit; } - src/tools/search_tasks.ts:255-288 (helper)Fetches all tasks from the Dart API with pagination. Caps at 2000 tasks without a dartboard filter, 10000 with one. Optionally filters out completed tasks.
async function fetchAllTasks( client: DartClient, dartboardId?: string, includeCompleted?: boolean, status?: string, assignee?: string, ): Promise<DartTask[]> { const allTasks: DartTask[] = []; const fetchLimit = 100; // Smaller pages to avoid API 500 errors on large responses const maxTotalTasks = dartboardId ? 10000 : 2000; // Tighter limit without dartboard filter let offset = 0; let hasMore = true; while (hasMore && allTasks.length < maxTotalTasks) { const response = await client.listTasks({ dartboard: dartboardId, status, assignee, limit: fetchLimit, offset, }); allTasks.push(...response.tasks); hasMore = offset + fetchLimit < response.total; offset += fetchLimit; } if (!includeCompleted) { return allTasks.filter(task => !task.completed_at); } return allTasks; } - src/index.ts:776-802 (registration)Registration of the search_tasks tool in the MCP server's tool list, defining its name, description, and input schema with required query field.
// Search { name: 'search_tasks', description: 'Full-text search across tasks with relevance ranking. Alternative to list_tasks for text-based discovery. Supports quoted phrases, exclusions (-term), and inline filters (dartboard:Name, status:Done, assignee:Name).', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query. Supports quoted phrases ("exact match"), exclusions (-term), inline filters (dartboard:Name, status:Done), and regular terms.', }, dartboard: { type: 'string', description: 'Optional dartboard filter (dart_id or name)', }, include_completed: { type: 'boolean', description: 'Include completed tasks in results (default: false)', }, limit: { type: 'integer', description: 'Max results to return (default: 50, max: 500)', }, }, required: ['query'], }, },