list_tasks
Retrieve tasks filtered by assignee, status, dartboard, priority, tags, date range, and parent status. Supports pagination and multiple detail levels.
Instructions
Query tasks with filters (assignee, status, dartboard, priority, tags, dates, has_parent), pagination, and detail levels. Parent filter uses client-side filtering.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| assignee | No | Filter by assignee (dart_id, name, or email) | |
| status | No | Filter by status (dart_id or name) | |
| dartboard | No | Filter by dartboard (dart_id or name) | |
| priority | No | Filter by priority (1-5) | |
| tags | No | Filter by tags (dart_ids or names) | |
| due_before | No | Filter tasks due before date (ISO8601) | |
| due_after | No | Filter tasks due after date (ISO8601) | |
| has_parent | No | Filter tasks with parent (true) or without parent (false). Client-side filter. | |
| limit | No | Max tasks to return (default: 50, max: 500) | |
| offset | No | Pagination offset (default: 0) | |
| detail_level | No | minimal=id+title+parent+blockers, standard=+description+status+assignee+priority, full=all fields including relationships |
Implementation Reference
- src/tools/list_tasks.ts:39-126 (handler)Main handler function for list_tasks tool. Validates input, resolves filters, calls API, applies client-side filtering and detail_level pruning, then returns paginated results with metadata.
export async function handleListTasks(input: ListTasksInput): Promise<ListTasksOutput> { // Defensive input handling const safeInput = input || {}; 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 }); // Validate and normalize pagination parameters const limit = validateLimit(safeInput.limit); const offset = validateOffset(safeInput.offset); // Validate detail_level const detailLevel = validateDetailLevel(safeInput.detail_level); // Validate and resolve filters const resolvedFilters = await resolveFilters(safeInput, client); // Build API request with resolved filters const apiRequest: ListTasksInput = { ...resolvedFilters, limit, offset, detail_level: detailLevel, }; // Call DartClient.listTasks() let apiResponse: { tasks: DartTask[]; total: number }; try { apiResponse = await client.listTasks(apiRequest); } catch (error) { // Enhance error messages for authentication issues if (error instanceof DartAPIError) { if (error.statusCode === 401) { throw new DartAPIError( 'Authentication failed: Invalid DART_TOKEN. Get a valid token from: https://app.dartai.com/?settings=account', 401, error.response ); } else if (error.statusCode === 403) { throw new DartAPIError( 'Access forbidden: Your DART_TOKEN does not have permission to list tasks.', 403, error.response ); } } // Re-throw other errors throw error; } // Extract tasks and total count let tasks = apiResponse.tasks || []; const totalCount = apiResponse.total || 0; // Apply client-side filtering fallback if API doesn't support certain filters // (Some filters might not be supported by the API, so we filter client-side) tasks = applyClientSideFilters(tasks, safeInput); // Apply detail_level pruning to reduce token usage tasks = applyDetailLevel(tasks, detailLevel); // Calculate pagination metadata const returnedCount = tasks.length; // Use limit (not returnedCount) to calculate hasMore, as client-side filtering could reduce returnedCount const hasMore = offset + limit < totalCount; const nextOffset = hasMore ? offset + limit : null; // Build filters_applied object const filtersApplied = buildFiltersApplied(safeInput, resolvedFilters); return { tasks, total_count: totalCount, returned_count: returnedCount, has_more: hasMore, next_offset: nextOffset, filters_applied: filtersApplied, }; } - src/tools/list_tasks.ts:127-512 (handler)Helper functions used by the handler: validateLimit, validateOffset, validateDetailLevel, resolveFilters (name-to-ID resolution for assignee/status/dartboard/tags), validateRelationshipFilters, applyClientSideFilters (has_parent filtering), applyDetailLevel (minimal/standard/full pruning), buildFiltersApplied.
/** * Validate and normalize limit parameter */ 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'); } if (limit < 1) { throw new ValidationError('limit must be at least 1'); } if (limit > 500) { throw new ValidationError('limit must not exceed 500 (max allowed)'); } return limit; } /** * Validate and normalize offset parameter */ function validateOffset(offset?: number): number { if (offset === undefined || offset === null) { return 0; // Default offset } if (typeof offset !== 'number' || !Number.isInteger(offset)) { throw new ValidationError('offset must be an integer'); } if (offset < 0) { throw new ValidationError('offset must be non-negative'); } return offset; } /** * Validate detail_level parameter */ function validateDetailLevel(detailLevel?: string): 'minimal' | 'standard' | 'full' { if (!detailLevel) { return 'standard'; // Default detail level } if (!['minimal', 'standard', 'full'].includes(detailLevel)) { throw new ValidationError( `detail_level must be one of: minimal, standard, full. Got: "${detailLevel}"` ); } return detailLevel as 'minimal' | 'standard' | 'full'; } /** * Resolve filter references (names to IDs) against workspace config */ async function resolveFilters( input: ListTasksInput, client: DartClient ): Promise<ListTasksInput> { // If no filters that need resolution, return as-is const needsResolution = input.assignee || input.status || input.dartboard || input.tags; if (!needsResolution) { return { ...input }; } // Get config to resolve names to IDs let config = configCache.get(); if (!config) { config = await client.getConfig(); configCache.set({ ...config, cached_at: new Date().toISOString(), cache_ttl_seconds: configCache.getTTL(), }); } const resolved: ListTasksInput = { ...input }; // Resolve assignee (dart_id, name, or email) if (input.assignee && typeof input.assignee === 'string') { const assigneeInput = input.assignee; // Type narrowing // Handle empty assignees array edge case if (!config.assignees || config.assignees.length === 0) { throw new ValidationError( 'No assignees configured in workspace. Cannot filter by assignee.', 'assignee', ['No assignees available'] ); } const assignee = config.assignees.find( (a) => a.name?.toLowerCase() === assigneeInput.toLowerCase() || a.email?.toLowerCase() === assigneeInput.toLowerCase() ); if (!assignee) { throw new ValidationError( `Assignee not found: "${assigneeInput}". Use get_config to see available assignees.`, 'assignee', config.assignees.map((a) => a.email ? `${a.name} <${a.email}>` : a.name) ); } // Use email if available, otherwise name resolved.assignee = assignee.email || assignee.name; } // Resolve status (dart_id or name) if (input.status && typeof input.status === 'string') { const statusInput = input.status; // Type narrowing // Handle empty statuses array edge case if (!config.statuses || config.statuses.length === 0) { throw new ValidationError( 'No statuses configured in workspace. Cannot filter by status.', 'status', ['No statuses available'] ); } const status = findStatus(config.statuses, statusInput); if (!status) { throw new ValidationError( `Status not found: "${statusInput}". Use get_config to see available statuses.`, 'status', getStatusNames(config.statuses) ); } // Return dart_id for API filtering resolved.status = typeof status === 'string' ? status : status.dart_id; } // Resolve dartboard (dart_id or name) if (input.dartboard && typeof input.dartboard === 'string') { const dartboardInput = input.dartboard; // Type narrowing // Handle empty dartboards array edge case if (!config.dartboards || config.dartboards.length === 0) { throw new ValidationError( 'No dartboards configured in workspace. Cannot filter by dartboard.', 'dartboard', ['No dartboards available'] ); } const dartboard = findDartboard(config.dartboards, dartboardInput); if (!dartboard) { throw new ValidationError( `Dartboard not found: "${dartboardInput}". Use get_config to see available dartboards.`, 'dartboard', getDartboardNames(config.dartboards).slice(0, 10) ); } // Return dart_id for API filtering resolved.dartboard = typeof dartboard === 'string' ? dartboard : dartboard.dart_id; } // Resolve tags (dart_ids or names) if (input.tags && Array.isArray(input.tags) && input.tags.length > 0) { // Handle empty tags array edge case if (!config.tags || config.tags.length === 0) { throw new ValidationError( 'No tags configured in workspace. Cannot filter by tags.', 'tags', ['No tags available'] ); } const resolvedTags: string[] = []; for (const tagInput of input.tags) { // Validate tagInput is a string if (typeof tagInput !== 'string') { throw new ValidationError( `Invalid tag value: tags must be strings. Got: ${typeof tagInput}`, 'tags' ); } const tag = findTag(config.tags, tagInput); if (!tag) { throw new ValidationError( `Tag not found: "${tagInput}". Use get_config to see available tags.`, 'tags', getTagNames(config.tags).slice(0, 20) ); } // Return dart_id for API filtering resolvedTags.push(typeof tag === 'string' ? tag : tag.dart_id); } resolved.tags = resolvedTags; } // Validate date formats if (input.due_before && !isValidISO8601Date(input.due_before)) { throw new ValidationError( `due_before must be in ISO8601 format (e.g., "2024-12-31T23:59:59Z"). Got: "${input.due_before}"`, 'due_before' ); } if (input.due_after && !isValidISO8601Date(input.due_after)) { throw new ValidationError( `due_after must be in ISO8601 format (e.g., "2024-01-01T00:00:00Z"). Got: "${input.due_after}"`, 'due_after' ); } // Validate priority range if (input.priority !== undefined) { if (typeof input.priority !== 'number' || !Number.isInteger(input.priority)) { throw new ValidationError('priority must be an integer', 'priority'); } if (input.priority < 1 || input.priority > 5) { throw new ValidationError( 'priority must be between 1 and 5 (1=lowest, 5=highest)', 'priority' ); } } // Validate relationship filters validateRelationshipFilters(input); return resolved; } /** * Validate relationship filter parameters * * Note: Only has_parent is supported because the list API returns parent_task. * Other relationship filters (has_subtasks, has_blockers, is_blocking) are not * available because the list API doesn't return taskRelationships data. */ function validateRelationshipFilters(input: ListTasksInput): void { // Validate has_parent boolean filter if (input.has_parent !== undefined && typeof input.has_parent !== 'boolean') { throw new ValidationError('has_parent must be a boolean', 'has_parent'); } } /** * Validate ISO8601 date format */ function isValidISO8601Date(dateString: string): boolean { const iso8601Regex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|[+-]\d{2}:\d{2})?)?$/; if (!iso8601Regex.test(dateString)) { return false; } const date = new Date(dateString); return !isNaN(date.getTime()); } /** * Check if relationship filters are being used (require client-side filtering) * * Note: Only has_parent is supported. Other relationship filters were removed * because the list API doesn't return taskRelationships data. */ function hasRelationshipFilters(input: ListTasksInput): boolean { return input.has_parent !== undefined; } /** * Apply client-side filtering for relationship filters. * * Note: Only has_parent is supported because the list API returns parent_task. * Other relationship filters (has_subtasks, has_blockers, is_blocking) were removed * because the list API doesn't return taskRelationships data. */ function applyClientSideFilters(tasks: DartTask[], input: ListTasksInput): DartTask[] { // If no relationship filters, return as-is (API handles other filters) if (!hasRelationshipFilters(input)) { return tasks; } return tasks.filter((task) => { // has_parent filter: tasks with or without a parent task if (input.has_parent !== undefined) { const hasParent = task.parent_task !== undefined && task.parent_task !== null && task.parent_task !== ''; if (input.has_parent !== hasParent) { return false; } } return true; }); } /** * Apply detail_level pruning to reduce token usage */ function applyDetailLevel( tasks: DartTask[], detailLevel: 'minimal' | 'standard' | 'full' ): DartTask[] { if (detailLevel === 'full') { return tasks; // Return all fields } return tasks.map((task) => { if (detailLevel === 'minimal') { // minimal: id + title + parent/blockers return { dart_id: task.dart_id, title: task.title, parent_task: task.parent_task, blocker_ids: task.blocker_ids, created_at: task.created_at, updated_at: task.updated_at, } as DartTask; } // standard: id + title + description + status + assignee + priority + parent/blockers return { dart_id: task.dart_id, title: task.title, description: task.description, status: task.status, status_id: task.status_id, priority: task.priority, assignees: task.assignees, dartboard: task.dartboard, dartboard_id: task.dartboard_id, parent_task: task.parent_task, blocker_ids: task.blocker_ids, created_at: task.created_at, updated_at: task.updated_at, } as DartTask; }); } /** * Build filters_applied object for response */ function buildFiltersApplied( input: ListTasksInput, resolved: ListTasksInput ): Record<string, unknown> { const filtersApplied: Record<string, unknown> = {}; // Echo back all applied filters if (resolved.assignee) filtersApplied.assignee = resolved.assignee; if (resolved.status) filtersApplied.status = resolved.status; if (resolved.dartboard) filtersApplied.dartboard = resolved.dartboard; if (resolved.priority !== undefined) filtersApplied.priority = resolved.priority; if (resolved.tags && resolved.tags.length > 0) filtersApplied.tags = resolved.tags; if (resolved.due_before) filtersApplied.due_before = resolved.due_before; if (resolved.due_after) filtersApplied.due_after = resolved.due_after; // Echo back relationship filters (client-side filters) // Note: Only has_parent is supported (list API returns parent_task) if (input.has_parent !== undefined) filtersApplied.has_parent = input.has_parent; // Include pagination info (using actual validated values, not defaults from input) filtersApplied.limit = input.limit !== undefined ? input.limit : 50; filtersApplied.offset = input.offset !== undefined ? input.offset : 0; filtersApplied.detail_level = input.detail_level || 'standard'; // Flag indicating client-side filtering was used if (hasRelationshipFilters(input)) { filtersApplied.client_side_filtered = true; } return filtersApplied; } - src/types/index.ts:414-443 (schema)Type definitions: ListTasksInput (filters, pagination, detail_level) and ListTasksOutput (tasks, pagination metadata, filters_applied).
export interface ListTasksInput { assignee?: string; status?: string; dartboard?: string; priority?: number; // 1-5 (1=lowest, 5=highest) tags?: string[]; due_before?: string; due_after?: string; limit?: number; offset?: number; detail_level?: 'minimal' | 'standard' | 'full'; // Relationship filters (client-side filtering) /** * Filter tasks that have a parent task (true) or no parent task (false). * Filters based on parent_task field being set or undefined. * Note: Other relationship filters (has_subtasks, has_blockers, is_blocking) * are not available because the list API doesn't return taskRelationships data. */ has_parent?: boolean; } export interface ListTasksOutput { tasks: DartTask[]; total_count: number; returned_count: number; has_more: boolean; next_offset: number | null; filters_applied: Record<string, unknown>; } - src/index.ts:286-340 (registration)Tool registration with name 'list_tasks', description, and inputSchema with all filter/pagination/detail_level parameters.
{ name: 'list_tasks', description: 'Query tasks with filters (assignee, status, dartboard, priority, tags, dates, has_parent), pagination, and detail levels. Parent filter uses client-side filtering.', inputSchema: { type: 'object', properties: { assignee: { type: 'string', description: 'Filter by assignee (dart_id, name, or email)', }, status: { type: 'string', description: 'Filter by status (dart_id or name)', }, dartboard: { type: 'string', description: 'Filter by dartboard (dart_id or name)', }, priority: { type: 'integer', description: 'Filter by priority (1-5)', }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags (dart_ids or names)', }, due_before: { type: 'string', description: 'Filter tasks due before date (ISO8601)', }, due_after: { type: 'string', description: 'Filter tasks due after date (ISO8601)', }, // Relationship filters (client-side) has_parent: { type: 'boolean', description: 'Filter tasks with parent (true) or without parent (false). Client-side filter.', }, // Pagination limit: { type: 'integer', description: 'Max tasks to return (default: 50, max: 500)', }, offset: { type: 'integer', description: 'Pagination offset (default: 0)', }, detail_level: { type: 'string', enum: ['minimal', 'standard', 'full'], description: 'minimal=id+title+parent+blockers, standard=+description+status+assignee+priority, full=all fields including relationships', }, }, - src/index.ts:1004-1014 (registration)Tool dispatch case in main handler - calls handleListTasks with args and returns JSON-stringified result.
case 'list_tasks': { const result = await handleListTasks((args || {}) as any); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; }