Skip to content

Repository files navigation

Cronometer MCP server

image image

Version Python Node OAuth


Read and write your Cronometer food diary from Claude.ai and Claude Code. It talks to mobile.cronometer.com, the same API the Cronometer Android app uses, and puts an OAuth 2.1 login in front so you can add it to Claude.ai as a custom connector. Claude Code can use a plain token instead. You do not need a Gold subscription, and there is no limit of ten exports a day like there is with CSV export.


Why not the other options

  • Terra API sends your Cronometer data to a webhook, but it can only read, and your food log passes through someone else's servers
  • gocronometer and similar export tools can only read, and they are rate limited
  • Tools that scrape the Cronometer website can write, but they rely on codes that change every time Cronometer ships an update, and they need Gold

Tools

Reading

Tool What you get
get_food_log Everything in the diary for one day. Each food comes with its name, where it came from, the serving size, how many servings, and what that food added to your nutrients. You also get calories (target, eaten, left) and totals for every nutrient you track
get_daily_nutrition Nutrient totals for one day
get_nutrition_scores Cronometer's nutrition scores
search_foods Search the food database
get_food_details Full nutrients and serving sizes for one food
get_targets Your nutrient goals and which nutrients are tracked, named and with units
get_macro_targets Your protein, carb and fat goals
list_biometrics What you can measure, and the units each one accepts
get_biometrics One measurement over time
get_fasting_history Fasts between two dates
get_fasting_stats Fasting totals and averages
list_nutrients Every nutrient you can set on a custom food, with units

Writing

Tool What it does
add_food_entry Add a food to a meal
edit_food_entry Change how much you ate, or when
remove_food_entry Delete food entries
add_custom_food Create your own food, with up to all 94 nutrients
retire_custom_food Retire a custom food, or bring one back
add_note Write a note on a day
edit_note Rewrite a note
add_biometric Record a measurement such as weight or body fat
edit_biometric Fix a measurement you got wrong
add_exercise Add an exercise
edit_exercise Change how long an exercise lasted, or how much it burned
add_fast Record a fast, finished or still running
edit_fast Change a fast's times or goal, including ending one that is still running
delete_fast Delete a fast
copy_day Copy one day's diary onto another day
mark_day_complete Mark a day done, or not done
set_nutrient_target Set a nutrient's target or limit, or start tracking it

Custom foods

add_custom_food takes a dict of nutrient name to amount, so you can give it anything from one nutrient to the whole catalog in a single call:

{
  "name": "Vaasan Ruispalat",
  "serving_name": "1 slice",
  "serving_grams": 33,
  "nutrients": {
    "energy": 79, "protein": 3.1, "carbs": 12.5, "fiber": 3.4,
    "fat": 0.8, "saturated": 0.2, "salt_g": 0.36,
    "iron": 0.9, "magnesium": 26, "b1_thiamine": 0.09, "folate": 11
  }
}

Amounts are for one whole serving, each in that nutrient's own unit. Call list_nutrients for the accepted names, which come from your account's own catalog rather than a table baked in here.

A nutrient you leave out stays blank in Cronometer. Passing 0 instead states that the food contains none of it, and the app treats the two differently, so only pass what you actually know. An unrecognised name is an error rather than being quietly dropped, because a food that silently lost a nutrient still looks complete.

Two conveniences the food label has and the catalog does not: energy_kj is converted to calories, and salt_g to sodium. Pass one or the other, not both.

Targets and tracked nutrients

set_nutrient_target covers both the goal and whether a nutrient is tracked at all, which is what makes a micronutrient appear in the diary:

{"nutrient": "protein", "minimum": 145}
{"nutrient": "iodine", "visible": true}
{"nutrient": "sodium", "maximum": 2300}

Cronometer's endpoint replaces the whole row rather than patching it, so this reads the nutrient's current settings and merges your change into them. Turning on visibility therefore keeps whatever target was already set, and each call reports the previous values alongside the new ones.

How it fits together

Claude.ai / Claude Code
        |  HTTPS
   Cloudflare Tunnel, or any proxy that gives you HTTPS
        |
   nginx  127.0.0.1:8431
        |
   auth-server.js  :8432    handles the login and the tokens
        |
   cronometer-mcp  :8430    the server itself, local only
        |
   mobile.cronometer.com

