Skip to main content
Glama
kureha4

HowToCook-MCP Server

by kureha4

mcp_howtocook_recommendMeals

Generate personalized weekly meal plans and shopping lists based on dietary restrictions, allergies, and number of people to solve daily meal decisions.

Instructions

根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
allergiesNo过敏原列表,如["大蒜", "虾"]
avoidItemsNo忌口食材列表,如["葱", "姜"]
peopleCountYes用餐人数,1-10之间的整数

Implementation Reference

  • The core handler function that implements the tool logic: filters recipes excluding allergies and avoid items, categorizes them, randomly selects meals for a weekly plan (weekdays and weekends with varying counts based on peopleCount), compiles ingredients into a grocery list with categorization, and returns the JSON meal plan.
    async ({ allergies = [], avoidItems = [], peopleCount }: { 
      allergies?: string[], 
      avoidItems?: string[], 
      peopleCount: number 
    }) => {
      // 过滤掉含有忌口和过敏原的菜谱
      const filteredRecipes = recipes.filter((recipe) => {
        // 检查是否包含过敏原或忌口食材
        const hasAllergiesOrAvoidItems = recipe.ingredients?.some((ingredient) => {
          const name = ingredient.name?.toLowerCase() || '';
          return allergies.some(allergy => name.includes(allergy.toLowerCase())) || 
                 avoidItems.some(item => name.includes(item.toLowerCase()));
        });
        
        return !hasAllergiesOrAvoidItems;
      });
    
      // 将菜谱按分类分组
      const recipesByCategory: Record<string, Recipe[]> = {};
      const targetCategories = ['水产', '早餐', '荤菜', '主食'];
      
      filteredRecipes.forEach((recipe) => {
        if (targetCategories.includes(recipe.category)) {
          if (!recipesByCategory[recipe.category]) {
            recipesByCategory[recipe.category] = [];
          }
          recipesByCategory[recipe.category].push(recipe);
        }
      });
    
      // 创建每周膳食计划
      const mealPlan: MealPlan = {
        weekdays: [],
        weekend: [],
        groceryList: {
          ingredients: [],
          shoppingPlan: {
            fresh: [],
            pantry: [],
            spices: [],
            others: []
          }
        }
      };
    
      // 用于跟踪已经选择的菜谱,以便后续处理食材信息
      const selectedRecipes: Recipe[] = [];
    
      // 周一至周五
      for (let i = 0; i < 5; i++) {
        const dayPlan: DayPlan = {
          day: ['周一', '周二', '周三', '周四', '周五'][i],
          breakfast: [],
          lunch: [],
          dinner: []
        };
    
        // 早餐 - 根据人数推荐1-2个早餐菜单
        const breakfastCount = Math.max(1, Math.ceil(peopleCount / 5));
        for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
          const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
          const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
          selectedRecipes.push(selectedRecipe);
          dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
          // 避免重复,从候选列表中移除
          recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
        }
    
        // 午餐和晚餐的菜谱数量,根据人数确定
        const mealCount = Math.max(2, Math.ceil(peopleCount / 3));
        
        // 午餐
        for (let j = 0; j < mealCount; j++) {
          // 随机选择菜系:主食、水产、蔬菜、荤菜等
          const categories = ['主食', '水产', '荤菜', '素菜', '甜品'];
          let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
          
          // 如果该分类没有菜谱或已用完,尝试其他分类
          while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
            selectedCategory = categories[Math.floor(Math.random() * categories.length)];
            if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
              break; // 所有分类都没有可用菜谱,退出循环
            }
          }
          
          if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
            const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
            const selectedRecipe = recipesByCategory[selectedCategory][index];
            selectedRecipes.push(selectedRecipe);
            dayPlan.lunch.push(simplifyRecipe(selectedRecipe));
            // 避免重复,从候选列表中移除
            recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
          }
        }
        
        // 晚餐
        for (let j = 0; j < mealCount; j++) {
          // 随机选择菜系,与午餐类似但可添加汤羹
          const categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹'];
          let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
          
          // 如果该分类没有菜谱或已用完,尝试其他分类
          while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
            selectedCategory = categories[Math.floor(Math.random() * categories.length)];
            if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
              break; // 所有分类都没有可用菜谱,退出循环
            }
          }
          
          if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
            const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
            const selectedRecipe = recipesByCategory[selectedCategory][index];
            selectedRecipes.push(selectedRecipe);
            dayPlan.dinner.push(simplifyRecipe(selectedRecipe));
            // 避免重复,从候选列表中移除
            recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
          }
        }
    
        mealPlan.weekdays.push(dayPlan);
      }
    
      // 周六和周日
      for (let i = 0; i < 2; i++) {
        const dayPlan: DayPlan = {
          day: ['周六', '周日'][i],
          breakfast: [],
          lunch: [],
          dinner: []
        };
    
        // 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加
        const breakfastCount = Math.max(2, Math.ceil(peopleCount / 3));
        for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
          const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
          const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
          selectedRecipes.push(selectedRecipe);
          dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
          recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
        }
    
        // 计算工作日的基础菜品数量
        const weekdayMealCount = Math.max(2, Math.ceil(peopleCount / 3));
        // 周末菜品数量:比工作日多1-2个菜,随人数增加
        const weekendAddition = peopleCount <= 4 ? 1 : 2; // 4人以下多1个菜,4人以上多2个菜
        const mealCount = weekdayMealCount + weekendAddition;
    
        const getMeals = (count: number): SimpleRecipe[] => {
          const result: SimpleRecipe[] = [];
          const categories = ['荤菜', '水产'];
          
          // 尽量平均分配不同分类的菜品
          for (let j = 0; j < count; j++) {
            const category = categories[j % categories.length];
            if (recipesByCategory[category] && recipesByCategory[category].length > 0) {
              const index = Math.floor(Math.random() * recipesByCategory[category].length);
              const selectedRecipe = recipesByCategory[category][index];
              selectedRecipes.push(selectedRecipe);
              result.push(simplifyRecipe(selectedRecipe));
              recipesByCategory[category] = recipesByCategory[category].filter((_, idx) => idx !== index);
            } else if (recipesByCategory['主食'] && recipesByCategory['主食'].length > 0) {
              // 如果没有足够的荤菜或水产,使用主食
              const index = Math.floor(Math.random() * recipesByCategory['主食'].length);
              const selectedRecipe = recipesByCategory['主食'][index];
              selectedRecipes.push(selectedRecipe);
              result.push(simplifyRecipe(selectedRecipe));
              recipesByCategory['主食'] = recipesByCategory['主食'].filter((_, idx) => idx !== index);
            }
          }
          
          return result;
        };
    
        dayPlan.lunch = getMeals(mealCount);
        dayPlan.dinner = getMeals(mealCount);
    
        mealPlan.weekend.push(dayPlan);
      }
    
      // 统计食材清单,收集所有菜谱的所有食材
      const ingredientMap = new Map<string, {
        totalQuantity: number | null,
        unit: string | null,
        recipeCount: number,
        recipes: string[]
      }>();
    
      // 处理所有菜谱
      selectedRecipes.forEach(recipe => processRecipeIngredients(recipe, ingredientMap));
      
      // 整理食材清单
      for (const [name, info] of ingredientMap.entries()) {
        mealPlan.groceryList.ingredients.push({
          name,
          totalQuantity: info.totalQuantity,
          unit: info.unit,
          recipeCount: info.recipeCount,
          recipes: info.recipes
        });
      }
    
      // 对食材按使用频率排序
      mealPlan.groceryList.ingredients.sort((a, b) => b.recipeCount - a.recipeCount);
    
      // 生成购物计划,根据食材类型进行分类
      categorizeIngredients(mealPlan.groceryList.ingredients, mealPlan.groceryList.shoppingPlan);
    
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(mealPlan, null, 2),
          },
        ],
      };
    }
  • Zod schema defining input parameters: allergies (optional array of strings), avoidItems (optional array), peopleCount (integer 1-10). Includes Chinese descriptions.
    {
      allergies: z.array(z.string()).optional()
                 .describe('过敏原列表,如["大蒜", "虾"]'),
      avoidItems: z.array(z.string()).optional()
                  .describe('忌口食材列表,如["葱", "姜"]'),
      peopleCount: z.number().int().min(1).max(10)
                   .describe('用餐人数,1-10之间的整数')
    },
  • The registration function exported from recommendMeals.ts that defines and registers the tool with MCP server using server.tool(), including name, description, schema, and handler.
    export function registerRecommendMealsTool(server: McpServer, recipes: Recipe[]) {
      server.tool(
        "mcp_howtocook_recommendMeals",
        "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单",
        {
          allergies: z.array(z.string()).optional()
                     .describe('过敏原列表,如["大蒜", "虾"]'),
          avoidItems: z.array(z.string()).optional()
                      .describe('忌口食材列表,如["葱", "姜"]'),
          peopleCount: z.number().int().min(1).max(10)
                       .describe('用餐人数,1-10之间的整数')
        },
        async ({ allergies = [], avoidItems = [], peopleCount }: { 
          allergies?: string[], 
          avoidItems?: string[], 
          peopleCount: number 
        }) => {
          // 过滤掉含有忌口和过敏原的菜谱
          const filteredRecipes = recipes.filter((recipe) => {
            // 检查是否包含过敏原或忌口食材
            const hasAllergiesOrAvoidItems = recipe.ingredients?.some((ingredient) => {
              const name = ingredient.name?.toLowerCase() || '';
              return allergies.some(allergy => name.includes(allergy.toLowerCase())) || 
                     avoidItems.some(item => name.includes(item.toLowerCase()));
            });
            
            return !hasAllergiesOrAvoidItems;
          });
    
          // 将菜谱按分类分组
          const recipesByCategory: Record<string, Recipe[]> = {};
          const targetCategories = ['水产', '早餐', '荤菜', '主食'];
          
          filteredRecipes.forEach((recipe) => {
            if (targetCategories.includes(recipe.category)) {
              if (!recipesByCategory[recipe.category]) {
                recipesByCategory[recipe.category] = [];
              }
              recipesByCategory[recipe.category].push(recipe);
            }
          });
    
          // 创建每周膳食计划
          const mealPlan: MealPlan = {
            weekdays: [],
            weekend: [],
            groceryList: {
              ingredients: [],
              shoppingPlan: {
                fresh: [],
                pantry: [],
                spices: [],
                others: []
              }
            }
          };
    
          // 用于跟踪已经选择的菜谱,以便后续处理食材信息
          const selectedRecipes: Recipe[] = [];
    
          // 周一至周五
          for (let i = 0; i < 5; i++) {
            const dayPlan: DayPlan = {
              day: ['周一', '周二', '周三', '周四', '周五'][i],
              breakfast: [],
              lunch: [],
              dinner: []
            };
    
            // 早餐 - 根据人数推荐1-2个早餐菜单
            const breakfastCount = Math.max(1, Math.ceil(peopleCount / 5));
            for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
              const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
              const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
              selectedRecipes.push(selectedRecipe);
              dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
              // 避免重复,从候选列表中移除
              recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
            }
    
            // 午餐和晚餐的菜谱数量,根据人数确定
            const mealCount = Math.max(2, Math.ceil(peopleCount / 3));
            
            // 午餐
            for (let j = 0; j < mealCount; j++) {
              // 随机选择菜系:主食、水产、蔬菜、荤菜等
              const categories = ['主食', '水产', '荤菜', '素菜', '甜品'];
              let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
              
              // 如果该分类没有菜谱或已用完,尝试其他分类
              while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
                selectedCategory = categories[Math.floor(Math.random() * categories.length)];
                if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
                  break; // 所有分类都没有可用菜谱,退出循环
                }
              }
              
              if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
                const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
                const selectedRecipe = recipesByCategory[selectedCategory][index];
                selectedRecipes.push(selectedRecipe);
                dayPlan.lunch.push(simplifyRecipe(selectedRecipe));
                // 避免重复,从候选列表中移除
                recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
              }
            }
            
            // 晚餐
            for (let j = 0; j < mealCount; j++) {
              // 随机选择菜系,与午餐类似但可添加汤羹
              const categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹'];
              let selectedCategory = categories[Math.floor(Math.random() * categories.length)];
              
              // 如果该分类没有菜谱或已用完,尝试其他分类
              while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) {
                selectedCategory = categories[Math.floor(Math.random() * categories.length)];
                if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) {
                  break; // 所有分类都没有可用菜谱,退出循环
                }
              }
              
              if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) {
                const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length);
                const selectedRecipe = recipesByCategory[selectedCategory][index];
                selectedRecipes.push(selectedRecipe);
                dayPlan.dinner.push(simplifyRecipe(selectedRecipe));
                // 避免重复,从候选列表中移除
                recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index);
              }
            }
    
            mealPlan.weekdays.push(dayPlan);
          }
    
          // 周六和周日
          for (let i = 0; i < 2; i++) {
            const dayPlan: DayPlan = {
              day: ['周六', '周日'][i],
              breakfast: [],
              lunch: [],
              dinner: []
            };
    
            // 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加
            const breakfastCount = Math.max(2, Math.ceil(peopleCount / 3));
            for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) {
              const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length);
              const selectedRecipe = recipesByCategory['早餐'][breakfastIndex];
              selectedRecipes.push(selectedRecipe);
              dayPlan.breakfast.push(simplifyRecipe(selectedRecipe));
              recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex);
            }
    
            // 计算工作日的基础菜品数量
            const weekdayMealCount = Math.max(2, Math.ceil(peopleCount / 3));
            // 周末菜品数量:比工作日多1-2个菜,随人数增加
            const weekendAddition = peopleCount <= 4 ? 1 : 2; // 4人以下多1个菜,4人以上多2个菜
            const mealCount = weekdayMealCount + weekendAddition;
    
            const getMeals = (count: number): SimpleRecipe[] => {
              const result: SimpleRecipe[] = [];
              const categories = ['荤菜', '水产'];
              
              // 尽量平均分配不同分类的菜品
              for (let j = 0; j < count; j++) {
                const category = categories[j % categories.length];
                if (recipesByCategory[category] && recipesByCategory[category].length > 0) {
                  const index = Math.floor(Math.random() * recipesByCategory[category].length);
                  const selectedRecipe = recipesByCategory[category][index];
                  selectedRecipes.push(selectedRecipe);
                  result.push(simplifyRecipe(selectedRecipe));
                  recipesByCategory[category] = recipesByCategory[category].filter((_, idx) => idx !== index);
                } else if (recipesByCategory['主食'] && recipesByCategory['主食'].length > 0) {
                  // 如果没有足够的荤菜或水产,使用主食
                  const index = Math.floor(Math.random() * recipesByCategory['主食'].length);
                  const selectedRecipe = recipesByCategory['主食'][index];
                  selectedRecipes.push(selectedRecipe);
                  result.push(simplifyRecipe(selectedRecipe));
                  recipesByCategory['主食'] = recipesByCategory['主食'].filter((_, idx) => idx !== index);
                }
              }
              
              return result;
            };
    
            dayPlan.lunch = getMeals(mealCount);
            dayPlan.dinner = getMeals(mealCount);
    
            mealPlan.weekend.push(dayPlan);
          }
    
          // 统计食材清单,收集所有菜谱的所有食材
          const ingredientMap = new Map<string, {
            totalQuantity: number | null,
            unit: string | null,
            recipeCount: number,
            recipes: string[]
          }>();
    
          // 处理所有菜谱
          selectedRecipes.forEach(recipe => processRecipeIngredients(recipe, ingredientMap));
          
          // 整理食材清单
          for (const [name, info] of ingredientMap.entries()) {
            mealPlan.groceryList.ingredients.push({
              name,
              totalQuantity: info.totalQuantity,
              unit: info.unit,
              recipeCount: info.recipeCount,
              recipes: info.recipes
            });
          }
    
          // 对食材按使用频率排序
          mealPlan.groceryList.ingredients.sort((a, b) => b.recipeCount - a.recipeCount);
    
          // 生成购物计划,根据食材类型进行分类
          categorizeIngredients(mealPlan.groceryList.ingredients, mealPlan.groceryList.shoppingPlan);
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(mealPlan, null, 2),
              },
            ],
          };
        }
      );
    } 
  • src/index.ts:58-58 (registration)
    The call to register the recommendMealsTool in the main server instance creation within createServerInstance() function.
    registerRecommendMealsTool(server, recipes);

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed2 schema fields changedv1.0.0
    • addedInput schema / $schema
      Added value: +"http://json-schema.org/draft-07/schema#"
    • addedInput schema / additionalProperties
      Added value: +false
  2. First observed

