brewers-almanack 0.1.2 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,277 @@
1
+ import { z } from "zod";
2
+ import { WATER_PROFILES } from "../data/water-profiles.js";
3
+ import { fuzzySearch } from "../lib/search.js";
4
+ /**
5
+ * Chemistry constants: ppm contribution per gram per litre of water.
6
+ */
7
+ const SALTS = {
8
+ gypsum: { calcium: 61.5, sulfate: 147.4 },
9
+ calciumChloride: { calcium: 72.0, chloride: 127.4 },
10
+ epsom: { magnesium: 26.1, sulfate: 103.0 },
11
+ bakingSoda: { sodium: 72.3, bicarbonate: 176.1 },
12
+ };
13
+ /**
14
+ * Lactic acid (88%, 1mL/L): reduces alkalinity by ~50 ppm as CaCO3.
15
+ * We approximate: 1 mL/L removes ~50 ppm bicarbonate equivalent.
16
+ */
17
+ const LACTIC_ACID_BICARB_REDUCTION_PER_ML = 50;
18
+ function resolveProfile(name) {
19
+ const results = fuzzySearch(WATER_PROFILES, name, ["name", "city"]);
20
+ if (results.length === 0)
21
+ return null;
22
+ const p = results[0];
23
+ return {
24
+ calcium: p.calcium,
25
+ magnesium: p.magnesium,
26
+ sodium: p.sodium,
27
+ chloride: p.chloride,
28
+ sulfate: p.sulfate,
29
+ bicarbonate: p.bicarbonate,
30
+ };
31
+ }
32
+ function calculateAdditions(source, target) {
33
+ const delta = {
34
+ calcium: Math.max(0, target.calcium - source.calcium),
35
+ magnesium: Math.max(0, target.magnesium - source.magnesium),
36
+ sodium: Math.max(0, target.sodium - source.sodium),
37
+ chloride: Math.max(0, target.chloride - source.chloride),
38
+ sulfate: Math.max(0, target.sulfate - source.sulfate),
39
+ bicarbonate: target.bicarbonate - source.bicarbonate, // can be negative
40
+ };
41
+ // Sulfate deficit: 80% from gypsum, 20% from Epsom
42
+ const sulfateFromGypsum = delta.sulfate * 0.8;
43
+ const sulfateFromEpsom = delta.sulfate * 0.2;
44
+ // g/L of gypsum needed for sulfate contribution
45
+ let gypsumGPerL = sulfateFromGypsum / SALTS.gypsum.sulfate;
46
+ // g/L of Epsom needed for sulfate contribution
47
+ let epsomGPerL = sulfateFromEpsom / SALTS.epsom.sulfate;
48
+ // Calcium contributed by gypsum
49
+ const caFromGypsum = gypsumGPerL * SALTS.gypsum.calcium;
50
+ // Chloride deficit: from CaCl2
51
+ let cacl2GPerL = delta.chloride / SALTS.calciumChloride.chloride;
52
+ const caFromCaCl2 = cacl2GPerL * SALTS.calciumChloride.calcium;
53
+ // Remaining calcium need after gypsum + CaCl2 contributions
54
+ const remainingCa = delta.calcium - caFromGypsum - caFromCaCl2;
55
+ if (remainingCa > 0) {
56
+ // Add more CaCl2 to cover remaining calcium
57
+ const extraCaCl2 = remainingCa / SALTS.calciumChloride.calcium;
58
+ cacl2GPerL += extraCaCl2;
59
+ }
60
+ // Magnesium beyond what Epsom already provides
61
+ const mgFromEpsom = epsomGPerL * SALTS.epsom.magnesium;
62
+ const remainingMg = delta.magnesium - mgFromEpsom;
63
+ if (remainingMg > 0) {
64
+ const extraEpsom = remainingMg / SALTS.epsom.magnesium;
65
+ epsomGPerL += extraEpsom;
66
+ }
67
+ // Sodium deficit: from baking soda
68
+ let bakingSodaGPerL = 0;
69
+ if (delta.sodium > 0) {
70
+ bakingSodaGPerL = delta.sodium / SALTS.bakingSoda.sodium;
71
+ }
72
+ // Bicarbonate: if target < source, need acid
73
+ let lacticAcidMlPerL = 0;
74
+ if (delta.bicarbonate < 0) {
75
+ const bicarbReduction = Math.abs(delta.bicarbonate);
76
+ lacticAcidMlPerL = bicarbReduction / LACTIC_ACID_BICARB_REDUCTION_PER_ML;
77
+ }
78
+ return {
79
+ gypsum: gypsumGPerL,
80
+ calciumChloride: cacl2GPerL,
81
+ epsom: epsomGPerL,
82
+ bakingSoda: bakingSodaGPerL,
83
+ lacticAcidMl: lacticAcidMlPerL,
84
+ };
85
+ }
86
+ function round1(n) {
87
+ return Math.round(n * 10) / 10;
88
+ }
89
+ export function registerCalculateWaterAdditions(server) {
90
+ server.registerTool("calculate_water_additions", {
91
+ title: "Calculate Water Additions",
92
+ description: "Calculate mineral salt additions needed to adjust brewing water from a source profile to a target profile. Supports named profiles or custom ion values.",
93
+ inputSchema: {
94
+ source_profile: z
95
+ .string()
96
+ .optional()
97
+ .describe("Named source water profile (e.g. 'Reverse Osmosis', 'Dublin')"),
98
+ source_calcium: z.number().optional().describe("Source calcium (ppm)"),
99
+ source_magnesium: z.number().optional().describe("Source magnesium (ppm)"),
100
+ source_sodium: z.number().optional().describe("Source sodium (ppm)"),
101
+ source_chloride: z.number().optional().describe("Source chloride (ppm)"),
102
+ source_sulfate: z.number().optional().describe("Source sulfate (ppm)"),
103
+ source_bicarbonate: z.number().optional().describe("Source bicarbonate (ppm)"),
104
+ target_profile: z
105
+ .string()
106
+ .optional()
107
+ .describe("Named target water profile (e.g. 'Burton-on-Trent', 'Pilsen')"),
108
+ target_calcium: z.number().optional().describe("Target calcium (ppm)"),
109
+ target_magnesium: z.number().optional().describe("Target magnesium (ppm)"),
110
+ target_sodium: z.number().optional().describe("Target sodium (ppm)"),
111
+ target_chloride: z.number().optional().describe("Target chloride (ppm)"),
112
+ target_sulfate: z.number().optional().describe("Target sulfate (ppm)"),
113
+ target_bicarbonate: z.number().optional().describe("Target bicarbonate (ppm)"),
114
+ volume_litres: z.number().describe("Water volume in litres"),
115
+ },
116
+ }, async ({ source_profile, source_calcium, source_magnesium, source_sodium, source_chloride, source_sulfate, source_bicarbonate, target_profile, target_calcium, target_magnesium, target_sodium, target_chloride, target_sulfate, target_bicarbonate, volume_litres, }) => {
117
+ // Resolve source
118
+ let source = null;
119
+ if (source_profile) {
120
+ source = resolveProfile(source_profile);
121
+ if (!source) {
122
+ return {
123
+ content: [
124
+ {
125
+ type: "text",
126
+ text: `Source profile '${source_profile}' not found. Try: ${WATER_PROFILES.map((p) => p.name).join(", ")}`,
127
+ },
128
+ ],
129
+ };
130
+ }
131
+ }
132
+ else if (source_calcium !== undefined ||
133
+ source_magnesium !== undefined ||
134
+ source_sodium !== undefined ||
135
+ source_chloride !== undefined ||
136
+ source_sulfate !== undefined ||
137
+ source_bicarbonate !== undefined) {
138
+ source = {
139
+ calcium: source_calcium ?? 0,
140
+ magnesium: source_magnesium ?? 0,
141
+ sodium: source_sodium ?? 0,
142
+ chloride: source_chloride ?? 0,
143
+ sulfate: source_sulfate ?? 0,
144
+ bicarbonate: source_bicarbonate ?? 0,
145
+ };
146
+ }
147
+ else {
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: "Please provide either a source_profile name or custom source ion values (source_calcium, source_magnesium, etc.).",
153
+ },
154
+ ],
155
+ };
156
+ }
157
+ // Resolve target
158
+ let target = null;
159
+ let targetName = "Custom";
160
+ if (target_profile) {
161
+ target = resolveProfile(target_profile);
162
+ if (!target) {
163
+ return {
164
+ content: [
165
+ {
166
+ type: "text",
167
+ text: `Target profile '${target_profile}' not found. Try: ${WATER_PROFILES.map((p) => p.name).join(", ")}`,
168
+ },
169
+ ],
170
+ };
171
+ }
172
+ targetName = target_profile;
173
+ }
174
+ else if (target_calcium !== undefined ||
175
+ target_magnesium !== undefined ||
176
+ target_sodium !== undefined ||
177
+ target_chloride !== undefined ||
178
+ target_sulfate !== undefined ||
179
+ target_bicarbonate !== undefined) {
180
+ target = {
181
+ calcium: target_calcium ?? 0,
182
+ magnesium: target_magnesium ?? 0,
183
+ sodium: target_sodium ?? 0,
184
+ chloride: target_chloride ?? 0,
185
+ sulfate: target_sulfate ?? 0,
186
+ bicarbonate: target_bicarbonate ?? 0,
187
+ };
188
+ }
189
+ else {
190
+ return {
191
+ content: [
192
+ {
193
+ type: "text",
194
+ text: "Please provide either a target_profile name or custom target ion values (target_calcium, target_sulfate, etc.).",
195
+ },
196
+ ],
197
+ };
198
+ }
199
+ // Check if source and target are identical
200
+ const ionsMatch = source.calcium === target.calcium &&
201
+ source.magnesium === target.magnesium &&
202
+ source.sodium === target.sodium &&
203
+ source.chloride === target.chloride &&
204
+ source.sulfate === target.sulfate &&
205
+ source.bicarbonate === target.bicarbonate;
206
+ if (ionsMatch) {
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text",
211
+ text: `# Water Additions: → ${targetName}\nVolume: ${volume_litres}L\n\n## No additions needed — source already matches target.`,
212
+ },
213
+ ],
214
+ };
215
+ }
216
+ // Calculate additions per litre
217
+ const additionsPerL = calculateAdditions(source, target);
218
+ // Scale to batch volume
219
+ const batch = {
220
+ gypsum: round1(additionsPerL.gypsum * volume_litres),
221
+ calciumChloride: round1(additionsPerL.calciumChloride * volume_litres),
222
+ epsom: round1(additionsPerL.epsom * volume_litres),
223
+ bakingSoda: round1(additionsPerL.bakingSoda * volume_litres),
224
+ lacticAcidMl: round1(additionsPerL.lacticAcidMl * volume_litres),
225
+ };
226
+ // Build output
227
+ const ions = [
228
+ { label: "Ca", src: source.calcium, tgt: target.calcium },
229
+ { label: "Mg", src: source.magnesium, tgt: target.magnesium },
230
+ { label: "Na", src: source.sodium, tgt: target.sodium },
231
+ { label: "Cl", src: source.chloride, tgt: target.chloride },
232
+ { label: "SO4", src: source.sulfate, tgt: target.sulfate },
233
+ { label: "HCO3", src: source.bicarbonate, tgt: target.bicarbonate },
234
+ ];
235
+ const lines = [
236
+ `# Water Additions: → ${targetName}`,
237
+ `Volume: ${volume_litres}L`,
238
+ "",
239
+ "## Source → Target",
240
+ "| Ion | Source | Target | Delta |",
241
+ "|-----|--------|--------|-------|",
242
+ ...ions.map((ion) => {
243
+ const delta = ion.tgt - ion.src;
244
+ const sign = delta >= 0 ? "+" : "";
245
+ return `| ${ion.label} | ${ion.src} | ${ion.tgt} | ${sign}${delta} |`;
246
+ }),
247
+ "",
248
+ "## Additions",
249
+ ];
250
+ const additionLines = [];
251
+ if (batch.gypsum > 0) {
252
+ additionLines.push(`- **Gypsum (CaSO4):** ${batch.gypsum} g`);
253
+ }
254
+ if (batch.calciumChloride > 0) {
255
+ additionLines.push(`- **Calcium Chloride (CaCl2):** ${batch.calciumChloride} g`);
256
+ }
257
+ if (batch.epsom > 0) {
258
+ additionLines.push(`- **Epsom Salt (MgSO4):** ${batch.epsom} g`);
259
+ }
260
+ if (batch.bakingSoda > 0) {
261
+ additionLines.push(`- **Baking Soda (NaHCO3):** ${batch.bakingSoda} g`);
262
+ }
263
+ if (batch.lacticAcidMl > 0) {
264
+ additionLines.push(`- **Lactic Acid (88%):** ${batch.lacticAcidMl} mL`);
265
+ }
266
+ if (additionLines.length === 0) {
267
+ additionLines.push("No salt additions needed.");
268
+ }
269
+ lines.push(...additionLines);
270
+ lines.push("");
271
+ lines.push("*Note: This is a simplified calculation. Actual results may vary based on water chemistry interactions, mash pH buffering, and grain bill composition. Always measure and adjust.*");
272
+ return {
273
+ content: [{ type: "text", text: lines.join("\n") }],
274
+ };
275
+ });
276
+ }
277
+ //# sourceMappingURL=calculate-water-additions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calculate-water-additions.js","sourceRoot":"","sources":["../../src/tools/calculate-water-additions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCheckGebStock(server: McpServer): void;
3
+ //# sourceMappingURL=check-geb-stock.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { z } from "zod";
2
+ import { GEB_INVENTORY, GEB_LAST_UPDATED } from "../data/geb-inventory.js";
3
+ import { HOPS } from "../data/hops.js";
4
+ import { MALTS } from "../data/malts.js";
5
+ import { YEASTS } from "../data/yeasts.js";
6
+ function searchInventory(query, category) {
7
+ let items = GEB_INVENTORY;
8
+ if (category) {
9
+ items = items.filter((p) => p.category.toLowerCase().includes(category.toLowerCase()));
10
+ }
11
+ const queryLower = query.toLowerCase();
12
+ return items.filter((p) => p.name.toLowerCase().includes(queryLower));
13
+ }
14
+ function formatPrice(product) {
15
+ if (product.unit === "per_gram") {
16
+ // Show as £/kg for readability (price is stored per gram)
17
+ return `£${(product.price_per_gram_gbp * 1000).toFixed(2)}/kg`;
18
+ }
19
+ return `£${product.price_per_gram_gbp.toFixed(2)} each`;
20
+ }
21
+ function formatStock(status) {
22
+ switch (status) {
23
+ case "in_stock": return "✓ In Stock";
24
+ case "low_stock": return "⚠ Low Stock";
25
+ case "out_of_stock": return "✗ Out of Stock";
26
+ }
27
+ }
28
+ function isAvailable(product) {
29
+ return product.stock_status !== "out_of_stock";
30
+ }
31
+ function findSubstitutes(query) {
32
+ const hop = HOPS.find((h) => h.name.toLowerCase().includes(query.toLowerCase()));
33
+ if (hop)
34
+ return hop.substitutes;
35
+ const malt = MALTS.find((m) => m.name.toLowerCase().includes(query.toLowerCase()));
36
+ if (malt) {
37
+ const colourMin = malt.colour * 0.5;
38
+ const colourMax = malt.colour * 1.5 + 2;
39
+ return MALTS.filter((m) => m.name !== malt.name &&
40
+ m.type === malt.type &&
41
+ m.colour >= colourMin &&
42
+ m.colour <= colourMax)
43
+ .slice(0, 3)
44
+ .map((m) => m.name);
45
+ }
46
+ const yeast = YEASTS.find((y) => y.name.toLowerCase().includes(query.toLowerCase()) ||
47
+ y.code.toLowerCase().includes(query.toLowerCase()));
48
+ if (yeast) {
49
+ return YEASTS.filter((y) => y.name !== yeast.name && y.type === yeast.type)
50
+ .slice(0, 3)
51
+ .map((y) => `${y.name} (${y.code})`);
52
+ }
53
+ return [];
54
+ }
55
+ export function registerCheckGebStock(server) {
56
+ server.registerTool("check_geb_stock", {
57
+ title: "Check GEB Stock",
58
+ description: "Check GetErBrewed.com custom kit builder inventory for ingredient availability and pricing. Data is updated weekly. If an item is out of stock, suggests substitutions.",
59
+ inputSchema: {
60
+ query: z
61
+ .string()
62
+ .describe("Ingredient to search for (e.g. 'Citra', 'Maris Otter', 'US-05')"),
63
+ category: z
64
+ .string()
65
+ .optional()
66
+ .describe("Filter by category (e.g. 'T90 Hops', 'Base Malt', 'Dried Yeast')"),
67
+ },
68
+ }, async ({ query, category }) => {
69
+ const matches = searchInventory(query, category);
70
+ const lines = [
71
+ `# GEB Stock: ${query}`,
72
+ `*Last updated: ${GEB_LAST_UPDATED.split("T")[0]}*`,
73
+ "",
74
+ ];
75
+ if (GEB_INVENTORY.length === 0) {
76
+ lines.push("Inventory data not yet synced. Run `npm run fetch-geb` or wait for the weekly sync.");
77
+ return {
78
+ content: [{ type: "text", text: lines.join("\n") }],
79
+ };
80
+ }
81
+ if (matches.length === 0) {
82
+ lines.push(`No products found matching "${query}".`);
83
+ const subs = findSubstitutes(query);
84
+ if (subs.length > 0) {
85
+ lines.push("", "## Try Instead");
86
+ for (const sub of subs) {
87
+ const subMatches = searchInventory(sub);
88
+ const available = subMatches.filter(isAvailable);
89
+ if (available.length > 0) {
90
+ lines.push(`- **${sub}** — ${formatPrice(available[0])} ${formatStock(available[0].stock_status)}`);
91
+ }
92
+ else if (subMatches.length > 0) {
93
+ lines.push(`- **${sub}** — listed but out of stock`);
94
+ }
95
+ else {
96
+ lines.push(`- **${sub}** — not found at GEB`);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ else {
102
+ const available = matches.filter(isAvailable);
103
+ const outOfStock = matches.filter((m) => !isAvailable(m));
104
+ if (available.length > 0) {
105
+ lines.push("## Available");
106
+ for (const p of available) {
107
+ lines.push(`- ${p.name} — ${formatPrice(p)} ${formatStock(p.stock_status)}`);
108
+ }
109
+ }
110
+ if (outOfStock.length > 0) {
111
+ lines.push("", "## Out of Stock");
112
+ for (const p of outOfStock) {
113
+ lines.push(`- ${p.name} — ${formatPrice(p)}`);
114
+ }
115
+ if (available.length === 0) {
116
+ const subs = findSubstitutes(query);
117
+ if (subs.length > 0) {
118
+ lines.push("", "## Substitutes Available at GEB");
119
+ for (const sub of subs) {
120
+ const subMatches = searchInventory(sub);
121
+ const subAvailable = subMatches.filter(isAvailable);
122
+ if (subAvailable.length > 0) {
123
+ lines.push(`- **${sub}** — ${formatPrice(subAvailable[0])} ${formatStock(subAvailable[0].stock_status)}`);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ return { content: [{ type: "text", text: lines.join("\n") }] };
131
+ });
132
+ }
133
+ //# sourceMappingURL=check-geb-stock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-geb-stock.js","sourceRoot":"","sources":["../../src/tools/check-geb-stock.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE3E,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C,SAAS,eAAe,CAAC,KAAa,EAAE,QAAiB;IACvD,IAAI,KAAK,GAAG,aAAa,CAAC;IAC1B,IAAI,QAAQ,EAAE,CAAC;QACb,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACzB,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAC1D,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACvC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,WAAW,CAAC,OAAmB;IACtC,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAChC,0DAA0D;QAC1D,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;IACjE,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAC1D,CAAC;AAED,SAAS,WAAW,CAAC,MAAkC;IACrD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,UAAU,CAAC,CAAC,OAAO,YAAY,CAAC;QACrC,KAAK,WAAW,CAAC,CAAC,OAAO,aAAa,CAAC;QACvC,KAAK,cAAc,CAAC,CAAC,OAAO,gBAAgB,CAAC;IAC/C,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAmB;IACtC,OAAO,OAAO,CAAC,YAAY,KAAK,cAAc,CAAC;AACjD,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC1B,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CACnD,CAAC;IACF,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC;IAEhC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5B,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CACnD,CAAC;IACF,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QACpC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC;QACxC,OAAO,KAAK,CAAC,MAAM,CACjB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;YACpB,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI;YACpB,CAAC,CAAC,MAAM,IAAI,SAAS;YACrB,CAAC,CAAC,MAAM,IAAI,SAAS,CACxB;aACE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxB,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CACvB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAClD,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CACrD,CAAC;IACF,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;aACxE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;IACzC,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,MAAM,CAAC,YAAY,CACjB,iBAAiB,EACjB;QACE,KAAK,EAAE,iBAAiB;QACxB,WAAW,EACT,yKAAyK;QAC3K,WAAW,EAAE;YACX,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,QAAQ,CACP,iEAAiE,CAClE;YACH,QAAQ,EAAE,CAAC;iBACR,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CACP,kEAAkE,CACnE;SACJ;KACF,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5B,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAEjD,MAAM,KAAK,GAAa;YACtB,gBAAgB,KAAK,EAAE;YACvB,kBAAkB,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG;YACnD,EAAE;SACH,CAAC;QAEF,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,KAAK,CAAC,IAAI,CACR,qFAAqF,CACtF,CAAC;YACF,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;aAC7D,CAAC;QACJ,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,+BAA+B,KAAK,IAAI,CAAC,CAAC;YAErD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;gBACjC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;oBACvB,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;oBACxC,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;oBACjD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACzB,KAAK,CAAC,IAAI,CACR,OAAO,GAAG,QAAQ,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CACxF,CAAC;oBACJ,CAAC;yBAAM,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACjC,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,8BAA8B,CAAC,CAAC;oBACvD,CAAC;yBAAM,CAAC;wBACN,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,uBAAuB,CAAC,CAAC;oBAChD,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;aAAM,CAAC;YACN,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAE1D,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzB,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBAC3B,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;oBAC1B,KAAK,CAAC,IAAI,CACR,KAAK,CAAC,CAAC,IAAI,MAAM,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CACjE,CAAC;gBACJ,CAAC;YACH,CAAC;YAED,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,iBAAiB,CAAC,CAAC;gBAClC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;oBAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAChD,CAAC;gBAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC3B,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;oBACpC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACpB,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,iCAAiC,CAAC,CAAC;wBAClD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;4BACvB,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;4BACxC,MAAM,YAAY,GAAG,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;4BACpD,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gCAC5B,KAAK,CAAC,IAAI,CACR,OAAO,GAAG,QAAQ,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,CAC9F,CAAC;4BACJ,CAAC;wBACH,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;IAC1E,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerReviewRecipe(server: McpServer): void;
3
+ //# sourceMappingURL=review-recipe.d.ts.map
@@ -0,0 +1,148 @@
1
+ import { z } from "zod";
2
+ import { STYLES } from "../data/styles.js";
3
+ import { fuzzySearch } from "../lib/search.js";
4
+ function findStyleOrSubStyle(query) {
5
+ const lower = query.toLowerCase();
6
+ // First: exact match on sub-style names
7
+ for (const style of STYLES) {
8
+ if (style.subStyles) {
9
+ for (const sub of style.subStyles) {
10
+ if (sub.name.toLowerCase() === lower) {
11
+ return { name: sub.name, vitalStats: sub.vitalStats };
12
+ }
13
+ }
14
+ }
15
+ }
16
+ // Second: partial match on sub-style names
17
+ for (const style of STYLES) {
18
+ if (style.subStyles) {
19
+ for (const sub of style.subStyles) {
20
+ if (sub.name.toLowerCase().includes(lower) || lower.includes(sub.name.toLowerCase())) {
21
+ return { name: sub.name, vitalStats: sub.vitalStats };
22
+ }
23
+ }
24
+ }
25
+ }
26
+ // Third: fuzzy search on main styles
27
+ const results = fuzzySearch(STYLES, query, ["name", "category"]);
28
+ if (results.length > 0) {
29
+ return { name: results[0].name, vitalStats: results[0].vitalStats };
30
+ }
31
+ return null;
32
+ }
33
+ function checkParam(label, value, min, max) {
34
+ if (value < min) {
35
+ return {
36
+ label,
37
+ value,
38
+ min,
39
+ max,
40
+ pass: false,
41
+ detail: `too low (min ${formatValue(label, min)})`,
42
+ };
43
+ }
44
+ if (value > max) {
45
+ return {
46
+ label,
47
+ value,
48
+ min,
49
+ max,
50
+ pass: false,
51
+ detail: `too high (max ${formatValue(label, max)})`,
52
+ };
53
+ }
54
+ return {
55
+ label,
56
+ value,
57
+ min,
58
+ max,
59
+ pass: true,
60
+ detail: "in range",
61
+ };
62
+ }
63
+ function formatValue(label, value) {
64
+ if (label === "OG" || label === "FG") {
65
+ return value.toFixed(3);
66
+ }
67
+ if (label === "ABV") {
68
+ return value.toFixed(1);
69
+ }
70
+ return String(value);
71
+ }
72
+ function formatRange(label, min, max) {
73
+ return `[${formatValue(label, min)}–${formatValue(label, max)}]`;
74
+ }
75
+ export function registerReviewRecipe(server) {
76
+ server.registerTool("review_recipe", {
77
+ title: "Review Recipe",
78
+ description: "Review a beer recipe against BJCP style guidelines. Checks OG, FG, IBU, SRM, and ABV against the style's vital statistics and returns a scorecard.",
79
+ inputSchema: {
80
+ style: z.string().describe("Target beer style to check against (e.g. 'American IPA', 'Black IPA')"),
81
+ og: z.number().describe("Original gravity (e.g. 1.065)"),
82
+ fg: z.number().describe("Final gravity (e.g. 1.012)"),
83
+ ibu: z.number().describe("International Bitterness Units"),
84
+ srm: z.number().describe("Standard Reference Method (colour)"),
85
+ abv: z.number().describe("Alcohol by volume percentage"),
86
+ },
87
+ }, async ({ style: styleQuery, og, fg, ibu, srm, abv }) => {
88
+ const matched = findStyleOrSubStyle(styleQuery);
89
+ if (!matched) {
90
+ return {
91
+ isError: true,
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Could not find a style matching '${styleQuery}'. Try names like 'American IPA', 'Stout', 'Pilsner', or 'Hefeweizen'.`,
96
+ },
97
+ ],
98
+ };
99
+ }
100
+ const { name, vitalStats: v } = matched;
101
+ const checks = [
102
+ checkParam("OG", og, v.ogMin, v.ogMax),
103
+ checkParam("FG", fg, v.fgMin, v.fgMax),
104
+ checkParam("IBU", ibu, v.ibuMin, v.ibuMax),
105
+ checkParam("SRM", srm, v.srmMin, v.srmMax),
106
+ checkParam("ABV", abv, v.abvMin, v.abvMax),
107
+ ];
108
+ const passCount = checks.filter((c) => c.pass).length;
109
+ const total = checks.length;
110
+ const lines = [
111
+ `# Recipe Review: ${name}`,
112
+ "",
113
+ `## Vital Stats Check (${passCount}/${total} in range)`,
114
+ "",
115
+ ];
116
+ for (const c of checks) {
117
+ const icon = c.pass ? "✓" : "✗";
118
+ const valueStr = formatValue(c.label, c.value);
119
+ const rangeStr = formatRange(c.label, c.min, c.max);
120
+ if (c.pass) {
121
+ lines.push(`- ${icon} **${c.label}**: ${valueStr} — in range ${rangeStr}`);
122
+ }
123
+ else {
124
+ lines.push(`- ${icon} **${c.label}**: ${valueStr} — ${c.detail} ${rangeStr}`);
125
+ }
126
+ }
127
+ lines.push("", "## Summary");
128
+ const issues = checks.filter((c) => !c.pass);
129
+ if (issues.length === 0) {
130
+ lines.push("All parameters are within BJCP guidelines for this style. Recipe looks good!");
131
+ }
132
+ else {
133
+ lines.push(`${issues.length} parameter${issues.length > 1 ? "s" : ""} out of range:`);
134
+ for (const issue of issues) {
135
+ lines.push(`- **${issue.label}** is ${issue.detail}`);
136
+ }
137
+ }
138
+ return {
139
+ content: [
140
+ {
141
+ type: "text",
142
+ text: lines.join("\n"),
143
+ },
144
+ ],
145
+ };
146
+ });
147
+ }
148
+ //# sourceMappingURL=review-recipe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-recipe.js","sourceRoot":"","sources":["../../src/tools/review-recipe.ts"],"names":[],"mappings":"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"}