Skip to main content
Glama

create_comments

Add comments to Todoist tasks or projects to provide context, notes, or additional information using markdown formatting.

Instructions

Create new comments in Todoist

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
itemsYes

Implementation Reference

  • Registration of the 'create_comments' tool, including schema for input parameters (task_id, project_id, content) and configuration for batch POST to Todoist /comments endpoint.
    createBatchApiHandler({ name: 'create_comments', description: 'Create new comments in Todoist', itemSchema: { task_id: z.string().optional(), project_id: z.string().optional(), content: z.string().describe('Markdown-formatted text and hyperlinks'), // attachment: z.object({}).optional(), }, method: 'POST', path: '/comments', mode: 'create', });
  • Zod schema definition for individual comment creation items in batch mode.
    itemSchema: { task_id: z.string().optional(), project_id: z.string().optional(), content: z.string().describe('Markdown-formatted text and hyperlinks'), // attachment: z.object({}).optional(), },
  • Generic handler factory that creates the MCP tool callback for 'create_comments'. For 'create' mode, it processes batch items and calls todoistApi.post('/comments', apiParams) to create comments.
    export function createBatchApiHandler<T extends z.ZodRawShape>( options: { name: string; description: string; itemSchema: T; method: HttpMethod; mode?: 'read' | 'create' | 'update' | 'delete'; idField?: string; nameField?: string; findByName?: (name: string, items: any[]) => any | undefined; validateItem?: (item: any) => { valid: boolean; error?: string }; } & ( // Or we specify full path | { path: string; basePath?: never; pathSuffix?: never; } // Or we specify base path and path suffix | { path?: never; basePath: string; pathSuffix: string; } ) ) { // Create basic description, we cant properly use 'anyOf' here so for now we will add info to description let finalDescription = options.description; const requiresIdOrName = options.idField && options.nameField && options.mode !== 'create'; if (requiresIdOrName) { const requirementText = `\nEither '${options.idField}' or the '${options.nameField}' to identify the target.`; finalDescription += requirementText; } const itemSchemaObject = z.object(options.itemSchema); const enhancedItemSchema: z.ZodTypeAny = requiresIdOrName ? itemSchemaObject.refine( (data: any) => data[options.idField!] !== undefined || data[options.nameField!] !== undefined, { message: `Either ${options.idField} or ${options.nameField} must be provided`, path: [options.idField!, options.nameField!], } ) : itemSchemaObject; const batchSchema = z.object({ items: z.array(enhancedItemSchema), }); const handler = async (args: z.infer<typeof batchSchema>): Promise<any> => { const { items } = args; // For modes other than create, check if name lookup is needed let allItems: any[] = []; const needsNameLookup = options.mode !== 'create' && options.nameField && options.findByName && items.some(item => item[options.nameField!] && !item[options.idField!]); if (needsNameLookup) { // Determine the base path for fetching all items // Example: /tasks from /tasks/{id} const lookupPath = options.basePath || (options.path ? options.path.split('/{')[0] : ''); allItems = await todoistApi.get(lookupPath, {}); } const results = await Promise.all( items.map(async item => { if (options.validateItem) { const validation = options.validateItem(item); if (!validation.valid) { return { success: false, error: validation.error || 'Validation failed', item, }; } } try { let finalPath = ''; const apiParams = { ...item }; // For modes where need id if (options.mode !== 'create' && options.idField) { let itemId = item[options.idField]; let matchedName = null; let matchedContent = null; // If no ID but name is provided, search by name if (!itemId && item[options.nameField!] && options.findByName) { const searchName = item[options.nameField!]; const matchedItem = options.findByName(searchName, allItems); if (!matchedItem) { return { success: false, error: `Item not found with name: ${searchName}`, item, }; } itemId = matchedItem.id; matchedName = searchName; matchedContent = matchedItem.content; } if (!itemId) { return { success: false, error: `Either ${options.idField} or ${options.nameField} must be provided`, item, }; } // Apply security validation to itemId before using in path const safeItemId = validatePathParameter(itemId, options.idField || 'id'); if (options.basePath && options.pathSuffix) { finalPath = `${options.basePath}${options.pathSuffix.replace('{id}', safeItemId)}`; } else if (options.path) { finalPath = options.path.replace('{id}', safeItemId); } delete apiParams[options.idField]; if (options.nameField) { delete apiParams[options.nameField]; } let result; switch (options.method) { case 'GET': result = await todoistApi.get(finalPath, apiParams); break; case 'POST': result = await todoistApi.post(finalPath, apiParams); break; case 'DELETE': result = await todoistApi.delete(finalPath); break; } const response: any = { success: true, id: itemId, result, }; if (matchedName) { response.found_by_name = matchedName; response.matched_content = matchedContent; } return response; } // Create mode else { finalPath = options.path || options.basePath || ''; let result; switch (options.method) { case 'GET': result = await todoistApi.get(finalPath, apiParams); break; case 'POST': result = await todoistApi.post(finalPath, apiParams); break; case 'DELETE': result = await todoistApi.delete(finalPath); break; } return { success: true, created_item: result, }; } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), item, }; } }) ); const successCount = results.filter(r => r.success).length; return { success: successCount === items.length, summary: { total: items.length, succeeded: successCount, failed: items.length - successCount, }, results, }; }; return createHandler(options.name, finalDescription, batchSchema.shape, handler); }
  • Utility function that registers the tool with the MCP server using server.tool, wrapping the handler logic with error handling.
    export function createHandler<T extends z.ZodRawShape>( name: string, description: string, paramsSchema: T, handler: (args: HandlerArgs<T>) => Promise<any> ): void { const mcpToolCallback = async (args: HandlerArgs<T>): Promise<CallToolResult> => { try { const result = await handler(args); return { content: [ { type: 'text', text: JSON.stringify(result ?? null, null, 2).trim(), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error in tool ${name}:`, error); return { isError: true, content: [ { type: 'text', text: `Error executing tool '${name}': ${errorMessage}`, }, ], }; } }; // Crazy cast, if you can do it better, please, let me knows server.tool(name, description, paramsSchema, mcpToolCallback as unknown as ToolCallback<T>); }

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/stanislavlysenko0912/todoist-mcp-server'

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