├── .gitignore ├── README.md ├── README_EN.md ├── package-lock.json ├── package.json ├── src ├── data │ └── recipes.ts ├── index.ts ├── tools │ ├── getAllRecipes.ts │ ├── getRecipesByCategory.ts │ ├── recommendMeals.ts │ └── whatToEat.ts ├── types │ └── index.ts └── utils │ └── recipeUtils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-MCP Server 🥘 -- 炫一周好饭,拒绝拼好饭 2 | 3 | [English](./README_EN.md) | 简体中文 4 | 5 | > 让 AI 助手变身私人大厨,为你的一日三餐出谋划策! 6 | 7 | 基于[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook)打造的 MCP(Model Context Protocol)服务器,让 AI 助手能够为你推荐菜谱、规划膳食,解决"今天吃什么"的世纪难题! 8 | 9 | 数据来源:[Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ 没有 star 的同学快去点个星星吧! 10 | 11 | ## 📸 效果预览 12 | 13 | ![功能预览1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) 14 | ![功能预览2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) 15 | 16 | ## 🔌 支持的 MCP 客户端 17 | 18 | 本服务器适用于所有支持 MCP 协议的 AI 助手和客户端,包括但不限于: 19 | 20 | - 🤖 Claude 桌面应用 21 | - 📝 Cursor 22 | - 💼 其他支持 MCP 的客户端 23 | 24 | ## ✨ 美味功能 25 | 26 | 该 MCP 服务器提供以下美食工具: 27 | 28 | 1. **📚 查询全部菜谱** - 获取所有可用菜谱数据,做菜百科全书 -- 慎用这个--上下文太大 29 | 2. **🔍 根据分类查询菜谱** - 按照分类筛选菜谱,想吃水产?早餐?荤菜?主食?一键搞定! 30 | 3. **🧩 智能推荐膳食** - 根据你的忌口、过敏原和用餐人数,为你规划整整一周的美味佳肴 31 | 4. **🎲 不知道吃什么** - 选择困难症福音!根据人数直接推荐今日菜单,再也不用纠结了 32 | 33 | ## 🚀 快速上手 34 | 35 | ### 📋 先决条件 36 | 37 | - Node.js 16.0.0+ 🟢 38 | - npm 或 yarn 📦 39 | 40 | ### 💻 安装步骤 41 | 42 | 1. 克隆美食仓库 43 | 44 | ```bash 45 | git clone https://github.com/worryzyy/howtocook-mcp.git 46 | cd howtocook-mcp 47 | ``` 48 | 49 | 2. 安装依赖(就像准备食材一样简单!) 50 | 51 | ```bash 52 | npm install 53 | ``` 54 | 55 | 3. 编译代码(烹饪过程...) 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | ## 🍽️ 开始使用 62 | 63 | ### 🔥 启动服务器 64 | 65 | ```bash 66 | npm start 67 | ``` 68 | 69 | ### 🔧 配置 MCP 客户端 70 | 71 | #### 推荐使用 Cursor 快速体验(两种方式) 72 | 73 | 1. 使用 npm 包:请先运行 `npm i -g howtocook-mcp` ,否则会出现 `Failed to create client` 74 | 75 | 然后在 Cursor 设置中添加 MCP 服务器配置: 76 | 77 | ```json 78 | { 79 | "mcpServers": { 80 | "howtocook-mcp": { 81 | "command": "npx", 82 | "args": ["-y", "howtocook-mcp"] 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | 2. 如果是克隆仓库本地运行,请使用如下配置 89 | 90 | ```json 91 | { 92 | "mcpServers": { 93 | "howtocook-mcp": { 94 | "command": "node", 95 | "args": ["youpath\\howtocook-mcp\\build\\index.js"] 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | #### 其他 MCP 客户端 102 | 103 | 对于其他支持 MCP 协议的客户端,请参考各自的文档进行配置,通常需要指定: 104 | 105 | - 服务器名称: `howtocook-mcp` 106 | - 命令: `npx -y howtocook-mcp` 107 | 108 | 3. 重启客户端,让美食魔法生效 ✨ 109 | 110 | ## 🧙‍♂️ 菜单魔法使用指南 111 | 112 | 以下是在各种 MCP 客户端中使用的示例提示语: 113 | 114 | ### 1. 📚 查询全部菜谱 115 | 116 | 无需参数,直接召唤美食全书! 117 | 118 | ``` 119 | 请使用howtocook的MCP服务查询所有菜谱 120 | ``` 121 | 122 | ### 2. 🔍 根据分类查询菜谱 123 | 124 | ``` 125 | 请使用howtocook的MCP服务查询水产类的菜谱 126 | ``` 127 | 128 | 参数: 129 | 130 | - `category`: 菜谱分类(水产、早餐、荤菜、主食等) 131 | 132 | ### 3. 🧩 智能推荐一周菜谱 133 | 134 | ``` 135 | 请使用howtocook的MCP服务为3人推荐一周菜谱,我们家不吃香菜,对虾过敏 136 | ``` 137 | 138 | 参数: 139 | 140 | - `allergies`: 过敏原列表,如 ["大蒜", "虾"] 141 | - `avoidItems`: 忌口食材,如 ["葱", "姜"] 142 | - `peopleCount`: 用餐人数 (1-10) 143 | 144 | ### 4. 🎲 今天吃什么? 145 | 146 | ``` 147 | 请使用howtocook的MCP服务为4人晚餐推荐菜单 148 | ``` 149 | 150 | 参数: 151 | 152 | - `peopleCount`: 用餐人数 (1-10) 153 | 154 | ## 📝 小贴士 155 | 156 | - 该包已发布至 npm,可直接通过`npm install -g howtocook-mcp`全局安装 157 | - 本服务兼容所有支持 MCP 协议的 AI 助手和应用 158 | - 首次使用时,AI 可能需要一点时间来熟悉如何使用这些工具(就像烧热锅一样) 159 | 160 | ## 🤝 贡献 161 | 162 | 欢迎 Fork 和 Pull Request,让我们一起完善这个美食助手! 163 | 164 | ## 📄 许可 165 | 166 | MIT License - 随意使用,就像分享美食配方一样慷慨! 167 | 168 | --- 169 | 170 | > 🍴 美食即将开始,胃口准备好了吗? 171 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # 🍳 HowToCook-MCP Server 🥘 -- Plan Your Weekly Meals, No More Daily Struggles 2 | 3 | English | [简体中文](./README.md) 4 | 5 | > Turn your AI assistant into a personal chef that helps plan your daily meals! 6 | 7 | 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?" 8 | 9 | Data Source: [Anduin2017/HowToCook](https://github.com/Anduin2017/HowToCook) ⭐ Don't forget to star the repo if you haven't already! 10 | 11 | ## 📸 Preview 12 | 13 | ![Feature Preview 1](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/1.png) 14 | ![Feature Preview 2](https://mp-bc8d1f0a-3356-4a4e-8592-f73a3371baa2.cdn.bspapp.com/npm/2.png) 15 | 16 | ## 🔌 Supported MCP Clients 17 | 18 | This server works with all AI assistants and clients that support the MCP protocol, including but not limited to: 19 | 20 | - 🤖 Claude Desktop App 21 | - 📝 Cursor 22 | - 💼 Other MCP-compatible clients 23 | 24 | ## ✨ Delicious Features 25 | 26 | This MCP server provides the following culinary tools: 27 | 28 | 1. **📚 Query All Recipes** - Access all available recipe data, your complete cooking encyclopedia -- Use with caution due to large context size 29 | 2. **🔍 Query Recipes by Category** - Filter recipes by category: seafood, breakfast, meat dishes, staple foods, and more! 30 | 3. **🧩 Smart Meal Planning** - Get a full week's meal plan based on dietary restrictions, allergies, and number of diners 31 | 4. **🎲 Don't Know What to Eat?** - Perfect for the indecisive! Get instant menu recommendations based on party size 32 | 33 | ## 🚀 Quick Start 34 | 35 | ### 📋 Prerequisites 36 | 37 | - Node.js 16.0.0+ 🟢 38 | - npm or yarn 📦 39 | 40 | ### 💻 Installation 41 | 42 | 1. Clone the repository 43 | 44 | ```bash 45 | git clone https://github.com/worryzyy/howtocook-mcp.git 46 | cd howtocook-mcp 47 | ``` 48 | 49 | 2. Install dependencies (as simple as preparing ingredients!) 50 | 51 | ```bash 52 | npm install 53 | ``` 54 | 55 | 3. Build the code (the cooking process...) 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | ## 🍽️ Getting Started 62 | 63 | ### 🔥 Start the Server 64 | 65 | ```bash 66 | npm start 67 | ``` 68 | 69 | ### 🔧 Configure MCP Clients 70 | 71 | #### It is recommended to use Cursor for quick experience (two methods)Cursor Configuration 72 | 73 | 1. Using npm package: Please run `npm i -g howtocook-mcp` first, otherwise `Failed to create client` will appear 74 | 75 | Then add the MCP server configuration in Cursor settings: 76 | 77 | ```json 78 | { 79 | "mcpServers": { 80 | "howtocook-mcp": { 81 | "command": "npx", 82 | "args": ["-y", "howtocook-mcp"] 83 | } 84 | } 85 | } 86 | ``` 87 | 88 | 2. If running from a local cloned repository, use this configuration: 89 | 90 | ```json 91 | { 92 | "mcpServers": { 93 | "howtocook-mcp": { 94 | "command": "node", 95 | "args": ["yourpath\\howtocook-mcp\\build\\index.js"] 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | #### Other MCP Clients 102 | 103 | For other clients supporting the MCP protocol, refer to their respective documentation. Generally, you'll need to specify: 104 | 105 | - Server name: `howtocook-mcp` 106 | - Command: `npx -y howtocook-mcp` 107 | 108 | 3. Restart the client to activate the culinary magic ✨ 109 | 110 | ## 🧙‍♂️ Culinary Magic Usage Guide 111 | 112 | Here are example prompts for using these tools in MCP clients: 113 | 114 | ### 1. 📚 Query All Recipes 115 | 116 | No parameters needed, just summon the culinary encyclopedia! 117 | 118 | ``` 119 | Please use the howtocook MCP service to query all recipes 120 | ``` 121 | 122 | ### 2. 🔍 Query Recipes by Category 123 | 124 | ``` 125 | Please use the howtocook MCP service to query seafood recipes 126 | ``` 127 | 128 | Parameters: 129 | 130 | - `category`: Recipe category (seafood, breakfast, meat dishes, staple foods, etc.) 131 | 132 | ### 3. 🧩 Smart Meal Planning 133 | 134 | ``` 135 | 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. 136 | ``` 137 | 138 | Parameters: 139 | 140 | - `allergies`: List of allergens, e.g., ["garlic", "shrimp"] 141 | - `avoidItems`: Dietary restrictions, e.g., ["green onion", "ginger"] 142 | - `peopleCount`: Number of diners (1-10) 143 | 144 | ### 4. 🎲 What to Eat Today? 145 | 146 | ``` 147 | Please use the howtocook MCP service to recommend a dinner menu for 4 people 148 | ``` 149 | 150 | Parameters: 151 | 152 | - `peopleCount`: Number of diners (1-10) 153 | 154 | ## 📝 Tips 155 | 156 | - This package is published on npm and can be installed globally via `npm install -g howtocook-mcp` 157 | - Compatible with all AI assistants and applications that support the MCP protocol 158 | - On first use, AI may need some time to familiarize itself with these tools (like preheating an oven) 159 | 160 | ## 🤝 Contributing 161 | 162 | Forks and Pull Requests are welcome! Let's improve this culinary assistant together! 163 | 164 | ## 📄 License 165 | 166 | MIT License - Feel free to use, just like sharing your favorite recipes! 167 | 168 | --- 169 | 170 | > 🍴 The feast is about to begin, is your appetite ready? 171 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "howtocook-mcp", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "howtocook-mcp", 9 | "version": "0.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.9.0", 13 | "zod": "^3.22.4" 14 | }, 15 | "bin": { 16 | "howtocook-mcp": "build/index.js" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20.11.24", 20 | "typescript": "^5.3.3" 21 | } 22 | }, 23 | "node_modules/@modelcontextprotocol/sdk": { 24 | "version": "1.9.0", 25 | "resolved": "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", 26 | "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", 27 | "license": "MIT", 28 | "dependencies": { 29 | "content-type": "^1.0.5", 30 | "cors": "^2.8.5", 31 | "cross-spawn": "^7.0.3", 32 | "eventsource": "^3.0.2", 33 | "express": "^5.0.1", 34 | "express-rate-limit": "^7.5.0", 35 | "pkce-challenge": "^5.0.0", 36 | "raw-body": "^3.0.0", 37 | "zod": "^3.23.8", 38 | "zod-to-json-schema": "^3.24.1" 39 | }, 40 | "engines": { 41 | "node": ">=18" 42 | } 43 | }, 44 | "node_modules/@types/node": { 45 | "version": "20.17.30", 46 | "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.30.tgz", 47 | "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", 48 | "dev": true, 49 | "license": "MIT", 50 | "dependencies": { 51 | "undici-types": "~6.19.2" 52 | } 53 | }, 54 | "node_modules/accepts": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", 57 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 58 | "license": "MIT", 59 | "dependencies": { 60 | "mime-types": "^3.0.0", 61 | "negotiator": "^1.0.0" 62 | }, 63 | "engines": { 64 | "node": ">= 0.6" 65 | } 66 | }, 67 | "node_modules/body-parser": { 68 | "version": "2.2.0", 69 | "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.0.tgz", 70 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 71 | "license": "MIT", 72 | "dependencies": { 73 | "bytes": "^3.1.2", 74 | "content-type": "^1.0.5", 75 | "debug": "^4.4.0", 76 | "http-errors": "^2.0.0", 77 | "iconv-lite": "^0.6.3", 78 | "on-finished": "^2.4.1", 79 | "qs": "^6.14.0", 80 | "raw-body": "^3.0.0", 81 | "type-is": "^2.0.0" 82 | }, 83 | "engines": { 84 | "node": ">=18" 85 | } 86 | }, 87 | "node_modules/bytes": { 88 | "version": "3.1.2", 89 | "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", 90 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 91 | "license": "MIT", 92 | "engines": { 93 | "node": ">= 0.8" 94 | } 95 | }, 96 | "node_modules/call-bind-apply-helpers": { 97 | "version": "1.0.2", 98 | "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 99 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 100 | "license": "MIT", 101 | "dependencies": { 102 | "es-errors": "^1.3.0", 103 | "function-bind": "^1.1.2" 104 | }, 105 | "engines": { 106 | "node": ">= 0.4" 107 | } 108 | }, 109 | "node_modules/call-bound": { 110 | "version": "1.0.4", 111 | "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", 112 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 113 | "license": "MIT", 114 | "dependencies": { 115 | "call-bind-apply-helpers": "^1.0.2", 116 | "get-intrinsic": "^1.3.0" 117 | }, 118 | "engines": { 119 | "node": ">= 0.4" 120 | }, 121 | "funding": { 122 | "url": "https://github.com/sponsors/ljharb" 123 | } 124 | }, 125 | "node_modules/content-disposition": { 126 | "version": "1.0.0", 127 | "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz", 128 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 129 | "license": "MIT", 130 | "dependencies": { 131 | "safe-buffer": "5.2.1" 132 | }, 133 | "engines": { 134 | "node": ">= 0.6" 135 | } 136 | }, 137 | "node_modules/content-type": { 138 | "version": "1.0.5", 139 | "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", 140 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 141 | "license": "MIT", 142 | "engines": { 143 | "node": ">= 0.6" 144 | } 145 | }, 146 | "node_modules/cookie": { 147 | "version": "0.7.2", 148 | "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", 149 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 150 | "license": "MIT", 151 | "engines": { 152 | "node": ">= 0.6" 153 | } 154 | }, 155 | "node_modules/cookie-signature": { 156 | "version": "1.2.2", 157 | "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", 158 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 159 | "license": "MIT", 160 | "engines": { 161 | "node": ">=6.6.0" 162 | } 163 | }, 164 | "node_modules/cors": { 165 | "version": "2.8.5", 166 | "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz", 167 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 168 | "license": "MIT", 169 | "dependencies": { 170 | "object-assign": "^4", 171 | "vary": "^1" 172 | }, 173 | "engines": { 174 | "node": ">= 0.10" 175 | } 176 | }, 177 | "node_modules/cross-spawn": { 178 | "version": "7.0.6", 179 | "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", 180 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 181 | "license": "MIT", 182 | "dependencies": { 183 | "path-key": "^3.1.0", 184 | "shebang-command": "^2.0.0", 185 | "which": "^2.0.1" 186 | }, 187 | "engines": { 188 | "node": ">= 8" 189 | } 190 | }, 191 | "node_modules/debug": { 192 | "version": "4.4.0", 193 | "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", 194 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 195 | "license": "MIT", 196 | "dependencies": { 197 | "ms": "^2.1.3" 198 | }, 199 | "engines": { 200 | "node": ">=6.0" 201 | }, 202 | "peerDependenciesMeta": { 203 | "supports-color": { 204 | "optional": true 205 | } 206 | } 207 | }, 208 | "node_modules/depd": { 209 | "version": "2.0.0", 210 | "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", 211 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 212 | "license": "MIT", 213 | "engines": { 214 | "node": ">= 0.8" 215 | } 216 | }, 217 | "node_modules/dunder-proto": { 218 | "version": "1.0.1", 219 | "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", 220 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 221 | "license": "MIT", 222 | "dependencies": { 223 | "call-bind-apply-helpers": "^1.0.1", 224 | "es-errors": "^1.3.0", 225 | "gopd": "^1.2.0" 226 | }, 227 | "engines": { 228 | "node": ">= 0.4" 229 | } 230 | }, 231 | "node_modules/ee-first": { 232 | "version": "1.1.1", 233 | "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", 234 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 235 | "license": "MIT" 236 | }, 237 | "node_modules/encodeurl": { 238 | "version": "2.0.0", 239 | "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", 240 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 241 | "license": "MIT", 242 | "engines": { 243 | "node": ">= 0.8" 244 | } 245 | }, 246 | "node_modules/es-define-property": { 247 | "version": "1.0.1", 248 | "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", 249 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 250 | "license": "MIT", 251 | "engines": { 252 | "node": ">= 0.4" 253 | } 254 | }, 255 | "node_modules/es-errors": { 256 | "version": "1.3.0", 257 | "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", 258 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 259 | "license": "MIT", 260 | "engines": { 261 | "node": ">= 0.4" 262 | } 263 | }, 264 | "node_modules/es-object-atoms": { 265 | "version": "1.1.1", 266 | "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 267 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 268 | "license": "MIT", 269 | "dependencies": { 270 | "es-errors": "^1.3.0" 271 | }, 272 | "engines": { 273 | "node": ">= 0.4" 274 | } 275 | }, 276 | "node_modules/escape-html": { 277 | "version": "1.0.3", 278 | "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", 279 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 280 | "license": "MIT" 281 | }, 282 | "node_modules/etag": { 283 | "version": "1.8.1", 284 | "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", 285 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 286 | "license": "MIT", 287 | "engines": { 288 | "node": ">= 0.6" 289 | } 290 | }, 291 | "node_modules/eventsource": { 292 | "version": "3.0.6", 293 | "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.6.tgz", 294 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 295 | "license": "MIT", 296 | "dependencies": { 297 | "eventsource-parser": "^3.0.1" 298 | }, 299 | "engines": { 300 | "node": ">=18.0.0" 301 | } 302 | }, 303 | "node_modules/eventsource-parser": { 304 | "version": "3.0.1", 305 | "resolved": "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 306 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 307 | "license": "MIT", 308 | "engines": { 309 | "node": ">=18.0.0" 310 | } 311 | }, 312 | "node_modules/express": { 313 | "version": "5.1.0", 314 | "resolved": "https://registry.npmmirror.com/express/-/express-5.1.0.tgz", 315 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 316 | "license": "MIT", 317 | "dependencies": { 318 | "accepts": "^2.0.0", 319 | "body-parser": "^2.2.0", 320 | "content-disposition": "^1.0.0", 321 | "content-type": "^1.0.5", 322 | "cookie": "^0.7.1", 323 | "cookie-signature": "^1.2.1", 324 | "debug": "^4.4.0", 325 | "encodeurl": "^2.0.0", 326 | "escape-html": "^1.0.3", 327 | "etag": "^1.8.1", 328 | "finalhandler": "^2.1.0", 329 | "fresh": "^2.0.0", 330 | "http-errors": "^2.0.0", 331 | "merge-descriptors": "^2.0.0", 332 | "mime-types": "^3.0.0", 333 | "on-finished": "^2.4.1", 334 | "once": "^1.4.0", 335 | "parseurl": "^1.3.3", 336 | "proxy-addr": "^2.0.7", 337 | "qs": "^6.14.0", 338 | "range-parser": "^1.2.1", 339 | "router": "^2.2.0", 340 | "send": "^1.1.0", 341 | "serve-static": "^2.2.0", 342 | "statuses": "^2.0.1", 343 | "type-is": "^2.0.1", 344 | "vary": "^1.1.2" 345 | }, 346 | "engines": { 347 | "node": ">= 18" 348 | }, 349 | "funding": { 350 | "type": "opencollective", 351 | "url": "https://opencollective.com/express" 352 | } 353 | }, 354 | "node_modules/express-rate-limit": { 355 | "version": "7.5.0", 356 | "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 357 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 358 | "license": "MIT", 359 | "engines": { 360 | "node": ">= 16" 361 | }, 362 | "funding": { 363 | "url": "https://github.com/sponsors/express-rate-limit" 364 | }, 365 | "peerDependencies": { 366 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 367 | } 368 | }, 369 | "node_modules/finalhandler": { 370 | "version": "2.1.0", 371 | "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.0.tgz", 372 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 373 | "license": "MIT", 374 | "dependencies": { 375 | "debug": "^4.4.0", 376 | "encodeurl": "^2.0.0", 377 | "escape-html": "^1.0.3", 378 | "on-finished": "^2.4.1", 379 | "parseurl": "^1.3.3", 380 | "statuses": "^2.0.1" 381 | }, 382 | "engines": { 383 | "node": ">= 0.8" 384 | } 385 | }, 386 | "node_modules/forwarded": { 387 | "version": "0.2.0", 388 | "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", 389 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 390 | "license": "MIT", 391 | "engines": { 392 | "node": ">= 0.6" 393 | } 394 | }, 395 | "node_modules/fresh": { 396 | "version": "2.0.0", 397 | "resolved": "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", 398 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 399 | "license": "MIT", 400 | "engines": { 401 | "node": ">= 0.8" 402 | } 403 | }, 404 | "node_modules/function-bind": { 405 | "version": "1.1.2", 406 | "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", 407 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 408 | "license": "MIT", 409 | "funding": { 410 | "url": "https://github.com/sponsors/ljharb" 411 | } 412 | }, 413 | "node_modules/get-intrinsic": { 414 | "version": "1.3.0", 415 | "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 416 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 417 | "license": "MIT", 418 | "dependencies": { 419 | "call-bind-apply-helpers": "^1.0.2", 420 | "es-define-property": "^1.0.1", 421 | "es-errors": "^1.3.0", 422 | "es-object-atoms": "^1.1.1", 423 | "function-bind": "^1.1.2", 424 | "get-proto": "^1.0.1", 425 | "gopd": "^1.2.0", 426 | "has-symbols": "^1.1.0", 427 | "hasown": "^2.0.2", 428 | "math-intrinsics": "^1.1.0" 429 | }, 430 | "engines": { 431 | "node": ">= 0.4" 432 | }, 433 | "funding": { 434 | "url": "https://github.com/sponsors/ljharb" 435 | } 436 | }, 437 | "node_modules/get-proto": { 438 | "version": "1.0.1", 439 | "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", 440 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 441 | "license": "MIT", 442 | "dependencies": { 443 | "dunder-proto": "^1.0.1", 444 | "es-object-atoms": "^1.0.0" 445 | }, 446 | "engines": { 447 | "node": ">= 0.4" 448 | } 449 | }, 450 | "node_modules/gopd": { 451 | "version": "1.2.0", 452 | "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", 453 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 454 | "license": "MIT", 455 | "engines": { 456 | "node": ">= 0.4" 457 | }, 458 | "funding": { 459 | "url": "https://github.com/sponsors/ljharb" 460 | } 461 | }, 462 | "node_modules/has-symbols": { 463 | "version": "1.1.0", 464 | "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", 465 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 466 | "license": "MIT", 467 | "engines": { 468 | "node": ">= 0.4" 469 | }, 470 | "funding": { 471 | "url": "https://github.com/sponsors/ljharb" 472 | } 473 | }, 474 | "node_modules/hasown": { 475 | "version": "2.0.2", 476 | "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", 477 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 478 | "license": "MIT", 479 | "dependencies": { 480 | "function-bind": "^1.1.2" 481 | }, 482 | "engines": { 483 | "node": ">= 0.4" 484 | } 485 | }, 486 | "node_modules/http-errors": { 487 | "version": "2.0.0", 488 | "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", 489 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 490 | "license": "MIT", 491 | "dependencies": { 492 | "depd": "2.0.0", 493 | "inherits": "2.0.4", 494 | "setprototypeof": "1.2.0", 495 | "statuses": "2.0.1", 496 | "toidentifier": "1.0.1" 497 | }, 498 | "engines": { 499 | "node": ">= 0.8" 500 | } 501 | }, 502 | "node_modules/iconv-lite": { 503 | "version": "0.6.3", 504 | "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", 505 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 506 | "license": "MIT", 507 | "dependencies": { 508 | "safer-buffer": ">= 2.1.2 < 3.0.0" 509 | }, 510 | "engines": { 511 | "node": ">=0.10.0" 512 | } 513 | }, 514 | "node_modules/inherits": { 515 | "version": "2.0.4", 516 | "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", 517 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 518 | "license": "ISC" 519 | }, 520 | "node_modules/ipaddr.js": { 521 | "version": "1.9.1", 522 | "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 523 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 524 | "license": "MIT", 525 | "engines": { 526 | "node": ">= 0.10" 527 | } 528 | }, 529 | "node_modules/is-promise": { 530 | "version": "4.0.0", 531 | "resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", 532 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 533 | "license": "MIT" 534 | }, 535 | "node_modules/isexe": { 536 | "version": "2.0.0", 537 | "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", 538 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 539 | "license": "ISC" 540 | }, 541 | "node_modules/math-intrinsics": { 542 | "version": "1.1.0", 543 | "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 544 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 545 | "license": "MIT", 546 | "engines": { 547 | "node": ">= 0.4" 548 | } 549 | }, 550 | "node_modules/media-typer": { 551 | "version": "1.1.0", 552 | "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", 553 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 554 | "license": "MIT", 555 | "engines": { 556 | "node": ">= 0.8" 557 | } 558 | }, 559 | "node_modules/merge-descriptors": { 560 | "version": "2.0.0", 561 | "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 562 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 563 | "license": "MIT", 564 | "engines": { 565 | "node": ">=18" 566 | }, 567 | "funding": { 568 | "url": "https://github.com/sponsors/sindresorhus" 569 | } 570 | }, 571 | "node_modules/mime-db": { 572 | "version": "1.54.0", 573 | "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", 574 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 575 | "license": "MIT", 576 | "engines": { 577 | "node": ">= 0.6" 578 | } 579 | }, 580 | "node_modules/mime-types": { 581 | "version": "3.0.1", 582 | "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.1.tgz", 583 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 584 | "license": "MIT", 585 | "dependencies": { 586 | "mime-db": "^1.54.0" 587 | }, 588 | "engines": { 589 | "node": ">= 0.6" 590 | } 591 | }, 592 | "node_modules/ms": { 593 | "version": "2.1.3", 594 | "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", 595 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 596 | "license": "MIT" 597 | }, 598 | "node_modules/negotiator": { 599 | "version": "1.0.0", 600 | "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", 601 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 602 | "license": "MIT", 603 | "engines": { 604 | "node": ">= 0.6" 605 | } 606 | }, 607 | "node_modules/object-assign": { 608 | "version": "4.1.1", 609 | "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", 610 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 611 | "license": "MIT", 612 | "engines": { 613 | "node": ">=0.10.0" 614 | } 615 | }, 616 | "node_modules/object-inspect": { 617 | "version": "1.13.4", 618 | "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", 619 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 620 | "license": "MIT", 621 | "engines": { 622 | "node": ">= 0.4" 623 | }, 624 | "funding": { 625 | "url": "https://github.com/sponsors/ljharb" 626 | } 627 | }, 628 | "node_modules/on-finished": { 629 | "version": "2.4.1", 630 | "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", 631 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 632 | "license": "MIT", 633 | "dependencies": { 634 | "ee-first": "1.1.1" 635 | }, 636 | "engines": { 637 | "node": ">= 0.8" 638 | } 639 | }, 640 | "node_modules/once": { 641 | "version": "1.4.0", 642 | "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", 643 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 644 | "license": "ISC", 645 | "dependencies": { 646 | "wrappy": "1" 647 | } 648 | }, 649 | "node_modules/parseurl": { 650 | "version": "1.3.3", 651 | "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", 652 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 653 | "license": "MIT", 654 | "engines": { 655 | "node": ">= 0.8" 656 | } 657 | }, 658 | "node_modules/path-key": { 659 | "version": "3.1.1", 660 | "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", 661 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 662 | "license": "MIT", 663 | "engines": { 664 | "node": ">=8" 665 | } 666 | }, 667 | "node_modules/path-to-regexp": { 668 | "version": "8.2.0", 669 | "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 670 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 671 | "license": "MIT", 672 | "engines": { 673 | "node": ">=16" 674 | } 675 | }, 676 | "node_modules/pkce-challenge": { 677 | "version": "5.0.0", 678 | "resolved": "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 679 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 680 | "license": "MIT", 681 | "engines": { 682 | "node": ">=16.20.0" 683 | } 684 | }, 685 | "node_modules/proxy-addr": { 686 | "version": "2.0.7", 687 | "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", 688 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 689 | "license": "MIT", 690 | "dependencies": { 691 | "forwarded": "0.2.0", 692 | "ipaddr.js": "1.9.1" 693 | }, 694 | "engines": { 695 | "node": ">= 0.10" 696 | } 697 | }, 698 | "node_modules/qs": { 699 | "version": "6.14.0", 700 | "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", 701 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 702 | "license": "BSD-3-Clause", 703 | "dependencies": { 704 | "side-channel": "^1.1.0" 705 | }, 706 | "engines": { 707 | "node": ">=0.6" 708 | }, 709 | "funding": { 710 | "url": "https://github.com/sponsors/ljharb" 711 | } 712 | }, 713 | "node_modules/range-parser": { 714 | "version": "1.2.1", 715 | "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", 716 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 717 | "license": "MIT", 718 | "engines": { 719 | "node": ">= 0.6" 720 | } 721 | }, 722 | "node_modules/raw-body": { 723 | "version": "3.0.0", 724 | "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.0.tgz", 725 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 726 | "license": "MIT", 727 | "dependencies": { 728 | "bytes": "3.1.2", 729 | "http-errors": "2.0.0", 730 | "iconv-lite": "0.6.3", 731 | "unpipe": "1.0.0" 732 | }, 733 | "engines": { 734 | "node": ">= 0.8" 735 | } 736 | }, 737 | "node_modules/router": { 738 | "version": "2.2.0", 739 | "resolved": "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", 740 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 741 | "license": "MIT", 742 | "dependencies": { 743 | "debug": "^4.4.0", 744 | "depd": "^2.0.0", 745 | "is-promise": "^4.0.0", 746 | "parseurl": "^1.3.3", 747 | "path-to-regexp": "^8.0.0" 748 | }, 749 | "engines": { 750 | "node": ">= 18" 751 | } 752 | }, 753 | "node_modules/safe-buffer": { 754 | "version": "5.2.1", 755 | "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", 756 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 757 | "funding": [ 758 | { 759 | "type": "github", 760 | "url": "https://github.com/sponsors/feross" 761 | }, 762 | { 763 | "type": "patreon", 764 | "url": "https://www.patreon.com/feross" 765 | }, 766 | { 767 | "type": "consulting", 768 | "url": "https://feross.org/support" 769 | } 770 | ], 771 | "license": "MIT" 772 | }, 773 | "node_modules/safer-buffer": { 774 | "version": "2.1.2", 775 | "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", 776 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 777 | "license": "MIT" 778 | }, 779 | "node_modules/send": { 780 | "version": "1.2.0", 781 | "resolved": "https://registry.npmmirror.com/send/-/send-1.2.0.tgz", 782 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 783 | "license": "MIT", 784 | "dependencies": { 785 | "debug": "^4.3.5", 786 | "encodeurl": "^2.0.0", 787 | "escape-html": "^1.0.3", 788 | "etag": "^1.8.1", 789 | "fresh": "^2.0.0", 790 | "http-errors": "^2.0.0", 791 | "mime-types": "^3.0.1", 792 | "ms": "^2.1.3", 793 | "on-finished": "^2.4.1", 794 | "range-parser": "^1.2.1", 795 | "statuses": "^2.0.1" 796 | }, 797 | "engines": { 798 | "node": ">= 18" 799 | } 800 | }, 801 | "node_modules/serve-static": { 802 | "version": "2.2.0", 803 | "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.0.tgz", 804 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 805 | "license": "MIT", 806 | "dependencies": { 807 | "encodeurl": "^2.0.0", 808 | "escape-html": "^1.0.3", 809 | "parseurl": "^1.3.3", 810 | "send": "^1.2.0" 811 | }, 812 | "engines": { 813 | "node": ">= 18" 814 | } 815 | }, 816 | "node_modules/setprototypeof": { 817 | "version": "1.2.0", 818 | "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", 819 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 820 | "license": "ISC" 821 | }, 822 | "node_modules/shebang-command": { 823 | "version": "2.0.0", 824 | "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", 825 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 826 | "license": "MIT", 827 | "dependencies": { 828 | "shebang-regex": "^3.0.0" 829 | }, 830 | "engines": { 831 | "node": ">=8" 832 | } 833 | }, 834 | "node_modules/shebang-regex": { 835 | "version": "3.0.0", 836 | "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", 837 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 838 | "license": "MIT", 839 | "engines": { 840 | "node": ">=8" 841 | } 842 | }, 843 | "node_modules/side-channel": { 844 | "version": "1.1.0", 845 | "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", 846 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 847 | "license": "MIT", 848 | "dependencies": { 849 | "es-errors": "^1.3.0", 850 | "object-inspect": "^1.13.3", 851 | "side-channel-list": "^1.0.0", 852 | "side-channel-map": "^1.0.1", 853 | "side-channel-weakmap": "^1.0.2" 854 | }, 855 | "engines": { 856 | "node": ">= 0.4" 857 | }, 858 | "funding": { 859 | "url": "https://github.com/sponsors/ljharb" 860 | } 861 | }, 862 | "node_modules/side-channel-list": { 863 | "version": "1.0.0", 864 | "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", 865 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 866 | "license": "MIT", 867 | "dependencies": { 868 | "es-errors": "^1.3.0", 869 | "object-inspect": "^1.13.3" 870 | }, 871 | "engines": { 872 | "node": ">= 0.4" 873 | }, 874 | "funding": { 875 | "url": "https://github.com/sponsors/ljharb" 876 | } 877 | }, 878 | "node_modules/side-channel-map": { 879 | "version": "1.0.1", 880 | "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", 881 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 882 | "license": "MIT", 883 | "dependencies": { 884 | "call-bound": "^1.0.2", 885 | "es-errors": "^1.3.0", 886 | "get-intrinsic": "^1.2.5", 887 | "object-inspect": "^1.13.3" 888 | }, 889 | "engines": { 890 | "node": ">= 0.4" 891 | }, 892 | "funding": { 893 | "url": "https://github.com/sponsors/ljharb" 894 | } 895 | }, 896 | "node_modules/side-channel-weakmap": { 897 | "version": "1.0.2", 898 | "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 899 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 900 | "license": "MIT", 901 | "dependencies": { 902 | "call-bound": "^1.0.2", 903 | "es-errors": "^1.3.0", 904 | "get-intrinsic": "^1.2.5", 905 | "object-inspect": "^1.13.3", 906 | "side-channel-map": "^1.0.1" 907 | }, 908 | "engines": { 909 | "node": ">= 0.4" 910 | }, 911 | "funding": { 912 | "url": "https://github.com/sponsors/ljharb" 913 | } 914 | }, 915 | "node_modules/statuses": { 916 | "version": "2.0.1", 917 | "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", 918 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 919 | "license": "MIT", 920 | "engines": { 921 | "node": ">= 0.8" 922 | } 923 | }, 924 | "node_modules/toidentifier": { 925 | "version": "1.0.1", 926 | "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", 927 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 928 | "license": "MIT", 929 | "engines": { 930 | "node": ">=0.6" 931 | } 932 | }, 933 | "node_modules/type-is": { 934 | "version": "2.0.1", 935 | "resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", 936 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 937 | "license": "MIT", 938 | "dependencies": { 939 | "content-type": "^1.0.5", 940 | "media-typer": "^1.1.0", 941 | "mime-types": "^3.0.0" 942 | }, 943 | "engines": { 944 | "node": ">= 0.6" 945 | } 946 | }, 947 | "node_modules/typescript": { 948 | "version": "5.8.3", 949 | "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", 950 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 951 | "dev": true, 952 | "license": "Apache-2.0", 953 | "bin": { 954 | "tsc": "bin/tsc", 955 | "tsserver": "bin/tsserver" 956 | }, 957 | "engines": { 958 | "node": ">=14.17" 959 | } 960 | }, 961 | "node_modules/undici-types": { 962 | "version": "6.19.8", 963 | "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz", 964 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 965 | "dev": true, 966 | "license": "MIT" 967 | }, 968 | "node_modules/unpipe": { 969 | "version": "1.0.0", 970 | "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", 971 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 972 | "license": "MIT", 973 | "engines": { 974 | "node": ">= 0.8" 975 | } 976 | }, 977 | "node_modules/vary": { 978 | "version": "1.1.2", 979 | "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", 980 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 981 | "license": "MIT", 982 | "engines": { 983 | "node": ">= 0.8" 984 | } 985 | }, 986 | "node_modules/which": { 987 | "version": "2.0.2", 988 | "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", 989 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 990 | "license": "ISC", 991 | "dependencies": { 992 | "isexe": "^2.0.0" 993 | }, 994 | "bin": { 995 | "node-which": "bin/node-which" 996 | }, 997 | "engines": { 998 | "node": ">= 8" 999 | } 1000 | }, 1001 | "node_modules/wrappy": { 1002 | "version": "1.0.2", 1003 | "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", 1004 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1005 | "license": "ISC" 1006 | }, 1007 | "node_modules/zod": { 1008 | "version": "3.24.3", 1009 | "resolved": "https://registry.npmmirror.com/zod/-/zod-3.24.3.tgz", 1010 | "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 1011 | "license": "MIT", 1012 | "funding": { 1013 | "url": "https://github.com/sponsors/colinhacks" 1014 | } 1015 | }, 1016 | "node_modules/zod-to-json-schema": { 1017 | "version": "3.24.5", 1018 | "resolved": "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1019 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1020 | "license": "ISC", 1021 | "peerDependencies": { 1022 | "zod": "^3.24.1" 1023 | } 1024 | } 1025 | } 1026 | } 1027 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "howtocook-mcp", 3 | "version": "0.0.7", 4 | "type": "module", 5 | "main": "build/index.js", 6 | "bin": { 7 | "howtocook-mcp": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "start": "node build/index.js", 15 | "dev": "tsc && node build/index.js" 16 | }, 17 | "keywords": [ 18 | "howtocook", 19 | "mcp", 20 | "server", 21 | "recipe", 22 | "food", 23 | "cook" 24 | ], 25 | "author": "worry", 26 | "license": "ISC", 27 | "description": "MCP Server for howtocook recipe database - 炫一周好饭,拒绝拼好饭", 28 | "dependencies": { 29 | "@modelcontextprotocol/sdk": "^1.9.0", 30 | "zod": "^3.22.4" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.11.24", 34 | "typescript": "^5.3.3" 35 | }, 36 | "publishConfig": { 37 | "access": "public", 38 | "registry": "https://registry.npmjs.org" 39 | } 40 | } -------------------------------------------------------------------------------- /src/data/recipes.ts: -------------------------------------------------------------------------------- 1 | import { Recipe } from '../types/index.js'; 2 | 3 | // 远程菜谱JSON文件URL 4 | const RECIPES_URL = 'https://weilei.site/all_recipes.json'; 5 | 6 | // 从远程URL获取数据的异步函数 7 | export async function fetchRecipes(): Promise { 8 | try { 9 | // 使用fetch API获取远程数据 10 | const response = await fetch(RECIPES_URL); 11 | 12 | if (!response.ok) { 13 | throw new Error(`HTTP error! Status: ${response.status}`); 14 | } 15 | 16 | // 解析JSON数据 17 | const data = await response.json(); 18 | return data as Recipe[]; 19 | } catch (error) { 20 | console.error('获取远程菜谱数据失败:', error); 21 | // 直接返回空数组,不尝试使用本地备份 22 | return []; 23 | } 24 | } 25 | 26 | // 获取所有分类 27 | export function getAllCategories(recipes: Recipe[]): string[] { 28 | const categories = new Set(); 29 | recipes.forEach((recipe) => { 30 | if (recipe.category) { 31 | categories.add(recipe.category); 32 | } 33 | }); 34 | return Array.from(categories); 35 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { fetchRecipes, getAllCategories } from "./data/recipes.js"; 6 | import { registerGetAllRecipesTool } from "./tools/getAllRecipes.js"; 7 | import { registerGetRecipesByCategoryTool } from "./tools/getRecipesByCategory.js"; 8 | import { registerRecommendMealsTool } from "./tools/recommendMeals.js"; 9 | import { registerWhatToEatTool } from "./tools/whatToEat.js"; 10 | 11 | // 创建MCP服务器 12 | const server = new McpServer({ 13 | name: "howtocook-mcp", 14 | version: "0.0.6", 15 | capabilities: { 16 | resources: {}, 17 | tools: {}, 18 | }, 19 | }); 20 | 21 | // 启动服务的主函数 22 | export async function startServer() { 23 | // 获取菜谱数据 24 | const recipes = await fetchRecipes(); 25 | 26 | // 确保我们读取到了菜谱数据 27 | if (recipes.length === 0) { 28 | console.error('无法获取菜谱数据,服务退出'); 29 | process.exit(1); 30 | } 31 | 32 | // 获取所有分类 33 | const categories = getAllCategories(recipes); 34 | 35 | // 注册所有工具 36 | registerGetAllRecipesTool(server, recipes); 37 | registerGetRecipesByCategoryTool(server, recipes, categories); 38 | registerRecommendMealsTool(server, recipes); 39 | registerWhatToEatTool(server, recipes); 40 | 41 | // 启动MCP服务器 42 | const transport = new StdioServerTransport(); 43 | 44 | try { 45 | await server.connect(transport); 46 | console.log('HowToCook MCP服务器启动成功'); 47 | } catch (error) { 48 | console.error('服务器启动失败:', error); 49 | process.exit(1); 50 | } 51 | } 52 | 53 | startServer(); 54 | 55 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | const selectedMeatDishes: Recipe[] = []; 45 | 46 | // 需要选择的荤菜数量 47 | const remainingMeatCount = fishDish ? meatCount - 1 : meatCount; 48 | 49 | // 尝试按照肉类优先级选择荤菜 50 | for (const meatType of meatTypes) { 51 | if (selectedMeatDishes.length >= remainingMeatCount) break; 52 | 53 | const meatTypeOptions = meatDishes.filter((dish) => { 54 | // 检查菜品的材料是否包含这种肉类 55 | return dish.ingredients?.some((ingredient) => { 56 | const name = ingredient.name?.toLowerCase() || ''; 57 | return name.includes(meatType.toLowerCase()); 58 | }); 59 | }); 60 | 61 | if (meatTypeOptions.length > 0) { 62 | // 随机选择一道这种肉类的菜 63 | const selected = meatTypeOptions[Math.floor(Math.random() * meatTypeOptions.length)]; 64 | selectedMeatDishes.push(selected); 65 | // 从可选列表中移除,避免重复选择 66 | meatDishes = meatDishes.filter((dish) => dish.id !== selected.id); 67 | } 68 | } 69 | 70 | // 如果通过肉类筛选的荤菜不够,随机选择剩余的 71 | while (selectedMeatDishes.length < remainingMeatCount && meatDishes.length > 0) { 72 | const randomIndex = Math.floor(Math.random() * meatDishes.length); 73 | selectedMeatDishes.push(meatDishes[randomIndex]); 74 | meatDishes.splice(randomIndex, 1); 75 | } 76 | 77 | // 随机选择素菜 78 | const selectedVegetableDishes: Recipe[] = []; 79 | while (selectedVegetableDishes.length < vegetableCount && vegetableDishes.length > 0) { 80 | const randomIndex = Math.floor(Math.random() * vegetableDishes.length); 81 | selectedVegetableDishes.push(vegetableDishes[randomIndex]); 82 | vegetableDishes.splice(randomIndex, 1); 83 | } 84 | 85 | // 合并推荐菜单 86 | recommendedDishes = recommendedDishes.concat(selectedMeatDishes, selectedVegetableDishes); 87 | 88 | // 构建推荐结果 89 | const recommendationDetails: DishRecommendation = { 90 | peopleCount, 91 | meatDishCount: selectedMeatDishes.length + (fishDish ? 1 : 0), 92 | vegetableDishCount: selectedVegetableDishes.length, 93 | dishes: recommendedDishes.map(simplifyRecipe), 94 | message: `为${peopleCount}人推荐的菜品,包含${selectedMeatDishes.length + (fishDish ? 1 : 0)}个荤菜和${selectedVegetableDishes.length}个素菜。` 95 | }; 96 | 97 | return { 98 | content: [ 99 | { 100 | type: "text", 101 | text: JSON.stringify(recommendationDetails, null, 2), 102 | }, 103 | ], 104 | }; 105 | } 106 | ); 107 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------