move_projects
Move Todoist projects to different parent folders to reorganize your workspace structure. This tool helps you restructure project hierarchies by changing parent-child relationships within your Todoist account.
Instructions
Move a projects to a different parent in Todoist
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| items | Yes |
Implementation Reference
- src/tools/projects.ts:92-112 (registration)Registration and schema for the 'move_projects' tool. Defines input schema (project id/name and target parent_id), sync command type 'project_move', lookup logic for projects, and command args builder that maps to Todoist sync args {id, parent_id}.createSyncApiHandler({ name: 'move_projects', description: 'Move a projects to a different parent in Todoist', itemSchema: { id: z.string().optional().describe('ID of the project to move (preferred over name)'), name: z.string().optional().describe('Name of the project to move'), parent_id: z.string().nullable(), }, commandType: 'project_move', idField: 'id', nameField: 'name', lookupPath: '/projects', findByName: (name, items) => items.find(item => item.name.toLowerCase().includes(name.toLowerCase())), buildCommandArgs: (item, itemId) => { return { id: itemId, parent_id: item.parent_id, }; }, });
- src/utils/handlers.ts:358-506 (handler)Handler factory createSyncApiHandler that implements the core logic for 'move_projects': parses batch input, resolves project IDs by name using GET /projects, builds 'project_move' SyncCommand objects using custom buildCommandArgs, validates IDs, executes todoistApi.sync, and formats batched results with success summary.export function createSyncApiHandler<T extends z.ZodRawShape>(options: { name: string; description: string; itemSchema: T; commandType: string; // The sync command type (e.g., 'item_move') idField: string; nameField?: string; lookupPath?: string; // Configurable path for lookup findByName?: (name: string, items: any[]) => any | undefined; buildCommandArgs: (item: any, itemId: string) => Record<string, any>; validateItem?: (item: any) => { valid: boolean; error?: string }; // Optional validation }) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const batchSchema = z.object({ items: z.array(z.object(options.itemSchema)), }); const handler = async (args: z.infer<typeof batchSchema>): Promise<any> => { const { items } = args; try { // For name lookup if needed let allItems: any[] = []; const needsNameLookup = options.nameField && options.findByName && items.some(item => item[options.nameField!] && !item[options.idField]); if (needsNameLookup) { if (!options.lookupPath) { throw new Error(`Error: lookupPath must be specified for name-based lookup`); } // Generic lookup path for any resource type allItems = await todoistApi.get(options.lookupPath, {}); } // Build commands array for Sync API const commands: SyncCommand[] = []; const failedItems = []; for (const item of items) { // Optional pre-validation if (options.validateItem) { const validation = options.validateItem(item); if (!validation.valid) { failedItems.push({ success: false, error: validation.error || 'Validation failed', item, }); continue; } } let itemId = item[options.idField]; // Lookup by name if needed if (!itemId && item[options.nameField!] && options.findByName) { const searchName = item[options.nameField!]; const matchedItem = options.findByName(searchName, allItems); if (!matchedItem) { failedItems.push({ success: false, error: `Item not found with name: ${searchName}`, item, }); continue; } itemId = matchedItem.id; } if (!itemId) { failedItems.push({ success: false, error: `Either ${options.idField} or ${options.nameField} must be provided`, item, }); continue; } // Apply security validation to itemId before using in sync commands const safeItemId = validatePathParameter(itemId, options.idField); // Use the provided function to build command args with validated ID const commandArgs = options.buildCommandArgs(item, safeItemId); commands.push({ type: options.commandType, uuid: uuidv4(), args: commandArgs, }); } // If all items failed validation, return early if (failedItems.length === items.length) { return { success: false, summary: { total: items.length, succeeded: 0, failed: items.length, }, results: failedItems, }; } // Execute the sync command if any valid commands exist let syncResult = null; if (commands.length > 0) { syncResult = await todoistApi.sync(commands); } // Combine successful and failed results const successfulResults = commands.map(command => ({ success: true, id: command.args.id, command, })); const results = [...successfulResults, ...failedItems]; return { success: failedItems.length === 0, summary: { total: items.length, succeeded: items.length - failedItems.length, failed: failedItems.length, }, results, sync_result: syncResult, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } }; return createHandler( options.name, options.description, { items: z.array(z.object(options.itemSchema)) }, handler ); }
- src/utils/handlers.ts:40-77 (helper)Generic createHandler utility that wraps the specific handler function, registers it as an MCP tool via server.tool, handles JSON output formatting, and error catching.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>); }