mcp-marmiton 1.1.0 → 1.2.0

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.
package/README.md CHANGED
@@ -71,6 +71,15 @@ original: half a sachet at factor 0.667 becomes a third of a sachet, never a who
71
71
  one. And scaling never **silently drops** an ingredient by rounding it to zero,
72
72
  which is why small amounts come back as fractions.
73
73
 
74
+ Ranges are read as one quantity: "2 à 3 gousses" doubled reads "4 à 6 gousses",
75
+ and `amount` reports the upper bound, since that is what a cook buys.
76
+
77
+ Below a quarter there is nothing a kitchen can measure, so the amount is clamped
78
+ up and **the line stops holding its share of the recipe**. Half a sachet of
79
+ baking powder against three pots of flour comes out four times too strong that
80
+ way, so the ingredient and the response both say it happened rather than leaving
81
+ you to find out in the oven.
82
+
74
83
  `scale_ingredients` exposes the same logic **without any network request**, so it
75
84
  also works on a recipe pasted from somewhere else.
76
85
 
@@ -157,6 +166,19 @@ and use your judgement.
157
166
 
158
167
  This is an unofficial project, with no affiliation to or endorsement by Marmiton.
159
168
 
169
+ ## Contributing
170
+
171
+ Bugs, questions and ideas all belong in
172
+ [the issue tracker](https://github.com/smeet666/mcp-marmiton/issues). Pull requests
173
+ are welcome; please open an issue first so we can agree on what the right
174
+ answer is before you write it. [CONTRIBUTING.md](CONTRIBUTING.md) has the
175
+ detail, and [SECURITY.md](SECURITY.md) covers anything exploitable.
176
+
177
+ ## Support
178
+
179
+ These servers are free and stay free. If one of them saved you an afternoon,
180
+ you can [buy me a coffee](https://buymeacoffee.com/smeet666).
181
+
160
182
  ## License
161
183
 
162
184
  MIT. See [LICENSE](./LICENSE). The license covers this source code only, not the
@@ -229,6 +251,16 @@ l'originale : un demi-sachet au facteur 0,667 devient un tiers de sachet, jamais
229
251
  sachet entier. Et l'adaptation ne **supprime jamais** un ingrédient en l'arrondissant
230
252
  à zéro, d'où les fractions pour les petites quantités.
231
253
 
254
+ Les fourchettes sont lues comme une seule quantité : « 2 à 3 gousses » doublé
255
+ donne « 4 à 6 gousses », et `amount` renvoie la borne haute, celle que l'on
256
+ achète.
257
+
258
+ Sous un quart, il n'y a plus rien qu'une cuisine puisse mesurer : la quantité est
259
+ alors remontée et **la ligne cesse de tenir sa part de la recette**. Un demi-sachet
260
+ de levure face à trois pots de farine ressort ainsi quatre fois trop dosé, donc
261
+ l'ingrédient et la réponse le disent, plutôt que de vous le laisser découvrir au
262
+ four.
263
+
232
264
  `scale_ingredients` expose la même logique **sans aucune requête réseau**, ce qui
233
265
  permet aussi d'adapter une recette venue d'ailleurs.
234
266
 
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
6
  // src/version.ts
7
- var PKG_VERSION = "1.1.0";
7
+ var PKG_VERSION = "1.2.0";
8
8
  var REPO_URL = "https://github.com/smeet666/mcp-marmiton";
9
9
 
10
10
  // src/config.ts
@@ -685,22 +685,22 @@ import { z as z2 } from "zod";
685
685
  // src/recipe/units.ts
686
686
  var UNITS = {
687
687
  // Mass
688
- g: { canonical: "g", kind: "measured" },
689
- gr: { canonical: "g", kind: "measured" },
690
- gramme: { canonical: "g", kind: "measured" },
691
- grammes: { canonical: "g", kind: "measured" },
692
- kg: { canonical: "kg", kind: "measured" },
693
- kilo: { canonical: "kg", kind: "measured" },
694
- kilos: { canonical: "kg", kind: "measured" },
695
- kilogramme: { canonical: "kg", kind: "measured" },
696
- mg: { canonical: "mg", kind: "measured" },
688
+ g: { canonical: "g", kind: "measured", symbol: true },
689
+ gr: { canonical: "g", kind: "measured", symbol: true },
690
+ gramme: { canonical: "g", kind: "measured", symbol: true },
691
+ grammes: { canonical: "g", kind: "measured", symbol: true },
692
+ kg: { canonical: "kg", kind: "measured", symbol: true },
693
+ kilo: { canonical: "kg", kind: "measured", symbol: true },
694
+ kilos: { canonical: "kg", kind: "measured", symbol: true },
695
+ kilogramme: { canonical: "kg", kind: "measured", symbol: true },
696
+ mg: { canonical: "mg", kind: "measured", symbol: true },
697
697
  // Volume
698
- ml: { canonical: "ml", kind: "measured" },
699
- cl: { canonical: "cl", kind: "measured" },
700
- dl: { canonical: "dl", kind: "measured" },
701
- l: { canonical: "l", kind: "measured" },
702
- litre: { canonical: "l", kind: "measured" },
703
- litres: { canonical: "l", kind: "measured" },
698
+ ml: { canonical: "ml", kind: "measured", symbol: true },
699
+ cl: { canonical: "cl", kind: "measured", symbol: true },
700
+ dl: { canonical: "dl", kind: "measured", symbol: true },
701
+ l: { canonical: "l", kind: "measured", symbol: true },
702
+ litre: { canonical: "l", kind: "measured", symbol: true },
703
+ litres: { canonical: "l", kind: "measured", symbol: true },
704
704
  // Spoons and cups: real measures, but only in sensible fractions.
705
705
  "cuillere a soupe": {
706
706
  canonical: "cuill\xE8re \xE0 soupe",
@@ -784,6 +784,12 @@ var DEMOTIONS = {
784
784
  cl: { to: "ml", per: 10 },
785
785
  g: { to: "mg", per: 1e3 }
786
786
  };
787
+ function demoteUnit(unit) {
788
+ const step = DEMOTIONS[unit.canonical];
789
+ if (!step) return null;
790
+ const target = lookupUnit(step.to);
791
+ return target ? { unit: target, per: step.per } : null;
792
+ }
787
793
  function convertToReadableUnit(unit, amount) {
788
794
  if (unit.kind !== "measured" || !Number.isFinite(amount) || amount <= 0) {
789
795
  return { amount, unit };
@@ -801,9 +807,9 @@ function convertToReadableUnit(unit, amount) {
801
807
  return { amount, unit };
802
808
  }
803
809
  function formatUnit(unit, amount) {
810
+ if (unit.symbol) return unit.canonical;
804
811
  if (amount < 2) return unit.canonical;
805
812
  if (unit.plural) return unit.plural;
806
- if (/^[a-z]{1,3}$/.test(unit.canonical)) return unit.canonical;
807
813
  return `${unit.canonical}s`;
808
814
  }
809
815
 
@@ -853,9 +859,18 @@ function parseLeadingQuantity(text) {
853
859
  function parseIngredient(line) {
854
860
  const original = line;
855
861
  const text = line.trim();
856
- const quantity = parseLeadingQuantity(text);
862
+ const range = parseLeadingRange(text);
863
+ const quantity = range ?? parseLeadingQuantity(text);
857
864
  if (!quantity) {
858
- return { original, amount: null, unit: null, unitText: null, item: text };
865
+ return {
866
+ original,
867
+ amount: null,
868
+ amountMax: null,
869
+ rangeSeparator: null,
870
+ unit: null,
871
+ unitText: null,
872
+ item: text
873
+ };
859
874
  }
860
875
  let rest = text.slice(quantity.length).trimStart();
861
876
  let unit = null;
@@ -872,7 +887,31 @@ function parseIngredient(line) {
872
887
  }
873
888
  }
874
889
  const item = rest.replace(/^(?:de\s+la\s+|de\s+l'|d'|de\s+|du\s+|des\s+)/i, "").trim();
875
- return { original, amount: quantity.amount, unit, unitText, item };
890
+ return {
891
+ original,
892
+ amount: quantity.amount,
893
+ amountMax: range?.max ?? null,
894
+ rangeSeparator: range?.separator ?? null,
895
+ unit,
896
+ unitText,
897
+ item
898
+ };
899
+ }
900
+ function parseLeadingRange(text) {
901
+ const low = parseLeadingQuantity(text);
902
+ if (!low) return null;
903
+ const after = text.slice(low.length);
904
+ const separator = /^\s*(à|a|ou|-|–|—|\/)\s*/.exec(after);
905
+ if (!separator) return null;
906
+ if (separator[1] === "/") return null;
907
+ const high = parseLeadingQuantity(after.slice(separator[0].length));
908
+ if (!high || high.amount <= low.amount) return null;
909
+ return {
910
+ amount: low.amount,
911
+ max: high.amount,
912
+ separator: separator[1],
913
+ length: low.length + separator[0].length + high.length
914
+ };
876
915
  }
877
916
  function formatAmount(amount, options = {}) {
878
917
  if (!Number.isFinite(amount)) return "";
@@ -907,17 +946,38 @@ function roundMeasured(value) {
907
946
  if (value >= 1) return roundTo(value, 0.5);
908
947
  return Math.round(value * 100) / 100;
909
948
  }
910
- function roundCountable(value, allowHalves) {
911
- if (value <= 0) return 0;
949
+ var SMALLEST_USABLE_FRACTION = 0.25;
950
+ function roundCountable(value, allowHalves, ceiling) {
951
+ if (value <= 0) return { value: 0, clamped: false };
912
952
  if (value < 1) {
913
- const USABLE_FRACTIONS = [0.25, 1 / 3, 0.5, 2 / 3, 0.75];
914
- let closest = USABLE_FRACTIONS[0];
915
- for (const fraction of USABLE_FRACTIONS) {
916
- if (Math.abs(value - fraction) < Math.abs(value - closest)) closest = fraction;
953
+ const candidates = [SMALLEST_USABLE_FRACTION, 1 / 3, 0.5, 2 / 3, 0.75, 1].filter(
954
+ (candidate) => candidate <= Math.max(ceiling, SMALLEST_USABLE_FRACTION)
955
+ );
956
+ let closest = candidates[0];
957
+ for (const candidate of candidates) {
958
+ if (Math.abs(value - candidate) < Math.abs(value - closest)) closest = candidate;
917
959
  }
918
- return Math.round(closest * 100) / 100;
960
+ return {
961
+ value: Math.round(closest * 100) / 100,
962
+ clamped: value < SMALLEST_USABLE_FRACTION
963
+ };
919
964
  }
920
- return allowHalves ? roundTo(value, 0.5) : Math.round(value);
965
+ return { value: allowHalves ? roundTo(value, 0.5) : Math.round(value), clamped: false };
966
+ }
967
+ function roundMeasuredInUsableUnit(unit, raw) {
968
+ let current = unit;
969
+ let value = raw;
970
+ while (value > 0 && value < 1) {
971
+ const step = demoteUnit(current);
972
+ if (!step) break;
973
+ value *= step.per;
974
+ current = step.unit;
975
+ }
976
+ const rounded = roundMeasured(value);
977
+ return {
978
+ amount: rounded === 0 && value > 0 ? Number(value.toPrecision(2)) : rounded,
979
+ unit: current
980
+ };
921
981
  }
922
982
  function agreeWithAmount(item, amount) {
923
983
  if (!item) return item;
@@ -935,6 +995,12 @@ function agreeWithAmount(item, amount) {
935
995
  }
936
996
  return words.join(" ");
937
997
  }
998
+ var MUTE_H_WORDS = /^(?:huile|huiles|huitre|huitres|huître|huîtres|herbe|herbes|hysope)\b/i;
999
+ function joinItem(item) {
1000
+ if (!item) return "";
1001
+ const elides = /^[aeiouàâäéèêëîïôöûü]/i.test(item) || MUTE_H_WORDS.test(item);
1002
+ return elides ? ` d'${item}` : ` de ${item}`;
1003
+ }
938
1004
  function scaleIngredient(line, options) {
939
1005
  const { factor } = options;
940
1006
  const parsed = parseIngredient(line);
@@ -945,6 +1011,7 @@ function scaleIngredient(line, options) {
945
1011
  amount: null,
946
1012
  unit: null,
947
1013
  scaling: "unscaled",
1014
+ adjusted: false,
948
1015
  note: "No quantity given; adjust to taste."
949
1016
  };
950
1017
  }
@@ -956,41 +1023,47 @@ function scaleIngredient(line, options) {
956
1023
  amount: parsed.amount,
957
1024
  unit: parsed.unit?.canonical ?? null,
958
1025
  scaling: "unscaled",
1026
+ adjusted: false,
959
1027
  note: "Measure is approximate by nature; adjust to taste."
960
1028
  };
961
1029
  }
962
- const raw = parsed.amount * factor;
963
- let amount;
964
- let scaling;
965
- let unit = parsed.unit;
966
- if (kind === "measured") {
967
- const converted = convertToReadableUnit(parsed.unit, roundMeasured(raw));
968
- amount = converted.amount;
969
- unit = converted.unit;
970
- scaling = "scaled";
971
- } else if (kind === "portioned") {
1030
+ const scaleBound = (source) => {
1031
+ const raw = source * factor;
1032
+ if (kind === "measured") {
1033
+ const converted = roundMeasuredInUsableUnit(parsed.unit, raw);
1034
+ const readable = convertToReadableUnit(converted.unit, converted.amount);
1035
+ return { amount: readable.amount, unit: readable.unit, raw, clamped: false };
1036
+ }
972
1037
  const allowHalves = /cuillère|verre|tasse|bol/.test(parsed.unit?.canonical ?? "");
973
- amount = roundCountable(raw, allowHalves);
974
- scaling = "rounded";
975
- } else {
976
- amount = roundCountable(raw, false);
977
- scaling = "rounded";
978
- }
1038
+ const ceiling = factor < 1 ? source : Number.POSITIVE_INFINITY;
1039
+ const rounded = roundCountable(raw, allowHalves, ceiling);
1040
+ return { amount: rounded.value, unit: parsed.unit, raw, clamped: rounded.clamped };
1041
+ };
1042
+ const low = scaleBound(parsed.amount);
1043
+ const high = parsed.amountMax === null ? null : scaleBound(parsed.amountMax);
1044
+ const amount = high?.amount ?? low.amount;
1045
+ const unit = high?.unit ?? low.unit;
1046
+ const scaling = kind === "measured" ? "scaled" : "rounded";
979
1047
  const unitLabel = unit ? ` ${formatUnit(unit, amount)}` : "";
980
- const separator = unit ? " de " : " ";
981
1048
  const itemText = unit ? parsed.item : agreeWithAmount(parsed.item, amount);
982
- const item = itemText ? `${separator}${itemText}` : "";
983
- const amountText = formatAmount(amount, { fractions: kind !== "measured" });
1049
+ const item = unit ? joinItem(parsed.item) : itemText ? ` ${itemText}` : "";
1050
+ const asText = (value) => formatAmount(value, { fractions: kind !== "measured" });
1051
+ const amountText = high === null ? asText(low.amount) : parsed.rangeSeparator === "-" || parsed.rangeSeparator === "\u2013" || parsed.rangeSeparator === "\u2014" ? `${asText(low.amount)}${parsed.rangeSeparator}${asText(high.amount)}` : `${asText(low.amount)} ${parsed.rangeSeparator} ${asText(high.amount)}`;
984
1052
  const text = `${amountText}${unitLabel}${item}`.trim();
1053
+ const bounds = high === null ? [low] : [low, high];
1054
+ const adjusted = bounds.some((bound) => Math.abs(bound.raw - bound.amount) > 0.01);
985
1055
  const result = {
986
1056
  original: parsed.original,
987
1057
  text,
988
1058
  amount,
989
1059
  unit: unit?.canonical ?? null,
990
- scaling
1060
+ scaling,
1061
+ adjusted
991
1062
  };
992
- if (scaling === "rounded" && Math.abs(raw - amount) > 0.01) {
993
- result.note = `Rounded from ${formatAmount(Math.round(raw * 100) / 100)}.`;
1063
+ if (bounds.some((bound) => bound.clamped)) {
1064
+ result.note = `Clamped up to ${formatAmount(SMALLEST_USABLE_FRACTION)} from ${formatAmount(Math.round(low.raw * 1e3) / 1e3)}, the smallest amount worth measuring. This line no longer holds its share of the recipe.`;
1065
+ } else if (scaling === "rounded" && adjusted) {
1066
+ result.note = `Rounded from ${formatAmount(Math.round(low.raw * 100) / 100)}.`;
994
1067
  }
995
1068
  return result;
996
1069
  }
@@ -1005,7 +1078,8 @@ function passthroughIngredients(lines) {
1005
1078
  text: parsed.original,
1006
1079
  amount: parsed.amount,
1007
1080
  unit: parsed.unit?.canonical ?? null,
1008
- scaling: "unscaled"
1081
+ scaling: "unscaled",
1082
+ adjusted: false
1009
1083
  };
1010
1084
  });
1011
1085
  }
@@ -1023,11 +1097,19 @@ var scaledIngredientSchema = z.object({
1023
1097
  scaling: z.enum(["scaled", "rounded", "unscaled"]).describe(
1024
1098
  "'scaled' was multiplied cleanly. 'rounded' was multiplied then rounded to something countable. 'unscaled' was left alone, either because it carries no quantity or because the measure is approximate by nature."
1025
1099
  ),
1026
- note: z.string().optional().describe("Why the line was rounded or left alone.")
1100
+ adjusted: z.boolean().describe(
1101
+ "True when rounding moved the value away from the exact product. A line can be 'rounded' and still land exactly, as three eggs doubled land on six."
1102
+ ),
1103
+ note: z.string().optional().describe("Why the line was rounded, clamped or left alone.")
1027
1104
  });
1028
- function ok(structured, text) {
1105
+ function ok(structured, text, notes = []) {
1106
+ const trailer = notes.map((note) => `Note: ${note}`).join("\n");
1107
+ const budget = MAX_TEXT_MIRROR_CHARS - (trailer ? trailer.length + 2 : 0);
1108
+ const body = truncate(text, Math.max(0, budget));
1029
1109
  return {
1030
- content: [{ type: "text", text: truncate(text, MAX_TEXT_MIRROR_CHARS) }],
1110
+ content: [{ type: "text", text: trailer ? `${body}
1111
+
1112
+ ${trailer}` : body }],
1031
1113
  structuredContent: structured
1032
1114
  };
1033
1115
  }
@@ -1119,8 +1201,21 @@ async function runGetRecipe(client, args) {
1119
1201
  notes.push(
1120
1202
  `Quantities rescaled from ${originalCount} to ${args.servings} ${unit} (factor ${Math.round(factor * 100) / 100}).`
1121
1203
  );
1122
- if (ingredients.some((entry) => entry.scaling === "rounded")) {
1123
- notes.push("Some quantities were rounded to remain usable; see the 'scaling' field.");
1204
+ const roundedCount = ingredients.filter(
1205
+ (entry) => entry.scaling === "rounded" && entry.adjusted
1206
+ ).length;
1207
+ if (roundedCount > 0) {
1208
+ notes.push(
1209
+ `${roundedCount} quantity(ies) were rounded to remain usable; see the 'scaling' field.`
1210
+ );
1211
+ }
1212
+ const clampedCount = ingredients.filter(
1213
+ (entry) => /clamped up/i.test(entry.note ?? "")
1214
+ ).length;
1215
+ if (clampedCount > 0) {
1216
+ notes.push(
1217
+ `${clampedCount} quantity(ies) fell below the smallest amount worth measuring and were clamped up, so their proportions no longer match the published recipe.`
1218
+ );
1124
1219
  }
1125
1220
  if (ingredients.some((entry) => entry.scaling === "unscaled")) {
1126
1221
  notes.push("Some ingredients carry no usable quantity and were left as published.");
@@ -1173,14 +1268,20 @@ async function runGetRecipe(client, args) {
1173
1268
  args.servings !== void 0 && factor !== null ? `Pour ${args.servings} ${data.recipeYield.unit ?? "portions"} (recette pr\xE9vue pour ${data.recipeYield.text}).` : `Recette pour ${data.recipeYield.text || "portions non pr\xE9cis\xE9es"}.`,
1174
1269
  times
1175
1270
  ].filter(Boolean).join("\n");
1271
+ const rescaled = args.servings !== void 0 && factor !== null;
1176
1272
  const ingredientLines = ingredients.map((entry) => {
1177
- const flag = entry.scaling === "unscaled" ? " (non ajust\xE9)" : "";
1273
+ const flag = rescaled && entry.scaling === "unscaled" ? " (non ajust\xE9)" : "";
1178
1274
  return `- ${entry.text}${flag}`;
1179
1275
  }).join("\n");
1180
- return ok(structured, `${header}
1181
-
1182
- Ingr\xE9dients:
1183
- ${ingredientLines}`);
1276
+ const stepLines = data.steps.map((step, index) => `${index + 1}. ${step}`).join("\n");
1277
+ const body = [
1278
+ header,
1279
+ `Ingr\xE9dients:
1280
+ ${ingredientLines}`,
1281
+ stepLines ? `Pr\xE9paration:
1282
+ ${stepLines}` : ""
1283
+ ].filter(Boolean).join("\n\n");
1284
+ return ok(structured, body, notes);
1184
1285
  } catch (error) {
1185
1286
  return toToolError(error);
1186
1287
  }
@@ -1206,15 +1307,26 @@ var scaleIngredientsOutputShape = {
1206
1307
  factor: z3.number(),
1207
1308
  ingredients: z3.array(scaledIngredientSchema),
1208
1309
  scaled_count: z3.number().int(),
1209
- rounded_count: z3.number().int(),
1310
+ rounded_count: z3.number().int().describe(
1311
+ "Lines whose value rounding moved away from the exact product, not lines that could have been rounded."
1312
+ ),
1210
1313
  unscaled_count: z3.number().int(),
1211
1314
  notes: z3.array(z3.string())
1212
1315
  };
1316
+ function formatFactor(factor) {
1317
+ return String(Number(factor.toPrecision(3)));
1318
+ }
1213
1319
  function runScaleIngredients(args) {
1214
1320
  try {
1215
1321
  let factor;
1322
+ const notes = [];
1216
1323
  if (args.factor !== void 0) {
1217
1324
  factor = args.factor;
1325
+ if (args.from_servings !== void 0 || args.to_servings !== void 0) {
1326
+ notes.push(
1327
+ "'factor' was given alongside 'from_servings'/'to_servings'. 'factor' was applied and the pair ignored; send only one of the two to remove the ambiguity."
1328
+ );
1329
+ }
1218
1330
  } else if (args.from_servings !== void 0 && args.to_servings !== void 0) {
1219
1331
  factor = args.to_servings / args.from_servings;
1220
1332
  } else {
@@ -1226,10 +1338,12 @@ function runScaleIngredients(args) {
1226
1338
  const ingredients = scaleIngredients(args.ingredients, { factor });
1227
1339
  const counts = {
1228
1340
  scaled: ingredients.filter((entry) => entry.scaling === "scaled").length,
1229
- rounded: ingredients.filter((entry) => entry.scaling === "rounded").length,
1230
- unscaled: ingredients.filter((entry) => entry.scaling === "unscaled").length
1341
+ // Lines that were actually moved, rather than lines that merely belong to
1342
+ // the roundable category: at a factor of 100 every egg lands whole.
1343
+ rounded: ingredients.filter((entry) => entry.scaling === "rounded" && entry.adjusted).length,
1344
+ unscaled: ingredients.filter((entry) => entry.scaling === "unscaled").length,
1345
+ clamped: ingredients.filter((entry) => /clamped up/i.test(entry.note ?? "")).length
1231
1346
  };
1232
- const notes = [];
1233
1347
  if (counts.rounded > 0) {
1234
1348
  notes.push(
1235
1349
  `${counts.rounded} quantity(ies) were rounded to stay usable, rather than left as fractions.`
@@ -1240,8 +1354,13 @@ function runScaleIngredients(args) {
1240
1354
  `${counts.unscaled} line(s) carry no usable quantity and were returned unchanged; adjust to taste.`
1241
1355
  );
1242
1356
  }
1357
+ if (counts.clamped > 0) {
1358
+ notes.push(
1359
+ `${counts.clamped} quantity(ies) fell below the smallest amount worth measuring and were clamped up, so their proportions no longer match the original recipe.`
1360
+ );
1361
+ }
1243
1362
  const structured = {
1244
- factor: Math.round(factor * 1e3) / 1e3,
1363
+ factor: Number(factor.toPrecision(3)),
1245
1364
  ingredients,
1246
1365
  scaled_count: counts.scaled,
1247
1366
  rounded_count: counts.rounded,
@@ -1252,8 +1371,8 @@ function runScaleIngredients(args) {
1252
1371
  const flag = entry.scaling === "unscaled" ? " (non ajust\xE9)" : "";
1253
1372
  return `- ${entry.text}${flag}`;
1254
1373
  }).join("\n");
1255
- return ok(structured, `Facteur ${Math.round(factor * 100) / 100}:
1256
- ${lines}`);
1374
+ return ok(structured, `Facteur ${formatFactor(factor)}:
1375
+ ${lines}`, notes);
1257
1376
  } catch (error) {
1258
1377
  return toToolError(error);
1259
1378
  }