Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
105 changes: 105 additions & 0 deletions src/cronometer_api_mcp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand All @@ -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={
Expand Down Expand Up @@ -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
# ------------------------------------------------------------------
Expand Down
31 changes: 26 additions & 5 deletions src/cronometer_api_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"""
Expand All @@ -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,
}
)
Expand Down Expand Up @@ -318,22 +329,32 @@ 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).
"""
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:
Expand Down
Loading