Skip to main content
Glama
recipes.py19.9 kB
""" Recipe tools for Mealie MCP server. Provides tools for searching, retrieving, and listing recipes from a Mealie instance. """ import json import re import sys from pathlib import Path from typing import Optional def _slugify(text: str) -> str: """Convert text to a slug (lowercase, hyphens for spaces, no special chars).""" text = text.lower().strip() text = re.sub(r'[^\w\s-]', '', text) text = re.sub(r'[-\s]+', '-', text) return text # 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 recipes_search( query: str = "", tags: Optional[list[str]] = None, categories: Optional[list[str]] = None, limit: int = 10 ) -> str: """Search for recipes in Mealie. Args: query: Search term to filter recipes by name/description tags: List of tag names to filter by categories: List of category names to filter by limit: Maximum number of results (default 10) Returns: JSON string with list of matching recipes (name, slug, description, tags) """ try: with MealieClient() as client: # Build query parameters params = { "perPage": limit, "page": 1, } if query: params["search"] = query if tags: params["tags"] = tags if categories: params["categories"] = categories # Make API request response = client.get("/api/recipes", params=params) # Extract relevant fields for readability if isinstance(response, dict) and "items" in response: recipes = [] for recipe in response["items"]: recipes.append({ "name": recipe.get("name"), "slug": recipe.get("slug"), "description": recipe.get("description"), "rating": recipe.get("rating"), "tags": [tag.get("name") for tag in recipe.get("tags", [])], "categories": [cat.get("name") for cat in recipe.get("recipeCategory", [])], }) result = { "total": response.get("total", len(recipes)), "count": len(recipes), "recipes": recipes } return json.dumps(result, indent=2) # If response doesn't match expected format, return as-is return json.dumps(response, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_get(slug: str) -> str: """Get complete details for a specific recipe. Args: slug: The recipe's URL slug identifier Returns: JSON string with full recipe details including ingredients, instructions, nutrition """ try: with MealieClient() as client: # Make API request response = client.get(f"/api/recipes/{slug}") # Return full recipe data return json.dumps(response, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_list(page: int = 1, per_page: int = 20) -> str: """List all recipes with pagination. Args: page: Page number (1-indexed) per_page: Number of recipes per page (default 20) Returns: JSON string with paginated recipe list and metadata """ try: with MealieClient() as client: # Build query parameters params = { "page": page, "perPage": per_page, } # Make API request response = client.get("/api/recipes", params=params) # Extract pagination metadata if isinstance(response, dict): result = { "page": response.get("page", page), "per_page": response.get("perPage", per_page), "total": response.get("total", 0), "total_pages": response.get("totalPages", 0), "items": response.get("items", []) } return json.dumps(result, indent=2) # If response doesn't match expected format, return as-is return json.dumps(response, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_create( name: str, description: str = "", recipe_yield: str = "", total_time: str = "", prep_time: str = "", cook_time: str = "", ingredients: Optional[list[str]] = None, instructions: Optional[list[str]] = None, tags: Optional[list[str]] = None, categories: Optional[list[str]] = None, ) -> str: """Create a new recipe in Mealie. Args: name: Recipe name (required) description: Recipe description recipe_yield: Yield/servings (e.g., "4 servings") total_time: Total time (e.g., "1 hour 30 minutes") prep_time: Prep time (e.g., "20 minutes") cook_time: Cook time (e.g., "1 hour") ingredients: List of ingredient strings (e.g., ["2 cups flour", "1 tsp salt"]) instructions: List of instruction strings (e.g., ["Preheat oven", "Mix ingredients"]) tags: List of tag names to apply categories: List of category names to apply Returns: JSON string with created recipe details """ try: with MealieClient() as client: # Step 1: Create the recipe stub with just the name create_response = client.post("/api/recipes", json={"name": name}) # The response should be a string (the slug) if isinstance(create_response, str): slug = create_response elif isinstance(create_response, dict): slug = create_response.get("slug") or create_response.get("id") else: slug = str(create_response) # Remove surrounding quotes if present slug = slug.strip('"') # Step 2: If we have additional fields, update the recipe has_updates = any([ description, recipe_yield, total_time, prep_time, cook_time, ingredients, instructions, tags, categories ]) if has_updates: # Get the created recipe to get its full structure recipe = client.get(f"/api/recipes/{slug}") # Build update payload update_payload = { "id": recipe.get("id"), "userId": recipe.get("userId"), "householdId": recipe.get("householdId"), "groupId": recipe.get("groupId"), "name": name, "slug": slug, } if description: update_payload["description"] = description if recipe_yield: update_payload["recipeYield"] = recipe_yield if total_time: update_payload["totalTime"] = total_time if prep_time: update_payload["prepTime"] = prep_time if cook_time: update_payload["cookTime"] = cook_time # Convert simple ingredient strings to Mealie format if ingredients: update_payload["recipeIngredient"] = [ {"note": ing, "display": ing} for ing in ingredients ] # Convert simple instruction strings to Mealie format if instructions: update_payload["recipeInstructions"] = [ {"text": inst} for inst in instructions ] # Handle tags - include groupId for proper tag creation group_id = recipe.get("groupId") if tags: update_payload["tags"] = [ {"name": tag, "slug": _slugify(tag), "groupId": group_id} for tag in tags ] # Handle categories - include groupId for proper category creation if categories: update_payload["recipeCategory"] = [ {"name": cat, "slug": _slugify(cat), "groupId": group_id} for cat in categories ] # Update the recipe client.put(f"/api/recipes/{slug}", json=update_payload) # Get the final recipe to return final_recipe = client.get(f"/api/recipes/{slug}") result = { "success": True, "message": f"Recipe '{name}' created", "recipe": { "name": final_recipe.get("name"), "slug": final_recipe.get("slug"), "id": final_recipe.get("id"), "description": final_recipe.get("description"), } } return json.dumps(result, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_create_from_url(url: str, include_tags: bool = False) -> str: """Import a recipe from a URL by scraping it. Args: url: URL of the recipe to import include_tags: Whether to include tags from the scraped recipe (default False) Returns: JSON string with imported recipe details """ try: with MealieClient() as client: # Scrape the recipe from URL response = client.post( "/api/recipes/create/url", json={"url": url, "includeTags": include_tags} ) # The response should be a string (the slug) if isinstance(response, str): slug = response.strip('"') elif isinstance(response, dict): slug = response.get("slug") or response.get("id", "") else: slug = str(response).strip('"') # Get the created recipe recipe = client.get(f"/api/recipes/{slug}") result = { "success": True, "message": f"Recipe imported from URL", "recipe": { "name": recipe.get("name"), "slug": recipe.get("slug"), "id": recipe.get("id"), "description": recipe.get("description"), "orgURL": recipe.get("orgURL"), } } return json.dumps(result, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_update( slug: str, name: Optional[str] = None, description: Optional[str] = None, recipe_yield: Optional[str] = None, total_time: Optional[str] = None, prep_time: Optional[str] = None, cook_time: Optional[str] = None, ingredients: Optional[list[str]] = None, instructions: Optional[list[str]] = None, tags: Optional[list[str]] = None, categories: Optional[list[str]] = None, ) -> str: """Update an existing recipe in Mealie. Args: slug: The recipe's slug identifier (required) name: New recipe name description: New description recipe_yield: New yield/servings total_time: New total time prep_time: New prep time cook_time: New cook time ingredients: New list of ingredient strings (replaces existing) instructions: New list of instruction strings (replaces existing) tags: New list of tag names (replaces existing) categories: New list of category names (replaces existing) Returns: JSON string with updated recipe details """ try: with MealieClient() as client: # Get existing recipe recipe = client.get(f"/api/recipes/{slug}") # Build update payload preserving existing values update_payload = { "id": recipe.get("id"), "userId": recipe.get("userId"), "householdId": recipe.get("householdId"), "groupId": recipe.get("groupId"), "name": name if name is not None else recipe.get("name"), "slug": recipe.get("slug"), "description": description if description is not None else recipe.get("description"), "recipeYield": recipe_yield if recipe_yield is not None else recipe.get("recipeYield"), "totalTime": total_time if total_time is not None else recipe.get("totalTime"), "prepTime": prep_time if prep_time is not None else recipe.get("prepTime"), "cookTime": cook_time if cook_time is not None else recipe.get("cookTime"), } # Handle ingredients - replace if provided, keep existing otherwise if ingredients is not None: update_payload["recipeIngredient"] = [ {"note": ing, "display": ing} for ing in ingredients ] else: update_payload["recipeIngredient"] = recipe.get("recipeIngredient", []) # Handle instructions - replace if provided, keep existing otherwise if instructions is not None: update_payload["recipeInstructions"] = [ {"text": inst} for inst in instructions ] else: update_payload["recipeInstructions"] = recipe.get("recipeInstructions", []) # Handle tags - include groupId for proper tag creation group_id = recipe.get("groupId") if tags is not None: update_payload["tags"] = [ {"name": tag, "slug": _slugify(tag), "groupId": group_id} for tag in tags ] else: update_payload["tags"] = recipe.get("tags", []) # Handle categories - include groupId for proper category creation if categories is not None: update_payload["recipeCategory"] = [ {"name": cat, "slug": _slugify(cat), "groupId": group_id} for cat in categories ] else: update_payload["recipeCategory"] = recipe.get("recipeCategory", []) # Update the recipe client.put(f"/api/recipes/{slug}", json=update_payload) # Get the updated recipe updated_recipe = client.get(f"/api/recipes/{slug}") result = { "success": True, "message": f"Recipe '{updated_recipe.get('name')}' updated", "recipe": { "name": updated_recipe.get("name"), "slug": updated_recipe.get("slug"), "id": updated_recipe.get("id"), "description": updated_recipe.get("description"), } } return json.dumps(result, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) def recipes_delete(slug: str) -> str: """Delete a recipe from Mealie. Args: slug: The recipe's slug identifier Returns: JSON string confirming deletion """ try: with MealieClient() as client: # Get recipe name before deleting for the message try: recipe = client.get(f"/api/recipes/{slug}") recipe_name = recipe.get("name", slug) except Exception: recipe_name = slug # Delete the recipe client.delete(f"/api/recipes/{slug}") result = { "success": True, "message": f"Recipe '{recipe_name}' deleted" } return json.dumps(result, indent=2) except MealieAPIError as e: error_result = { "error": str(e), "status_code": e.status_code, "response_body": e.response_body } return json.dumps(error_result, indent=2) except Exception as e: error_result = { "error": f"Unexpected error: {str(e)}" } return json.dumps(error_result, indent=2) if __name__ == "__main__": """ Test the recipe tools against the live Mealie instance. """ from dotenv import load_dotenv print("Testing Mealie Recipe Tools...") print("=" * 60) # Load environment variables load_dotenv() # Test 1: Search for recipes (no filters) print("\n1. Testing recipes_search with no parameters...") print("-" * 60) search_result = recipes_search(limit=5) print(search_result) # Extract a slug from search results for next test search_data = json.loads(search_result) test_slug = None if "recipes" in search_data and len(search_data["recipes"]) > 0: test_slug = search_data["recipes"][0].get("slug") print(f"\nFound test slug: {test_slug}") # Test 2: Get a specific recipe if test_slug: print("\n2. Testing recipes_get with slug:", test_slug) print("-" * 60) get_result = recipes_get(test_slug) print(get_result) else: print("\n2. Skipping recipes_get test (no slug found)") # Test 3: List recipes with pagination print("\n3. Testing recipes_list with pagination...") print("-" * 60) list_result = recipes_list(page=1, per_page=3) print(list_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