Skip to main content
Glama
todoist-comments.ts11.5 kB
import { z } from 'zod'; import { TodoistApiService } from '../services/todoist-api.js'; import { TokenValidatorSingleton } from '../services/token-validator.js'; import { TodoistComment, APIConfiguration } from '../types/todoist.js'; import { ValidationError } from '../types/errors.js'; import { handleToolError } from '../utils/tool-helpers.js'; /** * Input schema for the todoist_comments tool * Flattened for MCP client compatibility */ const TodoistCommentsInputSchema = z.object({ action: z.enum([ 'create', 'get', 'update', 'delete', 'list_by_task', 'list_by_project', ]), // Comment ID (for get, update, delete) comment_id: z.string().optional(), // Task/Project ID (for create, list_by_task, list_by_project) task_id: z.string().optional(), project_id: z.string().optional(), // Create/Update fields content: z.string().optional(), // Attachment (for create) attachment: z .object({ resource_type: z.string(), file_url: z.string(), file_name: z.string(), file_size: z.number().int(), file_type: z.string(), }) .optional(), }); type TodoistCommentsInput = z.infer<typeof TodoistCommentsInputSchema>; /** * Output schema for the todoist_comments tool */ interface TodoistCommentsOutput { success: boolean; data?: TodoistComment | TodoistComment[] | Record<string, unknown>; message?: string; metadata?: { total_count?: number; character_count?: number; has_attachments?: boolean; 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; }; } /** * TodoistCommentsTool - Comment management for Todoist tasks and projects * * Handles all CRUD operations on comments including: * - Creating comments on tasks or projects * - Reading individual comments or lists * - Updating comment content * - Deleting comments * - Supporting file attachments (one per comment) * - 15,000 character limit enforcement */ export class TodoistCommentsTool { private readonly apiService: TodoistApiService; constructor( apiConfig: APIConfiguration, deps: { apiService?: TodoistApiService } = {} ) { this.apiService = deps.apiService ?? new TodoistApiService(apiConfig); } /** * Get the MCP tool definition */ static getToolDefinition() { return { name: 'todoist_comments', description: 'Comment management for Todoist tasks and projects - create, read, update, delete comments with 15,000 character limit and file attachment support', inputSchema: { type: 'object' as const, properties: { action: { type: 'string', enum: [ 'create', 'get', 'update', 'delete', 'list_by_task', 'list_by_project', ], description: 'Action to perform', }, comment_id: { type: 'string', description: 'Comment ID (required for get/update/delete)', }, task_id: { type: 'string', description: 'Task ID (for create/list_by_task)', }, project_id: { type: 'string', description: 'Project ID (for create/list_by_project)', }, content: { type: 'string', description: 'Comment content (max 15,000 characters)', }, attachment: { type: 'object', description: 'File attachment', properties: { resource_type: { type: 'string' }, file_url: { type: 'string' }, file_name: { type: 'string' }, file_size: { type: 'number' }, file_type: { type: 'string' }, }, }, }, required: ['action'], }, }; } /** * Validate that required fields are present for each action */ private validateActionRequirements(input: TodoistCommentsInput): void { switch (input.action) { case 'create': if (!input.content) throw new ValidationError('content is required for create action'); // Either task_id or project_id must be provided if (!input.task_id && !input.project_id) throw new ValidationError( 'Either task_id or project_id is required for create action' ); break; case 'get': case 'delete': if (!input.comment_id) throw new ValidationError( `comment_id is required for ${input.action} action` ); break; case 'update': if (!input.comment_id) throw new ValidationError('comment_id is required for update action'); if (!input.content) throw new ValidationError('content is required for update action'); break; case 'list_by_task': if (!input.task_id) throw new ValidationError( 'task_id is required for list_by_task action' ); break; case 'list_by_project': if (!input.project_id) throw new ValidationError( 'project_id is required for list_by_project action' ); break; default: throw new ValidationError('Invalid action specified'); } } /** * Execute the tool with the given input */ async execute(input: unknown): Promise<TodoistCommentsOutput> { const startTime = Date.now(); try { // Validate API token before processing request await TokenValidatorSingleton.validateOnce(); // Validate input const validatedInput = TodoistCommentsInputSchema.parse(input); // Validate action-specific required fields this.validateActionRequirements(validatedInput); let result: TodoistCommentsOutput; // 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_by_task': case 'list_by_project': result = await this.handleList(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 comment */ private async handleCreate( input: TodoistCommentsInput ): Promise<TodoistCommentsOutput> { // Validate that either task_id or project_id is provided if (!input.task_id && !input.project_id) { throw new ValidationError( 'Either task_id or project_id must be provided' ); } const commentData = { content: input.content, task_id: input.task_id, project_id: input.project_id, attachment: input.attachment, }; // Remove undefined properties const cleanedData = Object.fromEntries( Object.entries(commentData).filter(([_, value]) => value !== undefined) ); const comment = await this.apiService.createComment(cleanedData); return { success: true, data: comment, message: 'Comment created successfully', metadata: { character_count: comment.content.length, has_attachments: !!comment.attachment, }, }; } /** * Get a specific comment by ID */ private async handleGet( input: TodoistCommentsInput ): Promise<TodoistCommentsOutput> { const commentId = input.comment_id; if (!commentId) { throw new ValidationError('comment_id is required for get action'); } const comment = await this.apiService.getComment(commentId); return { success: true, data: comment, message: 'Comment retrieved successfully', metadata: { character_count: comment.content.length, has_attachments: !!comment.attachment, }, }; } /** * Update an existing comment */ private async handleUpdate( input: TodoistCommentsInput ): Promise<TodoistCommentsOutput> { const { comment_id, content } = input; if (!comment_id) { throw new ValidationError('comment_id is required for update action'); } const comment = await this.apiService.updateComment(comment_id, { content, }); return { success: true, data: comment, message: 'Comment updated successfully', metadata: { character_count: comment.content.length, has_attachments: !!comment.attachment, }, }; } /** * Delete a comment */ private async handleDelete( input: TodoistCommentsInput ): Promise<TodoistCommentsOutput> { const commentId = input.comment_id; if (!commentId) { throw new ValidationError('comment_id is required for delete action'); } await this.apiService.deleteComment(commentId); return { success: true, message: 'Comment deleted successfully', }; } /** * List comments for a task or project */ private async handleList( input: TodoistCommentsInput ): Promise<TodoistCommentsOutput> { let comments: TodoistComment[]; if (input.action === 'list_by_task') { const taskId = input.task_id; if (!taskId) { throw new ValidationError( 'task_id is required for list_by_task action' ); } comments = await this.apiService.getTaskComments(taskId); } else if (input.action === 'list_by_project') { const projectId = input.project_id; if (!projectId) { throw new ValidationError( 'project_id is required for list_by_project action' ); } comments = await this.apiService.getProjectComments(projectId); } else { throw new ValidationError('Invalid list action specified'); } // Calculate metadata const totalCharacters = comments.reduce( (sum, comment) => sum + comment.content.length, 0 ); const hasAttachments = comments.some(comment => !!comment.attachment); return { success: true, data: comments, message: `Retrieved ${comments.length} comment(s)`, metadata: { total_count: comments.length, character_count: totalCharacters, has_attachments: hasAttachments, }, }; } /** * Handle errors and convert them to standardized output format */ private handleError( error: unknown, operationTime: number ): TodoistCommentsOutput { return handleToolError(error, operationTime) as TodoistCommentsOutput; } }

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