'use strict'; const taskRepository = require('../../tasks/repository'); const { serializeTask, serializeTasks, } = require('../../tasks/core/serializers'); const { buildTaskAttributes } = require('../../tasks/core/builders'); const { Op } = require('sequelize'); const { Task, Project, Tag } = require('../../../models'); /** * Helper to find task by ID or UID */ async function findTaskByIdentifier(identifier, userId) { const isNumeric = !isNaN(identifier); if (isNumeric) { return await taskRepository.findByIdAndUser( parseInt(identifier), userId, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], } ); } else { return await taskRepository.findByUid(identifier, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], }); } } /** * Register all task-related MCP tools */ function registerTaskTools(server, context, tools) { // 1. list_tasks - List tasks with filtering tools.push({ name: 'list_tasks', description: 'List tasks from tududi with optional filtering', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['today', 'upcoming', 'completed', 'archived', 'all'], description: 'Filter tasks by type', }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'archived'], description: 'Filter by status', }, project_id: { type: 'number', description: 'Filter by project ID', }, limit: { type: 'number', description: 'Maximum number of tasks to return', default: 50, }, }, }, handler: async (params) => { const where = { user_id: context.userId }; const limit = params.limit || 50; // Apply status filter if (params.status) { const statusMap = { pending: 0, in_progress: 1, completed: 2, archived: 6, }; where.status = statusMap[params.status]; } // Apply project filter if (params.project_id) { where.project_id = params.project_id; } // Apply type filter if (params.type === 'completed') { where.status = 2; } else if (params.type === 'archived') { where.status = 6; } else if (params.type === 'today' || params.type === 'upcoming') { where.status = { [Op.ne]: 6 }; // Not archived } const tasks = await taskRepository.findAll(where, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], limit: limit, order: [['created_at', 'DESC']], }); const serializedTasks = await serializeTasks( tasks, context.user.timezone ); return { content: [ { type: 'text', text: JSON.stringify( { count: serializedTasks.length, tasks: serializedTasks, }, null, 2 ), }, ], }; }, }); // 2. get_task - Get single task by ID or UID tools.push({ name: 'get_task', description: 'Get a specific task by ID or UID with full details', inputSchema: { type: 'object', properties: { id: { type: ['number', 'string'], description: 'Task ID (number) or UID (string)', }, }, required: ['id'], }, handler: async (params) => { const task = await findTaskByIdentifier(params.id, context.userId); if (!task) { throw new Error(`Task not found: ${params.id}`); } // Check ownership if (task.user_id !== context.userId) { throw new Error('Access denied'); } const serialized = await serializeTask(task, context.user.timezone); return { content: [ { type: 'text', text: JSON.stringify(serialized, null, 2), }, ], }; }, }); // 3. create_task - Create new task tools.push({ name: 'create_task', description: 'Create a new task in tududi', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Task name (required)', }, description: { type: 'string', description: 'Task description/note', }, priority: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Task priority', }, due_date: { type: 'string', description: 'Due date (ISO 8601 format)', }, project_id: { type: 'number', description: 'Project ID to assign task to', }, tags: { type: 'array', items: { type: 'string' }, description: 'Array of tag names', }, }, required: ['name'], }, handler: async (params) => { const priorityMap = { low: 0, medium: 1, high: 2 }; const taskData = { user_id: context.userId, name: params.name, note: params.description || '', priority: params.priority ? priorityMap[params.priority] : 1, status: 0, // pending due_date: params.due_date || null, project_id: params.project_id || null, }; const task = await taskRepository.create(taskData); // Handle tags if provided if (params.tags && params.tags.length > 0) { const tagInstances = await Promise.all( params.tags.map(async (tagName) => { const [tag] = await Tag.findOrCreate({ where: { name: tagName, user_id: context.userId }, }); return tag; }) ); await task.setTags(tagInstances); } // Reload with associations const reloadedTask = await taskRepository.findByIdAndUser( task.id, context.userId, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], } ); const serialized = await serializeTask( reloadedTask, context.user.timezone ); return { content: [ { type: 'text', text: JSON.stringify( { message: 'Task created successfully', task: serialized, }, null, 2 ), }, ], }; }, }); // 4. update_task - Update existing task tools.push({ name: 'update_task', description: 'Update an existing task', inputSchema: { type: 'object', properties: { id: { type: ['number', 'string'], description: 'Task ID or UID', }, name: { type: 'string', description: 'New task name' }, description: { type: 'string', description: 'New description' }, priority: { type: 'string', enum: ['low', 'medium', 'high'], }, status: { type: 'string', enum: ['pending', 'in_progress', 'completed', 'archived'], }, due_date: { type: 'string', description: 'New due date' }, today: { type: 'boolean', description: 'Add to Today list', }, }, required: ['id'], }, handler: async (params) => { const task = await findTaskByIdentifier(params.id, context.userId); if (!task) { throw new Error(`Task not found: ${params.id}`); } if (task.user_id !== context.userId) { throw new Error('Access denied'); } const updates = {}; if (params.name !== undefined) updates.name = params.name; if (params.description !== undefined) updates.note = params.description; if (params.priority) { const priorityMap = { low: 0, medium: 1, high: 2 }; updates.priority = priorityMap[params.priority]; } if (params.status) { const statusMap = { pending: 0, in_progress: 1, completed: 2, archived: 6, }; updates.status = statusMap[params.status]; } if (params.due_date !== undefined) updates.due_date = params.due_date; if (params.today !== undefined) updates.today = params.today; await task.update(updates); // Reload with associations const reloadedTask = await taskRepository.findByIdAndUser( task.id, context.userId, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], } ); const serialized = await serializeTask( reloadedTask, context.user.timezone ); return { content: [ { type: 'text', text: JSON.stringify( { message: 'Task updated successfully', task: serialized, }, null, 2 ), }, ], }; }, }); // 5. complete_task - Toggle task completion tools.push({ name: 'complete_task', description: 'Mark a task as completed or reopen it', inputSchema: { type: 'object', properties: { id: { type: ['number', 'string'], description: 'Task ID or UID', }, }, required: ['id'], }, handler: async (params) => { const task = await findTaskByIdentifier(params.id, context.userId); if (!task) { throw new Error(`Task not found: ${params.id}`); } if (task.user_id !== context.userId) { throw new Error('Access denied'); } // Toggle completion const newStatus = task.status === 2 ? 0 : 2; // 2 = completed, 0 = pending const updates = { status: newStatus, completed_at: newStatus === 2 ? new Date() : null, }; await task.update(updates); const reloadedTask = await taskRepository.findByIdAndUser( task.id, context.userId, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], } ); const serialized = await serializeTask( reloadedTask, context.user.timezone ); return { content: [ { type: 'text', text: JSON.stringify( { message: newStatus === 2 ? 'Task completed' : 'Task reopened', task: serialized, }, null, 2 ), }, ], }; }, }); // 6. delete_task - Delete task tools.push({ name: 'delete_task', description: 'Permanently delete a task', inputSchema: { type: 'object', properties: { id: { type: ['number', 'string'], description: 'Task ID or UID', }, }, required: ['id'], }, handler: async (params) => { const task = await findTaskByIdentifier(params.id, context.userId); if (!task) { throw new Error(`Task not found: ${params.id}`); } if (task.user_id !== context.userId) { throw new Error('Access denied'); } await task.destroy(); return { content: [ { type: 'text', text: JSON.stringify( { message: 'Task deleted successfully', task_id: params.id, }, null, 2 ), }, ], }; }, }); // 7. add_subtask - Add subtask to parent tools.push({ name: 'add_subtask', description: 'Add a subtask to an existing task', inputSchema: { type: 'object', properties: { parent_id: { type: ['number', 'string'], description: 'Parent task ID or UID', }, name: { type: 'string', description: 'Subtask name', }, priority: { type: 'string', enum: ['low', 'medium', 'high'], }, due_date: { type: 'string', description: 'Due date', }, }, required: ['parent_id', 'name'], }, handler: async (params) => { const parentTask = await findTaskByIdentifier( params.parent_id, context.userId ); if (!parentTask) { throw new Error(`Parent task not found: ${params.parent_id}`); } if (parentTask.user_id !== context.userId) { throw new Error('Access denied'); } const priorityMap = { low: 0, medium: 1, high: 2 }; const subtaskData = { user_id: context.userId, name: params.name, parent_task_id: parentTask.id, priority: params.priority ? priorityMap[params.priority] : 1, status: 0, due_date: params.due_date || null, project_id: parentTask.project_id, // Inherit parent's project }; const subtask = await taskRepository.create(subtaskData); const reloadedSubtask = await taskRepository.findByIdAndUser( subtask.id, context.userId, { include: [ { model: Project, as: 'Project' }, { model: Tag, as: 'Tags' }, ], } ); const serialized = await serializeTask( reloadedSubtask, context.user.timezone ); return { content: [ { type: 'text', text: JSON.stringify( { message: 'Subtask created successfully', subtask: serialized, }, null, 2 ), }, ], }; }, }); // 8. get_task_metrics - Get task statistics tools.push({ name: 'get_task_metrics', description: 'Get task statistics and productivity metrics', inputSchema: { type: 'object', properties: {}, }, handler: async (params) => { // Get counts by status const openCount = await taskRepository.count({ user_id: context.userId, status: { [Op.in]: [0, 1] }, // pending or in_progress }); const completedCount = await taskRepository.count({ user_id: context.userId, status: 2, // completed }); // Get overdue tasks const now = new Date(); const overdueCount = await taskRepository.count({ user_id: context.userId, status: { [Op.in]: [0, 1] }, due_date: { [Op.lt]: now }, }); // Get in_progress tasks const inProgressCount = await taskRepository.count({ user_id: context.userId, status: 1, }); // Get today's completions const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0); const todayCompletions = await taskRepository.count({ user_id: context.userId, status: 2, completed_at: { [Op.gte]: startOfDay }, }); // Get this week's completions const startOfWeek = new Date(); startOfWeek.setDate(startOfWeek.getDate() - startOfWeek.getDay()); startOfWeek.setHours(0, 0, 0, 0); const weekCompletions = await taskRepository.count({ user_id: context.userId, status: 2, completed_at: { [Op.gte]: startOfWeek }, }); const metrics = { open_tasks: openCount, completed_tasks: completedCount, overdue_tasks: overdueCount, in_progress_tasks: inProgressCount, completed_today: todayCompletions, completed_this_week: weekCompletions, }; return { content: [ { type: 'text', text: JSON.stringify(metrics, null, 2), }, ], }; }, }); } module.exports = { registerTaskTools };