Add Recipe Ingredients to List
add_recipe_ingredients_to_listAdd recipe ingredients to a shopping list while automatically checking for duplicates and managing previously checked-off items.
Instructions
Add all ingredients from a single recipe to a shopping list. Defaults to the "Groceries" list. Checks for duplicates: skips items already on the list, unchecks previously checked-off items.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| recipe_name | No | Recipe name (case-insensitive match) | |
| recipe_id | No | Recipe identifier | |
| list_name | No | Target list name (defaults to "Groceries") | Groceries |
Implementation Reference
- src/tools/groceries.ts:5-39 (handler)The `addRecipeIngredientsToList` function contains the core logic for processing recipe ingredients and adding them to the list, including handling duplicates and updating existing items.
async function addRecipeIngredientsToList( client: AnyListClient, recipe: { name: string; ingredients: Array<{ name: string; rawIngredient: string; quantity: string }> }, list: { getItemByName(name: string): { checked: boolean; save(): Promise<void> } | undefined; addItem(item: any): Promise<any> }, ): Promise<{ added: string[]; skipped: string[]; unchecked: string[] }> { const added: string[] = []; const skipped: string[] = []; const unchecked: string[] = []; for (const ingredient of recipe.ingredients) { const ingredientName = ingredient.name || ingredient.rawIngredient; if (!ingredientName) continue; const existing = list.getItemByName(ingredientName); if (existing) { if (existing.checked) { existing.checked = false; await existing.save(); unchecked.push(ingredientName); } else { skipped.push(ingredientName); } continue; } const item = client.createItem({ name: ingredientName, quantity: ingredient.quantity || undefined, }); await list.addItem(item); added.push(ingredientName); } return { added, skipped, unchecked }; } - src/tools/groceries.ts:54-111 (registration)The `add_recipe_ingredients_to_list` tool is registered in the `registerGroceryTools` function, which maps the tool name to an async handler that uses `addRecipeIngredientsToList` and provides input schema validation via Zod.
export function registerGroceryTools(server: McpServer) { server.registerTool( 'add_recipe_ingredients_to_list', { title: 'Add Recipe Ingredients to List', description: 'Add all ingredients from a single recipe to a shopping list. Defaults to the "Groceries" list. Checks for duplicates: skips items already on the list, unchecks previously checked-off items.', inputSchema: z.object({ recipe_name: z.string().optional().describe('Recipe name (case-insensitive match)'), recipe_id: z.string().optional().describe('Recipe identifier'), list_name: z.string().default('Groceries').describe('Target list name (defaults to "Groceries")'), }), }, async ({ recipe_name, recipe_id, list_name }) => { try { if (!recipe_name && !recipe_id) { return { content: [{ type: 'text', text: 'Error: provide either recipe_name or recipe_id' }], isError: true, }; } const client = AnyListClient.getInstance(); const recipe = await resolveRecipe(client, recipe_name, recipe_id); if (!recipe) { return { content: [{ type: 'text', text: `Recipe not found: ${recipe_name ?? recipe_id}` }], isError: true, }; } await client.getLists(); const list = client.getListByName(list_name); if (!list) { return { content: [{ type: 'text', text: `List not found: "${list_name}"` }], isError: true, }; } const result = await addRecipeIngredientsToList(client, recipe, list); const lines: string[] = [`Ingredients from "${recipe.name}" → "${list_name}":`]; if (result.added.length) lines.push(` Added: ${result.added.join(', ')}`); if (result.unchecked.length) lines.push(` Unchecked: ${result.unchecked.join(', ')}`); if (result.skipped.length) lines.push(` Already on list: ${result.skipped.join(', ')}`); return { content: [{ type: 'text', text: lines.join('\n') }], }; } catch (error) { return { content: [{ type: 'text', text: `Error adding ingredients: ${error instanceof Error ? error.message : String(error)}` }], isError: true, }; } }, );