Suggest Recipe
suggest_recipeGenerate a beer recipe for a target style, including grain bill, hop schedule, yeast selection, and process parameters.
Instructions
Suggest a beer recipe for a target style. Returns grain bill, hop schedule, yeast selection, and process parameters.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| style | Yes | Target beer style for the recipe | |
| batch_size_litres | No | Batch size in litres |
Implementation Reference
- src/register-tools.ts:9-16 (registration)Registration of the suggest_recipe tool in the central tool registry.
export function registerTools(server: McpServer): void { registerSearchStyles(server); registerSearchIngredients(server); registerDiagnoseOffFlavour(server); registerMatchWaterProfile(server); registerSuggestRecipe(server); registerPairingGuide(server); } - src/tools/suggest-recipe.ts:195-310 (handler)The main handler function that registers the suggest_recipe tool. Contains the entire tool logic: finding a style, selecting malts/hops/yeast/water, calculating grain weight, and returning a formatted recipe.
export function registerSuggestRecipe(server: McpServer): void { server.registerTool( "suggest_recipe", { title: "Suggest Recipe", description: "Suggest a beer recipe for a target style. Returns grain bill, hop schedule, yeast selection, and process parameters.", inputSchema: { style: z.string().describe("Target beer style for the recipe"), batch_size_litres: z .number() .default(20) .describe("Batch size in litres"), }, }, async ({ style: styleQuery, batch_size_litres }) => { const matchedStyle = findStyle(styleQuery); if (!matchedStyle) { return { isError: true, content: [ { type: "text" as const, text: `Could not find a style matching '${styleQuery}'. Try names like 'American IPA', 'Stout', 'Pilsner', or 'Hefeweizen'.`, }, ], }; } const { vitalStats: v } = matchedStyle; const ogTarget = (v.ogMin + v.ogMax) / 2; const ibuTarget = Math.round((v.ibuMin + v.ibuMax) / 2); const abvTarget = ((v.abvMin + v.abvMax) / 2).toFixed(1); const malts = selectMalts(matchedStyle.name, matchedStyle.category, matchedStyle.tags); const hops = selectHops(matchedStyle.name); const yeast = selectYeast(matchedStyle.name, matchedStyle.tags); const water = selectWater(matchedStyle.name); const totalGrainKg = calculateGrainWeight(ogTarget, batch_size_litres); // When specialty malts are present, hold base ≈ 85% and split the // remaining 15% across specialty grains. With no specialty (e.g. pale // styles like American IPA / Pilsner) the base malt provides 100%. const baseFraction = malts.specialty.length > 0 ? 0.85 : 1.0; const baseKg = (totalGrainKg * baseFraction).toFixed(2); const specialtyKg = malts.specialty.length > 0 ? (totalGrainKg * 0.15 / malts.specialty.length).toFixed(2) : "0"; // Determine mash temp based on style character const tags = matchedStyle.tags.join(" ").toLowerCase(); const isDry = tags.includes("bitter") || tags.includes("hoppy") || matchedStyle.name.toLowerCase().includes("ipa"); const mashTemp = isDry ? "65°C (149°F) — lower for drier finish" : "67°C (153°F) — higher for fuller body"; // Yeast tempMin/tempMax are stored in °C (see src/data/yeasts.ts — // ales 18–23, lagers 9–15, kveik 25–40). Convert °C → °F for display. const fermTempFMin = Math.round(yeast.tempMin * 9 / 5 + 32); const fermTempFMax = Math.round(yeast.tempMax * 9 / 5 + 32); const fermTemp = `${yeast.tempMin}-${yeast.tempMax}°C (${fermTempFMin}-${fermTempFMax}°F)`; const lines: string[] = [ `# Recipe: ${matchedStyle.name}`, `Batch size: ${batch_size_litres} litres | Target OG: ${ogTarget.toFixed(3)} | IBU: ${ibuTarget} | ABV: ~${abvTarget}%`, "", "## Grain Bill", `- ${malts.base.name}: ${baseKg} kg (base)`, ...malts.specialty.map((m) => `- ${m.name}: ${specialtyKg} kg (${m.type})`), "", "## Hop Schedule", ]; if (hops.length >= 1) { const bitteringHop = hops.find((h) => h.purpose === "bittering" || h.purpose === "dual") ?? hops[0]; lines.push(`- ${bitteringHop.name}: 60 min (bittering) — target ~${ibuTarget} IBU`); const aromaHop = hops.length > 1 ? hops[1] : hops[0]; lines.push(`- ${aromaHop.name}: 5 min (aroma)`); if (isDry) { lines.push(`- ${aromaHop.name}: dry hop 3-5 days`); } } lines.push( "", "## Yeast", `- ${yeast.name} (${yeast.producer} ${yeast.code})`, ` Type: ${yeast.type} | Attenuation: ${yeast.attenuationMin}-${yeast.attenuationMax}%`, ` Flavour: ${yeast.flavourProfile}`, "", "## Process", `- Mash: ${mashTemp}`, "- Boil: 60 minutes", `- Fermentation: ${fermTemp}`, `- Conditioning: ${tags.includes("lager") ? "4-6 weeks cold conditioning" : "2 weeks at room temperature"}`, ); if (water) { lines.push( "", "## Water", `- Target profile: ${water.name} (${water.city})`, ` Ca: ${water.calcium} | Mg: ${water.magnesium} | SO4: ${water.sulfate} | Cl: ${water.chloride}`, ); } return { content: [ { type: "text" as const, text: lines.join("\n"), }, ], }; }, ); } - src/tools/suggest-recipe.ts:202-208 (schema)Input schema for the suggest_recipe tool. Accepts a style string (required) and batch_size_litres (optional, default 20).
inputSchema: { style: z.string().describe("Target beer style for the recipe"), batch_size_litres: z .number() .default(20) .describe("Batch size in litres"), }, - src/tools/suggest-recipe.ts:10-13 (handler)Helper function to find a beer style by fuzzy-searching name and category fields.
function findStyle(query: string) { const results = fuzzySearch(STYLES, query, ["name", "category"]); return results.length > 0 ? results[0] : null; } - src/tools/suggest-recipe.ts:179-193 (helper)Helper function that calculates the required grain weight in kg based on target OG, batch size, and mash efficiency.
function calculateGrainWeight( ogTarget: number, batchLitres: number, efficiency: number = 0.72, ): number { // OG points: (OG - 1) * 1000. e.g. 1.065 → 65 gravity points per litre. const ogPoints = (ogTarget - 1) * 1000; // Total gravity-points needed across the whole batch (litre-points). const totalPoints = ogPoints * batchLitres; // Effective yield per kg of grain across the whole batch volume, // assuming a 37 PPG base malt at the given mash efficiency. // Result is in litre-points per kg of grain. const yieldPerKg = POINTS_PER_KG_PER_LITRE_AT_37_PPG * efficiency; return totalPoints / yieldPerKg; }