tududi/backend/modules/mcp/tools/taskTools.js

646 lines
20 KiB
JavaScript

'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 };