/**
* Project and label MCP resources
*/
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { TodoistService } from '../services/todoist-service.js';
export function registerProjectResources(server: McpServer, todoist: TodoistService) {
// ─────────────────────────────────────────────────────────────
// todoist://projects
// ─────────────────────────────────────────────────────────────
server.resource(
'projects',
'todoist://projects',
{ description: 'Project overview with task counts', mimeType: 'text/markdown' },
async () => {
try {
const projects = await todoist.listProjects();
const allTasks = await todoist.listTasks({ limit: 500 });
// Count tasks per project
const taskCounts: Map<string, { total: number; completed: number }> = new Map();
for (const project of projects) {
taskCounts.set(project.id, { total: 0, completed: 0 });
}
for (const task of allTasks) {
const counts = taskCounts.get(task.projectId);
if (counts) {
counts.total++;
if (task.isCompleted) {
counts.completed++;
}
}
}
// Build tree structure
const rootProjects = projects.filter(p => !p.parentId);
const childProjects = projects.filter(p => p.parentId);
const childrenByParent: Map<string, typeof projects> = new Map();
for (const child of childProjects) {
if (!childrenByParent.has(child.parentId!)) {
childrenByParent.set(child.parentId!, []);
}
childrenByParent.get(child.parentId!)!.push(child);
}
const lines: string[] = ['# Projects', ''];
function renderProject(project: typeof projects[0], indent: string = '') {
const counts = taskCounts.get(project.id) || { total: 0, completed: 0 };
const pending = counts.total - counts.completed;
const taskInfo = pending > 0 ? ` (${pending} task${pending !== 1 ? 's' : ''})` : ' (empty)';
const icon = project.isInboxProject ? '📥' : '📁';
lines.push(`${indent}${icon} **${project.name}**${taskInfo}`);
const children = childrenByParent.get(project.id);
if (children) {
for (const child of children) {
renderProject(child, indent + ' ');
}
}
}
// Render Inbox first
const inbox = rootProjects.find(p => p.isInboxProject);
if (inbox) {
renderProject(inbox);
lines.push('');
}
// Then other root projects
for (const project of rootProjects.filter(p => !p.isInboxProject)) {
renderProject(project);
lines.push('');
}
// Summary
const totalTasks = Array.from(taskCounts.values()).reduce((sum, c) => sum + c.total - c.completed, 0);
lines.push(`**Total:** ${projects.length} projects, ${totalTasks} pending tasks`);
return {
contents: [
{
uri: 'todoist://projects',
mimeType: 'text/markdown',
text: lines.join('\n'),
},
],
};
} catch (error) {
return {
contents: [
{
uri: 'todoist://projects',
mimeType: 'text/markdown',
text: `Error loading projects: ${(error as Error).message}`,
},
],
};
}
}
);
// ─────────────────────────────────────────────────────────────
// todoist://labels
// ─────────────────────────────────────────────────────────────
server.resource(
'labels',
'todoist://labels',
{ description: 'Available labels and usage counts', mimeType: 'text/markdown' },
async () => {
try {
const labels = await todoist.listLabels();
const allTasks = await todoist.listTasks({ limit: 500 });
// Count label usage
const labelCounts: Map<string, number> = new Map();
for (const label of labels) {
labelCounts.set(label.name, 0);
}
for (const task of allTasks) {
for (const labelName of task.labels) {
const current = labelCounts.get(labelName) || 0;
labelCounts.set(labelName, current + 1);
}
}
const lines: string[] = ['# Labels', ''];
if (labels.length === 0) {
lines.push('No labels defined.');
lines.push('');
lines.push('Use `create_label` to create labels for organizing tasks.');
} else {
// Sort by usage count
const sortedLabels = [...labels].sort((a, b) => {
const countA = labelCounts.get(a.name) || 0;
const countB = labelCounts.get(b.name) || 0;
return countB - countA;
});
for (const label of sortedLabels) {
const count = labelCounts.get(label.name) || 0;
const usage = count > 0 ? ` (${count} task${count !== 1 ? 's' : ''})` : ' (unused)';
const favorite = label.isFavorite ? ' ⭐' : '';
lines.push(`- @${label.name}${usage}${favorite}`);
}
}
lines.push('');
lines.push(`**Total:** ${labels.length} labels`);
return {
contents: [
{
uri: 'todoist://labels',
mimeType: 'text/markdown',
text: lines.join('\n'),
},
],
};
} catch (error) {
return {
contents: [
{
uri: 'todoist://labels',
mimeType: 'text/markdown',
text: `Error loading labels: ${(error as Error).message}`,
},
],
};
}
}
);
}