diff --git a/src/cronometer_api_mcp/client.py b/src/cronometer_api_mcp/client.py index 38c24c0..1ad83e1 100644 --- a/src/cronometer_api_mcp/client.py +++ b/src/cronometer_api_mcp/client.py @@ -958,8 +958,10 @@ def enrich_diary_servings(self, diary: dict) -> dict: - measure: {measure_id, name, grams_per_unit} for the entry's measureId (falls back to the food's defaultMeasureId) - servings: grams / grams_per_unit, when derivable - - nutrients: the food's per-100g profile scaled to the entry's grams, - labeled with name/unit/category via the nutrient definitions catalog + - nutrients: the food's nutrient profile scaled to the entry's amount + (per-100g for Weight/Atomic measures, per-serving for Recipe + measures), labeled with name/unit/category via the nutrient + definitions catalog Enrichment is best-effort: if the get_foods call fails or a food is not returned, the corresponding entries are left unchanged. The diary dict @@ -1029,9 +1031,17 @@ def enrich_diary_servings(self, diary: dict) -> dict: ): entry["servings"] = round(grams / grams_per_unit, 4) - # Food nutrients are stored per-100g; scale to the entry's grams. + # Nutrient scaling depends on the measure type: + # - Recipe measures: nutrients are stored per one reference + # serving and the diary "grams" field is a serving count, so + # scale by grams directly. + # - Weight/Atomic measures: nutrients are stored per-100g and + # "grams" is real grams, so scale by grams / 100. if isinstance(grams, (int, float)): - scale = grams / 100.0 + if measure and measure.get("type") == "Recipe": + scale = grams + else: + scale = grams / 100.0 scaled: list[dict] = [] for n in food.get("nutrients", []): if not isinstance(n, dict): diff --git a/tests/test_client.py b/tests/test_client.py index 80a9337..91a661e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -204,6 +204,13 @@ def _enrich_client(tmp_path: Path): "grams": 50, "order": 65538, }, + { + "type": "Serving", + "foodId": 300, + "measureId": 30, + "grams": 1.1, # Recipe: "grams" is a serving count, not grams + "order": 65539, + }, {"type": "Exercise", "name": "Running", "order": 1}, ] } @@ -227,6 +234,18 @@ def _enrich_client(tmp_path: Path): "measures": [{"id": 20, "name": "glass", "value": 244.0}], "nutrients": [{"id": 208, "amount": 42.0}], }, + { + "id": 300, + "name": "Recipe Food", + "source": "Custom", + "defaultMeasureId": 30, + "measures": [ + {"id": 30, "name": "serving", "value": 1, "amount": 1, "type": "Recipe"}, + {"id": 31, "name": "g", "value": 233.4, "amount": 1, "type": "Recipe"}, + ], + # stored per one reference serving (not per-100g) + "nutrients": [{"id": 208, "amount": 708.538}, {"id": 203, "amount": 56.03}], + }, ] NUTRIENT_DEFS = { @@ -268,8 +287,19 @@ def test_enrich_diary_merges_names_measures_and_scaled_nutrients(tmp_path): # 42 kcal/100g scaled to 50g -> 21 assert next(n for n in milk["nutrients"] if n["id"] == 208)["amount"] == 21.0 + recipe = entries[2] + assert recipe["name"] == "Recipe Food" + assert recipe["measure"]["measure_id"] == 30 + assert recipe["measure"]["name"] == "serving" + # Recipe measure: nutrients are per-serving and "grams" is a serving count, + # so 708.538 kcal/serving * 1.1 servings -> 779.39 (not grams/100) + energy = next(n for n in recipe["nutrients"] if n["id"] == 208) + assert energy["amount"] == 779.3918 + protein = next(n for n in recipe["nutrients"] if n["id"] == 203) + assert protein["amount"] == 61.633 + # Non-Serving entry untouched - assert entries[2] == {"type": "Exercise", "name": "Running", "order": 1} + assert entries[3] == {"type": "Exercise", "name": "Running", "order": 1} def test_enrich_diary_is_best_effort_when_get_foods_fails(tmp_path):