Skip to main content
Glama
todoist-projects.ts11.5 kB
import { z } from 'zod'; import { TodoistApiService } from '../services/todoist-api.js'; import { CacheService } from '../services/cache.js'; import { TokenValidatorSingleton } from '../services/token-validator.js'; import { TodoistProject, APIConfiguration } from '../types/todoist.js'; import { ValidationError } from '../types/errors.js'; import { handleToolError, removeUndefinedProperties, } from '../utils/tool-helpers.js'; /** * Input schema for the todoist_projects tool * Flattened for MCP client compatibility */ const TodoistProjectsInputSchema = z.object({ action: z.enum([ 'create', 'get', 'update', 'delete', 'list', 'archive', 'unarchive', ]), // Project ID (for get, update, delete, archive, unarchive) project_id: z.string().optional(), // Create/Update fields name: z.string().optional(), parent_id: z.string().optional(), color: z.string().optional(), is_favorite: z.boolean().optional(), view_style: z.enum(['list', 'board']).optional(), // List fields include_archived: z.boolean().optional(), }); type TodoistProjectsInput = z.infer<typeof TodoistProjectsInputSchema>; /** * Output schema for the todoist_projects tool */ interface TodoistProjectsOutput { success: boolean; data?: TodoistProject | TodoistProject[] | Record<string, unknown>; message?: string; metadata?: { total_count?: number; active_count?: number; archived_count?: number; operation_time?: number; rate_limit_remaining?: number; rate_limit_reset?: string; }; error?: { code: string; message: string; details?: Record<string, unknown>; retryable: boolean; retry_after?: number; }; } /** * TodoistProjectsTool - Complete project management for Todoist * * Handles all CRUD operations on projects including: * - Creating projects with full metadata * - Reading individual projects or lists * - Updating project properties * - Deleting projects * - Archiving/unarchiving projects * - Querying projects with optional archived inclusion */ export class TodoistProjectsTool { private readonly apiService: TodoistApiService; private readonly cacheService: CacheService; constructor( apiConfig: APIConfiguration, deps: { apiService?: TodoistApiService; cacheService?: CacheService; } = {} ) { this.apiService = deps.apiService ?? new TodoistApiService(apiConfig); this.cacheService = deps.cacheService ?? new CacheService(); } /** * Get the MCP tool definition */ static getToolDefinition() { return { name: 'todoist_projects', description: 'Complete project management for Todoist - create, read, update, archive, and query projects with metadata support', inputSchema: { type: 'object' as const, properties: { action: { type: 'string', enum: [ 'create', 'get', 'update', 'delete', 'list', 'archive', 'unarchive', ], description: 'Action to perform', }, project_id: { type: 'string', description: 'Project ID (required for get/update/delete/archive/unarchive)', }, name: { type: 'string', description: 'Project name' }, parent_id: { type: 'string', description: 'Parent project ID' }, color: { type: 'string', description: 'Project color' }, is_favorite: { type: 'boolean', description: 'Mark as favorite' }, view_style: { type: 'string', enum: ['list', 'board'], description: 'View style', }, include_archived: { type: 'boolean', description: 'Include archived projects (for list)', }, }, required: ['action'], }, }; } /** * Validate that required fields are present for each action */ private validateActionRequirements(input: TodoistProjectsInput): void { switch (input.action) { case 'create': if (!input.name) throw new ValidationError('name is required for create action'); if (!input.color) throw new ValidationError('color is required for create action'); break; case 'get': case 'delete': case 'archive': case 'unarchive': if (!input.project_id) throw new ValidationError( `project_id is required for ${input.action} action` ); break; case 'update': if (!input.project_id) throw new ValidationError('project_id is required for update action'); break; case 'list': // No required fields for list break; default: throw new ValidationError('Invalid action specified'); } } /** * Execute the tool with the given input */ async execute(input: unknown): Promise<TodoistProjectsOutput> { const startTime = Date.now(); try { // Validate API token before processing request await TokenValidatorSingleton.validateOnce(); // Validate input const validatedInput = TodoistProjectsInputSchema.parse(input); // Validate action-specific required fields this.validateActionRequirements(validatedInput); let result: TodoistProjectsOutput; // Route to appropriate handler based on action switch (validatedInput.action) { case 'create': result = await this.handleCreate(validatedInput); break; case 'get': result = await this.handleGet(validatedInput); break; case 'update': result = await this.handleUpdate(validatedInput); break; case 'delete': result = await this.handleDelete(validatedInput); break; case 'list': result = await this.handleList(validatedInput); break; case 'archive': result = await this.handleArchive(validatedInput); break; case 'unarchive': result = await this.handleUnarchive(validatedInput); break; default: throw new ValidationError('Invalid action specified'); } // Add operation metadata const operationTime = Date.now() - startTime; const rateLimitStatus = this.apiService.getRateLimitStatus(); result.metadata = { ...result.metadata, operation_time: operationTime, rate_limit_remaining: rateLimitStatus.rest.remaining, rate_limit_reset: new Date( rateLimitStatus.rest.resetTime ).toISOString(), }; return result; } catch (error) { return this.handleError(error, Date.now() - startTime); } } /** * Create a new project */ private async handleCreate( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projectData = { name: input.name, parent_id: input.parent_id, color: input.color, is_favorite: input.is_favorite, view_style: input.view_style, }; // Remove undefined properties const cleanedData = removeUndefinedProperties(projectData); const project = await this.apiService.createProject(cleanedData); // Invalidate projects cache since we added a new project this.cacheService.invalidateProjects(); return { success: true, data: project, message: 'Project created successfully', }; } /** * Get a specific project by ID */ private async handleGet( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for get action'); } const project = await this.apiService.getProject(projectId); return { success: true, data: project, message: 'Project retrieved successfully', }; } /** * Update an existing project */ private async handleUpdate( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const { project_id, ...updateData } = input; if (!project_id) { throw new ValidationError('project_id is required for update action'); } // Remove undefined properties const cleanedData = removeUndefinedProperties(updateData); const project = await this.apiService.updateProject( project_id, cleanedData ); // Invalidate projects cache since we updated a project this.cacheService.invalidateProjects(); return { success: true, data: project, message: 'Project updated successfully', }; } /** * Delete a project */ private async handleDelete( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for delete action'); } await this.apiService.deleteProject(projectId); // Invalidate projects cache since we deleted a project this.cacheService.invalidateProjects(); return { success: true, message: 'Project deleted successfully', }; } /** * List projects with optional archived inclusion */ private async handleList( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projects = await this.apiService.getProjects(); // Filter projects based on archived status const filteredProjects = input.include_archived ? projects : projects.filter(project => !project.is_archived); // Calculate counts const activeCount = projects.filter(p => !p.is_archived).length; const archivedCount = projects.filter(p => p.is_archived).length; return { success: true, data: filteredProjects, message: `Retrieved ${filteredProjects.length} project(s)`, metadata: { total_count: filteredProjects.length, active_count: activeCount, archived_count: archivedCount, }, }; } /** * Archive a project */ private async handleArchive( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for archive action'); } await this.apiService.archiveProject(projectId); // Invalidate projects cache since we archived a project this.cacheService.invalidateProjects(); return { success: true, message: 'Project archived successfully', }; } /** * Unarchive a project */ private async handleUnarchive( input: TodoistProjectsInput ): Promise<TodoistProjectsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for unarchive action'); } await this.apiService.unarchiveProject(projectId); // Invalidate projects cache since we unarchived a project this.cacheService.invalidateProjects(); return { success: true, message: 'Project unarchived successfully', }; } /** * Handle errors and convert them to standardized output format */ private handleError( error: unknown, operationTime: number ): TodoistProjectsOutput { return handleToolError(error, operationTime) as TodoistProjectsOutput; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shayonpal/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server