/**
* Task-related MCP tools
*/
import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { TodoistService } from '../services/todoist-service.js';
export function registerTaskTools(server: McpServer, todoist: TodoistService) {
// ─────────────────────────────────────────────────────────────
// list_tasks
// ─────────────────────────────────────────────────────────────
server.tool(
'list_tasks',
'List tasks with optional filtering by project, label, priority, due date, or custom filter',
{
projectId: z.string().optional().describe('Filter by project ID'),
sectionId: z.string().optional().describe('Filter by section ID'),
priority: z.number().min(1).max(4).optional().describe('Filter by priority (4=P1 highest, 1=P4 lowest)'),
dueToday: z.boolean().optional().describe('Show only tasks due today'),
dueThisWeek: z.boolean().optional().describe('Show only tasks due within 7 days'),
overdue: z.boolean().optional().describe('Show only overdue tasks'),
noDueDate: z.boolean().optional().describe('Show only tasks without a due date'),
filter: z.string().optional().describe('Todoist filter syntax (e.g., "today | overdue", "@computer & p1")'),
limit: z.number().optional().default(50).describe('Maximum number of tasks to return'),
},
async (params) => {
try {
const tasks = await todoist.listTasks({
projectId: params.projectId,
sectionId: params.sectionId,
priority: params.priority as 1 | 2 | 3 | 4 | undefined,
dueToday: params.dueToday,
dueThisWeek: params.dueThisWeek,
overdue: params.overdue,
noDueDate: params.noDueDate,
filter: params.filter,
limit: params.limit,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ tasks, total: tasks.length }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// get_task
// ─────────────────────────────────────────────────────────────
server.tool(
'get_task',
'Get detailed information about a specific task including comments and subtasks',
{
taskId: z.string().describe('The task ID'),
includeComments: z.boolean().optional().default(true).describe('Include task comments'),
},
async (params) => {
try {
const task = await todoist.getTask(params.taskId);
const project = await todoist.getProject(task.projectId);
let section;
if (task.sectionId) {
const sections = await todoist.listSections(task.projectId);
section = sections.find(s => s.id === task.sectionId);
}
const labels = await todoist.listLabels();
const taskLabels = labels.filter(l => task.labels.includes(l.name));
let comments: Awaited<ReturnType<typeof todoist.getComments>> = [];
if (params.includeComments) {
comments = await todoist.getComments(params.taskId);
}
const subtasks = await todoist.getSubtasks(params.taskId);
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
task,
project,
section,
labels: taskLabels,
comments,
subtasks,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// create_task
// ─────────────────────────────────────────────────────────────
server.tool(
'create_task',
'Create a new task with optional due date, priority, labels, and project',
{
content: z.string().describe('Task title/content'),
description: z.string().optional().describe('Extended notes (markdown supported)'),
projectId: z.string().optional().describe('Project ID (defaults to Inbox)'),
sectionId: z.string().optional().describe('Section ID within the project'),
labels: z.array(z.string()).optional().describe('Label names to apply'),
priority: z.number().min(1).max(4).optional().describe('Priority (4=P1 highest, 1=P4 default)'),
dueString: z.string().optional().describe('Natural language due date (e.g., "tomorrow at 2pm", "every monday")'),
dueDate: z.string().optional().describe('Due date in YYYY-MM-DD format'),
dueDatetime: z.string().optional().describe('Due datetime in ISO 8601 format'),
duration: z.number().optional().describe('Estimated duration amount'),
durationUnit: z.enum(['minute', 'day']).optional().describe('Duration unit'),
parentId: z.string().optional().describe('Parent task ID (to create as subtask)'),
},
async (params) => {
try {
const task = await todoist.createTask({
content: params.content,
description: params.description,
projectId: params.projectId,
sectionId: params.sectionId,
labels: params.labels,
priority: params.priority as 1 | 2 | 3 | 4 | undefined,
dueString: params.dueString,
dueDate: params.dueDate,
dueDatetime: params.dueDatetime,
duration: params.duration,
durationUnit: params.durationUnit,
parentId: params.parentId,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ task }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// update_task
// ─────────────────────────────────────────────────────────────
server.tool(
'update_task',
'Update an existing task (content, due date, priority, labels, etc.)',
{
taskId: z.string().describe('The task ID to update'),
content: z.string().optional().describe('New task title/content'),
description: z.string().optional().describe('New notes (markdown supported)'),
labels: z.array(z.string()).optional().describe('New label names (replaces existing)'),
priority: z.number().min(1).max(4).optional().describe('New priority (4=P1 highest, 1=P4)'),
dueString: z.string().optional().describe('New due date in natural language'),
dueDate: z.string().optional().describe('New due date in YYYY-MM-DD format'),
dueDatetime: z.string().optional().describe('New due datetime in ISO 8601 format'),
duration: z.number().optional().describe('New estimated duration'),
durationUnit: z.enum(['minute', 'day']).optional().describe('Duration unit'),
},
async (params) => {
try {
const { taskId, ...updateData } = params;
const task = await todoist.updateTask(taskId, {
content: updateData.content,
description: updateData.description,
labels: updateData.labels,
priority: updateData.priority as 1 | 2 | 3 | 4 | undefined,
dueString: updateData.dueString,
dueDate: updateData.dueDate,
dueDatetime: updateData.dueDatetime,
duration: updateData.duration,
durationUnit: updateData.durationUnit,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ task }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// complete_task
// ─────────────────────────────────────────────────────────────
server.tool(
'complete_task',
'Mark a task as complete. For recurring tasks, this completes the current occurrence.',
{
taskId: z.string().describe('The task ID to complete'),
},
async (params) => {
try {
const result = await todoist.completeTask(params.taskId);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// uncomplete_task
// ─────────────────────────────────────────────────────────────
server.tool(
'uncomplete_task',
'Reopen a completed task',
{
taskId: z.string().describe('The task ID to reopen'),
},
async (params) => {
try {
const result = await todoist.uncompleteTask(params.taskId);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// delete_task
// ─────────────────────────────────────────────────────────────
server.tool(
'delete_task',
'Permanently delete a task',
{
taskId: z.string().describe('The task ID to delete'),
},
async (params) => {
try {
const result = await todoist.deleteTask(params.taskId);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// move_task
// ─────────────────────────────────────────────────────────────
server.tool(
'move_task',
'Move a task to a different project, section, or make it a subtask',
{
taskId: z.string().describe('The task ID to move'),
projectId: z.string().optional().describe('New project ID'),
sectionId: z.string().optional().describe('New section ID'),
parentId: z.string().optional().describe('Parent task ID (to make subtask)'),
},
async (params) => {
try {
const task = await todoist.moveTask(params.taskId, {
projectId: params.projectId,
sectionId: params.sectionId,
parentId: params.parentId,
});
return {
content: [
{
type: 'text',
text: JSON.stringify({ task }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
}