Skip to main content
Glama
todoist-sections.ts13.2 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 { TodoistSection, APIConfiguration } from '../types/todoist.js'; import { ValidationError } from '../types/errors.js'; import { handleToolError } from '../utils/tool-helpers.js'; /** * Input schema for the todoist_sections tool * Flattened for MCP client compatibility */ const TodoistSectionsInputSchema = z.object({ action: z.enum(['create', 'get', 'update', 'delete', 'list', 'reorder']), // Section ID (for get, update, delete) section_id: z.string().optional(), // Project ID (for create, list, reorder) project_id: z.string().optional(), // Create/Update fields name: z.string().optional(), order: z.number().int().optional(), // Reorder fields section_orders: z .array( z.object({ id: z.string(), order: z.number().int(), }) ) .optional(), }); type TodoistSectionsInput = z.infer<typeof TodoistSectionsInputSchema>; /** * Output schema for the todoist_sections tool */ interface TodoistSectionsOutput { success: boolean; data?: TodoistSection | TodoistSection[] | Record<string, unknown>; message?: string; metadata?: { total_count?: number; project_name?: string; 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; }; } /** * TodoistSectionsTool - Section management within Todoist projects * * Handles all CRUD operations on sections including: * - Creating sections within projects * - Reading individual sections or lists by project * - Updating section properties * - Deleting sections * - Reordering sections within a project */ export class TodoistSectionsTool { 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_sections', description: 'Section management within Todoist projects - create, read, update, delete, and reorder sections for better task organization', inputSchema: { type: 'object' as const, properties: { action: { type: 'string', enum: ['create', 'get', 'update', 'delete', 'list', 'reorder'], description: 'Action to perform', }, section_id: { type: 'string', description: 'Section ID (required for get/update/delete)', }, project_id: { type: 'string', description: 'Project ID (required for create/list/reorder)', }, name: { type: 'string', description: 'Section name' }, order: { type: 'number', description: 'Section order' }, section_orders: { type: 'array', description: 'Section reordering array (for reorder action)', items: { type: 'object', properties: { id: { type: 'string' }, order: { type: 'number' }, }, }, }, }, required: ['action'], }, }; } /** * Validate that required fields are present for each action */ private validateActionRequirements(input: TodoistSectionsInput): void { switch (input.action) { case 'create': if (!input.name) throw new ValidationError('name is required for create action'); if (!input.project_id) throw new ValidationError('project_id is required for create action'); break; case 'get': case 'delete': if (!input.section_id) throw new ValidationError( `section_id is required for ${input.action} action` ); break; case 'update': if (!input.section_id) throw new ValidationError('section_id is required for update action'); break; case 'list': if (!input.project_id) throw new ValidationError('project_id is required for list action'); break; case 'reorder': if (!input.project_id) throw new ValidationError( 'project_id is required for reorder action' ); if (!input.section_orders || input.section_orders.length === 0) throw new ValidationError( 'section_orders is required for reorder action' ); break; default: throw new ValidationError('Invalid action specified'); } } /** * Execute the tool with the given input */ async execute(input: unknown): Promise<TodoistSectionsOutput> { const startTime = Date.now(); try { // Validate API token before processing request await TokenValidatorSingleton.validateOnce(); // Validate input const validatedInput = TodoistSectionsInputSchema.parse(input); // Validate action-specific required fields this.validateActionRequirements(validatedInput); let result: TodoistSectionsOutput; // 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 'reorder': result = await this.handleReorder(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 section */ private async handleCreate( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for create action'); } const sectionData = { name: input.name, project_id: projectId, order: input.order, }; // Remove undefined properties const cleanedData = Object.fromEntries( Object.entries(sectionData).filter(([_, value]) => value !== undefined) ); const section = await this.apiService.createSection(cleanedData); // Invalidate sections cache for this project this.cacheService.invalidateSections(projectId); return { success: true, data: section, message: 'Section created successfully', }; } /** * Get a specific section by ID */ private async handleGet( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { const sectionId = input.section_id; if (!sectionId) { throw new ValidationError('section_id is required for get action'); } const section = await this.apiService.getSection(sectionId); // Try to get project name for metadata let projectName: string | undefined; try { const projects = await this.cacheService.getProjects(); const project = projects?.find(p => p.id === section.project_id); projectName = project?.name; } catch (error) { // Ignore errors in getting project name } return { success: true, data: section, message: 'Section retrieved successfully', metadata: { project_name: projectName, }, }; } /** * Update an existing section */ private async handleUpdate( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { const { section_id, ...updateData } = input; if (!section_id) { throw new ValidationError('section_id is required for update action'); } // Remove undefined properties const cleanedData = Object.fromEntries( Object.entries(updateData).filter(([_, value]) => value !== undefined) ); const section = await this.apiService.updateSection( section_id, cleanedData ); // Invalidate sections cache for this project this.cacheService.invalidateSections(section.project_id); return { success: true, data: section, message: 'Section updated successfully', }; } /** * Delete a section */ private async handleDelete( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { // Get section first to know which project cache to invalidate let projectId: string | undefined; const sectionId = input.section_id; if (!sectionId) { throw new ValidationError('section_id is required for delete action'); } try { const section = await this.apiService.getSection(sectionId); projectId = section.project_id; } catch (error) { // Ignore errors - we'll proceed with deletion } await this.apiService.deleteSection(sectionId); // Invalidate sections cache for this project if we know the project ID if (projectId) { this.cacheService.invalidateSections(projectId); } return { success: true, message: 'Section deleted successfully', }; } /** * List sections in a project */ private async handleList( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for list action'); } const sections = await this.apiService.getSections(projectId); // Try to get project name for metadata let projectName: string | undefined; try { const projects = await this.cacheService.getProjects(); const project = projects?.find(p => p.id === projectId); projectName = project?.name; } catch (error) { // Ignore errors in getting project name } return { success: true, data: sections, message: `Retrieved ${sections.length} section(s)`, metadata: { total_count: sections.length, project_name: projectName, }, }; } /** * Reorder sections within a project */ private async handleReorder( input: TodoistSectionsInput ): Promise<TodoistSectionsOutput> { const projectId = input.project_id; if (!projectId) { throw new ValidationError('project_id is required for reorder action'); } const sectionOrders = input.section_orders; if (!sectionOrders || sectionOrders.length === 0) { throw new ValidationError( 'section_orders is required for reorder action' ); } // Validate that all section IDs exist and belong to the specified project try { const existingSections = await this.apiService.getSections(projectId); const existingSectionIds = new Set(existingSections.map(s => s.id)); const invalidSectionIds = sectionOrders .map(so => so.id) .filter(id => !existingSectionIds.has(id)); if (invalidSectionIds.length > 0) { throw new ValidationError( `Invalid section IDs: ${invalidSectionIds.join(', ')}` ); } } catch (error) { if (error instanceof ValidationError) { throw error; } // If we can't validate, proceed - API will handle invalid IDs } // Execute reorder operations sequentially to avoid conflicts for (const sectionOrder of sectionOrders) { await this.apiService.updateSection(sectionOrder.id, { order: sectionOrder.order, }); } // Invalidate sections cache for this project this.cacheService.invalidateSections(projectId); return { success: true, message: 'Sections reordered successfully', metadata: { total_count: sectionOrders.length, }, }; } /** * Handle errors and convert them to standardized output format */ private handleError( error: unknown, operationTime: number ): TodoistSectionsOutput { return handleToolError(error, operationTime) as TodoistSectionsOutput; } }

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