Skip to main content
Glama

update_labels

Modify existing Todoist labels to change names, colors, order, or favorite status for better task organization.

Instructions

Update a personal label in Todoist

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
itemsYes

Implementation Reference

  • The core batch handler function that executes the logic for update_labels: processes multiple items, supports ID or name lookup by fetching all labels, validates path params, constructs /labels/{id} path, calls todoistApi.post with the update fields (name, order, color, is_favorite). Returns batched results with success summary.
    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,
        };
    };
  • Registration of the update_labels MCP tool via createBatchApiHandler, configuring it for batch updates to Todoist labels API endpoint /labels/{id} with POST method.
    createBatchApiHandler({
        name: 'update_labels',
        description: 'Update a personal label in Todoist',
        itemSchema: {
            id: z.string(),
            name: z.string().optional(),
            order: z.number().int().optional(),
            color: z
                .string()
                .optional()
                .describe('Refer to the name column in the `utils_get_colors` tool for more info'),
            is_favorite: z.boolean().optional(),
        },
        method: 'POST',
        path: '/labels/{id}',
        mode: 'update',
        idField: 'id',
    });
  • Input schema validation using Zod for update_labels: requires label id, optional fields for name, order, color, is_favorite.
    itemSchema: {
        id: z.string(),
        name: z.string().optional(),
        order: z.number().int().optional(),
        color: z
            .string()
            .optional()
            .describe('Refer to the name column in the `utils_get_colors` tool for more info'),
        is_favorite: z.boolean().optional(),
    },
  • createBatchApiHandler helper function that generates the schema, description refinements, and registers the batch-capable MCP tool handler based on API endpoint config.
    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);
    }

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