/**
* Utility MCP tools (comments, stats, quick add)
*/
import { z } from 'zod';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { TodoistService } from '../services/todoist-service.js';
import type { Task, ProductivityByProject, ProductivityByDay } from '../types/index.js';
export function registerUtilityTools(server: McpServer, todoist: TodoistService) {
// ─────────────────────────────────────────────────────────────
// add_comment
// ─────────────────────────────────────────────────────────────
server.tool(
'add_comment',
'Add a comment to a task',
{
taskId: z.string().describe('The task ID'),
content: z.string().describe('Comment content (markdown supported)'),
},
async (params) => {
try {
const comment = await todoist.addComment(params.taskId, params.content);
return {
content: [
{
type: 'text',
text: JSON.stringify({ comment }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// get_productivity_stats
// ─────────────────────────────────────────────────────────────
server.tool(
'get_productivity_stats',
'Get task completion statistics and productivity insights',
{
period: z.enum(['today', 'week', 'month']).optional().default('week').describe('Time period for stats'),
},
async (params) => {
try {
// Get all tasks to calculate stats
const allTasks = await todoist.listTasks({ limit: 500 });
const projects = await todoist.listProjects();
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let periodStart: Date;
switch (params.period) {
case 'today':
periodStart = today;
break;
case 'month':
periodStart = new Date(today.getFullYear(), today.getMonth(), 1);
break;
case 'week':
default:
periodStart = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
}
// Calculate stats
let overdue = 0;
let upcoming = 0;
const byProject: Map<string, ProductivityByProject> = new Map();
// Initialize project stats
for (const project of projects) {
byProject.set(project.id, {
projectId: project.id,
projectName: project.name,
completed: 0,
pending: 0,
});
}
for (const task of allTasks) {
// Count by project
const projectStats = byProject.get(task.projectId);
if (projectStats) {
if (task.isCompleted) {
projectStats.completed++;
} else {
projectStats.pending++;
}
}
// Count overdue and upcoming
if (!task.isCompleted && task.due) {
const dueDate = new Date(task.due.date);
if (dueDate < today) {
overdue++;
} else if (dueDate <= new Date(today.getTime() + 7 * 24 * 60 * 60 * 1000)) {
upcoming++;
}
}
}
// Build daily stats (simplified - would need completed task history for real data)
const byDay: ProductivityByDay[] = [];
const daysInPeriod = Math.ceil((today.getTime() - periodStart.getTime()) / (24 * 60 * 60 * 1000)) + 1;
for (let i = 0; i < daysInPeriod; i++) {
const date = new Date(periodStart.getTime() + i * 24 * 60 * 60 * 1000);
byDay.push({
date: date.toISOString().split('T')[0],
completed: 0, // Would need activity log for real data
created: 0,
});
}
// Note: Real completed/created counts would require the Sync API's activity log
// For now, we provide what we can from the current task list
const completedTasks = allTasks.filter(t => t.isCompleted);
const result = {
completed: completedTasks.length,
created: allTasks.length,
overdue,
upcoming,
byProject: Array.from(byProject.values()).filter(p => p.completed > 0 || p.pending > 0),
byDay,
currentStreak: 0, // Would need activity history
longestStreak: 0,
};
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
// ─────────────────────────────────────────────────────────────
// quick_add
// ─────────────────────────────────────────────────────────────
server.tool(
'quick_add',
'Quickly add a task using natural language. Todoist will parse dates, priorities, labels, and projects from the text.',
{
text: z.string().describe('Natural language task (e.g., "Buy milk tomorrow p1 @errands #Shopping")'),
},
async (params) => {
try {
const task = await todoist.quickAdd(params.text);
// Try to determine what was parsed
const parsed: {
content: string;
dueString?: string;
priority?: number;
labels?: string[];
project?: string;
} = {
content: task.content,
};
if (task.due) {
parsed.dueString = task.due.string;
}
if (task.priority > 1) {
parsed.priority = task.priority;
}
if (task.labels.length > 0) {
parsed.labels = task.labels;
}
// Get project name
if (task.projectId) {
try {
const project = await todoist.getProject(task.projectId);
if (!project.isInboxProject) {
parsed.project = project.name;
}
} catch {
// Ignore project fetch errors
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify({ task, parsed }, null, 2),
},
],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
}