The server itself has no login of its own, and it refuses to listen on anything but the local machine. So anything that reaches it has already got past the login. That login accepts either an OAuth token, which is what Claude.ai sets up for you, or a fixed token, which is quicker for Claude Code.

Install

git clone https://github.com/rollecode/cronometer-mcp.git
cd cronometer-mcp
./install.sh

The installer sets up Python and Node, asks for your Cronometer login and a password for the connector's login page, makes a token, and writes the service files and the nginx site with your own hostname and username filled in.

You need Node 18 or newer, Python 3.12 or newer, and uv.

Putting the server online is left to you, because this is where setups differ the most, and a wrong guess here would put your food diary on the public internet. Point a tunnel or a proxy at 127.0.0.1:8431. With Cloudflare Tunnel:

ingress:
  - hostname: cronometer-mcp.example.com
    service: http://localhost:8431

It has to be HTTPS. OAuth will not work over plain HTTP.

Self-hosting it by hand

If you would rather see every step than run the installer, this is all of it. The end state is two services on your own machine, reachable over HTTPS.

1. Get the code and its dependencies

git clone https://github.com/rollecode/cronometer-mcp.git
cd cronometer-mcp
npm install --omit=dev
uv venv && uv pip install -e .

2. Store your Cronometer login

./set-credentials.sh

It prompts for your email, password and time zone, and writes them to ~/.config/cronometer-mcp/env with mode 0600. The password is never echoed and never reaches your shell history. Do it by hand if you prefer:

mkdir -p ~/.config/cronometer-mcp && chmod 700 ~/.config/cronometer-mcp
cat > ~/.config/cronometer-mcp/env <<'EOF'
CRONOMETER_USERNAME=you@example.com
CRONOMETER_PASSWORD=your-password
CRONOMETER_ACCOUNT_TZ=Europe/Helsinki
EOF
chmod 600 ~/.config/cronometer-mcp/env

Check it works before going further. This logs in and prints your diary:

set -a && . ~/.config/cronometer-mcp/env && set +a
.venv/bin/python -c "from cronometer_mcp import CronometerClient; c=CronometerClient(); print(c.get_diary()['summary'])"

3. Set the connector password and a token

The password is what you type on the sign-in page when adding the connector in Claude.ai. Only its scrypt hash is stored.

CONFIG_DIR=~/.config/cronometer-mcp node set-password.js 'your-password-here'

The token is the shortcut for Claude Code, which sends a header and skips the browser entirely.

openssl rand -hex 32 > ~/.config/cronometer-mcp/token
chmod 600 ~/.config/cronometer-mcp/token

4. Install the two services

systemd/ holds both unit files. Replace YOUR_USER with your username and cronometer-mcp.example.com with your hostname, then:

mkdir -p ~/.cache/cronometer-mcp
sudo cp systemd/cronometer-mcp.service systemd/cronometer-mcp-auth.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cronometer-mcp cronometer-mcp-auth
systemctl status cronometer-mcp cronometer-mcp-auth

cronometer-mcp is the server itself on :8430, reachable only from the machine it runs on. cronometer-mcp-auth is the login layer on :8432, and it is the only thing that talks to :8430.

One trap worth naming, because the symptom is confusing: do not add IPAddressDeny=any to cronometer-mcp.service. It is a sensible hardening line for a server that only reads local files, but this one has to reach mobile.cronometer.com, and with it set every tool call hangs until it times out while systemd still reports the service as active. Nothing is gained by it either, since the server already refuses to listen beyond the local machine.

5. Put nginx in front

sudo cp nginx/cronometer-mcp.conf /etc/nginx/sites-enabled/cronometer-mcp
sudo nginx -t && sudo systemctl reload nginx

It listens on 127.0.0.1:8431 and passes everything to the login layer. The long read timeout and proxy_buffering off matter: the MCP holds the connection open and sends as it goes, and buffering would stall it.

6. Give it an HTTPS address

A Cloudflare Tunnel avoids opening a router port. Any HTTPS reverse proxy works just as well.

ingress:
  - hostname: cronometer-mcp.example.com
    service: http://localhost:8431
