Files
mcp_arch/mealie-mcp-bundle/server/tools/recipe_tools.py
2025-12-19 23:59:54 +01:00

306 lines
12 KiB
Python

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)