Skip to main content
Glama

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
NameRequiredDescriptionDefault
itemsYes

Implementation Reference

  • 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,
            };
        },
    });
  • 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
        );
    }
  • 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>);
    }

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