cloudflared tunnel route dns YOUR_TUNNEL cronometer-mcp.example.com
sudo systemctl restart cloudflared

7. Check it from outside

curl https://cronometer-mcp.example.com/.well-known/oauth-authorization-server
curl -o /dev/null -w '%{http_code}\n' -X POST https://cronometer-mcp.example.com/mcp

The first returns the login details. The second must return 401: anything else means the login layer is being bypassed and your diary is exposed.

Then connect a client as described under Connecting.

Updating

git pull
uv pip install -e . && npm install --omit=dev
sudo systemctl restart cronometer-mcp cronometer-mcp-auth

After adding or renaming a tool, press Reconnect on the connector in Claude.ai. That refreshes the tool list inside a conversation you already have open, and your sign-in survives it, because tokens live in oauth.db on disk rather than in memory.

When something is wrong

journalctl -u cronometer-mcp -n 50 --no-pager
journalctl -u cronometer-mcp-auth -n 50 --no-pager
What you see What it usually is
Tool calls hang, service says active IPAddressDeny on the MCP unit, see step 4
401 on every call from Claude Code Token mismatch, compare the header against ~/.config/cronometer-mcp/token
Sign-in page rejects the password No hash stored yet, run step 3
Login fails asking for a 2FA code See If you use two-factor
502 from nginx The login layer is down, systemctl status cronometer-mcp-auth

Connecting

Claude.ai. Go to Settings, Connectors, Add custom connector, and give it https://your-host/mcp. Leave the client ID and secret empty. Sign in with the password the installer set. Doing this once covers web, desktop and mobile, because connectors belong to your account rather than one device.

Claude Code, through the browser:

claude mcp add --transport http cronometer https://your-host/mcp --scope user

Then run /mcp to sign in.

Claude Code, with a token, no browser:

claude mcp add --transport http cronometer https://your-host/mcp \
  --header "Authorization: Bearer $(cat ~/.config/cronometer-mcp/token)" \
  --scope user

Using it without a server at all

If Claude runs on the same machine, skip the web server and the login entirely and let it start the MCP directly:

claude mcp add cronometer -- /path/to/cronometer-mcp/.venv/bin/cronometer-mcp

It reads your login from ~/.config/cronometer-mcp/env or from a .env file.

Settings

Variable What it is for
CRONOMETER_USERNAME Your Cronometer email
CRONOMETER_PASSWORD Your Cronometer password
CRONOMETER_ACCOUNT_TZ The time zone your diary days are counted in
CRONOMETER_TOTP_SECRET Your two-factor secret, only if you have two-factor on. Needs the totp extra
ISSUER The public address of the server
PORT Login server port, 8432 by default
UPSTREAM Where the MCP server is, http://127.0.0.1:8430 by default
CONFIG_DIR Where the password, token and database are kept
CALL_TIMEOUT_MS How long a call may go quiet before it is cut off, 120000 by default
MCP_PORT MCP server port, 8430 by default
MCP_PUBLIC_URL Public address, used to advertise the icon to clients

Everything secret lives in ~/.config/cronometer-mcp/, readable only by you: env holds your Cronometer login, password-hash the password for the connector's login page, token the fixed token, and oauth.db the apps and tokens the login server has handed out. Tokens are stored scrambled, so a stolen copy of the database gives nobody a working key.

Your Cronometer session is saved in ~/.cache/cronometer-mcp/session.json, so restarting the server does not log in again and again and hit Cronometer's limit.

If you use two-factor

A server left running on its own cannot type a code, so it needs the secret behind the code instead:

uv pip install -e '.[totp]'

Then set CRONOMETER_TOTP_SECRET to the secret from your authenticator app. Without it, an account with two-factor turned on will fail to log in and tell you exactly this.

Working on the code

uv venv && uv pip install -e . && uv pip install pytest ruff
.venv/bin/python -m pytest tests -q
.venv/bin/python -m ruff check src/ tests/

Credits

The Cronometer client started as a copy of rwestergren/cronometer-api-mcp. The login layer comes from rollecode/obsidian-remote-mcp.

About

Read and write your Cronometer food diary from Claude.ai and Claude Code

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages