Skip to main content
Glama
shopping.py21.2 kB
""" Shopping List tools for Mealie MCP server. Provides tools for managing shopping lists - creating lists, adding items, checking items off, and generating lists from meal plans. """ import json import sys from datetime import date, timedelta from pathlib import Path from typing import Optional # Handle imports for both module usage and standalone execution try: from ..client import MealieClient, MealieAPIError except ImportError: # Add parent directory to path for standalone execution sys.path.insert(0, str(Path(__file__).parent.parent)) from client import MealieClient, MealieAPIError def shopping_lists_list() -> str: """List all shopping lists. Returns: JSON string with list of shopping lists and their metadata """ try: with MealieClient() as client: response = client.get("/api/households/shopping/lists") # Handle paginated response shopping_lists = [] if response: if isinstance(response, dict) and "items" in response: shopping_lists = response["items"] elif isinstance(response, list): shopping_lists = response else: shopping_lists = [response] # Format output lists = [] for sl in shopping_lists: items = sl.get("listItems", []) checked_count = sum(1 for item in items if item.get("checked", False)) lists.append({ "id": sl.get("id"), "name": sl.get("name"), "created_at": sl.get("createdAt"), "updated_at": sl.get("updateAt"), "total_items": len(items), "checked_items": checked_count, "unchecked_items": len(items) - checked_count, }) return json.dumps({ "count": len(lists), "lists": lists }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_lists_get(list_id: str) -> str: """Get a specific shopping list with all items. Args: list_id: The shopping list ID Returns: JSON string with shopping list details and items """ try: with MealieClient() as client: response = client.get(f"/api/households/shopping/lists/{list_id}") if not response: return json.dumps({ "error": f"Shopping list '{list_id}' not found" }, indent=2) # Format items items = response.get("listItems", []) formatted_items = [] for item in items: unit = item.get("unit") food = item.get("food") formatted_items.append({ "id": item.get("id"), "checked": item.get("checked", False), "quantity": item.get("quantity"), "unit": unit.get("name") if isinstance(unit, dict) else unit, "food": food.get("name") if isinstance(food, dict) else food, "note": item.get("note"), "display": item.get("display"), }) return json.dumps({ "id": response.get("id"), "name": response.get("name"), "created_at": response.get("createdAt"), "updated_at": response.get("updateAt"), "items": formatted_items, "total_items": len(formatted_items), "checked_count": sum(1 for i in formatted_items if i["checked"]), }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_lists_create(name: str) -> str: """Create a new shopping list. Args: name: Name for the new shopping list Returns: JSON string with created shopping list details """ try: with MealieClient() as client: response = client.post("/api/households/shopping/lists", json={ "name": name }) return json.dumps({ "success": True, "message": f"Shopping list '{name}' created", "list": { "id": response.get("id"), "name": response.get("name"), "created_at": response.get("createdAt"), } }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_lists_delete(list_id: str) -> str: """Delete a shopping list. Args: list_id: The shopping list ID to delete Returns: JSON string confirming deletion """ try: with MealieClient() as client: client.delete(f"/api/households/shopping/lists/{list_id}") return json.dumps({ "success": True, "message": f"Shopping list '{list_id}' deleted" }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_items_add( list_id: str, note: Optional[str] = None, quantity: Optional[float] = None, unit_id: Optional[str] = None, food_id: Optional[str] = None, display: Optional[str] = None ) -> str: """Add an item to a shopping list. Args: list_id: The shopping list ID to add the item to note: Text description of the item (simplest way to add items) quantity: Quantity of the item unit_id: ID of the unit (e.g., cups, pounds) food_id: ID of the food from Mealie's food database display: Display text for the item Returns: JSON string with the added item """ try: with MealieClient() as client: payload = { "shoppingListId": list_id, } if note: payload["note"] = note if quantity is not None: payload["quantity"] = quantity if unit_id: payload["unitId"] = unit_id if food_id: payload["foodId"] = food_id if display: payload["display"] = display response = client.post("/api/households/shopping/items", json=payload) return json.dumps({ "success": True, "message": "Item added to shopping list", "item": response }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_items_add_bulk( list_id: str, items: list[str] ) -> str: """Add multiple items to a shopping list at once. Args: list_id: The shopping list ID to add items to items: List of item descriptions (text notes) Returns: JSON string with count of added items """ try: with MealieClient() as client: added_count = 0 errors = [] for item_text in items: try: payload = { "shoppingListId": list_id, "note": item_text, } client.post("/api/households/shopping/items", json=payload) added_count += 1 except Exception as e: errors.append({"item": item_text, "error": str(e)}) result = { "success": True, "message": f"Added {added_count} of {len(items)} items", "added_count": added_count, "total_requested": len(items), } if errors: result["errors"] = errors return json.dumps(result, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_items_check( item_id: str, checked: bool = True ) -> str: """Mark a shopping list item as checked or unchecked. Args: item_id: The shopping list item ID checked: True to mark as checked/purchased, False to uncheck Returns: JSON string confirming the update """ try: with MealieClient() as client: # First get the item to get its current data # The shopping item endpoint requires the full object for update response = client.put(f"/api/households/shopping/items/{item_id}", json={ "id": item_id, "checked": checked, }) status = "checked" if checked else "unchecked" return json.dumps({ "success": True, "message": f"Item '{item_id}' marked as {status}", "item": response }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_items_delete(item_id: str) -> str: """Remove an item from a shopping list. Args: item_id: The shopping list item ID to remove Returns: JSON string confirming deletion """ try: with MealieClient() as client: client.delete(f"/api/households/shopping/items/{item_id}") return json.dumps({ "success": True, "message": f"Item '{item_id}' removed from shopping list" }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_items_add_recipe( list_id: str, recipe_id: str, scale: float = 1.0 ) -> str: """Add all ingredients from a recipe to a shopping list. Args: list_id: The shopping list ID to add ingredients to recipe_id: The recipe ID to get ingredients from scale: Scale factor for ingredient quantities (default 1.0) Returns: JSON string with count of added items """ try: with MealieClient() as client: # Add recipe ingredients to shopping list # The endpoint expects recipeId in the body payload = { "recipeId": recipe_id, } # Add scale if not 1.0 if scale != 1.0: payload["recipeIncrementQuantity"] = scale response = client.post( f"/api/households/shopping/lists/{list_id}/recipe/{recipe_id}", json=payload ) return json.dumps({ "success": True, "message": f"Recipe ingredients added to shopping list", "list": response }, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_generate_from_mealplan( start_date: Optional[str] = None, end_date: Optional[str] = None, list_name: Optional[str] = None ) -> str: """Generate a shopping list from meal plan entries. This is the highest-value tool for household workflow - it reads the meal plan for a date range and creates a shopping list with all required ingredients. Args: start_date: Start date in YYYY-MM-DD format (defaults to today) end_date: End date in YYYY-MM-DD format (defaults to 7 days from start) list_name: Optional name for the shopping list (defaults to "Meal Plan - {date range}") Returns: JSON string with created shopping list details """ try: with MealieClient() as client: # Default to current week if no dates provided if not start_date: start = date.today() else: start = date.fromisoformat(start_date) if not end_date: end = start + timedelta(days=6) else: end = date.fromisoformat(end_date) # Get meal plans for the date range mealplan_response = client.get("/api/households/mealplans", params={ "start_date": start.isoformat(), "end_date": end.isoformat(), }) if not mealplan_response or not isinstance(mealplan_response, list): return json.dumps({ "error": "No meal plans found for the specified date range", "start_date": start.isoformat(), "end_date": end.isoformat(), }, indent=2) # Collect all recipe IDs recipe_ids = [] for entry in mealplan_response: recipe_id = entry.get("recipeId") if recipe_id: recipe_ids.append(recipe_id) if not recipe_ids: return json.dumps({ "error": "No recipes found in meal plan for the specified date range", "start_date": start.isoformat(), "end_date": end.isoformat(), }, indent=2) # Create the shopping list if not list_name: list_name = f"Meal Plan - {start.strftime('%b %d')} to {end.strftime('%b %d')}" create_response = client.post("/api/households/shopping/lists", json={ "name": list_name }) if not create_response or "id" not in create_response: return json.dumps({ "error": "Failed to create shopping list" }, indent=2) list_id = create_response["id"] # Add each recipe's ingredients to the list recipes_added = [] recipes_failed = [] for recipe_id in recipe_ids: try: client.post( f"/api/households/shopping/lists/{list_id}/recipe/{recipe_id}", json={"recipeId": recipe_id} ) recipes_added.append(recipe_id) except Exception as e: recipes_failed.append({"recipe_id": recipe_id, "error": str(e)}) # Get the final list with all items final_list = client.get(f"/api/households/shopping/lists/{list_id}") item_count = len(final_list.get("listItems", [])) if final_list else 0 result = { "success": True, "message": f"Shopping list '{list_name}' created with {item_count} items", "list_id": list_id, "list_name": list_name, "date_range": { "start": start.isoformat(), "end": end.isoformat(), }, "recipes_processed": len(recipes_added), "total_items": item_count, } if recipes_failed: result["recipes_failed"] = recipes_failed return json.dumps(result, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) def shopping_lists_clear_checked(list_id: str) -> str: """Remove all checked items from a shopping list. Args: list_id: The shopping list ID Returns: JSON string with count of removed items """ try: with MealieClient() as client: # Get the list with all items response = client.get(f"/api/households/shopping/lists/{list_id}") if not response: return json.dumps({ "error": f"Shopping list '{list_id}' not found" }, indent=2) items = response.get("listItems", []) checked_items = [item for item in items if item.get("checked", False)] if not checked_items: return json.dumps({ "success": True, "message": "No checked items to remove", "removed_count": 0 }, indent=2) # Delete each checked item removed_count = 0 errors = [] for item in checked_items: item_id = item.get("id") if item_id: try: client.delete(f"/api/households/shopping/items/{item_id}") removed_count += 1 except Exception as e: errors.append({"item_id": item_id, "error": str(e)}) result = { "success": True, "message": f"Removed {removed_count} checked items", "removed_count": removed_count, } if errors: result["errors"] = errors return json.dumps(result, indent=2) except MealieAPIError as e: return json.dumps({ "error": str(e), "status_code": e.status_code, "response_body": e.response_body }, indent=2) except Exception as e: return json.dumps({"error": f"Unexpected error: {str(e)}"}, indent=2) if __name__ == "__main__": """ Test the shopping list tools against the live Mealie instance. """ from dotenv import load_dotenv print("Testing Mealie Shopping List Tools...") print("=" * 60) # Load environment variables load_dotenv() # Test 1: List all shopping lists print("\n1. Testing shopping_lists_list...") print("-" * 60) result = shopping_lists_list() print(result) # Parse to get a list ID for further tests lists_data = json.loads(result) test_list_id = None if "lists" in lists_data and len(lists_data["lists"]) > 0: test_list_id = lists_data["lists"][0]["id"] print(f"\nFound test list ID: {test_list_id}") # Test 2: Get specific shopping list if test_list_id: print("\n2. Testing shopping_lists_get...") print("-" * 60) result = shopping_lists_get(test_list_id) print(result) else: print("\n2. Skipping shopping_lists_get (no list found)") # Test 3: Create a new list (will clean up after) print("\n3. Testing shopping_lists_create...") print("-" * 60) result = shopping_lists_create("Test List - Can Delete") print(result) # Parse to get the new list ID create_data = json.loads(result) new_list_id = None if "list" in create_data and "id" in create_data["list"]: new_list_id = create_data["list"]["id"] print(f"\nCreated test list ID: {new_list_id}") # Test 4: Add item to the new list if new_list_id: print("\n4. Testing shopping_items_add...") print("-" * 60) result = shopping_items_add(new_list_id, note="Test item - milk") print(result) # Test 5: Delete the test list if new_list_id: print("\n5. Testing shopping_lists_delete...") print("-" * 60) result = shopping_lists_delete(new_list_id) print(result) print("\n" + "=" * 60) print("All tests completed!")

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/mdlopresti/mealie-mcp'

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