From c95daafffc1880c5d4b3ed4a1cd87c8897e9a50b Mon Sep 17 00:00:00 2001 From: Randy Westergren Date: Mon, 8 Jun 2026 14:44:36 -0400 Subject: [PATCH] Surface tracked nutrient totals in daily nutrition and food log get_daily_nutrition returned the nutrient catalog instead of consumed amounts. Rewire it to report consumed totals for every tracked nutrient, and add the same summary to get_food_log. Closes #13 --- README.md | 4 +- src/cronometer_api_mcp/client.py | 105 +++++++++++++++++++++++++++++++ src/cronometer_api_mcp/server.py | 31 +++++++-- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1c324c6..c542fde 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ export CRONOMETER_PASSWORD="your-password" | Tool | Description | |------|-------------| -| `get_food_log` | Diary entries for a date with food names, amounts, and meal groups, plus an energy_summary (target/consumed/remaining kcal) | -| `get_daily_nutrition` | Daily macro and micronutrient totals | +| `get_food_log` | Diary entries for a date with food names, amounts, and meal groups, plus an energy_summary (target/consumed/remaining kcal) and a nutrition_summary of consumed totals for every tracked nutrient | +| `get_daily_nutrition` | Consumed macro and micronutrient totals for every nutrient tracked in Cronometer | | `get_nutrition_scores` | Category scores (Vitamins, Minerals, etc.) with per-nutrient consumed amounts and confidence levels | ### Food Search & Details diff --git a/src/cronometer_api_mcp/client.py b/src/cronometer_api_mcp/client.py index 2057c5c..fc0701e 100644 --- a/src/cronometer_api_mcp/client.py +++ b/src/cronometer_api_mcp/client.py @@ -38,6 +38,24 @@ "sodium": 307, "alcohol": 221, "net_carbs": -1205, + "saturated_fat": 606, + "cholesterol": 601, + "trans_fat": 605, + "omega_3": 10001, + "omega_6": 10002, +} + +# Macro fields surfaced as a flat convenience block in the daily summary, +# mapped to their nutrient IDs. These are the values most relevant when +# summarizing a day at a glance. +SUMMARY_MACRO_IDS = { + "energy": 208, + "protein": 203, + "carbs": 205, + "net_carbs": -1205, + "fat": 204, + "fiber": 291, + "alcohol": 221, } @@ -55,6 +73,9 @@ class CronometerClient: def __init__(self) -> None: self._user_id: int | None = None self._token: str | None = None + # Cache of nutrient definitions (id -> {name, unit, category}). + # Definitions are stable for an account, so fetch them once. + self._nutrient_defs: dict[int, dict] | None = None self._http = httpx.Client( base_url=BASE_URL, headers={ @@ -618,6 +639,90 @@ def get_nutrition_scores( ) return data + def get_nutrient_definitions(self) -> dict[int, dict]: + """Get the nutrient definition map (id -> {name, unit, category}). + + The get_nutrients endpoint returns the account's nutrient catalog -- + names, units, RDIs, and categories -- not consumed amounts. We use it + purely to label nutrient IDs. Cached after the first call since the + catalog is stable. + """ + if self._nutrient_defs is None: + data = self.get_nutrients() + defs: dict[int, dict] = {} + for n in data.get("nutrients", []): + nid = n.get("id") + if nid is None: + continue + defs[nid] = { + "name": n.get("name"), + "unit": n.get("unit"), + "category": n.get("category"), + } + self._nutrient_defs = defs + return self._nutrient_defs + + def get_consumed_nutrients(self, day: date | None = None) -> dict: + """Get consumed nutrient totals for a day, labeled and summarized. + + Builds a clean summary from the server-computed per-nutrient totals in + get_nutrition_scores (the "All Targets" category), which reflect exactly + the nutrients the user is tracking (i.e. has targets set for). Each + nutrient is labeled with its name, unit, and category via the nutrient + definition catalog. + + Returns a dict: + { + "macros": {energy, protein, carbs, net_carbs, fat, fiber, + alcohol}, # flat amounts (None if not tracked) + "nutrients": [ + {id, name, amount, unit, category, confidence}, ... + ], + } + + Note: a nutrient only appears if the user tracks it in Cronometer. To + see e.g. saturated fat, the user must have a target set for it. + """ + scores = self.get_nutrition_scores(day) + + # The "All Targets" category contains every tracked nutrient. + all_targets = next( + (c for c in scores.get("scores", []) if c.get("title") == "All Targets"), + None, + ) + components = (all_targets or {}).get("components", []) if all_targets else [] + + defs = self.get_nutrient_definitions() + + nutrients: list[dict] = [] + amounts_by_id: dict[int, float] = {} + for comp in components: + nid = comp.get("nutrientId") + if nid is None: + continue + amount = comp.get("amount") + amounts_by_id[nid] = amount + meta = defs.get(nid, {}) + nutrients.append( + { + "id": nid, + "name": meta.get("name"), + "amount": amount, + "unit": meta.get("unit"), + "category": meta.get("category"), + "confidence": comp.get("confidence"), + } + ) + + macros = {key: amounts_by_id.get(nid) for key, nid in SUMMARY_MACRO_IDS.items()} + + logger.info( + "Built consumed nutrient summary for %s (%d tracked nutrients)", + self._format_day(day), + len(nutrients), + ) + return {"macros": macros, "nutrients": nutrients} + # ------------------------------------------------------------------ # Macro targets # ------------------------------------------------------------------ diff --git a/src/cronometer_api_mcp/server.py b/src/cronometer_api_mcp/server.py index 5a6b463..ddf0d2d 100644 --- a/src/cronometer_api_mcp/server.py +++ b/src/cronometer_api_mcp/server.py @@ -100,6 +100,14 @@ def get_food_log(date: str | None = None) -> str: when summarizing the user's day. Prefer this over manually deriving values from the burn breakdown fields. + Also returns a nutrition_summary field with consumed totals for every + nutrient the user tracks in Cronometer (macros plus any tracked + micronutrients such as saturated fat, cholesterol, or omega-3/6): + + - macros: flat macro totals (energy, protein, carbs, net_carbs, fat, + fiber, alcohol) + - nutrients: the full list of tracked nutrients with amounts and units + Args: date: Date as YYYY-MM-DD (defaults to today). """ @@ -119,10 +127,13 @@ def get_food_log(date: str | None = None) -> str: "remaining_kcal": int(round(target - consumed)), } + nutrition_summary = client.get_consumed_nutrients(day) + return _ok( { "date": date or str(date_module_today()), "energy_summary": energy_summary, + "nutrition_summary": nutrition_summary, "diary": data, } ) @@ -318,10 +329,19 @@ def copy_day(date: str | None = None) -> str: } ) def get_daily_nutrition(date: str | None = None) -> str: - """Get daily nutrition summary with macro and micronutrient totals. + """Get daily nutrition summary with consumed macro and micronutrient totals. + + Returns the amounts actually consumed for the day, covering every nutrient + the user tracks in Cronometer (i.e. has a target set for). The response has: + + - summary: flat macro totals (energy, protein, carbs, net_carbs, fat, + fiber, alcohol). A value is null if that macro isn't tracked. + - nutrients: the full list of tracked nutrients, each with id, name, + amount, unit, category, and confidence. - Returns calorie, protein, carb, fat, fiber totals and micronutrients - for the given day. + A nutrient only appears if it's tracked in Cronometer. To surface e.g. + saturated fat, cholesterol, or trans fat, set a target for it in Cronometer + and it will flow through automatically. Args: date: Date as YYYY-MM-DD (defaults to today). @@ -329,11 +349,12 @@ def get_daily_nutrition(date: str | None = None) -> str: try: client = _get_client() day = _parse_date(date) - data = client.get_nutrients(day) + data = client.get_consumed_nutrients(day) return _ok( { "date": date or str(date_module_today()), - "nutrients": data, + "summary": data["macros"], + "nutrients": data["nutrients"], } ) except Exception as e: