cook-mcp-windy
Version:
HowToCook MCP Server - Intelligent Chinese recipe management and meal planning for AI assistants
279 lines • 13.1 kB
JavaScript
import { z } from 'zod';
import { recipeDataManager } from '../data/recipeData.js';
import { DishCombination, CombinationRequest, CombinationAnalysis } from '../types/index.js';
import { generateDishCombination } from '../utils/dishCombinationUtils.js';
// Input schema for recommend_dish_combination tool
const RecommendDishCombinationInput = z.object({
preferences: z.object({
numberOfPeople: z.number().min(1).max(20).describe('Number of diners'),
occasion: z.enum(['日常', '聚餐', '节日', '招待客人', '特殊场合']).optional().describe('Dining occasion'),
budgetLevel: z.enum(['经济', '中等', '高档']).optional().describe('Budget preference'),
dietaryRestrictions: z.array(z.string()).optional().describe('Dietary restrictions'),
preferredFlavors: z.array(z.string()).optional().describe('Preferred flavor profiles (酸甜苦辣咸鲜香)'),
cookingTime: z.string().optional().describe('Available cooking time (e.g., "2小时")'),
skillLevel: z.enum(['初学者', '中级', '高级']).optional().describe('Cooking skill level'),
seasonalPreference: z.boolean().optional().describe('Prefer seasonal ingredients'),
nutritionalBalance: z.boolean().optional().describe('Focus on nutritional balance'),
}).describe('User preferences for dish combination'),
excludeRecipeIds: z.array(z.string()).optional().describe('Recipe IDs to exclude from recommendations'),
includeRecipeIds: z.array(z.string()).optional().describe('Recipe IDs that must be included'),
maxDishes: z.number().min(2).max(10).default(6).optional().describe('Maximum number of dishes in combination'),
minDishes: z.number().min(2).max(8).default(3).optional().describe('Minimum number of dishes in combination'),
includeAnalysis: z.boolean().default(true).optional().describe('Whether to include detailed combination analysis'),
includeAlternatives: z.boolean().default(true).optional().describe('Whether to suggest alternative combinations')
});
// Response type for dish combination recommendation
const DishCombinationResponse = z.object({
combination: z.any().describe('The recommended dish combination'),
analysis: z.object({
nutritionalScore: z.number().min(0).max(10).describe('Nutritional balance score (0-10)'),
flavorHarmony: z.number().min(0).max(10).describe('Flavor harmony score (0-10)'),
difficultyBalance: z.number().min(0).max(10).describe('Cooking difficulty balance score (0-10)'),
timeEfficiency: z.number().min(0).max(10).describe('Time efficiency score (0-10)'),
overallScore: z.number().min(0).max(10).describe('Overall combination score (0-10)'),
strengths: z.array(z.string()).describe('Strengths of this combination'),
improvements: z.array(z.string()).describe('Suggested improvements')
}).optional().describe('Detailed analysis of the combination'),
alternatives: z.array(z.object({
name: z.string().describe('Alternative combination name'),
description: z.string().describe('Brief description'),
keyDifferences: z.array(z.string()).describe('Key differences from main recommendation')
})).optional().describe('Alternative combination suggestions'),
cookingPlan: z.object({
preparationOrder: z.array(z.string()).describe('Suggested order of preparation'),
timelineEstimate: z.string().describe('Estimated total cooking timeline'),
parallelTasks: z.array(z.string()).describe('Tasks that can be done in parallel'),
criticalTiming: z.array(z.string()).describe('Critical timing points to watch')
}).describe('Detailed cooking plan and timeline')
});
/**
* Tool definition for recommending dish combinations
*/
export const recommendDishCombinationTool = {
name: 'recommend_dish_combination',
description: 'Suggest suitable dish combinations based on number of diners, occasion, dietary restrictions, and cooking preferences. Provides balanced meal combinations with nutritional analysis, flavor harmony assessment, and detailed cooking plans. Considers skill level, time constraints, and budget to create optimal dining experiences.',
inputSchema: {
type: 'object',
properties: {
preferences: {
type: 'object',
properties: {
numberOfPeople: { type: 'number', minimum: 1, maximum: 20 },
occasion: { type: 'string', enum: ['日常', '聚餐', '节日', '招待客人', '特殊场合'] },
budgetLevel: { type: 'string', enum: ['经济', '中等', '高档'] },
dietaryRestrictions: { type: 'array', items: { type: 'string' } },
cookingTime: { type: 'string' },
skillLevel: { type: 'string', enum: ['初学者', '中级', '高级'] }
},
required: ['numberOfPeople']
},
maxDishes: { type: 'number', minimum: 2, maximum: 10, default: 6 },
minDishes: { type: 'number', minimum: 2, maximum: 8, default: 3 },
includeAnalysis: { type: 'boolean', default: true }
},
required: ['preferences']
}
};
/**
* Handler for recommend_dish_combination tool
*/
export async function handleRecommendDishCombination(input) {
try {
// Validate input
const validatedInput = RecommendDishCombinationInput.parse(input);
const { preferences, excludeRecipeIds, includeRecipeIds, maxDishes = 6, minDishes = 3, includeAnalysis = true, includeAlternatives = true } = validatedInput;
// Validate dish count constraints
if (minDishes > maxDishes) {
return {
success: false,
error: 'Invalid dish count constraints',
details: 'Minimum dishes cannot be greater than maximum dishes'
};
}
// Get all available recipes
const allRecipes = recipeDataManager.getAllRecipes();
if (allRecipes.length === 0) {
return {
success: false,
error: 'No recipes available',
details: 'Recipe database is empty'
};
}
// Create combination request
const combinationRequest = {
preferences,
excludeRecipeIds,
includeRecipeIds,
maxDishes,
minDishes
};
// Generate dish combination
const combination = generateDishCombination(allRecipes, combinationRequest);
// Prepare response
const response = {
combination,
cookingPlan: generateCookingPlan(combination)
};
// Add analysis if requested
if (includeAnalysis) {
response.analysis = analyzeCombination(combination);
}
// Add alternatives if requested
if (includeAlternatives) {
response.alternatives = generateAlternativeCombinations(allRecipes, combinationRequest, combination);
}
return {
success: true,
message: `Generated ${combination.dishes.length}-dish combination for ${preferences.numberOfPeople} people`,
data: response
};
}
catch (error) {
return {
success: false,
error: 'Failed to generate dish combination',
details: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
/**
* Generate detailed cooking plan
*/
function generateCookingPlan(combination) {
// Sort dishes by preparation order
const sortedDishes = [...combination.dishes].sort((a, b) => a.preparationOrder - b.preparationOrder);
const preparationOrder = sortedDishes.map(dish => `${dish.preparationOrder}. ${dish.recipe.name} (${dish.role})`);
// Calculate timeline
const totalPrepTime = parseTimeInMinutes(combination.totalPrepTime);
const totalCookTime = parseTimeInMinutes(combination.totalCookTime);
const timelineEstimate = `总计约${formatTimeFromMinutes(totalPrepTime + totalCookTime)},其中准备${combination.totalPrepTime},烹饪${combination.totalCookTime}`;
// Generate parallel tasks
const parallelTasks = [
'可以同时处理多种蔬菜的清洗和切配',
'汤类可以先开始炖煮,同时准备其他菜品',
'利用蒸煮时间处理其他食材'
];
// Generate critical timing points
const criticalTiming = [
'注意掌握各道菜的最佳出锅时间',
'热菜要最后制作,确保上桌时的温度',
'汤品可以提前完成,保温即可'
];
// Add specific timing based on dishes
const hasStirFry = sortedDishes.some(dish => dish.recipe.cookingMethods?.includes('炒'));
if (hasStirFry) {
criticalTiming.push('炒菜要大火快炒,掌握好火候');
}
const hasSoup = sortedDishes.some(dish => dish.role === '汤品');
if (hasSoup) {
parallelTasks.push('汤品可以最先开始制作,慢炖出味');
}
return {
preparationOrder,
timelineEstimate,
parallelTasks,
criticalTiming
};
}
/**
* Analyze combination quality
*/
function analyzeCombination(combination) {
const { dishes, nutritionalBalance, flavorProfile } = combination;
// Calculate nutritional score
let nutritionalScore = 0;
if (nutritionalBalance.hasProtein)
nutritionalScore += 3;
if (nutritionalBalance.hasVegetables)
nutritionalScore += 3;
if (nutritionalBalance.hasCarbs)
nutritionalScore += 2;
if (nutritionalBalance.isBalanced)
nutritionalScore += 2;
// Calculate flavor harmony (based on variety and balance)
const flavorHarmony = Math.min(10, flavorProfile.length * 2);
// Calculate difficulty balance
const difficulties = dishes.map(d => d.recipe.difficulty);
const difficultyVariety = new Set(difficulties).size;
const difficultyBalance = Math.max(1, 11 - difficultyVariety * 2);
// Calculate time efficiency
const avgCookTime = dishes.reduce((sum, dish) => sum + parseTimeInMinutes(dish.recipe.cookTime), 0) / dishes.length;
const timeEfficiency = Math.max(1, Math.min(10, 10 - avgCookTime / 10));
// Overall score
const overallScore = Math.round((nutritionalScore + flavorHarmony + difficultyBalance + timeEfficiency) / 4);
// Generate strengths and improvements
const strengths = [];
const improvements = [];
if (nutritionalBalance.isBalanced) {
strengths.push('营养搭配均衡,包含蛋白质、蔬菜和主食');
}
else {
improvements.push('建议增加缺失的营养成分以达到更好的平衡');
}
if (flavorProfile.length >= 3) {
strengths.push('口味丰富多样,层次分明');
}
else {
improvements.push('可以考虑增加不同口味的菜品以丰富整体体验');
}
if (difficultyVariety <= 2) {
strengths.push('制作难度适中,便于掌控');
}
return {
nutritionalScore,
flavorHarmony,
difficultyBalance,
timeEfficiency,
overallScore,
strengths,
improvements
};
}
/**
* Generate alternative combinations
*/
function generateAlternativeCombinations(_allRecipes, originalRequest, _originalCombination) {
const alternatives = [];
// Budget-conscious alternative
if (originalRequest.preferences.budgetLevel !== '经济') {
alternatives.push({
name: '经济实惠版',
description: '使用更经济的食材,保持营养和口味平衡',
keyDifferences: ['选用价格更亲民的食材', '简化部分制作工艺', '保持营养均衡']
});
}
// Quick version
alternatives.push({
name: '快手版',
description: '缩短制作时间,适合忙碌时准备',
keyDifferences: ['选择制作时间更短的菜品', '简化准备步骤', '保持口味丰富']
});
// Seasonal alternative
alternatives.push({
name: '时令版',
description: '根据当季食材调整搭配',
keyDifferences: ['使用当季新鲜食材', '调整菜品组合', '突出季节特色']
});
return alternatives.slice(0, 3);
}
// Helper functions
function parseTimeInMinutes(timeStr) {
const hourMatch = timeStr.match(/(\d+)小时/);
const minuteMatch = timeStr.match(/(\d+)分钟/);
let totalMinutes = 0;
if (hourMatch && hourMatch[1])
totalMinutes += parseInt(hourMatch[1]) * 60;
if (minuteMatch && minuteMatch[1])
totalMinutes += parseInt(minuteMatch[1]);
return totalMinutes;
}
function formatTimeFromMinutes(minutes) {
if (minutes < 60)
return `${minutes}分钟`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (remainingMinutes === 0)
return `${hours}小时`;
return `${hours}小时${remainingMinutes}分钟`;
}
//# sourceMappingURL=recommendDishCombination.js.map