import json import logging import traceback from typing import List, Optional from mcp.server.fastmcp import FastMCP from mealie import MealieFetcher from models.recipe import Recipe, RecipeIngredient, RecipeInstruction, RecipeNutrition from utils import format_error_response logger = logging.getLogger("mealie-mcp") def register_recipe_tools(mcp: FastMCP, mealie: MealieFetcher) -> None: """Register all recipe-related tools with the MCP server.""" @mcp.tool() def get_recipes( search: Optional[str] = None, page: Optional[int] = None, per_page: Optional[int] = None, categories: Optional[List[str]] = None, tags: Optional[List[str]] = None, ) -> str: """Provides a paginated list of recipes with optional filtering. Args: search: Filters recipes by name or description. page: Page number for pagination. per_page: Number of items per page. categories: Filter by specific recipe categories. tags: Filter by specific recipe tags. Returns: str: Recipe summaries with details like ID, name, description, and image information. """ try: logger.info( { "message": "Fetching recipes", "search": search, "page": page, "per_page": per_page, "categories": categories, "tags": tags, } ) result = mealie.get_recipes( search=search, page=page, per_page=per_page, categories=categories, tags=tags, ) return json.dumps(result, indent=2) except Exception as e: error_msg = f"Error fetching recipes: {str(e)}" logger.error({"message": error_msg}) logger.debug( {"message": "Error traceback", "traceback": traceback.format_exc()} ) return format_error_response(error_msg) @mcp.tool() def get_recipe_detailed(slug: str) -> str: """Retrieve a specific recipe by its slug identifier. Use this when to get full recipe details for tasks like updating or displaying the recipe. Args: slug: The unique text identifier for the recipe, typically found in recipe URLs or from get_recipes results. Returns: str: Comprehensive recipe details including ingredients, instructions, nutrition information, notes, and associated metadata. """ try: logger.info({"message": "Fetching recipe", "slug": slug}) result = mealie.get_recipe(slug) return json.dumps(result, indent=2) except Exception as e: error_msg = f"Error fetching recipe with slug '{slug}': {str(e)}" logger.error({"message": error_msg}) logger.debug( {"message": "Error traceback", "traceback": traceback.format_exc()} ) return format_error_response(error_msg) @mcp.tool() def get_recipe_concise(slug: str) -> str: """Retrieve a concise version of a specific recipe by its slug identifier. Use this when you only need a summary of the recipe, such as for when mealplaning. Args: slug: The unique text identifier for the recipe, typically found in recipe URLs or from get_recipes results. """ try: logger.info({"message": "Fetching recipe", "slug": slug}) recipe_json = mealie.get_recipe(slug) recipe = Recipe.model_validate(recipe_json) return json.dumps(recipe.model_dump( include={ "name", "slug", "recipeServings", "recipeYieldQuantity", "recipeYield", "totalTime", "rating", "recipeIngredient", "lastMade", }, exclude_none=True, ), indent=2) except Exception as e: error_msg = f"Error fetching recipe with slug '{slug}': {str(e)}" logger.error({"message": error_msg}) logger.debug( {"message": "Error traceback", "traceback": traceback.format_exc()} ) return format_error_response(error_msg) @mcp.tool() def create_recipe( name: str, ingredients: list[str], instructions: list[str], prep_time: Optional[str] = None, cook_time: Optional[str] = None, total_time: Optional[str] = None, recipe_yield: Optional[str] = None, servings: Optional[int] = None, description: Optional[str] = None, calories: Optional[str] = None, protein: Optional[str] = None, carbohydrates: Optional[str] = None, fat: Optional[str] = None, fiber: Optional[str] = None, sugar: Optional[str] = None, sodium: Optional[str] = None, ) -> str: """Create a new recipe Args: name: The name of the new recipe to be created. ingredients: A list of ingredients for the recipe include quantities and units. instructions: A list of instructions for preparing the recipe. prep_time: Preparation time (e.g., "30min", "1h", "1h 30min") cook_time: Cooking time (e.g., "45min", "1h 15min") total_time: Total time (e.g., "90min", "2h") recipe_yield: What the recipe yields (e.g., "4 servings", "12 cookies") servings: Number of servings as integer description: A brief description of the recipe calories: Calories per serving (e.g., "350") protein: Protein content (e.g., "25g") carbohydrates: Carbohydrate content (e.g., "40g") fat: Fat content (e.g., "15g") fiber: Fiber content (e.g., "5g") sugar: Sugar content (e.g., "8g") sodium: Sodium content (e.g., "600mg") Returns: str: Confirmation message or details about the created recipe. """ try: logger.info({"message": "Creating recipe", "name": name}) slug = mealie.create_recipe(name) recipe_json = mealie.get_recipe(slug) recipe = Recipe.model_validate(recipe_json) recipe.recipeIngredient = [RecipeIngredient(note=i) for i in ingredients] recipe.recipeInstructions = [ RecipeInstruction(text=i) for i in instructions ] # Set time information if provided if prep_time: recipe.prepTime = prep_time if cook_time: recipe.cookTime = cook_time if total_time: recipe.totalTime = total_time if recipe_yield: recipe.recipeYield = recipe_yield if servings: recipe.recipeServings = servings if description: recipe.description = description # Set nutrition information if provided if any([calories, protein, carbohydrates, fat, fiber, sugar, sodium]): nutrition = RecipeNutrition() if calories: nutrition.calories = calories if protein: nutrition.proteinContent = protein if carbohydrates: nutrition.carbohydrateContent = carbohydrates if fat: nutrition.fatContent = fat if fiber: nutrition.fiberContent = fiber if sugar: nutrition.sugarContent = sugar if sodium: nutrition.sodiumContent = sodium recipe.nutrition = nutrition result = mealie.update_recipe(slug, recipe.model_dump(exclude_none=True)) return json.dumps(result, indent=2) except Exception as e: error_msg = f"Error creating recipe '{name}': {str(e)}" logger.error({"message": error_msg}) logger.debug( {"message": "Error traceback", "traceback": traceback.format_exc()} ) return format_error_response(error_msg) @mcp.tool() def update_recipe( slug: str, ingredients: Optional[list[str]] = None, instructions: Optional[list[str]] = None, prep_time: Optional[str] = None, cook_time: Optional[str] = None, total_time: Optional[str] = None, recipe_yield: Optional[str] = None, servings: Optional[int] = None, description: Optional[str] = None, calories: Optional[str] = None, protein: Optional[str] = None, carbohydrates: Optional[str] = None, fat: Optional[str] = None, fiber: Optional[str] = None, sugar: Optional[str] = None, sodium: Optional[str] = None, ) -> str: """Update an existing recipe with new information. Args: slug: The unique text identifier for the recipe to be updated. ingredients: A list of ingredients for the recipe include quantities and units. instructions: A list of instructions for preparing the recipe. prep_time: Preparation time (e.g., "PT15M" for 15 minutes in ISO 8601 format) cook_time: Cooking time (e.g., "PT30M" for 30 minutes) total_time: Total time (e.g., "PT45M" for 45 minutes) recipe_yield: What the recipe yields (e.g., "4 servings") servings: Number of servings as integer description: A brief description of the recipe Returns: str: Confirmation message or details about the updated recipe. """ try: logger.info({"message": "Updating recipe", "slug": slug}) recipe_json = mealie.get_recipe(slug) recipe = Recipe.model_validate(recipe_json) if ingredients: recipe.recipeIngredient = [RecipeIngredient(note=i) for i in ingredients] if instructions: recipe.recipeInstructions = [ RecipeInstruction(text=i) for i in instructions ] if prep_time: recipe.prepTime = prep_time if cook_time: recipe.cookTime = cook_time if total_time: recipe.totalTime = total_time if recipe_yield: recipe.recipeYield = recipe_yield if servings: recipe.recipeServings = servings if description: recipe.description = description if any([calories, protein, carbohydrates, fat, fiber, sugar, sodium]): if not recipe.nutrition: recipe.nutrition = RecipeNutrition() if calories: recipe.nutrition.calories = calories if protein: recipe.nutrition.proteinContent = protein if carbohydrates: recipe.nutrition.carbohydrateContent = carbohydrates if fat: recipe.nutrition.fatContent = fat if fiber: recipe.nutrition.fiberContent = fiber if sugar: recipe.nutrition.sugarContent = sugar if sodium: recipe.nutrition.sodiumContent = sodium result = mealie.update_recipe(slug, recipe.model_dump(exclude_none=True)) return json.dumps(result, indent=2) except Exception as e: error_msg = f"Error updating recipe '{slug}': {str(e)}" logger.error({"message": error_msg}) logger.debug( {"message": "Error traceback", "traceback": traceback.format_exc()} ) return format_error_response(error_msg)