├── .gitignore ├── public ├── dxt.png ├── dxt2.png ├── dxt3.png ├── wechat.jpg └── edgeone.png ├── HowToCook-mcp.dxt ├── tsconfig.json ├── src ├── tools │ ├── getAllRecipes.ts │ ├── getRecipesByCategory.ts │ ├── getRecipeById.ts │ ├── whatToEat.ts │ └── recommendMeals.ts ├── data │ └── recipes.ts ├── types │ └── index.ts ├── utils │ └── recipeUtils.ts └── index.ts ├── manifest.json ├── package.json ├── README.md └── README_EN.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /public/dxt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/public/dxt.png -------------------------------------------------------------------------------- /public/dxt2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/public/dxt2.png -------------------------------------------------------------------------------- /public/dxt3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/public/dxt3.png -------------------------------------------------------------------------------- /HowToCook-mcp.dxt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/HowToCook-mcp.dxt -------------------------------------------------------------------------------- /public/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/public/wechat.jpg -------------------------------------------------------------------------------- /public/edgeone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worryzyy/HowToCook-mcp/HEAD/public/edgeone.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /src/tools/getAllRecipes.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Recipe } from "../types/index.js"; 3 | import { simplifyRecipeNameOnly } from "../utils/recipeUtils.js"; 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | 6 | export function registerGetAllRecipesTool(server: McpServer, recipes: Recipe[]) { 7 | server.tool( 8 | "mcp_howtocook_getAllRecipes", 9 | "获取所有菜谱", 10 | { 11 | 'no_param': z.string().optional() 12 | .describe('无参数') 13 | }, 14 | async () => { 15 | // 返回更简化版的菜谱数据,只包含name和description 16 | const simplifiedRecipes = recipes.map(simplifyRecipeNameOnly); 17 | return { 18 | content: [ 19 | { 20 | type: "text", 21 | text: JSON.stringify(simplifiedRecipes, null, 2), 22 | }, 23 | ], 24 | }; 25 | } 26 | ); 27 | } -------------------------------------------------------------------------------- /src/data/recipes.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | import { fileURLToPath } from 'node:url' 3 | import { dirname, join } from 'node:path' 4 | import { Recipe } from '../types/index.js' 5 | 6 | // 获取本地JSON文件路径 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | const LOCAL_RECIPES_PATH = join(__dirname, '../../all_recipes.json') 10 | 11 | // 从本地文件加载菜谱数据 12 | export function fetchRecipes(): Recipe[] { 13 | try { 14 | const data = readFileSync(LOCAL_RECIPES_PATH, 'utf-8') 15 | return JSON.parse(data) as Recipe[] 16 | } catch (error) { 17 | console.error('加载菜谱数据失败:', error) 18 | return [] 19 | } 20 | } 21 | 22 | // 获取所有分类 23 | export function getAllCategories(recipes: Recipe[]): string[] { 24 | const categories = new Set() 25 | recipes.forEach((recipe) => { 26 | if (recipe.category) { 27 | categories.add(recipe.category) 28 | } 29 | }) 30 | return Array.from(categories) 31 | } 32 | -------------------------------------------------------------------------------- /src/tools/getRecipesByCategory.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Recipe } from "../types/index.js"; 3 | import { simplifyRecipe } from "../utils/recipeUtils.js"; 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | 6 | export function registerGetRecipesByCategoryTool(server: McpServer, recipes: Recipe[], categories: string[]) { 7 | server.tool( 8 | "mcp_howtocook_getRecipesByCategory", 9 | `根据分类查询菜谱,可选分类有: ${categories.join(', ')}`, 10 | { 11 | category: z.enum(categories as [string, ...string[]]) 12 | .describe('菜谱分类名称,如水产、早餐、荤菜、主食等') 13 | }, 14 | async ({ category }: { category: string }) => { 15 | const filteredRecipes = recipes.filter((recipe) => recipe.category === category); 16 | // 返回简化版的菜谱数据 17 | const simplifiedRecipes = filteredRecipes.map(simplifyRecipe); 18 | return { 19 | content: [ 20 | { 21 | type: "text", 22 | text: JSON.stringify(simplifiedRecipes, null, 2), 23 | }, 24 | ], 25 | }; 26 | } 27 | ); 28 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dxt_version": "0.1", 3 | "name": "howtocook-mcp", 4 | "version": "0.1.1", 5 | "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭", 6 | "author": { 7 | "name": "worry", 8 | "email": "weileihhh@gmail.com", 9 | "url": "https://github.com/worryzyy" 10 | }, 11 | "homepage": "https://howtocookmcp.weilei.site", 12 | "documentation": "https://github.com/worryzyy/HowToCook-mcp/blob/master/README.md", 13 | "server": { 14 | "type": "node", 15 | "entry_point": "build/index.js", 16 | "mcp_config": { 17 | "command": "node", 18 | "args": [ 19 | "${__dirname}/build/index.js" 20 | ], 21 | "env": {} 22 | } 23 | }, 24 | "tools": [ 25 | { 26 | "name": "mcp_howtocook_getAllRecipes", 27 | "description": "获取所有菜谱" 28 | }, 29 | { 30 | "name": "mcp_howtocook_getRecipeById", 31 | "description": "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等" 32 | }, 33 | { 34 | "name": "mcp_howtocook_getRecipesByCategory", 35 | "description": "根据分类查询菜谱,菜谱分类名称,如水产、早餐、荤菜、主食等" 36 | }, 37 | { 38 | "name": "mcp_howtocook_recommendMeals", 39 | "description": "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单" 40 | }, 41 | { 42 | "name": "mcp_howtocook_whatToEat", 43 | "description": "不知道吃什么?根据人数直接推荐适合的菜品组合" 44 | } 45 | ], 46 | "license": "ISC", 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/worryzyy/HowToCook-mcp" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "howtocook-mcp", 3 | "version": "0.2.0", 4 | "type": "module", 5 | "main": "build/index.js", 6 | "bin": { 7 | "howtocook-mcp": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build", 11 | "all_recipes.json", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "build": "tsc", 16 | "start": "node build/index.js", 17 | "start:stdio": "node build/index.js --transport stdio", 18 | "start:http": "node build/index.js --transport http", 19 | "start:sse": "node build/index.js --transport sse", 20 | "dev": "tsc && node build/index.js", 21 | "dev:stdio": "tsc && node build/index.js --transport stdio", 22 | "dev:http": "tsc && node build/index.js --transport http", 23 | "dev:sse": "tsc && node build/index.js --transport sse", 24 | "prepublishOnly": "npm run build", 25 | "publish:npm": "npm publish", 26 | "publish:patch": "npm version patch && npm publish", 27 | "publish:minor": "npm version minor && npm publish", 28 | "publish:major": "npm version major && npm publish" 29 | }, 30 | "keywords": [ 31 | "howtocook", 32 | "mcp", 33 | "server", 34 | "recipe", 35 | "food", 36 | "cook" 37 | ], 38 | "author": "worry", 39 | "license": "ISC", 40 | "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭", 41 | "dependencies": { 42 | "@modelcontextprotocol/sdk": "^1.9.0", 43 | "@types/express": "^5.0.3", 44 | "commander": "^14.0.0", 45 | "express": "^5.1.0", 46 | "zod": "^3.22.4" 47 | }, 48 | "devDependencies": { 49 | "@types/node": "^20.11.24", 50 | "typescript": "^5.3.3" 51 | }, 52 | "publishConfig": { 53 | "access": "public", 54 | "registry": "https://registry.npmjs.org" 55 | } 56 | } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // 定义菜谱的类型接口 2 | export interface Ingredient { 3 | name: string; 4 | quantity: number | null; 5 | unit: string | null; 6 | text_quantity: string; 7 | notes: string; 8 | } 9 | 10 | export interface Step { 11 | step: number; 12 | description: string; 13 | } 14 | 15 | export interface Recipe { 16 | id: string; 17 | name: string; 18 | description: string; 19 | source_path: string; 20 | image_path: string | null; 21 | category: string; 22 | difficulty: number; 23 | tags: string[]; 24 | servings: number; 25 | ingredients: Ingredient[]; 26 | steps: Step[]; 27 | prep_time_minutes: number | null; 28 | cook_time_minutes: number | null; 29 | total_time_minutes: number | null; 30 | additional_notes: string[]; 31 | } 32 | 33 | // 添加简化版的Recipe接口,只包含id、name和description 34 | export interface SimpleRecipe { 35 | id: string; 36 | name: string; 37 | description: string; 38 | ingredients: { 39 | name: string; 40 | text_quantity: string; 41 | }[]; 42 | } 43 | 44 | // 更简化的Recipe接口,只包含name和description,用于getAllRecipes 45 | export interface NameOnlyRecipe { 46 | name: string; 47 | description: string; 48 | } 49 | 50 | // 定义膳食计划相关接口 51 | export interface MealPlan { 52 | weekdays: Array; 53 | weekend: Array; 54 | groceryList: GroceryList; 55 | } 56 | 57 | export interface DayPlan { 58 | day: string; 59 | breakfast: SimpleRecipe[]; 60 | lunch: SimpleRecipe[]; 61 | dinner: SimpleRecipe[]; 62 | } 63 | 64 | export interface GroceryList { 65 | ingredients: Array<{ 66 | name: string; 67 | totalQuantity: number | null; 68 | unit: string | null; 69 | recipeCount: number; 70 | recipes: string[]; 71 | }>; 72 | shoppingPlan: { 73 | fresh: string[]; 74 | pantry: string[]; 75 | spices: string[]; 76 | others: string[]; 77 | }; 78 | } 79 | 80 | // 定义推荐菜品的接口 81 | export interface DishRecommendation { 82 | peopleCount: number; 83 | meatDishCount: number; 84 | vegetableDishCount: number; 85 | dishes: SimpleRecipe[]; 86 | message: string; 87 | } -------------------------------------------------------------------------------- /src/tools/getRecipeById.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Recipe } from "../types/index.js"; 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | 5 | export function registerGetRecipeByIdTool(server: McpServer, recipes: Recipe[]) { 6 | server.tool( 7 | "mcp_howtocook_getRecipeById", 8 | "根据菜谱名称或ID查询指定菜谱的完整详情,包括食材、步骤等", 9 | { 10 | query: z.string().describe('菜谱名称或ID,支持模糊匹配菜谱名称') 11 | }, 12 | async ({ query }: { query: string }) => { 13 | // 首先尝试精确匹配ID 14 | let foundRecipe = recipes.find(recipe => recipe.id === query); 15 | 16 | // 如果没有找到,尝试精确匹配名称 17 | if (!foundRecipe) { 18 | foundRecipe = recipes.find(recipe => recipe.name === query); 19 | } 20 | 21 | // 如果还没有找到,尝试模糊匹配名称 22 | if (!foundRecipe) { 23 | foundRecipe = recipes.find(recipe => 24 | recipe.name.toLowerCase().includes(query.toLowerCase()) 25 | ); 26 | } 27 | 28 | // 如果仍然没有找到,返回所有可能的匹配项(最多5个) 29 | if (!foundRecipe) { 30 | const possibleMatches = recipes.filter(recipe => 31 | recipe.name.toLowerCase().includes(query.toLowerCase()) || 32 | recipe.description.toLowerCase().includes(query.toLowerCase()) 33 | ).slice(0, 5); 34 | 35 | if (possibleMatches.length === 0) { 36 | return { 37 | content: [ 38 | { 39 | type: "text", 40 | text: JSON.stringify({ 41 | error: "未找到匹配的菜谱", 42 | query: query, 43 | suggestion: "请检查菜谱名称是否正确,或尝试使用关键词搜索" 44 | }, null, 2), 45 | }, 46 | ], 47 | }; 48 | } 49 | 50 | return { 51 | content: [ 52 | { 53 | type: "text", 54 | text: JSON.stringify({ 55 | message: "未找到精确匹配,以下是可能的匹配项:", 56 | query: query, 57 | possibleMatches: possibleMatches.map(recipe => ({ 58 | id: recipe.id, 59 | name: recipe.name, 60 | description: recipe.description, 61 | category: recipe.category 62 | })) 63 | }, null, 2), 64 | }, 65 | ], 66 | }; 67 | } 68 | 69 | // 返回找到的完整菜谱信息 70 | return { 71 | content: [ 72 | { 73 | type: "text", 74 | text: JSON.stringify(foundRecipe, null, 2), 75 | }, 76 | ], 77 | }; 78 | } 79 | ); 80 | } -------------------------------------------------------------------------------- /src/utils/recipeUtils.ts: -------------------------------------------------------------------------------- 1 | import { Recipe, SimpleRecipe, NameOnlyRecipe, Ingredient } from '../types/index.js'; 2 | 3 | // 创建简化版的Recipe数据 4 | export function simplifyRecipe(recipe: Recipe): SimpleRecipe { 5 | return { 6 | id: recipe.id, 7 | name: recipe.name, 8 | description: recipe.description, 9 | ingredients: recipe.ingredients.map((ingredient: Ingredient) => ({ 10 | name: ingredient.name, 11 | text_quantity: ingredient.text_quantity 12 | })) 13 | }; 14 | } 15 | 16 | // 创建只包含name和description的Recipe数据 17 | export function simplifyRecipeNameOnly(recipe: Recipe): NameOnlyRecipe { 18 | return { 19 | name: recipe.name, 20 | description: recipe.description 21 | }; 22 | } 23 | 24 | // 处理食材清单,收集菜谱的所有食材 25 | export function processRecipeIngredients(recipe: Recipe, ingredientMap: Map) { 31 | recipe.ingredients?.forEach((ingredient: Ingredient) => { 32 | const key = ingredient.name.toLowerCase(); 33 | 34 | if (!ingredientMap.has(key)) { 35 | ingredientMap.set(key, { 36 | totalQuantity: ingredient.quantity, 37 | unit: ingredient.unit, 38 | recipeCount: 1, 39 | recipes: [recipe.name] 40 | }); 41 | } else { 42 | const existing = ingredientMap.get(key)!; 43 | 44 | // 对于有明确数量和单位的食材,进行汇总 45 | if (existing.unit && ingredient.unit && existing.unit === ingredient.unit && existing.totalQuantity !== null && ingredient.quantity !== null) { 46 | existing.totalQuantity += ingredient.quantity; 47 | } else { 48 | // 否则保留 null,表示数量不确定 49 | existing.totalQuantity = null; 50 | existing.unit = null; 51 | } 52 | 53 | existing.recipeCount += 1; 54 | if (!existing.recipes.includes(recipe.name)) { 55 | existing.recipes.push(recipe.name); 56 | } 57 | } 58 | }); 59 | } 60 | 61 | // 根据食材类型进行分类 62 | export function categorizeIngredients(ingredients: Array<{ 63 | name: string, 64 | totalQuantity: number | null, 65 | unit: string | null, 66 | recipeCount: number, 67 | recipes: string[] 68 | }>, shoppingPlan: { 69 | fresh: string[], 70 | pantry: string[], 71 | spices: string[], 72 | others: string[] 73 | }) { 74 | const spiceKeywords = ['盐', '糖', '酱油', '醋', '料酒', '香料', '胡椒', '孜然', '辣椒', '花椒', '姜', '蒜', '葱', '调味']; 75 | const freshKeywords = ['肉', '鱼', '虾', '蛋', '奶', '菜', '菠菜', '白菜', '青菜', '豆腐', '生菜', '水产', '豆芽', '西红柿', '番茄', '水果', '香菇', '木耳', '蘑菇']; 76 | const pantryKeywords = ['米', '面', '粉', '油', '酒', '醋', '糖', '盐', '酱', '豆', '干', '罐头', '方便面', '面条', '米饭', '意大利面', '燕麦']; 77 | 78 | ingredients.forEach(ingredient => { 79 | const name = ingredient.name.toLowerCase(); 80 | 81 | if (spiceKeywords.some(keyword => name.includes(keyword))) { 82 | shoppingPlan.spices.push(ingredient.name); 83 | } else if (freshKeywords.some(keyword => name.includes(keyword))) { 84 | shoppingPlan.fresh.push(ingredient.name); 85 | } else if (pantryKeywords.some(keyword => name.includes(keyword))) { 86 | shoppingPlan.pantry.push(ingredient.name); 87 | } else { 88 | shoppingPlan.others.push(ingredient.name); 89 | } 90 | }); 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-MCP Server 🥘 -- 炫一周好饭,拒绝拼好饭 2 | 3 | [English](./README_EN.md) | 简体中文 4 | 5 |
6 | 7 | 本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助 8 | 9 | [亚洲最佳 CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github) 10 | 11 | 12 | 13 |
14 | 15 | > 让 AI 助手变身私人大厨,为你的一日三餐出谋划策! 16 | 17 | 基于[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook)打造的 MCP(Model Context Protocol)服务器,让 AI 助手能够为你推荐菜谱、规划膳食,解决"今天吃什么"的世纪难题! 18 | 19 | 数据来源:[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ 没有 star 的同学快去点个星星吧! 20 | 21 | 🎉 **想直接使用当前 MCP?立即体验** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/) 22 | 23 | 🎉 **同时,我们也提供了 DXT(Desktop Extensions)供大家体验,一键安装到 Claude Desktop** 24 | 25 | 如下:请确保你已经安装了最新版的 Claude Desktop, 当前 MCP 的 DXT 文件已上传代码库,可以自行下载或者 Fork 本仓库自行构建 26 | 27 | ![DXT](./public/dxt.png) 28 | ![DXT](./public/dxt2.png) 29 | ![DXT](./public/dxt3.png) 30 | 31 | 本地开发如何打包成 DXT? 32 | 33 | 1.运行 `npm install -g @anthropic-ai/dxt` 34 | 35 | 2.在包含本地 MCP 服务器的文件夹中,运行 `dxt init`。也就是您 MCP 的根目录,此命令将引导您创建`manifest.json` 36 | 37 | 3.运行`dxt pack`创建 dxt 文件 38 | 39 | 现在,任何支持 DXT 的应用都可以运行您的本地 MCP 服务器。例如,使用适用于 macOS 和 Windows 的 Claude 打开该文件即可显示安装对话框 40 | 41 | 具体参阅:[anthropics/dxt](https://github.com/anthropics/dxt) 42 | 43 | ## 📸 效果预览 44 | 45 | ![功能预览1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) 46 | ![功能预览2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) 47 | 48 | ## 🔌 支持的 MCP 客户端 49 | 50 | 本服务器适用于所有支持 MCP 协议的 AI 助手和客户端,包括但不限于: 51 | 52 | - 🤖 Claude 桌面应用 53 | - 📝 Cursor 54 | - 💼 其他支持 MCP 的客户端 55 | 56 | ## ✨ 美味功能 57 | 58 | 该 MCP 服务器提供以下美食工具: 59 | 60 | 1. **📚 查询全部菜谱** - 获取所有可用菜谱数据,做菜百科全书 -- 慎用这个--上下文太大 61 | 2. **🔍 根据分类查询菜谱** - 按照分类筛选菜谱,想吃水产?早餐?荤菜?主食?一键搞定! 62 | 3. **📖 查询指定菜谱** - 根据菜谱名称查询特定菜谱的完整详情,包括食材、步骤等 63 | 4. **🧩 智能推荐膳食** - 根据你的忌口、过敏原和用餐人数,为你规划整整一周的美味佳肴 64 | 5. **🎲 不知道吃什么** - 选择困难症福音!根据人数直接推荐今日菜单,再也不用纠结了 65 | 66 | ## 🚀 快速上手 67 | 68 | ### 📋 先决条件 69 | 70 | - Node.js 16.0.0+ 🟢 71 | - npm 或 yarn 📦 72 | 73 | ### 💻 安装步骤 74 | 75 | 1. 克隆美食仓库 76 | 77 | ```bash 78 | git clone https://github.com/worryzyy/howtocook-mcp.git 79 | cd howtocook-mcp 80 | ``` 81 | 82 | 2. 安装依赖(就像准备食材一样简单!) 83 | 84 | ```bash 85 | npm install 86 | ``` 87 | 88 | 3. 编译代码(烹饪过程...) 89 | 90 | ```bash 91 | npm run build 92 | ``` 93 | 94 | ### 🎯 命令行参数 95 | 96 | 服务器支持以下命令行参数: 97 | 98 | - `--transport ` - 选择传输方式(默认为 stdio) 99 | - `--port ` - 使用 http 或 sse 传输时的监听端口(默认为 3000) 100 | 101 | 示例:使用 http 传输并监听 8080 端口 102 | 103 | ```bash 104 | node build/index.js --transport http --port 8080 105 | ``` 106 | 107 | ## 🍽️ 开始使用 108 | 109 | ### 🔥 启动服务器 110 | 111 | ```bash 112 | npm start 113 | ``` 114 | 115 | ### 🔧 配置 MCP 客户端 116 | 117 | #### 推荐使用 Cursor 快速体验(两种方式) 118 | 119 | 1. 使用 npm 包:请先运行 `npm i -g howtocook-mcp` ,否则会出现 `Failed to create client` 120 | 121 | 然后在 Cursor 设置中添加 MCP 服务器配置: 122 | 123 | ```json 124 | { 125 | "mcpServers": { 126 | "howtocook-mcp": { 127 | "command": "npx", 128 | "args": ["-y", "howtocook-mcp"] 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | 2. 如果是克隆仓库本地运行,请使用如下配置 135 | 136 | ```json 137 | { 138 | "mcpServers": { 139 | "howtocook-mcp": { 140 | "command": "node", 141 | "args": ["youpath\\howtocook-mcp\\build\\index.js"] 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | #### 其他 MCP 客户端 148 | 149 | 对于其他支持 MCP 协议的客户端,请参考各自的文档进行配置,通常需要指定: 150 | 151 | - 服务器名称: `howtocook-mcp` 152 | - 命令: `npx -y howtocook-mcp` 153 | 154 | 3. 重启客户端,让美食魔法生效 ✨ 155 | 156 | ## 🧙‍♂️ 菜单魔法使用指南 157 | 158 | 以下是在各种 MCP 客户端中使用的示例提示语: 159 | 160 | ### 1. 📚 查询全部菜谱 161 | 162 | 无需参数,直接召唤美食全书! 163 | 164 | ``` 165 | 请使用howtocook的MCP服务查询所有菜谱 166 | ``` 167 | 168 | ### 2. 🔍 根据分类查询菜谱 169 | 170 | ``` 171 | 请使用howtocook的MCP服务查询水产类的菜谱 172 | ``` 173 | 174 | 参数: 175 | 176 | - `category`: 菜谱分类(水产、早餐、荤菜、主食等) 177 | 178 | ### 3. 🧩 智能推荐一周菜谱 179 | 180 | ``` 181 | 请使用howtocook的MCP服务为3人推荐一周菜谱,我们家不吃香菜,对虾过敏 182 | ``` 183 | 184 | 参数: 185 | 186 | - `allergies`: 过敏原列表,如 ["大蒜", "虾"] 187 | - `avoidItems`: 忌口食材,如 ["葱", "姜"] 188 | - `peopleCount`: 用餐人数 (1-10) 189 | 190 | ### 4. 🎲 今天吃什么? 191 | 192 | ``` 193 | 请使用howtocook的MCP服务为4人晚餐推荐菜单 194 | ``` 195 | 196 | 参数: 197 | 198 | - `peopleCount`: 用餐人数 (1-10) 199 | 200 | ## 📝 小贴士 201 | 202 | - 该包已发布至 npm,可直接通过`npm install -g howtocook-mcp`全局安装 203 | - 本服务兼容所有支持 MCP 协议的 AI 助手和应用 204 | - 首次使用时,AI 可能需要一点时间来熟悉如何使用这些工具(就像烧热锅一样) 205 | 206 | ## 🤝 贡献 207 | 208 | 欢迎 Fork 和 Pull Request,让我们一起完善这个美食助手! 209 | 210 | ## 📄 许可 211 | 212 | MIT License - 随意使用,就像分享美食配方一样慷慨! 213 | 214 | --- 215 | 216 | > 🍴 美食即将开始,胃口准备好了吗? 217 | 218 | -------------------------------------------------------------------------------- /src/tools/whatToEat.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Recipe, DishRecommendation } from "../types/index.js"; 3 | import { simplifyRecipe } from "../utils/recipeUtils.js"; 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | 6 | export function registerWhatToEatTool(server: McpServer, recipes: Recipe[]) { 7 | server.tool( 8 | "mcp_howtocook_whatToEat", 9 | "不知道吃什么?根据人数直接推荐适合的菜品组合", 10 | { 11 | peopleCount: z.number().int().min(1).max(10) 12 | .describe('用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品') 13 | }, 14 | async ({ peopleCount }: { peopleCount: number }) => { 15 | // 根据人数计算荤素菜数量 16 | const vegetableCount = Math.floor((peopleCount + 1) / 2); 17 | const meatCount = Math.ceil((peopleCount + 1) / 2); 18 | 19 | // 获取所有荤菜 20 | let meatDishes = recipes.filter((recipe) => 21 | recipe.category === '荤菜' || recipe.category === '水产' 22 | ); 23 | 24 | // 获取其他可能的菜品(当做素菜) 25 | let vegetableDishes = recipes.filter((recipe) => 26 | recipe.category !== '荤菜' && recipe.category !== '水产' && 27 | recipe.category !== '早餐' && recipe.category !== '主食' 28 | ); 29 | 30 | // 特别处理:如果人数超过8人,增加鱼类荤菜 31 | let recommendedDishes: Recipe[] = []; 32 | let fishDish: Recipe | null = null; 33 | 34 | if (peopleCount > 8) { 35 | const fishDishes = recipes.filter((recipe) => recipe.category === '水产'); 36 | if (fishDishes.length > 0) { 37 | fishDish = fishDishes[Math.floor(Math.random() * fishDishes.length)]; 38 | recommendedDishes.push(fishDish); 39 | } 40 | } 41 | 42 | // 打乱肉类优先级顺序,增加随机性 43 | const meatTypes = ['猪肉', '鸡肉', '牛肉', '羊肉', '鸭肉', '鱼肉']; 44 | // 使用 Fisher-Yates 洗牌算法打乱数组 45 | for (let i = meatTypes.length - 1; i > 0; i--) { 46 | const j = Math.floor(Math.random() * (i + 1)); 47 | [meatTypes[i], meatTypes[j]] = [meatTypes[j], meatTypes[i]]; 48 | } 49 | 50 | const selectedMeatDishes: Recipe[] = []; 51 | 52 | // 需要选择的荤菜数量 53 | const remainingMeatCount = fishDish ? meatCount - 1 : meatCount; 54 | 55 | // 尝试按照随机化的肉类优先级选择荤菜 56 | for (const meatType of meatTypes) { 57 | if (selectedMeatDishes.length >= remainingMeatCount) break; 58 | 59 | const meatTypeOptions = meatDishes.filter((dish) => { 60 | // 检查菜品的材料是否包含这种肉类 61 | return dish.ingredients?.some((ingredient) => { 62 | const name = ingredient.name?.toLowerCase() || ''; 63 | return name.includes(meatType.toLowerCase()); 64 | }); 65 | }); 66 | 67 | if (meatTypeOptions.length > 0) { 68 | // 随机选择一道这种肉类的菜 69 | const selected = meatTypeOptions[Math.floor(Math.random() * meatTypeOptions.length)]; 70 | selectedMeatDishes.push(selected); 71 | // 从可选列表中移除,避免重复选择 72 | meatDishes = meatDishes.filter((dish) => dish.id !== selected.id); 73 | } 74 | } 75 | 76 | // 如果通过肉类筛选的荤菜不够,随机选择剩余的 77 | while (selectedMeatDishes.length < remainingMeatCount && meatDishes.length > 0) { 78 | const randomIndex = Math.floor(Math.random() * meatDishes.length); 79 | selectedMeatDishes.push(meatDishes[randomIndex]); 80 | meatDishes.splice(randomIndex, 1); 81 | } 82 | 83 | // 随机选择素菜 84 | const selectedVegetableDishes: Recipe[] = []; 85 | while (selectedVegetableDishes.length < vegetableCount && vegetableDishes.length > 0) { 86 | const randomIndex = Math.floor(Math.random() * vegetableDishes.length); 87 | selectedVegetableDishes.push(vegetableDishes[randomIndex]); 88 | vegetableDishes.splice(randomIndex, 1); 89 | } 90 | 91 | // 合并推荐菜单 92 | recommendedDishes = recommendedDishes.concat(selectedMeatDishes, selectedVegetableDishes); 93 | 94 | // 构建推荐结果 95 | const recommendationDetails: DishRecommendation = { 96 | peopleCount, 97 | meatDishCount: selectedMeatDishes.length + (fishDish ? 1 : 0), 98 | vegetableDishCount: selectedVegetableDishes.length, 99 | dishes: recommendedDishes.map(simplifyRecipe), 100 | message: `为${peopleCount}人推荐的菜品,包含${selectedMeatDishes.length + (fishDish ? 1 : 0)}个荤菜和${selectedVegetableDishes.length}个素菜。` 101 | }; 102 | 103 | return { 104 | content: [ 105 | { 106 | type: "text", 107 | text: JSON.stringify(recommendationDetails, null, 2), 108 | }, 109 | ], 110 | }; 111 | } 112 | ); 113 | } -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-MCP Server 🥘 -- Plan Your Weekly Meals, No More Daily Struggles 2 | 3 | English | [简体中文](./README.md) 4 | 5 |
6 | 7 | CDN acceleration and security protection for this project are sponsored by Tencent EdgeOne. 8 | 9 | [Best Asian CDN, Edge, and Secure Solutions - Tencent EdgeOne](https://edgeone.ai/zh?from=github) 10 | 11 | 12 | 13 |
14 | 15 | > Turn your AI assistant into a personal chef that helps plan your daily meals! 16 | 17 | An MCP (Model Context Protocol) server based on [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook), allowing AI assistants to recommend recipes, plan meals, and solve the age-old question of "what should I eat today?" 18 | 19 | Data Source: [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ Don't forget to star the repo if you haven't already! 20 | 21 | 🎉 **Want to use MCP right away? Try it now** [https://howtocookmcp.weilei.site/](https://howtocookmcp.weilei.site/) 22 | 23 | 🎉 **At the same time, we also provide DXT (Desktop Extensions) for everyone to experience, one-click installation to Claude Desktop** 24 | 25 | As follows: Please make sure you have installed the latest version of Claude Desktop. The current MCP DXT file has been uploaded to the code library. You can download it yourself or fork this repository to build it yourself 26 | 27 | ![DXT](./public/dxt.png) 28 | 29 | ![DXT](./public/dxt2.png) 30 | 31 | ![DXT](./public/dxt3.png) 32 | 33 | How to package local development into DXT? 34 | 35 | 1. Run `npm install -g @anthropic-ai/dxt` 36 | 37 | 2. In the folder containing the local MCP server, run `dxt init`. That is, the root directory of your MCP. This command will guide you to create `manifest.json` 38 | 39 | 3. Run `dxt pack` to create a dxt file 40 | 41 | Now, any application that supports DXT can run your local MCP server. For example, opening the file with Claude for macOS and Windows will display the installation dialog 42 | 43 | For more information, see: [anthropics/dxt](https://github.com/anthropics/dxt) 44 | 45 | ## 📸 Preview 46 | 47 | ![Feature Preview 1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) 48 | ![Feature Preview 2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) 49 | 50 | ## 🔌 Supported MCP Clients 51 | 52 | This server works with all AI assistants and clients that support the MCP protocol, including but not limited to: 53 | 54 | - 🤖 Claude Desktop App 55 | - 📝 Cursor 56 | - 💼 Other MCP-compatible clients 57 | 58 | ## ✨ Delicious Features 59 | 60 | This MCP server provides the following culinary tools: 61 | 62 | 1. **📚 Query All Recipes** - Access all available recipe data, your complete cooking encyclopedia -- Use with caution due to large context size 63 | 2. **🔍 Query Recipes by Category** - Filter recipes by category: seafood, breakfast, meat dishes, staple foods, and more! 64 | 3. **🧩 Smart Meal Planning** - Get a full week's meal plan based on dietary restrictions, allergies, and number of diners 65 | 4. **🎲 Don't Know What to Eat?** - Perfect for the indecisive! Get instant menu recommendations based on party size 66 | 5. **🔎 Query Specific Recipe** - Search for specific recipes by name or ID, supports both exact and fuzzy matching to save tokens 67 | 68 | ## 🚀 Quick Start 69 | 70 | ### 📋 Prerequisites 71 | 72 | - Node.js 16.0.0+ 🟢 73 | - npm or yarn 📦 74 | 75 | ### 💻 Installation 76 | 77 | 1. Clone the repository 78 | 79 | ```bash 80 | git clone https://github.com/worryzyy/howtocook-mcp.git 81 | cd howtocook-mcp 82 | ``` 83 | 84 | 2. Install dependencies (as simple as preparing ingredients!) 85 | 86 | ```bash 87 | npm install 88 | ``` 89 | 90 | 3. Build the code (the cooking process...) 91 | 92 | ```bash 93 | npm run build 94 | ``` 95 | 96 | ### 🎯 CLI Arguments 97 | 98 | The server accepts the following command-line arguments: 99 | 100 | - `--transport ` - Transport to use (stdio by default) 101 | - `--port ` - Port to listen on when using http or sse transport (default 3000) 102 | 103 | Example with http transport and port 8080: 104 | 105 | ```bash 106 | node build/index.js --transport http --port 8080 107 | ``` 108 | 109 | ## ��️ Getting Started 110 | 111 | ### 🔥 Start the Server 112 | 113 | ```bash 114 | npm start 115 | ``` 116 | 117 | ### 🔧 Configure MCP Clients 118 | 119 | #### It is recommended to use Cursor for quick experience (two methods)Cursor Configuration 120 | 121 | 1. Using npm package: Please run `npm i -g howtocook-mcp` first, otherwise `Failed to create client` will appear 122 | 123 | Then add the MCP server configuration in Cursor settings: 124 | 125 | ```json 126 | { 127 | "mcpServers": { 128 | "howtocook-mcp": { 129 | "command": "npx", 130 | "args": ["-y", "howtocook-mcp"] 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | 2. If running from a local cloned repository, use this configuration: 137 | 138 | ```json 139 | { 140 | "mcpServers": { 141 | "howtocook-mcp": { 142 | "command": "node", 143 | "args": ["yourpath\\howtocook-mcp\\build\\index.js"] 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | #### Other MCP Clients 150 | 151 | For other clients supporting the MCP protocol, refer to their respective documentation. Generally, you'll need to specify: 152 | 153 | - Server name: `howtocook-mcp` 154 | - Command: `npx -y howtocook-mcp` 155 | 156 | 3. Restart the client to activate the culinary magic ✨ 157 | 158 | ## 🧙‍♂️ Culinary Magic Usage Guide 159 | 160 | Here are example prompts for using these tools in MCP clients: 161 | 162 | ### 1. 📚 Query All Recipes 163 | 164 | No parameters needed, just summon the culinary encyclopedia! 165 | 166 | ``` 167 | Please use the howtocook MCP service to query all recipes 168 | ``` 169 | 170 | ### 2. 🔍 Query Recipes by Category 171 | 172 | ``` 173 | Please use the howtocook MCP service to query seafood recipes 174 | ``` 175 | 176 | Parameters: 177 | 178 | - `category`: Recipe category (seafood, breakfast, meat dishes, staple foods, etc.) 179 | 180 | ### 3. 🧩 Smart Meal Planning 181 | 182 | ``` 183 | Please use the howtocook MCP service to recommend a weekly meal plan for 3 people. We don't eat cilantro and are allergic to shrimp. 184 | ``` 185 | 186 | Parameters: 187 | 188 | - `allergies`: List of allergens, e.g., ["garlic", "shrimp"] 189 | - `avoidItems`: Dietary restrictions, e.g., ["green onion", "ginger"] 190 | - `peopleCount`: Number of diners (1-10) 191 | 192 | ### 4. 🎲 What to Eat Today? 193 | 194 | ``` 195 | Please use the howtocook MCP service to recommend a dinner menu for 4 people 196 | ``` 197 | 198 | Parameters: 199 | 200 | - `peopleCount`: Number of diners (1-10) 201 | 202 | ### 5. 🔎 Query Specific Recipe 203 | 204 | ``` 205 | Please use the howtocook MCP service to query the recipe for "Kung Pao Chicken" 206 | ``` 207 | 208 | Parameters: 209 | 210 | - `recipeId`: Recipe name or ID to search for 211 | 212 | ## 📝 Tips 213 | 214 | - This package is published on npm and can be installed globally via `npm install -g howtocook-mcp` 215 | - Compatible with all AI assistants and applications that support the MCP protocol 216 | - On first use, AI may need some time to familiarize itself with these tools (like preheating an oven) 217 | 218 | ## 🤝 Contributing 219 | 220 | Forks and Pull Requests are welcome! Let's improve this culinary assistant together! 221 | 222 | ## 📄 License 223 | 224 | MIT License - Feel free to use, just like sharing your favorite recipes! 225 | 226 | --- 227 | 228 | > 🍴 The feast is about to begin, is your appetite ready? 229 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 5 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 6 | import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; 7 | import { Command } from 'commander'; 8 | import { createServer } from 'http'; 9 | import { fetchRecipes, getAllCategories } from "./data/recipes.js"; 10 | import { registerGetAllRecipesTool } from "./tools/getAllRecipes.js"; 11 | import { registerGetRecipeByIdTool } from "./tools/getRecipeById.js"; 12 | import { registerGetRecipesByCategoryTool } from "./tools/getRecipesByCategory.js"; 13 | import { registerRecommendMealsTool } from "./tools/recommendMeals.js"; 14 | import { registerWhatToEatTool } from "./tools/whatToEat.js"; 15 | import { Recipe } from './types/index.js'; 16 | 17 | // 全局变量存储数据 18 | let recipes: Recipe[] = []; 19 | let categories: string[] = []; 20 | 21 | // 命令行参数处理 22 | const program = new Command() 23 | .option("--transport ", "transport type", "stdio") 24 | .option("--port ", "port for HTTP/SSE transport", "3000") 25 | .parse(process.argv); 26 | 27 | const cliOptions = program.opts<{ 28 | transport: string; 29 | port: string; 30 | }>(); 31 | 32 | const allowedTransports = ["stdio", "http", "sse"]; 33 | if (!allowedTransports.includes(cliOptions.transport)) { 34 | console.error( 35 | `Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http, sse.` 36 | ); 37 | process.exit(1); 38 | } 39 | 40 | const TRANSPORT_TYPE = (cliOptions.transport || "stdio") as "stdio" | "http" | "sse"; 41 | const PORT = parseInt(cliOptions.port, 10); 42 | // SSE transports 43 | const sseTransports: Record = {}; 44 | // 创建MCP服务器实例 45 | function createServerInstance(): McpServer { 46 | const server = new McpServer({ 47 | name: 'howtocook-mcp', 48 | version: '0.1.1', 49 | }, { 50 | capabilities: { 51 | logging: {}, 52 | }, 53 | }); 54 | 55 | // 注册所有工具 56 | registerGetAllRecipesTool(server, recipes); 57 | registerGetRecipesByCategoryTool(server, recipes, categories); 58 | registerRecommendMealsTool(server, recipes); 59 | registerWhatToEatTool(server, recipes); 60 | registerGetRecipeByIdTool(server, recipes); 61 | 62 | return server; 63 | } 64 | 65 | // 加载菜谱数据 66 | function loadRecipeData() { 67 | try { 68 | recipes = fetchRecipes(); 69 | categories = getAllCategories(recipes); 70 | console.log(`📚 已加载 ${recipes.length} 个菜谱`); 71 | } catch (error) { 72 | console.error('加载菜谱数据失败:', error); 73 | recipes = []; 74 | categories = []; 75 | throw error; 76 | } 77 | } 78 | 79 | // 启动服务的主函数 80 | async function main() { 81 | // 加载菜谱数据 82 | await loadRecipeData(); 83 | 84 | if (TRANSPORT_TYPE === "http" || TRANSPORT_TYPE === "sse") { 85 | const httpServer = createServer(async (req, res) => { 86 | const url = new URL(req.url || "", `http://${req.headers.host}`).pathname; 87 | 88 | // 设置 CORS 头 89 | res.setHeader("Access-Control-Allow-Origin", "*"); 90 | res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE"); 91 | res.setHeader("Access-Control-Allow-Headers", "Content-Type, MCP-Session-Id, mcp-session-id"); 92 | 93 | // 处理预检请求 94 | if (req.method === "OPTIONS") { 95 | res.writeHead(200); 96 | res.end(); 97 | return; 98 | } 99 | 100 | try { 101 | // 为每个请求创建新的服务器实例 102 | const requestServer = createServerInstance(); 103 | 104 | if (url === "/mcp") { 105 | const transport = new StreamableHTTPServerTransport({ 106 | sessionIdGenerator: undefined, 107 | }); 108 | await requestServer.connect(transport); 109 | await transport.handleRequest(req, res); 110 | }else if (url === "/sse" && req.method === "GET") { 111 | // Create new SSE transport for GET request 112 | const sseTransport = new SSEServerTransport("/messages", res); 113 | // Store the transport by session ID 114 | sseTransports[sseTransport.sessionId] = sseTransport; 115 | // Clean up transport when connection closes 116 | res.on("close", () => { 117 | delete sseTransports[sseTransport.sessionId]; 118 | }); 119 | await requestServer.connect(sseTransport); 120 | } else if (url === "/messages" && req.method === "POST") { 121 | // Get session ID from query parameters 122 | const sessionId = 123 | new URL(req.url || "", `http://${req.headers.host}`).searchParams.get("sessionId") ?? 124 | ""; 125 | 126 | if (!sessionId) { 127 | res.writeHead(400); 128 | res.end("Missing sessionId parameter"); 129 | return; 130 | } 131 | 132 | // Get existing transport for this session 133 | const sseTransport = sseTransports[sessionId]; 134 | if (!sseTransport) { 135 | res.writeHead(400); 136 | res.end(`No transport found for sessionId: ${sessionId}`); 137 | return; 138 | } 139 | 140 | // Handle the POST message with the existing transport 141 | await sseTransport.handlePostMessage(req, res); 142 | } 143 | else if (url === "/health") { 144 | res.writeHead(200, { "Content-Type": "application/json" }); 145 | res.end(JSON.stringify({ status: "ok", transport: TRANSPORT_TYPE })); 146 | } else if (url === "/info") { 147 | res.writeHead(200, { "Content-Type": "application/json" }); 148 | res.end(JSON.stringify({ 149 | name: "HowToCook MCP Server", 150 | version: "0.1.1", 151 | transport: TRANSPORT_TYPE, 152 | endpoints: { 153 | mcp: "/mcp", 154 | sse: "/sse", 155 | health: "/health", 156 | info: "/info" 157 | }, 158 | recipeCount: recipes.length 159 | })); 160 | } else { 161 | res.writeHead(404); 162 | res.end("Not found"); 163 | } 164 | } catch (error) { 165 | console.error("处理请求时出错:", error); 166 | if (!res.headersSent) { 167 | res.writeHead(500); 168 | res.end("Internal Server Error"); 169 | } 170 | } 171 | }); 172 | 173 | httpServer.listen(PORT, () => { 174 | console.log(`🚀 HowToCook MCP ${TRANSPORT_TYPE.toUpperCase()} 服务器启动成功`); 175 | if(TRANSPORT_TYPE === "http"){ 176 | console.log(`🔗 MCP 端点: http://localhost:${PORT}/mcp`); 177 | }else if(TRANSPORT_TYPE === "sse"){ 178 | console.log(`🔗 MCP 端点: http://localhost:${PORT}/sse`); 179 | } 180 | console.log(`💡 健康检查: http://localhost:${PORT}/health`); 181 | console.log(`ℹ️ 服务器信息: http://localhost:${PORT}/info`); 182 | }); 183 | } else { 184 | // stdio 模式 185 | const server = createServerInstance(); 186 | const transport = new StdioServerTransport(); 187 | try { 188 | await server.connect(transport); 189 | console.log('HowToCook MCP STDIO 服务器启动成功'); 190 | } catch (error) { 191 | console.error('服务器启动失败:', error); 192 | process.exit(1); 193 | } 194 | } 195 | } 196 | 197 | // 优雅关闭 198 | process.on('SIGINT', async () => { 199 | console.log('\n正在关闭服务器...'); 200 | process.exit(0); 201 | }); 202 | 203 | process.on('SIGTERM', async () => { 204 | console.log('\n收到终止信号,正在关闭服务器...'); 205 | process.exit(0); 206 | }); 207 | 208 | // 启动服务器 209 | main().catch((error) => { 210 | console.error('启动服务器失败:', error); 211 | process.exit(1); 212 | }); 213 | 214 | -------------------------------------------------------------------------------- /src/tools/recommendMeals.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Recipe, MealPlan, SimpleRecipe, DayPlan } from "../types/index.js"; 3 | import { simplifyRecipe, processRecipeIngredients, categorizeIngredients } from "../utils/recipeUtils.js"; 4 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 5 | 6 | export function registerRecommendMealsTool(server: McpServer, recipes: Recipe[]) { 7 | server.tool( 8 | "mcp_howtocook_recommendMeals", 9 | "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单", 10 | { 11 | allergies: z.array(z.string()).optional() 12 | .describe('过敏原列表,如["大蒜", "虾"]'), 13 | avoidItems: z.array(z.string()).optional() 14 | .describe('忌口食材列表,如["葱", "姜"]'), 15 | peopleCount: z.number().int().min(1).max(10) 16 | .describe('用餐人数,1-10之间的整数') 17 | }, 18 | async ({ allergies = [], avoidItems = [], peopleCount }: { 19 | allergies?: string[], 20 | avoidItems?: string[], 21 | peopleCount: number 22 | }) => { 23 | // 过滤掉含有忌口和过敏原的菜谱 24 | const filteredRecipes = recipes.filter((recipe) => { 25 | // 检查是否包含过敏原或忌口食材 26 | const hasAllergiesOrAvoidItems = recipe.ingredients?.some((ingredient) => { 27 | const name = ingredient.name?.toLowerCase() || ''; 28 | return allergies.some(allergy => name.includes(allergy.toLowerCase())) || 29 | avoidItems.some(item => name.includes(item.toLowerCase())); 30 | }); 31 | 32 | return !hasAllergiesOrAvoidItems; 33 | }); 34 | 35 | // 将菜谱按分类分组 36 | const recipesByCategory: Record = {}; 37 | const targetCategories = ['水产', '早餐', '荤菜', '主食']; 38 | 39 | filteredRecipes.forEach((recipe) => { 40 | if (targetCategories.includes(recipe.category)) { 41 | if (!recipesByCategory[recipe.category]) { 42 | recipesByCategory[recipe.category] = []; 43 | } 44 | recipesByCategory[recipe.category].push(recipe); 45 | } 46 | }); 47 | 48 | // 创建每周膳食计划 49 | const mealPlan: MealPlan = { 50 | weekdays: [], 51 | weekend: [], 52 | groceryList: { 53 | ingredients: [], 54 | shoppingPlan: { 55 | fresh: [], 56 | pantry: [], 57 | spices: [], 58 | others: [] 59 | } 60 | } 61 | }; 62 | 63 | // 用于跟踪已经选择的菜谱,以便后续处理食材信息 64 | const selectedRecipes: Recipe[] = []; 65 | 66 | // 周一至周五 67 | for (let i = 0; i < 5; i++) { 68 | const dayPlan: DayPlan = { 69 | day: ['周一', '周二', '周三', '周四', '周五'][i], 70 | breakfast: [], 71 | lunch: [], 72 | dinner: [] 73 | }; 74 | 75 | // 早餐 - 根据人数推荐1-2个早餐菜单 76 | const breakfastCount = Math.max(1, Math.ceil(peopleCount / 5)); 77 | for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) { 78 | const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length); 79 | const selectedRecipe = recipesByCategory['早餐'][breakfastIndex]; 80 | selectedRecipes.push(selectedRecipe); 81 | dayPlan.breakfast.push(simplifyRecipe(selectedRecipe)); 82 | // 避免重复,从候选列表中移除 83 | recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex); 84 | } 85 | 86 | // 午餐和晚餐的菜谱数量,根据人数确定 87 | const mealCount = Math.max(2, Math.ceil(peopleCount / 3)); 88 | 89 | // 午餐 90 | for (let j = 0; j < mealCount; j++) { 91 | // 随机选择菜系:主食、水产、蔬菜、荤菜等 92 | const categories = ['主食', '水产', '荤菜', '素菜', '甜品']; 93 | let selectedCategory = categories[Math.floor(Math.random() * categories.length)]; 94 | 95 | // 如果该分类没有菜谱或已用完,尝试其他分类 96 | while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) { 97 | selectedCategory = categories[Math.floor(Math.random() * categories.length)]; 98 | if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) { 99 | break; // 所有分类都没有可用菜谱,退出循环 100 | } 101 | } 102 | 103 | if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) { 104 | const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length); 105 | const selectedRecipe = recipesByCategory[selectedCategory][index]; 106 | selectedRecipes.push(selectedRecipe); 107 | dayPlan.lunch.push(simplifyRecipe(selectedRecipe)); 108 | // 避免重复,从候选列表中移除 109 | recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index); 110 | } 111 | } 112 | 113 | // 晚餐 114 | for (let j = 0; j < mealCount; j++) { 115 | // 随机选择菜系,与午餐类似但可添加汤羹 116 | const categories = ['主食', '水产', '荤菜', '素菜', '甜品', '汤羹']; 117 | let selectedCategory = categories[Math.floor(Math.random() * categories.length)]; 118 | 119 | // 如果该分类没有菜谱或已用完,尝试其他分类 120 | while (!recipesByCategory[selectedCategory] || recipesByCategory[selectedCategory].length === 0) { 121 | selectedCategory = categories[Math.floor(Math.random() * categories.length)]; 122 | if (categories.every(cat => !recipesByCategory[cat] || recipesByCategory[cat].length === 0)) { 123 | break; // 所有分类都没有可用菜谱,退出循环 124 | } 125 | } 126 | 127 | if (recipesByCategory[selectedCategory] && recipesByCategory[selectedCategory].length > 0) { 128 | const index = Math.floor(Math.random() * recipesByCategory[selectedCategory].length); 129 | const selectedRecipe = recipesByCategory[selectedCategory][index]; 130 | selectedRecipes.push(selectedRecipe); 131 | dayPlan.dinner.push(simplifyRecipe(selectedRecipe)); 132 | // 避免重复,从候选列表中移除 133 | recipesByCategory[selectedCategory] = recipesByCategory[selectedCategory].filter((_, idx) => idx !== index); 134 | } 135 | } 136 | 137 | mealPlan.weekdays.push(dayPlan); 138 | } 139 | 140 | // 周六和周日 141 | for (let i = 0; i < 2; i++) { 142 | const dayPlan: DayPlan = { 143 | day: ['周六', '周日'][i], 144 | breakfast: [], 145 | lunch: [], 146 | dinner: [] 147 | }; 148 | 149 | // 早餐 - 根据人数推荐菜品,至少2个菜品,随人数增加 150 | const breakfastCount = Math.max(2, Math.ceil(peopleCount / 3)); 151 | for (let j = 0; j < breakfastCount && recipesByCategory['早餐'] && recipesByCategory['早餐'].length > 0; j++) { 152 | const breakfastIndex = Math.floor(Math.random() * recipesByCategory['早餐'].length); 153 | const selectedRecipe = recipesByCategory['早餐'][breakfastIndex]; 154 | selectedRecipes.push(selectedRecipe); 155 | dayPlan.breakfast.push(simplifyRecipe(selectedRecipe)); 156 | recipesByCategory['早餐'] = recipesByCategory['早餐'].filter((_, idx) => idx !== breakfastIndex); 157 | } 158 | 159 | // 计算工作日的基础菜品数量 160 | const weekdayMealCount = Math.max(2, Math.ceil(peopleCount / 3)); 161 | // 周末菜品数量:比工作日多1-2个菜,随人数增加 162 | const weekendAddition = peopleCount <= 4 ? 1 : 2; // 4人以下多1个菜,4人以上多2个菜 163 | const mealCount = weekdayMealCount + weekendAddition; 164 | 165 | const getMeals = (count: number): SimpleRecipe[] => { 166 | const result: SimpleRecipe[] = []; 167 | const categories = ['荤菜', '水产']; 168 | 169 | // 尽量平均分配不同分类的菜品 170 | for (let j = 0; j < count; j++) { 171 | const category = categories[j % categories.length]; 172 | if (recipesByCategory[category] && recipesByCategory[category].length > 0) { 173 | const index = Math.floor(Math.random() * recipesByCategory[category].length); 174 | const selectedRecipe = recipesByCategory[category][index]; 175 | selectedRecipes.push(selectedRecipe); 176 | result.push(simplifyRecipe(selectedRecipe)); 177 | recipesByCategory[category] = recipesByCategory[category].filter((_, idx) => idx !== index); 178 | } else if (recipesByCategory['主食'] && recipesByCategory['主食'].length > 0) { 179 | // 如果没有足够的荤菜或水产,使用主食 180 | const index = Math.floor(Math.random() * recipesByCategory['主食'].length); 181 | const selectedRecipe = recipesByCategory['主食'][index]; 182 | selectedRecipes.push(selectedRecipe); 183 | result.push(simplifyRecipe(selectedRecipe)); 184 | recipesByCategory['主食'] = recipesByCategory['主食'].filter((_, idx) => idx !== index); 185 | } 186 | } 187 | 188 | return result; 189 | }; 190 | 191 | dayPlan.lunch = getMeals(mealCount); 192 | dayPlan.dinner = getMeals(mealCount); 193 | 194 | mealPlan.weekend.push(dayPlan); 195 | } 196 | 197 | // 统计食材清单,收集所有菜谱的所有食材 198 | const ingredientMap = new Map(); 204 | 205 | // 处理所有菜谱 206 | selectedRecipes.forEach(recipe => processRecipeIngredients(recipe, ingredientMap)); 207 | 208 | // 整理食材清单 209 | for (const [name, info] of ingredientMap.entries()) { 210 | mealPlan.groceryList.ingredients.push({ 211 | name, 212 | totalQuantity: info.totalQuantity, 213 | unit: info.unit, 214 | recipeCount: info.recipeCount, 215 | recipes: info.recipes 216 | }); 217 | } 218 | 219 | // 对食材按使用频率排序 220 | mealPlan.groceryList.ingredients.sort((a, b) => b.recipeCount - a.recipeCount); 221 | 222 | // 生成购物计划,根据食材类型进行分类 223 | categorizeIngredients(mealPlan.groceryList.ingredients, mealPlan.groceryList.shoppingPlan); 224 | 225 | return { 226 | content: [ 227 | { 228 | type: "text", 229 | text: JSON.stringify(mealPlan, null, 2), 230 | }, 231 | ], 232 | }; 233 | } 234 | ); 235 | } --------------------------------------------------------------------------------