TDQS

C2.9/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions creating a weekly meal plan and shopping list, which implies generation/mutation behavior, but doesn't specify whether this creates persistent data, requires authentication, has rate limits, or what the output format looks like. For a tool that appears to generate plans (potentially mutating state), this leaves significant behavioral gaps.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that packs substantial information: it covers the input criteria, the core recommendation function, and the two outputs (meal plan and shopping list). There's zero wasted verbiage, and it's appropriately front-loaded with the main purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no annotations and no output schema that appears to generate meal plans (potentially a mutation operation), the description is insufficient. It doesn't explain what the output looks like (e.g., format of the meal plan, structure of shopping list), whether the plan is saved or transient, or any error conditions. The completeness is inadequate given the tool's apparent complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all three parameters thoroughly. The description mentions the parameters (忌口, 过敏原, 人数) but adds no additional semantic context beyond what's in the schema. The baseline score of 3 is appropriate when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: '根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单' (Intelligently recommend recipes based on user's dietary restrictions, allergies, and number of people, create a weekly meal plan and approximate shopping list). This specifies the verb (recommend, create) and resources (recipes, meal plan, shopping list). However, it doesn't explicitly distinguish this from sibling tools like 'mcp_howtocook_whatToEat' which might have similar functionality.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention any prerequisites, constraints, or compare it to sibling tools like 'mcp_howtocook_getAllRecipes' or 'mcp_howtocook_whatToEat'. The agent must infer usage from the purpose alone without explicit direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.