/**
* Task-related MCP resources
*/
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { TodoistService } from '../services/todoist-service.js';
import type { Task } from '../types/index.js';
export function registerTaskResources(server: McpServer, todoist: TodoistService) {
// ─────────────────────────────────────────────────────────────
// todoist://today
// ─────────────────────────────────────────────────────────────
server.resource(
'today',
'todoist://today',
{ description: "Today's tasks including overdue items", mimeType: 'text/markdown' },
async () => {
try {
const today = new Date();
const dateStr = today.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
// Get today's tasks and overdue
const todayTasks = await todoist.listTasks({ filter: 'today | overdue' });
// Enrich with project names
const projects = await todoist.listProjects();
const projectMap = new Map(projects.map(p => [p.id, p.name]));
for (const task of todayTasks) {
task.projectName = projectMap.get(task.projectId) || 'Inbox';
}
// Separate overdue and today
const overdueTasks = todayTasks.filter(t => t.isOverdue);
const dueTodayTasks = todayTasks.filter(t => !t.isOverdue);
const lines: string[] = [`# Today's Tasks (${dateStr})`, ''];
if (overdueTasks.length > 0) {
lines.push(`## ⚠️ Overdue (${overdueTasks.length})`);
lines.push('');
for (const task of overdueTasks) {
const priority = task.priority > 1 ? ` (P${5 - task.priority})` : '';
const daysOverdue = Math.abs(task.daysUntilDue || 0);
lines.push(`- [ ] ${task.content}${priority} - ${daysOverdue} day${daysOverdue !== 1 ? 's' : ''} overdue`);
}
lines.push('');
}
if (dueTodayTasks.length > 0) {
lines.push(`## 📋 Due Today (${dueTodayTasks.length})`);
lines.push('');
for (const task of dueTodayTasks) {
const priority = task.priority > 1 ? ` (P${5 - task.priority})` : '';
const labels = task.labels.length > 0 ? ` @${task.labels.join(' @')}` : '';
let time = '';
if (task.due?.datetime) {
const dt = new Date(task.due.datetime);
time = ` at ${dt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
}
lines.push(`- [ ] ${task.content}${priority}${labels}${time}`);
}
lines.push('');
}
if (todayTasks.length === 0) {
lines.push('🎉 No tasks due today! Enjoy your free time or pick something from your backlog.');
}
lines.push('');
lines.push(`**Total:** ${todayTasks.length} tasks`);
return {
contents: [
{
uri: 'todoist://today',
mimeType: 'text/markdown',
text: lines.join('\n'),
},
],
};
} catch (error) {
return {
contents: [
{
uri: 'todoist://today',
mimeType: 'text/markdown',
text: `Error loading today's tasks: ${(error as Error).message}`,
},
],
};
}
}
);
// ─────────────────────────────────────────────────────────────
// todoist://upcoming
// ─────────────────────────────────────────────────────────────
server.resource(
'upcoming',
'todoist://upcoming',
{ description: 'Tasks due in the next 7 days', mimeType: 'text/markdown' },
async () => {
try {
const tasks = await todoist.listTasks({ filter: '7 days' });
// Enrich with project names
const projects = await todoist.listProjects();
const projectMap = new Map(projects.map(p => [p.id, p.name]));
for (const task of tasks) {
task.projectName = projectMap.get(task.projectId) || 'Inbox';
}
// Group by day
const byDay: Map<string, Task[]> = new Map();
const today = new Date();
today.setHours(0, 0, 0, 0);
for (const task of tasks) {
if (!task.due) continue;
const dueDate = new Date(task.due.date);
const diffDays = Math.ceil((dueDate.getTime() - today.getTime()) / (24 * 60 * 60 * 1000));
let dayLabel: string;
if (diffDays < 0) {
dayLabel = 'Overdue';
} else if (diffDays === 0) {
dayLabel = 'Today';
} else if (diffDays === 1) {
dayLabel = 'Tomorrow';
} else {
dayLabel = dueDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
}
if (!byDay.has(dayLabel)) {
byDay.set(dayLabel, []);
}
byDay.get(dayLabel)!.push(task);
}
const lines: string[] = ['# Upcoming Tasks (Next 7 Days)', ''];
for (const [dayLabel, dayTasks] of byDay) {
lines.push(`## ${dayLabel} (${dayTasks.length})`);
lines.push('');
for (const task of dayTasks) {
const priority = task.priority > 1 ? ` (P${5 - task.priority})` : '';
const project = task.projectName !== 'Inbox' ? ` [${task.projectName}]` : '';
lines.push(`- [ ] ${task.content}${priority}${project}`);
}
lines.push('');
}
if (tasks.length === 0) {
lines.push('No upcoming tasks in the next 7 days.');
}
lines.push(`**Total:** ${tasks.length} tasks`);
return {
contents: [
{
uri: 'todoist://upcoming',
mimeType: 'text/markdown',
text: lines.join('\n'),
},
],
};
} catch (error) {
return {
contents: [
{
uri: 'todoist://upcoming',
mimeType: 'text/markdown',
text: `Error loading upcoming tasks: ${(error as Error).message}`,
},
],
};
}
}
);
// ─────────────────────────────────────────────────────────────
// todoist://overdue
// ─────────────────────────────────────────────────────────────
server.resource(
'overdue',
'todoist://overdue',
{ description: 'All overdue tasks requiring attention', mimeType: 'text/markdown' },
async () => {
try {
const tasks = await todoist.listTasks({ filter: 'overdue' });
// Enrich with project names
const projects = await todoist.listProjects();
const projectMap = new Map(projects.map(p => [p.id, p.name]));
for (const task of tasks) {
task.projectName = projectMap.get(task.projectId) || 'Inbox';
}
// Sort by days overdue (most overdue first)
tasks.sort((a, b) => (a.daysUntilDue || 0) - (b.daysUntilDue || 0));
const lines: string[] = ['# ⚠️ Overdue Tasks', ''];
if (tasks.length === 0) {
lines.push('🎉 No overdue tasks! Great job staying on top of things.');
} else {
lines.push(`You have **${tasks.length}** overdue task${tasks.length !== 1 ? 's' : ''} that need attention.`);
lines.push('');
for (const task of tasks) {
const priority = task.priority > 1 ? ` (P${5 - task.priority})` : '';
const daysOverdue = Math.abs(task.daysUntilDue || 0);
const project = task.projectName !== 'Inbox' ? ` [${task.projectName}]` : '';
lines.push(`- [ ] ${task.content}${priority}${project}`);
lines.push(` *${daysOverdue} day${daysOverdue !== 1 ? 's' : ''} overdue*`);
}
}
return {
contents: [
{
uri: 'todoist://overdue',
mimeType: 'text/markdown',
text: lines.join('\n'),
},
],
};
} catch (error) {
return {
contents: [
{
uri: 'todoist://overdue',
mimeType: 'text/markdown',
text: `Error loading overdue tasks: ${(error as Error).message}`,
},
],
};
}
}
);
}