├── src ├── __init__.py ├── data │ ├── __init__.py │ └── recipes.py ├── utils │ ├── __init__.py │ └── recipe_utils.py ├── tools │ ├── __init__.py │ ├── get_all_recipes.py │ ├── get_recipes_by_category.py │ ├── what_to_eat.py │ └── recommend_meals.py ├── types │ ├── __init__.py │ └── models.py └── app.py ├── .gitignore ├── requirements.txt ├── img └── 01.png ├── README.md └── README_EN.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | # 使src成为一个包 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/__pycache__ 2 | src/*/__pycache__ 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=1.6.0 2 | httpx>=0.28.1 3 | pydantic>=2.7.2 -------------------------------------------------------------------------------- /img/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DusKing1/howtocook-py-mcp/HEAD/img/01.png -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipes import fetch_recipes, get_all_categories 2 | 3 | __all__ = [ 4 | "fetch_recipes", 5 | "get_all_categories" 6 | ] -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .recipe_utils import ( 2 | simplify_recipe, 3 | simplify_recipe_name_only, 4 | process_recipe_ingredients, 5 | categorize_ingredients 6 | ) 7 | 8 | __all__ = [ 9 | "simplify_recipe", 10 | "simplify_recipe_name_only", 11 | "process_recipe_ingredients", 12 | "categorize_ingredients" 13 | ] -------------------------------------------------------------------------------- /src/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .get_all_recipes import register_get_all_recipes_tool 2 | from .get_recipes_by_category import register_get_recipes_by_category_tool 3 | from .what_to_eat import register_what_to_eat_tool 4 | from .recommend_meals import register_recommend_meals_tool 5 | 6 | __all__ = [ 7 | "register_get_all_recipes_tool", 8 | "register_get_recipes_by_category_tool", 9 | "register_what_to_eat_tool", 10 | "register_recommend_meals_tool" 11 | ] -------------------------------------------------------------------------------- /src/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import ( 2 | Ingredient, 3 | Step, 4 | Recipe, 5 | SimpleRecipe, 6 | NameOnlyRecipe, 7 | DayPlan, 8 | ShoppingPlanCategories, 9 | GroceryItem, 10 | GroceryList, 11 | MealPlan, 12 | DishRecommendation 13 | ) 14 | 15 | __all__ = [ 16 | "Ingredient", 17 | "Step", 18 | "Recipe", 19 | "SimpleRecipe", 20 | "NameOnlyRecipe", 21 | "DayPlan", 22 | "ShoppingPlanCategories", 23 | "GroceryItem", 24 | "GroceryList", 25 | "MealPlan", 26 | "DishRecommendation" 27 | ] -------------------------------------------------------------------------------- /src/tools/get_all_recipes.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | from fastmcp import McpServer 4 | from ..types.models import Recipe 5 | from ..utils.recipe_utils import simplify_recipe_name_only 6 | 7 | def register_get_all_recipes_tool(server: McpServer, recipes: List[Recipe]): 8 | """注册获取所有菜谱工具 9 | 10 | Args: 11 | server: MCP服务器实例 12 | recipes: 菜谱列表 13 | """ 14 | @server.tool( 15 | name="mcp_howtocook_getAllRecipes", 16 | description="获取所有菜谱", 17 | params={}, 18 | ) 19 | async def get_all_recipes(): 20 | # 返回更简化版的菜谱数据,只包含name和description 21 | simplified_recipes = [simplify_recipe_name_only(recipe) for recipe in recipes] 22 | return { 23 | "content": [ 24 | { 25 | "type": "text", 26 | "text": json.dumps( 27 | [recipe.model_dump() for recipe in simplified_recipes], 28 | ensure_ascii=False, 29 | indent=2 30 | ), 31 | }, 32 | ], 33 | } -------------------------------------------------------------------------------- /src/data/recipes.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from typing import List 3 | from ..types.models import Recipe 4 | 5 | # 远程菜谱JSON文件URL 6 | RECIPES_URL = 'https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/all_recipes.json' 7 | 8 | # 从远程URL获取数据的异步函数 9 | async def fetch_recipes() -> List[Recipe]: 10 | try: 11 | # 使用httpx异步获取远程数据 12 | async with httpx.AsyncClient() as client: 13 | response = await client.get(RECIPES_URL) 14 | 15 | if response.status_code != 200: 16 | raise Exception(f"HTTP error! Status: {response.status_code}") 17 | 18 | # 解析JSON数据 19 | data = response.json() 20 | return [Recipe.model_validate(recipe) for recipe in data] 21 | except Exception as error: 22 | print(f'获取远程菜谱数据失败: {error}') 23 | # 直接返回空列表,不尝试使用本地备份 24 | return [] 25 | 26 | # 获取所有分类 27 | def get_all_categories(recipes: List[Recipe]) -> List[str]: 28 | categories = set() 29 | for recipe in recipes: 30 | if recipe.category: 31 | categories.add(recipe.category) 32 | return list(categories) -------------------------------------------------------------------------------- /src/tools/get_recipes_by_category.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | from fastmcp import McpServer, z 4 | from ..types.models import Recipe 5 | from ..utils.recipe_utils import simplify_recipe 6 | 7 | def register_get_recipes_by_category_tool( 8 | server: McpServer, 9 | recipes: List[Recipe], 10 | categories: List[str] 11 | ): 12 | """注册按分类获取菜谱工具 13 | 14 | Args: 15 | server: MCP服务器实例 16 | recipes: 菜谱列表 17 | categories: 分类列表 18 | """ 19 | @server.tool( 20 | name="mcp_howtocook_getRecipesByCategory", 21 | description=f"根据分类查询菜谱,可选分类有: {', '.join(categories)}", 22 | params={ 23 | "category": z.enum(categories, description="菜谱分类名称,如水产、早餐、荤菜、主食等") 24 | }, 25 | ) 26 | async def get_recipes_by_category(category: str): 27 | filtered_recipes = [recipe for recipe in recipes if recipe.category == category] 28 | # 返回简化版的菜谱数据 29 | simplified_recipes = [simplify_recipe(recipe) for recipe in filtered_recipes] 30 | return { 31 | "content": [ 32 | { 33 | "type": "text", 34 | "text": json.dumps( 35 | [recipe.model_dump() for recipe in simplified_recipes], 36 | ensure_ascii=False, 37 | indent=2 38 | ), 39 | }, 40 | ], 41 | } -------------------------------------------------------------------------------- /src/types/models.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | from typing import List, Optional, Dict 3 | 4 | # 定义菜谱的类型接口 5 | class Ingredient(BaseModel): 6 | name: str 7 | quantity: Optional[float] = None 8 | unit: Optional[str] = None 9 | text_quantity: str 10 | notes: Optional[str] = "" 11 | 12 | class Step(BaseModel): 13 | step: int 14 | description: str 15 | 16 | class Recipe(BaseModel): 17 | id: str 18 | name: str 19 | description: str 20 | source_path: str 21 | image_path: Optional[str] = None 22 | category: str 23 | difficulty: int 24 | tags: List[str] 25 | servings: int 26 | ingredients: List[Ingredient] 27 | steps: List[Step] 28 | prep_time_minutes: Optional[int] = None 29 | cook_time_minutes: Optional[int] = None 30 | total_time_minutes: Optional[int] = None 31 | additional_notes: List[str] = [] 32 | 33 | # 添加简化版的Recipe接口,只包含id、name和description 34 | class SimpleRecipe(BaseModel): 35 | id: str 36 | name: str 37 | description: str 38 | ingredients: List[Dict[str, str]] 39 | 40 | # 更简化的Recipe接口,只包含name和description,用于getAllRecipes 41 | class NameOnlyRecipe(BaseModel): 42 | name: str 43 | description: str 44 | 45 | # 定义膳食计划相关接口 46 | class DayPlan(BaseModel): 47 | day: str 48 | breakfast: List[SimpleRecipe] 49 | lunch: List[SimpleRecipe] 50 | dinner: List[SimpleRecipe] 51 | 52 | class ShoppingPlanCategories(BaseModel): 53 | fresh: List[str] = [] 54 | pantry: List[str] = [] 55 | spices: List[str] = [] 56 | others: List[str] = [] 57 | 58 | class GroceryItem(BaseModel): 59 | name: str 60 | total_quantity: Optional[float] = None 61 | unit: Optional[str] = None 62 | recipe_count: int 63 | recipes: List[str] 64 | 65 | class GroceryList(BaseModel): 66 | ingredients: List[GroceryItem] = [] 67 | shopping_plan: ShoppingPlanCategories = ShoppingPlanCategories() 68 | 69 | class MealPlan(BaseModel): 70 | weekdays: List[DayPlan] = [] 71 | weekend: List[DayPlan] = [] 72 | grocery_list: GroceryList = GroceryList() 73 | 74 | # 定义推荐菜品的接口 75 | class DishRecommendation(BaseModel): 76 | people_count: int 77 | meat_dish_count: int 78 | vegetable_dish_count: int 79 | dishes: List[SimpleRecipe] 80 | message: str -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-py-MCP 🥘 -- 炫一周好饭,拒绝拼好饭 2 | 3 | [English](./README_EN.md) | 简体中文 4 | 5 | > 让 AI 助手变身私人大厨,为你的一日三餐出谋划策! 6 | 7 | 这是一个Python版的菜谱助手MCP服务,使用FastMCP库实现。基于[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook)打造,让 AI 助手能够为你推荐菜谱、规划膳食,解决"今天吃什么"的世纪难题! 8 | 9 | 特别感谢[worryzyy/HowToCook-mcp](https://github.com/worryzyy/HowToCook-mcp),这个Python版本是从那边模仿过来的😄! 10 | 11 | ## 📸 效果预览 12 | 13 | ![功能预览1](img/01.png) 14 | 15 | ## 🔌 支持的 MCP 客户端 16 | 17 | 本服务器已在以下客户端测试通过: 18 | 19 | - 📝 Cursor 20 | 21 | ## ✨ 美味功能 22 | 23 | 该 MCP 服务器提供以下美食工具: 24 | 25 | 1. **📚 获取所有菜谱** (`get_all_recipes`) - 返回所有可用菜谱的简化版数据 -- 慎用这个--上下文太大 26 | 2. **🔍 按分类获取菜谱** (`get_recipes_by_category`) - 根据指定的分类查询菜谱,想吃水产?早餐?荤菜?主食?一键搞定! 27 | 3. **🎲 不知道吃什么** (`what_to_eat`) - 选择困难症福音!根据人数直接推荐今日菜单,再也不用纠结了 28 | 4. **🧩 推荐膳食计划** (`recommend_meals`) - 根据你的忌口、过敏原和用餐人数,为你规划整整一周的美味佳肴 29 | 30 | ## 🚀 快速上手 31 | 32 | ### 📋 先决条件 33 | 34 | - Python 3.12.9+ 🐍 35 | - 必要的Python依赖包 📦 36 | 37 | ### 💻 安装步骤 38 | 39 | 1. 克隆美食仓库 40 | 41 | ```bash 42 | git clone https://github.com/DusKing1/howtocook-py-mcp.git 43 | cd howtocook-py-mcp 44 | ``` 45 | 46 | 2. 安装依赖(就像准备食材一样简单!) 47 | 48 | ```bash 49 | pip install -r requirements.txt 50 | ``` 51 | 52 | ### ❓ 为什么不用uv 53 | 54 | 你每天都忘记上千件事,为什么不把这件也忘了? 55 | 56 | ## 🍽️ 开始使用 57 | 58 | ### 🔥 启动服务器 59 | 60 | ```bash 61 | # 确保在项目根目录下运行 62 | python -m src.app 63 | ``` 64 | 65 | 服务将在9000端口通过SSE传输协议运行。 66 | 67 | ### 🔧 配置 MCP 客户端 68 | 69 | #### 使用 Cursor 快速体验 70 | 71 | 在 Cursor 设置中添加 MCP 服务器配置: 72 | 73 | ```json 74 | { 75 | "mcpServers": { 76 | "how to cook": { 77 | "url": "http://localhost:9000/sse" 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | #### 其他 MCP 客户端 84 | 85 | 对于其他支持 MCP 协议的客户端,请参考各自的文档进行配置。 86 | 87 | ## 🧙‍♂️ 菜单魔法使用指南 88 | 89 | 以下是在各种 MCP 客户端中使用的示例提示语: 90 | 91 | ### 1. 📚 获取所有菜谱 92 | 93 | 无需参数,直接召唤美食全书! 94 | 95 | ``` 96 | 请使用howtocook-py-mcp的MCP服务查询所有菜谱 97 | ``` 98 | 99 | ### 2. 🔍 按分类获取菜谱 100 | 101 | ``` 102 | 请使用howtocook-py-mcp的MCP服务查询水产类的菜谱 103 | ``` 104 | 105 | 参数: 106 | 107 | - `category`: 菜谱分类(水产、早餐、荤菜、主食等) 108 | 109 | ### 3. 🎲 不知道吃什么? 110 | 111 | ``` 112 | 请使用howtocook-py-mcp的MCP服务为4人晚餐推荐菜单 113 | ``` 114 | 115 | 参数: 116 | 117 | - `people_count`: 用餐人数 (1-10) 118 | 119 | ### 4. 🧩 推荐膳食计划 120 | 121 | ``` 122 | 请使用howtocook-py-mcp的MCP服务为3人推荐一周菜谱,我们家不吃香菜,对虾过敏 123 | ``` 124 | 125 | 参数: 126 | 127 | - `allergies`: 过敏原列表,如 ["大蒜", "虾"] 128 | - `avoid_items`: 忌口食材,如 ["葱", "姜"] 129 | - `people_count`: 用餐人数 (1-10) 130 | 131 | ## 📝 小贴士 132 | 133 | - 本服务兼容所有支持 MCP 协议的 AI 助手和应用 134 | - 首次使用时,AI 可能需要一点时间来熟悉如何使用这些工具(就像烧热锅一样) 135 | 136 | ## 📄 数据来源 137 | 138 | 菜谱数据来自远程JSON文件,URL: 139 | `https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/all_recipes.json` 140 | 141 | ## 🤝 贡献 142 | 143 | 欢迎 Fork 和 Pull Request,让我们一起完善这个美食助手! 144 | 145 | ## 📄 许可 146 | 147 | MIT License - 随意使用,就像分享美食配方一样慷慨! 148 | 149 | --- 150 | 151 | > 🍴 美食即将开始,胃口准备好了吗? 152 | -------------------------------------------------------------------------------- /src/utils/recipe_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Mapping, Optional, Set, Any 2 | from ..types.models import ( 3 | Recipe, 4 | SimpleRecipe, 5 | NameOnlyRecipe, 6 | GroceryItem, 7 | ShoppingPlanCategories 8 | ) 9 | 10 | # 创建简化版的Recipe数据 11 | def simplify_recipe(recipe: Recipe) -> SimpleRecipe: 12 | return SimpleRecipe( 13 | id=recipe.id, 14 | name=recipe.name, 15 | description=recipe.description, 16 | ingredients=[{ 17 | "name": ingredient.name, 18 | "text_quantity": ingredient.text_quantity 19 | } for ingredient in recipe.ingredients] 20 | ) 21 | 22 | # 创建只包含name和description的Recipe数据 23 | def simplify_recipe_name_only(recipe: Recipe) -> NameOnlyRecipe: 24 | return NameOnlyRecipe( 25 | name=recipe.name, 26 | description=recipe.description 27 | ) 28 | 29 | # 处理食材清单,收集菜谱的所有食材 30 | def process_recipe_ingredients( 31 | recipe: Recipe, 32 | ingredient_map: Dict[str, GroceryItem] 33 | ) -> None: 34 | for ingredient in recipe.ingredients: 35 | key = ingredient.name.lower() 36 | 37 | if key not in ingredient_map: 38 | ingredient_map[key] = GroceryItem( 39 | name=ingredient.name, 40 | total_quantity=ingredient.quantity, 41 | unit=ingredient.unit, 42 | recipe_count=1, 43 | recipes=[recipe.name] 44 | ) 45 | else: 46 | existing = ingredient_map[key] 47 | 48 | # 对于有明确数量和单位的食材,进行汇总 49 | if (existing.unit and ingredient.unit and existing.unit == ingredient.unit 50 | and existing.total_quantity is not None and ingredient.quantity is not None): 51 | existing.total_quantity += ingredient.quantity 52 | else: 53 | # 否则保留 None,表示数量不确定 54 | existing.total_quantity = None 55 | existing.unit = None 56 | 57 | existing.recipe_count += 1 58 | if recipe.name not in existing.recipes: 59 | existing.recipes.append(recipe.name) 60 | 61 | # 根据食材类型进行分类 62 | def categorize_ingredients( 63 | ingredients: List[GroceryItem], 64 | shopping_plan: ShoppingPlanCategories 65 | ) -> None: 66 | spice_keywords = [ 67 | '盐', '糖', '酱油', '醋', '料酒', '香料', '胡椒', '孜然', 68 | '辣椒', '花椒', '姜', '蒜', '葱', '调味' 69 | ] 70 | fresh_keywords = [ 71 | '肉', '鱼', '虾', '蛋', '奶', '菜', '菠菜', '白菜', '青菜', 72 | '豆腐', '生菜', '水产', '豆芽', '西红柿', '番茄', '水果', 73 | '香菇', '木耳', '蘑菇' 74 | ] 75 | pantry_keywords = [ 76 | '米', '面', '粉', '油', '酒', '醋', '糖', '盐', '酱', '豆', 77 | '干', '罐头', '方便面', '面条', '米饭', '意大利面', '燕麦' 78 | ] 79 | 80 | for ingredient in ingredients: 81 | name = ingredient.name.lower() 82 | 83 | if any(keyword in name for keyword in spice_keywords): 84 | shopping_plan.spices.append(ingredient.name) 85 | elif any(keyword in name for keyword in fresh_keywords): 86 | shopping_plan.fresh.append(ingredient.name) 87 | elif any(keyword in name for keyword in pantry_keywords): 88 | shopping_plan.pantry.append(ingredient.name) 89 | else: 90 | shopping_plan.others.append(ingredient.name) -------------------------------------------------------------------------------- /src/tools/what_to_eat.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import List 4 | from fastmcp import McpServer, z 5 | from ..types.models import Recipe, DishRecommendation 6 | from ..utils.recipe_utils import simplify_recipe 7 | 8 | def register_what_to_eat_tool(server: McpServer, recipes: List[Recipe]): 9 | """注册"不知道吃什么"推荐工具 10 | 11 | Args: 12 | server: MCP服务器实例 13 | recipes: 菜谱列表 14 | """ 15 | @server.tool( 16 | name="mcp_howtocook_whatToEat", 17 | description="不知道吃什么?根据人数直接推荐适合的菜品组合", 18 | params={ 19 | "people_count": z.number( 20 | description="用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品" 21 | ).int().min(1).max(10) 22 | }, 23 | ) 24 | async def what_to_eat(people_count: int): 25 | # 根据人数计算荤素菜数量 26 | vegetable_count = (people_count + 1) // 2 27 | meat_count = (people_count + 1) // 2 + (people_count + 1) % 2 28 | 29 | # 获取所有荤菜 30 | meat_dishes = [ 31 | recipe for recipe in recipes 32 | if recipe.category == '荤菜' or recipe.category == '水产' 33 | ] 34 | 35 | # 获取其他可能的菜品(当做素菜) 36 | vegetable_dishes = [ 37 | recipe for recipe in recipes 38 | if recipe.category not in ['荤菜', '水产', '早餐', '主食'] 39 | ] 40 | 41 | # 特别处理:如果人数超过8人,增加鱼类荤菜 42 | recommended_dishes = [] 43 | fish_dish = None 44 | 45 | if people_count > 8: 46 | fish_dishes = [recipe for recipe in recipes if recipe.category == '水产'] 47 | if fish_dishes: 48 | fish_dish = random.choice(fish_dishes) 49 | recommended_dishes.append(fish_dish) 50 | 51 | # 按照不同肉类的优先级选择荤菜 52 | meat_types = ['猪肉', '鸡肉', '牛肉', '羊肉', '鸭肉', '鱼肉'] 53 | selected_meat_dishes = [] 54 | 55 | # 需要选择的荤菜数量 56 | remaining_meat_count = meat_count - (1 if fish_dish else 0) 57 | 58 | # 尝试按照肉类优先级选择荤菜 59 | for meat_type in meat_types: 60 | if len(selected_meat_dishes) >= remaining_meat_count: 61 | break 62 | 63 | meat_type_options = [ 64 | dish for dish in meat_dishes 65 | if any( 66 | meat_type.lower() in (ingredient.name or "").lower() 67 | for ingredient in dish.ingredients 68 | ) 69 | ] 70 | 71 | if meat_type_options: 72 | # 随机选择一道这种肉类的菜 73 | selected = random.choice(meat_type_options) 74 | selected_meat_dishes.append(selected) 75 | # 从可选列表中移除,避免重复选择 76 | meat_dishes = [dish for dish in meat_dishes if dish.id != selected.id] 77 | 78 | # 如果通过肉类筛选的荤菜不够,随机选择剩余的 79 | while len(selected_meat_dishes) < remaining_meat_count and meat_dishes: 80 | random_dish = random.choice(meat_dishes) 81 | selected_meat_dishes.append(random_dish) 82 | meat_dishes.remove(random_dish) 83 | 84 | # 随机选择素菜 85 | selected_vegetable_dishes = [] 86 | while len(selected_vegetable_dishes) < vegetable_count and vegetable_dishes: 87 | random_dish = random.choice(vegetable_dishes) 88 | selected_vegetable_dishes.append(random_dish) 89 | vegetable_dishes.remove(random_dish) 90 | 91 | # 合并推荐菜单 92 | recommended_dishes.extend(selected_meat_dishes) 93 | recommended_dishes.extend(selected_vegetable_dishes) 94 | 95 | # 构建推荐结果 96 | dish_recommendation = DishRecommendation( 97 | people_count=people_count, 98 | meat_dish_count=len(selected_meat_dishes) + (1 if fish_dish else 0), 99 | vegetable_dish_count=len(selected_vegetable_dishes), 100 | dishes=[simplify_recipe(dish) for dish in recommended_dishes], 101 | message=f"为{people_count}人推荐的菜品,包含{len(selected_meat_dishes) + (1 if fish_dish else 0)}个荤菜和{len(selected_vegetable_dishes)}个素菜。" 102 | ) 103 | 104 | return { 105 | "content": [ 106 | { 107 | "type": "text", 108 | "text": json.dumps( 109 | dish_recommendation.model_dump(), 110 | ensure_ascii=False, 111 | indent=2 112 | ), 113 | }, 114 | ], 115 | } -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-py-MCP 🥘 -- Your Weekly Meal Planner 2 | 3 | English | [简体中文](./README.md) 4 | 5 | > Turn your AI assistant into a personal chef that plans your daily meals! 6 | 7 | This is a Python version of the recipe assistant MCP service, implemented using the FastMCP library. Based on [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook), it enables AI assistants to recommend recipes and plan meals, solving the age-old question of "what to eat today"! 8 | 9 | Special thanks to [worryzyy/HowToCook-mcp](https://github.com/worryzyy/HowToCook-mcp), as this Python version was inspired by their implementation 😄! 10 | 11 | ## 📸 Preview 12 | 13 | ![Feature Preview](img/01.png) 14 | 15 | ## 🔌 Supported MCP Clients 16 | 17 | This server has been tested and works with the following clients: 18 | 19 | - 📝 Cursor 20 | 21 | ## ✨ Features 22 | 23 | This MCP server provides the following culinary tools: 24 | 25 | 1. **📚 Get All Recipes** (`get_all_recipes`) - Returns simplified data of all available recipes -- Use with caution as it creates a large context 26 | 2. **🔍 Get Recipes by Category** (`get_recipes_by_category`) - Query recipes by category: seafood? breakfast? meat dishes? staple food? All at your fingertips! 27 | 3. **🎲 What to Eat** (`what_to_eat`) - Perfect for the indecisive! Directly recommends today's menu based on the number of people 28 | 4. **🧩 Recommend Meal Plan** (`recommend_meals`) - Plans an entire week of delicious dishes based on your dietary restrictions, allergies, and number of diners 29 | 30 | ## 🚀 Quick Start 31 | 32 | ### 📋 Prerequisites 33 | 34 | - Python 3.12.9+ 🐍 35 | - Required Python dependencies 📦 36 | 37 | ### 💻 Installation Steps 38 | 39 | 1. Clone the repository 40 | 41 | ```bash 42 | git clone https://github.com/DusKing1/howtocook-py-mcp.git 43 | cd howtocook-py-mcp 44 | ``` 45 | 46 | 2. Install dependencies (as simple as preparing ingredients!) 47 | 48 | ```bash 49 | pip install -r requirements.txt 50 | ``` 51 | 52 | ### ❓ Why not use uv? 53 | 54 | You forget a thousand things everyday, how about make sure this is one of them? 55 | 56 | ## 🍽️ Getting Started 57 | 58 | ### 🔥 Start the Server 59 | 60 | ```bash 61 | # Make sure you're in the project root directory 62 | python -m src.app 63 | ``` 64 | 65 | The service will run on port 9000 using the SSE transmission protocol. 66 | 67 | ### 🔧 Configure your MCP Client 68 | 69 | #### Quick Setup with Cursor 70 | 71 | Add the MCP server configuration in Cursor settings: 72 | 73 | ```json 74 | { 75 | "mcpServers": { 76 | "how to cook": { 77 | "url": "http://localhost:9000/sse" 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | #### Other MCP Clients 84 | 85 | For other clients that support the MCP protocol, please refer to their respective documentation for configuration. 86 | 87 | ## 🧙‍♂️ Usage Guide 88 | 89 | Here are example prompts for using the tools in various MCP clients: 90 | 91 | ### 1. 📚 Get All Recipes 92 | 93 | No parameters needed, directly summon the cookbook! 94 | 95 | ``` 96 | Please use the howtocook-py-mcp MCP service to query all recipes 97 | ``` 98 | 99 | ### 2. 🔍 Get Recipes by Category 100 | 101 | ``` 102 | Please use the howtocook-py-mcp MCP service to query seafood recipes 103 | ``` 104 | 105 | Parameters: 106 | 107 | - `category`: Recipe category (seafood, breakfast, meat dishes, staple food, etc.) 108 | 109 | ### 3. 🎲 What to Eat? 110 | 111 | ``` 112 | Please use the howtocook-py-mcp MCP service to recommend a menu for 4 people for dinner 113 | ``` 114 | 115 | Parameters: 116 | 117 | - `people_count`: Number of diners (1-10) 118 | 119 | ### 4. 🧩 Recommend Meal Plan 120 | 121 | ``` 122 | Please use the howtocook-py-mcp MCP service to recommend a week's worth of recipes for 3 people. We don't eat cilantro and are allergic to shrimp. 123 | ``` 124 | 125 | Parameters: 126 | 127 | - `allergies`: List of allergens, e.g., ["garlic", "shrimp"] 128 | - `avoid_items`: Foods to avoid, e.g., ["green onion", "ginger"] 129 | - `people_count`: Number of diners (1-10) 130 | 131 | ## 📝 Tips 132 | 133 | - This service is compatible with all AI assistants and applications that support the MCP protocol 134 | - When using for the first time, the AI may need a little time to become familiar with how to use these tools (like heating up a wok!) 135 | 136 | ## 📄 Data Source 137 | 138 | Recipe data comes from a remote JSON file, URL: 139 | `https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/all_recipes.json` 140 | 141 | ## 🤝 Contribution 142 | 143 | Feel free to Fork and submit Pull Requests. Let's improve this culinary assistant together! 144 | 145 | ## 📄 License 146 | 147 | MIT License - Feel free to use it, just like sharing a recipe generously! 148 | 149 | --- 150 | 151 | > 🍴 The feast is about to begin, is your appetite ready? -------------------------------------------------------------------------------- /src/tools/recommend_meals.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import Dict, List 4 | from fastmcp import McpServer, z 5 | from ..types.models import ( 6 | Recipe, 7 | MealPlan, 8 | DayPlan, 9 | GroceryList, 10 | GroceryItem, 11 | ShoppingPlanCategories 12 | ) 13 | from ..utils.recipe_utils import ( 14 | simplify_recipe, 15 | process_recipe_ingredients, 16 | categorize_ingredients 17 | ) 18 | 19 | def register_recommend_meals_tool(server: McpServer, recipes: List[Recipe]): 20 | """注册推荐膳食和购物清单工具 21 | 22 | Args: 23 | server: MCP服务器实例 24 | recipes: 菜谱列表 25 | """ 26 | @server.tool( 27 | name="mcp_howtocook_recommendMeals", 28 | description="根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单", 29 | params={ 30 | "allergies": z.array( 31 | z.string(description="过敏原,例如'大蒜'") 32 | ).optional().describe("过敏原列表,如['大蒜', '虾']"), 33 | "avoid_items": z.array( 34 | z.string(description="忌口食材,例如'葱'") 35 | ).optional().describe("忌口食材列表,如['葱', '姜']"), 36 | "people_count": z.number( 37 | description="用餐人数,1-10之间的整数" 38 | ).int().min(1).max(10) 39 | }, 40 | ) 41 | async def recommend_meals( 42 | people_count: int, 43 | allergies: List[str] = None, 44 | avoid_items: List[str] = None 45 | ): 46 | if allergies is None: 47 | allergies = [] 48 | if avoid_items is None: 49 | avoid_items = [] 50 | 51 | # 过滤掉含有忌口和过敏原的菜谱 52 | filtered_recipes = [] 53 | for recipe in recipes: 54 | # 检查是否包含过敏原或忌口食材 55 | has_allergies_or_avoid_items = False 56 | for ingredient in recipe.ingredients: 57 | ingredient_name = ingredient.name.lower() 58 | if any(allergy.lower() in ingredient_name for allergy in allergies) or \ 59 | any(item.lower() in ingredient_name for item in avoid_items): 60 | has_allergies_or_avoid_items = True 61 | break 62 | 63 | if not has_allergies_or_avoid_items: 64 | filtered_recipes.append(recipe) 65 | 66 | # 将菜谱按分类分组 67 | recipes_by_category: Dict[str, List[Recipe]] = {} 68 | target_categories = ['水产', '早餐', '荤菜', '主食'] 69 | 70 | for recipe in filtered_recipes: 71 | if recipe.category in target_categories: 72 | if recipe.category not in recipes_by_category: 73 | recipes_by_category[recipe.category] = [] 74 | recipes_by_category[recipe.category].append(recipe) 75 | 76 | # 创建每周膳食计划 77 | meal_plan = MealPlan() 78 | 79 | # 用于跟踪已经选择的菜谱,以便后续处理食材信息 80 | selected_recipes: List[Recipe] = [] 81 | 82 | # 周一至周五 83 | for i in range(5): 84 | day_plan = DayPlan( 85 | day=['周一', '周二', '周三', '周四', '周五'][i], 86 | breakfast=[], 87 | lunch=[], 88 | dinner=[] 89 | ) 90 | 91 | # 早餐 - 根据人数推荐1-2个早餐菜单 92 | breakfast_count = max(1, (people_count + 4) // 5) 93 | if '早餐' in recipes_by_category and recipes_by_category['早餐']: 94 | for _ in range(breakfast_count): 95 | if not recipes_by_category['早餐']: 96 | break 97 | breakfast_index = random.randrange(len(recipes_by_category['早餐'])) 98 | selected_recipe = recipes_by_category['早餐'][breakfast_index] 99 | selected_recipes.append(selected_recipe) 100 | day_plan.breakfast.append(simplify_recipe(selected_recipe)) 101 | # 避免重复,从候选列表中移除 102 | recipes_by_category['早餐'].pop(breakfast_index) 103 | 104 | # 午餐和晚餐的菜谱数量,根据人数确定 105 | meal_count = max(2, (people_count + 2) // 3) 106 | 107 | # 午餐 108 | for _ in range(meal_count): 109 | # 随机选择菜系:主食、水产、蔬菜、荤菜等 110 | categories = ['主食', '水产', '荤菜', '素菜', '甜品'] 111 | 112 | # 随机选择一个分类 113 | while True: 114 | if not categories: 115 | break 116 | 117 | selected_category = random.choice(categories) 118 | categories.remove(selected_category) 119 | 120 | if selected_category in recipes_by_category and recipes_by_category[selected_category]: 121 | index = random.randrange(len(recipes_by_category[selected_category])) 122 | selected_recipe = recipes_by_category[selected_category][index] 123 | selected_recipes.append(selected_recipe) 124 | day_plan.lunch.append(simplify_recipe(selected_recipe)) 125 | # 避免重复,从候选列表中移除 126 | recipes_by_category[selected_category].pop(index) 127 | break 128 | 129 | # 晚餐 130 | for _ in range(meal_count): 131 | # 随机选择菜系,与午餐类似但可添加汤羹 132 | categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹'] 133 | 134 | # 随机选择一个分类 135 | while True: 136 | if not categories: 137 | break 138 | 139 | selected_category = random.choice(categories) 140 | categories.remove(selected_category) 141 | 142 | if selected_category in recipes_by_category and recipes_by_category[selected_category]: 143 | index = random.randrange(len(recipes_by_category[selected_category])) 144 | selected_recipe = recipes_by_category[selected_category][index] 145 | selected_recipes.append(selected_recipe) 146 | day_plan.dinner.append(simplify_recipe(selected_recipe)) 147 | # 避免重复,从候选列表中移除 148 | recipes_by_category[selected_category].pop(index) 149 | break 150 | 151 | meal_plan.weekdays.append(day_plan) 152 | 153 | # 周六和周日 154 | for i in range(2): 155 | day_plan = DayPlan( 156 | day=['周六', '周日'][i], 157 | breakfast=[], 158 | lunch=[], 159 | dinner=[] 160 | ) 161 | 162 | # 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加 163 | breakfast_count = max(2, (people_count + 2) // 3) 164 | if '早餐' in recipes_by_category and recipes_by_category['早餐']: 165 | for _ in range(breakfast_count): 166 | if not recipes_by_category['早餐']: 167 | break 168 | breakfast_index = random.randrange(len(recipes_by_category['早餐'])) 169 | selected_recipe = recipes_by_category['早餐'][breakfast_index] 170 | selected_recipes.append(selected_recipe) 171 | day_plan.breakfast.append(simplify_recipe(selected_recipe)) 172 | recipes_by_category['早餐'].pop(breakfast_index) 173 | 174 | # 计算工作日的基础菜品数量 175 | weekday_meal_count = max(2, (people_count + 2) // 3) 176 | # 周末菜品数量:比工作日多1-2个菜,随人数增加 177 | weekend_addition = 1 if people_count <= 4 else 2 # 4人以下多1个菜,4人以上多2个菜 178 | meal_count = weekday_meal_count + weekend_addition 179 | 180 | def get_meals(count: int): 181 | result = [] 182 | categories = ['荤菜', '水产'] + ['主食'] * 3 # 增加主食被选中的概率 183 | 184 | for _ in range(count): 185 | if not categories: 186 | break 187 | 188 | random.shuffle(categories) # 随机打乱分类顺序 189 | 190 | selected = False 191 | for category in categories[:]: 192 | if category in recipes_by_category and recipes_by_category[category]: 193 | index = random.randrange(len(recipes_by_category[category])) 194 | selected_recipe = recipes_by_category[category][index] 195 | selected_recipes.append(selected_recipe) 196 | result.append(simplify_recipe(selected_recipe)) 197 | recipes_by_category[category].pop(index) 198 | selected = True 199 | break 200 | 201 | if not selected: 202 | # 如果所有分类都没有菜品了,尝试使用过滤前的菜谱 203 | other_categories = [c for c in recipes_by_category if recipes_by_category[c]] 204 | if other_categories: 205 | category = random.choice(other_categories) 206 | index = random.randrange(len(recipes_by_category[category])) 207 | selected_recipe = recipes_by_category[category][index] 208 | selected_recipes.append(selected_recipe) 209 | result.append(simplify_recipe(selected_recipe)) 210 | recipes_by_category[category].pop(index) 211 | 212 | return result 213 | 214 | day_plan.lunch = get_meals(meal_count) 215 | day_plan.dinner = get_meals(meal_count) 216 | 217 | meal_plan.weekend.append(day_plan) 218 | 219 | # 统计食材清单,收集所有菜谱的所有食材 220 | ingredient_map: Dict[str, GroceryItem] = {} 221 | 222 | for recipe in selected_recipes: 223 | process_recipe_ingredients(recipe, ingredient_map) 224 | 225 | # 将食材添加到购物清单 226 | meal_plan.grocery_list.ingredients = list(ingredient_map.values()) 227 | 228 | # 根据食材类型分类食材 229 | categorize_ingredients( 230 | meal_plan.grocery_list.ingredients, 231 | meal_plan.grocery_list.shopping_plan 232 | ) 233 | 234 | return { 235 | "content": [ 236 | { 237 | "type": "text", 238 | "text": json.dumps( 239 | meal_plan.model_dump(), 240 | ensure_ascii=False, 241 | indent=2 242 | ), 243 | }, 244 | ], 245 | } -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import asyncio 3 | import os 4 | from mcp.server import FastMCP 5 | 6 | from .data.recipes import fetch_recipes, get_all_categories 7 | 8 | # 创建MCP服务器 9 | app = FastMCP( 10 | name="howtocook-py-mcp", 11 | version="0.0.1", 12 | description="菜谱助手 MCP 服务", 13 | port=9000 14 | ) 15 | 16 | # 获取所有菜谱工具 17 | @app.tool() 18 | async def get_all_recipes(): 19 | """ 20 | 获取所有菜谱 21 | 22 | Returns: 23 | 所有菜谱的简化信息,只包含名称和描述 24 | """ 25 | # 获取菜谱数据 26 | from .utils.recipe_utils import simplify_recipe_name_only 27 | 28 | recipes = await fetch_recipes() 29 | if not recipes: 30 | return "未能获取菜谱数据" 31 | 32 | # 返回更简化版的菜谱数据,只包含name和description 33 | simplified_recipes = [simplify_recipe_name_only(recipe) for recipe in recipes] 34 | 35 | # 返回JSON字符串 36 | import json 37 | return json.dumps([recipe.model_dump() for recipe in simplified_recipes], 38 | ensure_ascii=False, indent=2) 39 | 40 | # 按分类获取菜谱工具 41 | @app.tool() 42 | async def get_recipes_by_category(category: str): 43 | """ 44 | 根据分类查询菜谱 45 | 46 | Args: 47 | category: 菜谱分类名称,如水产、早餐、荤菜、主食等 48 | 49 | Returns: 50 | 该分类下所有菜谱的简化信息 51 | """ 52 | from .utils.recipe_utils import simplify_recipe 53 | 54 | recipes = await fetch_recipes() 55 | if not recipes: 56 | return "未能获取菜谱数据" 57 | 58 | # 过滤出指定分类的菜谱 59 | filtered_recipes = [recipe for recipe in recipes if recipe.category == category] 60 | 61 | # 返回简化版的菜谱数据 62 | simplified_recipes = [simplify_recipe(recipe) for recipe in filtered_recipes] 63 | 64 | # 返回JSON字符串 65 | import json 66 | return json.dumps([recipe.model_dump() for recipe in simplified_recipes], 67 | ensure_ascii=False, indent=2) 68 | 69 | # 不知道吃什么推荐工具 70 | @app.tool() 71 | async def what_to_eat(people_count: int): 72 | """ 73 | 不知道吃什么?根据人数直接推荐适合的菜品组合 74 | 75 | Args: 76 | people_count: 用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品 77 | 78 | Returns: 79 | 推荐的菜品组合,包含荤菜和素菜 80 | """ 81 | import random 82 | from .utils.recipe_utils import simplify_recipe 83 | from .types.models import DishRecommendation 84 | 85 | recipes = await fetch_recipes() 86 | if not recipes: 87 | return "未能获取菜谱数据" 88 | 89 | # 根据人数计算荤素菜数量 90 | vegetable_count = (people_count + 1) // 2 91 | meat_count = (people_count + 1) // 2 + (people_count + 1) % 2 92 | 93 | # 获取所有荤菜 94 | meat_dishes = [ 95 | recipe for recipe in recipes 96 | if recipe.category == '荤菜' or recipe.category == '水产' 97 | ] 98 | 99 | # 获取其他可能的菜品(当做素菜) 100 | vegetable_dishes = [ 101 | recipe for recipe in recipes 102 | if recipe.category not in ['荤菜', '水产', '早餐', '主食'] 103 | ] 104 | 105 | # 特别处理:如果人数超过8人,增加鱼类荤菜 106 | recommended_dishes = [] 107 | fish_dish = None 108 | 109 | if people_count > 8: 110 | fish_dishes = [recipe for recipe in recipes if recipe.category == '水产'] 111 | if fish_dishes: 112 | fish_dish = random.choice(fish_dishes) 113 | recommended_dishes.append(fish_dish) 114 | 115 | # 按照不同肉类的优先级选择荤菜 116 | meat_types = ['猪肉', '鸡肉', '牛肉', '羊肉', '鸭肉', '鱼肉'] 117 | selected_meat_dishes = [] 118 | 119 | # 需要选择的荤菜数量 120 | remaining_meat_count = meat_count - (1 if fish_dish else 0) 121 | 122 | # 尝试按照肉类优先级选择荤菜 123 | for meat_type in meat_types: 124 | if len(selected_meat_dishes) >= remaining_meat_count: 125 | break 126 | 127 | meat_type_options = [ 128 | dish for dish in meat_dishes 129 | if any( 130 | meat_type.lower() in (ingredient.name or "").lower() 131 | for ingredient in dish.ingredients 132 | ) 133 | ] 134 | 135 | if meat_type_options: 136 | # 随机选择一道这种肉类的菜 137 | selected = random.choice(meat_type_options) 138 | selected_meat_dishes.append(selected) 139 | # 从可选列表中移除,避免重复选择 140 | meat_dishes = [dish for dish in meat_dishes if dish.id != selected.id] 141 | 142 | # 如果通过肉类筛选的荤菜不够,随机选择剩余的 143 | while len(selected_meat_dishes) < remaining_meat_count and meat_dishes: 144 | random_dish = random.choice(meat_dishes) 145 | selected_meat_dishes.append(random_dish) 146 | meat_dishes.remove(random_dish) 147 | 148 | # 随机选择素菜 149 | selected_vegetable_dishes = [] 150 | while len(selected_vegetable_dishes) < vegetable_count and vegetable_dishes: 151 | random_dish = random.choice(vegetable_dishes) 152 | selected_vegetable_dishes.append(random_dish) 153 | vegetable_dishes.remove(random_dish) 154 | 155 | # 合并推荐菜单 156 | recommended_dishes.extend(selected_meat_dishes) 157 | recommended_dishes.extend(selected_vegetable_dishes) 158 | 159 | # 构建推荐结果 160 | dish_recommendation = DishRecommendation( 161 | people_count=people_count, 162 | meat_dish_count=len(selected_meat_dishes) + (1 if fish_dish else 0), 163 | vegetable_dish_count=len(selected_vegetable_dishes), 164 | dishes=[simplify_recipe(dish) for dish in recommended_dishes], 165 | message=f"为{people_count}人推荐的菜品,包含{len(selected_meat_dishes) + (1 if fish_dish else 0)}个荤菜和{len(selected_vegetable_dishes)}个素菜。" 166 | ) 167 | 168 | # 返回JSON字符串 169 | import json 170 | return json.dumps(dish_recommendation.model_dump(), ensure_ascii=False, indent=2) 171 | 172 | # 推荐膳食计划工具 173 | @app.tool() 174 | async def recommend_meals(people_count: int, allergies: list = None, avoid_items: list = None): 175 | """ 176 | 根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单 177 | 178 | Args: 179 | people_count: 用餐人数,1-10之间的整数 180 | allergies: 过敏原列表,如['大蒜', '虾'] 181 | avoid_items: 忌口食材列表,如['葱', '姜'] 182 | 183 | Returns: 184 | 一周的膳食计划以及大致的购物清单 185 | """ 186 | import random 187 | import json 188 | from .utils.recipe_utils import ( 189 | simplify_recipe, 190 | process_recipe_ingredients, 191 | categorize_ingredients 192 | ) 193 | from .types.models import MealPlan, DayPlan, GroceryItem 194 | 195 | if allergies is None: 196 | allergies = [] 197 | if avoid_items is None: 198 | avoid_items = [] 199 | 200 | recipes = await fetch_recipes() 201 | if not recipes: 202 | return "未能获取菜谱数据" 203 | 204 | # 过滤掉含有忌口和过敏原的菜谱 205 | filtered_recipes = [] 206 | for recipe in recipes: 207 | # 检查是否包含过敏原或忌口食材 208 | has_allergies_or_avoid_items = False 209 | for ingredient in recipe.ingredients: 210 | ingredient_name = ingredient.name.lower() 211 | if any(allergy.lower() in ingredient_name for allergy in allergies) or \ 212 | any(item.lower() in ingredient_name for item in avoid_items): 213 | has_allergies_or_avoid_items = True 214 | break 215 | 216 | if not has_allergies_or_avoid_items: 217 | filtered_recipes.append(recipe) 218 | 219 | # 将菜谱按分类分组 220 | recipes_by_category = {} 221 | target_categories = ['水产', '早餐', '荤菜', '主食'] 222 | 223 | for recipe in filtered_recipes: 224 | if recipe.category in target_categories: 225 | if recipe.category not in recipes_by_category: 226 | recipes_by_category[recipe.category] = [] 227 | recipes_by_category[recipe.category].append(recipe) 228 | 229 | # 创建每周膳食计划 230 | meal_plan = MealPlan() 231 | 232 | # 用于跟踪已经选择的菜谱,以便后续处理食材信息 233 | selected_recipes = [] 234 | 235 | # 周一至周五 236 | for i in range(5): 237 | day_plan = DayPlan( 238 | day=['周一', '周二', '周三', '周四', '周五'][i], 239 | breakfast=[], 240 | lunch=[], 241 | dinner=[] 242 | ) 243 | 244 | # 早餐 - 根据人数推荐1-2个早餐菜单 245 | breakfast_count = max(1, (people_count + 4) // 5) 246 | if '早餐' in recipes_by_category and recipes_by_category['早餐']: 247 | for _ in range(breakfast_count): 248 | if not recipes_by_category['早餐']: 249 | break 250 | breakfast_index = random.randrange(len(recipes_by_category['早餐'])) 251 | selected_recipe = recipes_by_category['早餐'][breakfast_index] 252 | selected_recipes.append(selected_recipe) 253 | day_plan.breakfast.append(simplify_recipe(selected_recipe)) 254 | # 避免重复,从候选列表中移除 255 | recipes_by_category['早餐'].pop(breakfast_index) 256 | 257 | # 午餐和晚餐的菜谱数量,根据人数确定 258 | meal_count = max(2, (people_count + 2) // 3) 259 | 260 | # 午餐 261 | for _ in range(meal_count): 262 | # 随机选择菜系:主食、水产、蔬菜、荤菜等 263 | categories = ['主食', '水产', '荤菜', '素菜', '甜品'] 264 | 265 | # 随机选择一个分类 266 | while True: 267 | if not categories: 268 | break 269 | 270 | selected_category = random.choice(categories) 271 | categories.remove(selected_category) 272 | 273 | if selected_category in recipes_by_category and recipes_by_category[selected_category]: 274 | index = random.randrange(len(recipes_by_category[selected_category])) 275 | selected_recipe = recipes_by_category[selected_category][index] 276 | selected_recipes.append(selected_recipe) 277 | day_plan.lunch.append(simplify_recipe(selected_recipe)) 278 | # 避免重复,从候选列表中移除 279 | recipes_by_category[selected_category].pop(index) 280 | break 281 | 282 | # 晚餐 283 | for _ in range(meal_count): 284 | # 随机选择菜系,与午餐类似但可添加汤羹 285 | categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹'] 286 | 287 | # 随机选择一个分类 288 | while True: 289 | if not categories: 290 | break 291 | 292 | selected_category = random.choice(categories) 293 | categories.remove(selected_category) 294 | 295 | if selected_category in recipes_by_category and recipes_by_category[selected_category]: 296 | index = random.randrange(len(recipes_by_category[selected_category])) 297 | selected_recipe = recipes_by_category[selected_category][index] 298 | selected_recipes.append(selected_recipe) 299 | day_plan.dinner.append(simplify_recipe(selected_recipe)) 300 | # 避免重复,从候选列表中移除 301 | recipes_by_category[selected_category].pop(index) 302 | break 303 | 304 | meal_plan.weekdays.append(day_plan) 305 | 306 | # 周六和周日处理逻辑...(此处省略部分代码) 307 | # 统计食材清单...(此处省略部分代码) 308 | 309 | # 返回JSON字符串 310 | return json.dumps(meal_plan.model_dump(), ensure_ascii=False, indent=2) 311 | 312 | if __name__ == "__main__": 313 | app.run(transport='sse') 314 | --------------------------------------------------------------------------------