Skip to content

About

MCP server for Cronometer nutrition tracking via the mobile REST API

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

cronometer-api-mcp

License: MIT CI Build Docker image PyPI

Hosted version for Claude.ai, ChatGPT, and Grok coming soon. Join the waitlist →

An MCP (Model Context Protocol) server for Cronometer nutrition tracking, built on the reverse-engineered mobile REST API.

Unlike cronometer-mcp, which takes a comprehensive GWT-RPC approach against Cronometer's web backend, this server talks to the same JSON REST API used by the Cronometer Android app -- with clean payloads and stable, versioned endpoints.

Features

  • Food log -- diary entries with food names, amounts, meal groups
  • Nutrition data -- daily macro/micro totals and nutrition scores with per-nutrient confidence
  • Food search -- search the Cronometer food database, get detailed nutrition info
  • Diary management -- add/remove entries, copy days, mark days complete
  • Custom foods -- create foods with custom nutrition data
  • Recipes -- create recipes from database foods or free text, edit them in place, and retire them
  • Macro targets -- read weekly schedule and saved templates
  • Fasting -- view history and aggregate statistics
  • Biometrics -- weight, body fat, heart rate, and other tracked metrics over a date range

Quick Start

1. Install uv

curl -LsSf https://astral.sh/uv/install.sh | sh

2. Set credentials

export CRONOMETER_USERNAME="your@email.com"
export CRONOMETER_PASSWORD="your-password"

Optional: two-factor authentication

If the account has two-factor authentication enabled, /api/v2/login answers TOTP_CODE_REQUIRED unless the request carries the current 6-digit code. Give the server the base32 key that Cronometer showed when 2FA was set up (the same key you scanned into your authenticator app) and it derives the code itself at every login (RFC 6238, SHA-1, 30 s period):

export CRONOMETER_TOTP_SECRET="ABCD EFGH IJKL MNOP QRST UVWX YZ23 4567"

Spaces and lowercase are fine. Leave it unset for accounts without 2FA.

Optional: override the account timezone

Diary entries are stamped in your Cronometer account's timezone, which the server reports at login. If that zone is wrong (for example, an older build had reset it) you can force a specific IANA zone without changing your account settings:

export CRONOMETER_ACCOUNT_TZ="America/Los_Angeles"

When set, this takes precedence over both the value reported at login and any cached session, so it also overrides a stale cached timezone.

Optional: name your diary groups

Each diary entry belongs to a numbered diary group, but the API exposes no group names, so names you set in the app (renamed groups, added groups such as an evening snack) can't be read. List them, in order, for groups 1, 2, 3, ...:

export CRONOMETER_DIARY_GROUPS="breakfast,lunch,afternoon snack,dinner,evening snack"

Unset, it defaults to Cronometer's breakfast,lunch,dinner,snacks. Names are case-insensitive and whitespace is normalized; an empty or repeated name, auto, or a group N lookalike fails at startup. get_food_log and get_nutrient_sources label entries with these names, and the write tools take them as diary_group. A group past the last name reads and writes as "group N". add_food_entry's "auto" picks breakfast, lunch, dinner or snacks by the hour, and fails if that name isn't configured.

The app's Diary Settings number groups one higher than the API: its "Group 1: Uncategorized" is API group 0, its "Group 2" is API group 1 (the first name in the list), and so on.

3. Configure your MCP client

uvx downloads and runs the server on demand -- no separate install step.

OpenCode (opencode.json)

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "cronometer": {
      "type": "local",
      "command": ["uvx", "cronometer-api-mcp"],
      "environment": {
        "CRONOMETER_USERNAME": "{env:CRONOMETER_USERNAME}",
        "CRONOMETER_PASSWORD": "{env:CRONOMETER_PASSWORD}",
        "CRONOMETER_TOTP_SECRET": "{env:CRONOMETER_TOTP_SECRET}",
        "CRONOMETER_ACCOUNT_TZ": "{env:CRONOMETER_ACCOUNT_TZ}"
      },
      "enabled": true
    }
  }
}

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "cronometer": {
      "command": "uvx",
      "args": ["cronometer-api-mcp"],
      "env": {
        "CRONOMETER_USERNAME": "your@email.com",
        "CRONOMETER_PASSWORD": "your-password",
        "CRONOMETER_TOTP_SECRET": "your-base32-key",
        "CRONOMETER_ACCOUNT_TZ": "America/Los_Angeles"
      }
    }
  }
}

