The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Bodylog listing page.
A workout and food log for personal agents. You tell your agent your sets and your meals in chat, the way you would say them out loud. It stores them, counts calories and macros from real nutrition data, keeps your streaks, and sends back shareable cards.
One package for any agent: an MCP server (bodylog-mcp), a CLI (bodylog), a Python library and a
Claude skill. Everything lives in one local SQLite file. No accounts, no cloud, no social features.
| Food day | Story | Workout |
|---|---|---|
![]() | ![]() | ![]() |
More: food, light, food + training on one day (full), workout story, 8-exercise workout, two pages, mixed kg and lb, sticker for a photo.
Python 3.10+.
Claude Code, as a plugin (skill and MCP server together):
Claude Code, MCP server only:
Any other MCP client:
Settings, all optional:
| Variable | Default | What |
|---|---|---|
BODYLOG_DB | ~/.bodylog/log.db | the SQLite file |
BODYLOG_CARDS | ~/.bodylog/cards/ | where card PNGs go |
BODYLOG_UNIT | most-logged unit | kg or lb for workout totals |
FDC_API_KEY | DEMO_KEY | USDA FoodData Central key; DEMO_KEY allows only a few lookups an hour |
BODYLOG_OFFLINE | off | 1 uses only the bundled food table, no network |
The skill is skills/bodylog/SKILL.md. Outside the plugin, copy or symlink skills/bodylog/ into
your agent's skills folder.
The CLI prints what the MCP tools return (bodylog eat is log_food). A real run, offline:
A bare count uses USDA's portion for that food: a large egg (50 g), a slice of toast, a scoop of whey (the FNDDS "1 scoop, NFS", 26 g). Through MCP, the agent asks one short question about item 6 instead of guessing.
How a food gets its numbers:
scripts/build_common_foods.py.POST /fdc/v1/foods/search, survey, SR Legacy and Foundation
foods), with FDC_API_KEY or DEMO_KEY.GET /api/v2/product/<barcode>.json, no key).A search result is used only when its name contains every word you said. Otherwise the item is kept
as unknown with the closest candidates, so the agent can ask which one you meant. An item whose
amount cannot become grams (a handful, or millilitres of something with no USDA volume weight) is kept
as needs_amount. Neither counts toward totals until it is fixed, and neither is ever filled with
made-up numbers. Numbers you read off a label are stored as given (manual).
Also: daily goals for kcal, protein, carbs and fat; a logging streak in days (a day you have not logged yet does not break it) and a training streak in weeks; and a day card in dark, light, clear or story format. On a day you also trained, the card adds a Training block.
The chat below comes from a real run through log_set(text=...). The right column shows what the
store recorded for each message.
The last message ends the workout, so the tool returns this card along with the workout PNG at the top of
this page (end_session does the same). PRs are counted against the three earlier sessions in
tests/fixtures/chat.txt.
60kg x 8, 60 x 8, 50lbs for 9, 8 reps at 60kg, 3x10 @ 90lb, 60kg 3x8, 12 reps
(bodyweight). Flags: warmup, drop set, to failure, rpe 9.incline db press 55 lb x 10), announced (now squats),
or named after the fact (this is tricep cable pushdowns btw). A set without a name continues the
last exercise.edit_last_set(exercise=...) for the earlier sets.db/bb shorthand, word order, filler (on the),
and muscle words that don't change the movement. So tricep cable pushdowns, cable pushdown and
triceps pushdowns on the cable are one exercise, while triceps curl and biceps curl stay apart.
bodylog alias teaches any other name.same / one more repeats the last set; done, 1h 5m / workout took 45 min ends the
workout. Agent replies in a pasted log (Agent:, Assistant:, Claude:) are skipped.unit you pass, else $BODYLOG_UNIT, else whichever unit most sets used.muscles.py that covers the common lifts (unknown names count as Other). Percentages always add
to 100.weight x (1 + reps / 30).| Tool | What it does |
|---|---|
log_set | Add a set to the open workout (opens one if needed). Structured fields or the raw message as text. Returns any PRs the set broke. |
edit_last_set | Fix or delete the last set. |
end_session | End the workout with the duration the user gives ("1h 5m"). Returns the card as text plus PNG pages. |
session_card | Card for any session: theme dark, light or clear; style full or story; unit kg or lb. |
exercise_history, prs, weekly_volume | Progress per exercise, all-time records, volume per week. |
import_chat | Backfill workouts from a pasted chat log. Safe to repeat. |
log_food | Log food from the user's words, or one item by name, barcode or food id with an amount, or with label numbers. Returns each item's status and the day's totals. |
edit_food | Fix an item: amount, food, meal or numbers; or delete it. |
lookup_food | Search by name or barcode without logging. |
food_day | A day's items by meal, totals, goals, what is left, streaks, flagged items, workouts. |
food_card | The day card as text plus a PNG: theme, style full or story. |
set_goals, streaks | Daily targets; logging and training streaks. |
The MCP Python SDK needs a few lines per tool, Pillow draws the cards with no browser, and sqlite3
and urllib ship with Python. Runtime dependencies are Pillow and mcp. Cards use Inter (SIL Open
Font License, bundled in src/bodylog/fonts/), so they look the same everywhere.
Tests never touch the network or the repo: tests/fixtures/http/ holds real USDA and Open Food Facts
responses, and every file a test writes goes to a temp dir.
Layout: store.py (SQLite), names.py, chatlog.py, stats.py, muscles.py, card.py (workouts),
food.py (meal parsing, matching, totals, streaks), sources.py (USDA and Open Food Facts),
foodcard.py (day card), cli.py, server.py (MCP), data/common_foods.json (bundled USDA table).
Food data: USDA FoodData Central (public domain) and Open Food Facts (Open Database License; product data is attributed to Open Food Facts contributors).