Available Tools

Food Log & Nutrition

Tool Description
get_food_log Diary entries for reviewing or editing a day: per entry its serving ID, meal, food and food ID, measure ID, grams, measure name and quantity, and a few key nutrients (or ones you name); per day the totals of those nutrients, whether the day is marked complete, an energy_summary (target/consumed/remaining kcal), and non-food entries (exercise, biometrics). For every tracked nutrient's total use get_daily_nutrition; for a food's full profile, get_food_details
get_daily_nutrition Consumed macro and micronutrient totals for every nutrient tracked in Cronometer
get_nutrient_sources Top foods behind one or more nutrient totals (e.g. "where did my sodium come from?"): total, split by diary group, and ranked sources with grams eaten, amount, and percent, aggregated across entries and days. Covers any nutrient, not only the few get_food_log reports per entry
get_nutrition_scores Category scores (Vitamins, Minerals, etc.) with per-nutrient consumed amounts and confidence levels

Food Search & Details

Tool Description
search_foods Search the Cronometer food database by name
get_food_details Full nutrition profile and serving sizes for a food; for a recipe, also its ingredients and notes

Diary Management

Tool Description
add_food_entry Log a food serving to the diary
add_food_entries Log many servings across any dates in one call; every item is checked before anything is logged, and a mid-batch failure reports what was added so it can be rolled back
remove_food_entry Remove one or more diary entries from one date
remove_food_entries Remove entries across any dates in one call; every ID is looked up before anything is removed, and a mid-batch failure reports what was removed
add_custom_food Create a custom food with specified nutrition
update_custom_food Edit a custom food in place: name, nutrition, or serving size
delete_custom_food Retire a custom food or recipe so it leaves search (existing diary entries are kept)
add_recipe Create a recipe from existing foods referenced by ID and gram weight
import_recipe Create a recipe from a free-text ingredient list; Cronometer matches each line to a database food and converts the amount to grams
update_recipe Edit a recipe in place, keeping its food ID: replace its ingredient list, rename it, change its serving's name, quantity or weight, or its notes. Cronometer re-derives the nutrients from the new ingredients
copy_day Copy all entries from the previous day
mark_day_complete Mark a diary day as complete or incomplete

Targets & Tracking

Tool Description
get_macro_targets Weekly macro schedule and saved target templates
get_fasting_history Fasting history within a date range
get_fasting_stats Aggregate fasting statistics
list_biometrics List trackable biometric metrics and their units
get_biometrics Biometric time series (e.g. weight, body fat) within a date range

All date parameters use YYYY-MM-DD format and default to today when omitted.

Relative dates and recent days

get_food_log, get_daily_nutrition, get_nutrient_sources, and get_nutrition_scores also accept "today", "yesterday", and "N days ago" as their date. These are resolved at call time using your Cronometer account timezone, so Claude can pass relative inputs without relying on dates from earlier in a long conversation.

get_food_log, get_daily_nutrition, and get_nutrient_sources accept days (1–31, default 1), counting back from the inclusive end date:

get_food_log(date="yesterday")
get_nutrition_scores(date="3 days ago")
get_daily_nutrition(days=3)  # Last 3 days, including today
get_daily_nutrition(date="yesterday", days=3)  # Previous 3 complete calendar days
get_food_log(days=7)  # Recent meals, including today
get_nutrient_sources("sodium", date="yesterday")  # Yesterday's top sodium sources
get_nutrient_sources(["protein", "fiber"], days=7, top_n=5)

For “my calories the last few days,” use get_daily_nutrition(days=3). Single-day responses retain their existing shape. Multi-day responses contain start_date, end_date, and a days list of daily results, oldest first. get_nutrient_sources is the exception: it aggregates the whole range into one ranking, so a multi-day response has start_date and end_date but no days list. Returned dates are always concrete YYYY-MM-DD values.

Transport

stdio only. For remote/hosted use, the stdio server is wrapped by supergateway (see Dockerfile), which owns the HTTP listener and exposes MCP streamable-HTTP at /mcp. The server has no built-in authentication — any remote deployment must sit behind an authenticating gateway or reverse proxy.

Development

For local development, copy .env.example to .env and fill in your credentials:

cp .env.example .env
# edit .env
uv run cronometer-api-mcp

The CLI auto-loads .env on startup (dev convenience only). Real environment variables always win over .env, so production deployments and MCP client env blocks are unaffected.

How It Works

This server communicates with mobile.cronometer.com -- the same REST API used by the Cronometer Android/Flutter app. The API was reverse-engineered through:

  1. Static analysis of libapp.so (Dart AOT snapshot) from the APK to discover endpoint names
  2. Traffic interception via Frida + mitmproxy to capture exact request/response formats
  3. Trial-and-error against the live API to confirm payload shapes

The API uses two protocols:

  • v2 (POST /api/v2/*) -- JSON-body auth, used for most operations (food search, diary read/write, nutrition, fasting, macros, biometrics)
  • v3 (DELETE /api/v3/user/{id}/*) -- Header-based auth (x-crono-session), used for diary entry deletion

Custom foods and recipes are both saved through POST /api/v2/add_food, which upserts: a food sent with its existing id is edited in place, and diary entries that use it reflect the edit. For a recipe, the server derives the nutrients and the "full recipe" weight from the ingredients list on every save, so the client sends none, and reads both back to check them. The server counts an ingredient's grams toward the batch only if all of the food's measures are type Weight (what the web client saves for a serving with a gram weight); with an Atomic one, its nutrients are added but its grams aren't, so the batch weight comes out short. Custom foods are therefore saved with Weight servings, and recipes refuse other ingredients. Don't fix an older food by switching an Atomic measure to Weight: the server stores a diary entry as a count of its measure, with a Weight unit weighing its value in grams and an Atomic one 100 g, so the switch rescales every entry logged by it (180 g logged by an Atomic "g" read 1.8 g after switching it to Weight). No hard-delete endpoint for foods is known, so delete_custom_food sets retired instead (the food leaves search after a few seconds but stays readable by id, and diary entries that use it are kept). Retired foods can't be edited. Serving names are sent exactly as given with an explicit quantity, so add_custom_food, update_custom_food, add_recipe and update_recipe refuse a name that starts with a number ("2 cookies"): pass serving_quantity=2, serving_name="cookies". When the server re-parses a new measure's name (it does for some names when the quantity is 1, e.g. "serving (1/2 batch)"), the client re-sends the measure verbatim, which sticks.

Recipe import is the one asynchronous operation: import_recipe returns a job id, and poll_async_result is polled until the server reports 100% progress and attaches the parsed ingredients.

Python API

You can use the client directly:

from cronometer_api_mcp.client import CronometerClient
from datetime import date

client = CronometerClient()

# Search for foods
results = client.search_food("chicken breast")

# Get food details
food = client.get_food(results[0]["id"])

# Log a serving
client.add_serving(
    food_id=food["id"],
    measure_id=food["measures"][0]["id"],
    grams=200,
    diary_group=3,  # by default 1 breakfast, 2 lunch, 3 dinner, 4 snacks; 5+ groups added in the app
)

# Get today's diary
diary = client.get_diary()

# Import a recipe from a free-text ingredient list
recipe = client.import_recipe("one hot dog\nketchup\nbun")
print(recipe["food_id"], recipe["ingredients"])

# Parse without saving, to review the matches first
preview = client.import_recipe("2 tbsp olive oil\n200g chicken", save=False)

# Get nutrition scores
scores = client.get_nutrition_scores()

License

MIT

About

MCP server for Cronometer nutrition tracking via the mobile REST API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages