Skip to main content
Glama
lucia-urcuyo

foodlog

by lucia-urcuyo
README.md
# Food Log MCP

A personal food journal you use through an AI assistant, with a mobile-friendly nutrition
dashboard. Log meals in conversation, save foods you eat regularly, and review calories,
protein, carbs, fat, fiber, meal history, and eating patterns.

MCP (Model Context Protocol) lets a compatible assistant call this project's tools to read
and write your journal. Use your connected assistant to add or correct foods, or log a saved staple directly from
the dashboard with its serving count.

This is a self-hosted, single-person project. Each person should use their own checkout,
database, profile, and deployment. There are no separate user accounts inside one instance.

## What you get

- **Daily overview:** calories against your target, current macro intake, and fiber progress.
- **Macro ranges:** energy shares labeled under, within, or over your configured ranges.
- **Meal journal:** expandable ingredients, nutrients, notes, and estimated-data indicators.
- **History:** 7-day, 30-day, and available-history views, with charts and eating patterns.
- **Saved foods:** searchable staples with serving controls and a “Log now” button, plus aliases such as “my usual oatmeal.”
- **Daily browsing:** previous/next day buttons and a date picker show the full summary for any day in the loaded history.
- **Consistent grading:** the server calculates grades from nutrients and processing level.
- **Your storage:** a local SQLite database, or your own Turso database for remote hosting.

The included personal metrics and targets are example configuration, not universal goals.
Replace them with your own information before interpreting your results.

## Quick start

These commands use a macOS/Linux shell. Use Python 3.10 or newer.

```bash
git clone https://github.com/lucia-urcuyo/food-log-mcp.git
cd food-log-mcp
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
cp .env.example .env
```

Edit `.env` with your own body measurements and activity. Then load it into your shell:

```bash
set -a
. ./.env
set +a
```

**The application does not automatically read `.env`.** Load it before starting the server
or generating a dashboard. Reload the environment and restart running processes after changes.
Only source an environment file you created or reviewed: this command executes shell syntax.

Check the installation:

```bash
./.venv/bin/python test_server.py
```

The tests use temporary databases and print `all good` and `libsql backend: ok` when successful.
A new checkout starts with an empty journal; the database is created automatically on first use.

## Make it yours

### 1. Set your measurements and activity

The commented [.env.example](.env.example) lists every supported environment setting.

| Setting | Meaning |
| --- | --- |
| `FOODLOG_AGE`, `FOODLOG_SEX` | Inputs to the dashboard's resting-energy calculation; the formula uses `male` or `female` |
| `FOODLOG_WEIGHT_KG`, `FOODLOG_HEIGHT_CM` | Metric body measurements; nonzero values take precedence over imperial inputs |
| `FOODLOG_WEIGHT_LB`, `FOODLOG_HEIGHT_IN` | Imperial inputs; keep them equivalent to the metric values for the profile explanation |
| `FOODLOG_BASE_PAL` | Activity multiplier excluding the workouts below |
| `FOODLOG_LIFT_DAYS`, `FOODLOG_LIFT_MIN`, `FOODLOG_LIFT_MET` | Lifting sessions per week, minutes per session, and intensity factor |
| `FOODLOG_SPORT_DAYS`, `FOODLOG_SPORT_MIN`, `FOODLOG_SPORT_MET` | Other sport sessions, duration, and intensity factor |
| `FOODLOG_GOAL` | `maintain`, `cut`, or `gain`; affects the calculated calorie target |
| `FOODLOG_WINDOW_PCT` | Width of the calorie window around the target |
| `FOODLOG_PROTEIN_G_PER_KG` | Factor for the calculated protein target |

Set workout days to `0` for activities you do not do. These inputs affect the dashboard's
calculated targets; saved targets can override those calculations.

### 2. Save your journal profile

Once an assistant is connected, ask it to set your Food Log profile using your chosen values.
For example, this is the JSON input to the `profile` tool—not a shell command:

```json
{
  "set_values": {
    "kcal_target": 2200,
    "protein_target_g": 120,
    "weight_kg": 70,
    "late_hour": 20
  }
}
```

Use your own numbers. Call `profile` without arguments to inspect saved settings.

| Value | Where it is used |
| --- | --- |
| `kcal_target` | Stats and dashboard; overrides the dashboard's calculated calorie target |
| `protein_target_g` | Stats and dashboard; overrides the dashboard's calculated protein target |
| `weight_kg` | Stats calculations; also set `FOODLOG_WEIGHT_KG` for the dashboard |
| `late_hour` | Stats and dashboard; hour after which calories are counted as late |

Profile values are stored in the database. Changing `.env` does not clear saved overrides.
The stats tool does not inherit the dashboard's calculated targets: set the profile to make
its target comparisons available. Keep profile weight and environment weight in sync.

### 3. Choose macro and fiber ranges

The current dashboard ranges are:

| Nutrient | Range |
| --- | --- |
| Protein | 25–30% of macro energy |
| Carbs | 40–45% of macro energy |
| Fat | 25–30% of macro energy |
| Dietary fiber | 25–28 g per day |

To use different ranges, edit these entries in `targets()` in [dashboard.py](dashboard.py):

```python
"fiber_g": 25,  # keep this equal to the lower end of your fiber range
"fiber_range_g": [25, 28],
"macro_ranges_pct": {"p": [25, 30], "c": [40, 45], "f": [25, 30]},
```

These ranges currently have **no environment-variable or profile override**. Restart or
redeploy the dashboard after editing them. Percentage ranges are separate from the daily
protein goal in grams. Choose ranges that can accommodate a combined 100% macro split.

## Connect an assistant

### Local MCP client

A client that supports local MCP servers can launch `server.py` over stdio. Here is an
example MCP configuration; replace the absolute path with your checkout's location:

```json
{
  "mcpServers": {
    "foodlog": {
      "command": "/bin/sh",
      "args": [
        "-c",
        "cd '/absolute/path/to/food-log-mcp' && set -a && . ./.env && set +a && exec ./.venv/bin/python server.py"
      ]
    }
  }
}
```

The exact configuration location depends on your client. The wrapper loads your `.env`
every time the client starts the server. You can also use your client's environment-variable
settings and launch the Python executable directly.

### Remote MCP client

Clients that cannot start local processes need a reachable HTTPS MCP endpoint. Run the ASGI
entrypoint below behind your own HTTPS host or tunnel, then provide the client with:

```text
https://YOUR-HOST/YOUR-FOODLOG-PATH
```

Use your own database and secrets. The bundled entrypoint uses `FOODLOG_PATH` as its access
secret; it has no account login or OAuth implementation. Anyone with that URL can access the
journal's tools. Keep both the MCP and dashboard URLs private.

## Open the dashboard

After loading `.env`, run the entrypoint that serves both MCP and the dashboard:

```bash
./.venv/bin/python -m uvicorn api.index:app --host 127.0.0.1 --port 8123
```

With the example's local `FOODLOG_PATH=/mcp`, open:

- Dashboard: `http://127.0.0.1:8123/mcp/dashboard`
- MCP endpoint: `http://127.0.0.1:8123/mcp`

If you change `FOODLOG_PATH`, replace `/mcp` in both URLs. Do not expose the example path
publicly. `server.py` alone serves MCP, not the dashboard.

For a standalone, private HTML snapshot:

```bash
./.venv/bin/python dashboard.py > /tmp/foodlog-dashboard.html
```

Open that file in a browser. It contains your meal data; regenerate it to see new entries.
Live refresh is available through the hosted dashboard, not a saved file.

## Start logging

Example requests to your connected assistant:

- “Set my Food Log profile to my calorie target, protein target, weight, and late-meal hour.”
- “Log this lunch. Break it into ingredients and include fiber for each one.”
- “Save this labeled oatmeal serving as ‘my usual oatmeal,’ including its fiber.”
- “Log two servings of my usual oatmeal.”
- “Show my last seven days and compare them with my targets.”
- “Correct the portion size in meal 12.”

Meals are ingredient lists. The assistant supplies nutrients and portions; the server adds
them up and calculates the grade. For photos, the logging protocol asks for estimated weights,
visual anchors in the notes, and a processing level for each ingredient.

**Fiber is required for each new ingredient and saved food.** Use explicit `0` for fiber-free
foods. If the value is unavailable, obtain it before logging. Sugar, saturated fat, and sodium
remain optional. Fiber cannot be cleared or set to a negative/non-finite value through edits.

For saved foods, fiber is entered once per serving and scaled automatically: a serving with
4 g of fiber contributes 8 g when you log two servings. Older saved foods missing fiber must
be updated before reuse. Older meal records remain readable without invented fiber values.

### Log a staple from the dashboard

Find a saved food under **Your staples**, choose a serving count (in quarter-serving steps),
and tap **Log now**. This logs it using your browser’s current local date and time, scales
all nutrients from the saved serving, and infers breakfast, lunch, dinner, or snack using
the existing meal-time rules. The dashboard returns to Today after a successful save.
Even when browsing a past day, **Log now always records the food for now**.

Staples missing fiber cannot be logged until corrected in chat. A saved HTML snapshot can
show staples but cannot log them; use the served dashboard. If a connection error leaves
save status uncertain, refresh and check your meals before adding the same serving again.

## Understand your numbers

- Macro percentages use **4 kcal/g for protein and carbs, and 9 kcal/g for fat**, divided by
  their combined energy. They describe the food logged so far, not a percentage of your daily
  calorie target. Range boundaries are inclusive.
- Fiber progress uses your daily gram range. If any meals lack fiber, the main summary shows
  the reported amount as a minimum and marks the data incomplete.
- The dashboard’s 7-day and 30-day views end yesterday; All also excludes today. Their
  averages use **logged days**, excluding days without entries. Daily browsing includes
  today’s partial intake. The MCP stats tool retains its existing period definitions.
- The dashboard loads up to 365 days of meals; its All view covers that loaded history.
  The daily chart shows at most the latest 60 days in the selected period. Older stored meals
  remain available through the journal tools.
- Grades are a project-specific heuristic based on nutrient density per 100 kcal and NOVA
  processing levels. They describe the scoring model, not a clinical assessment.
- `estimated_fields` identifies missing nutrients filled by the scorer. Confidence describes
  nutrient completeness; it does not certify the accuracy of photo portions or supplied values.

## Available tools

| Tool | Purpose |
| --- | --- |
| `log_meal` | Add ingredients as a meal and calculate its grade |
| `edit_meal` | Correct or delete an entry; corrections recalculate its grade |
| `query_log` | Filter meals by date, meal type, search text, or grade |
| `find_food` | Look up saved foods and previously logged ingredients |
| `save_staple` | Save a reusable serving, nutrients including fiber, and aliases |
| `stats` | Review energy, macros, quality, timing, and habits |
| `profile` | Read or update saved personal targets |

## Storage, backups, and hosting

Local storage defaults to `foodlog.db` beside `server.py`. Use `FOODLOG_DB` to select another
file, preferably an absolute path. Stop local writers before copying the database for a simple
backup. Keep databases, HTML exports, tokens, and `.env` private; `.env.example` contains only
sample values and is safe to commit.

Dates and times are stored as local, timezone-naive values. Use a consistent server timezone
and supply explicit meal dates/times if the server runs in a different timezone from you.
The dashboard determines Today from the browser's local date.

For a persistent host, run the ASGI app with durable storage. For Vercel, this repository
includes `api/index.py` and `vercel.json`; use your own Turso database because local files do
not provide persistent storage across serverless requests. Configure these variables in the
hosting platform, not just in your laptop's `.env`:

- `TURSO_DATABASE_URL`: your database URL.
- `TURSO_AUTH_TOKEN`: your database access token.
- `FOODLOG_PATH`: a private, unpredictable path beginning with `/`.
- Your `FOODLOG_*` personal metrics and activity settings.

When `TURSO_DATABASE_URL` is nonempty, it takes precedence over local SQLite. Switching to it
**does not migrate your existing local meals**. A new database starts empty. Create the saved
profile in the database your deployed instance uses, and use your provider's backup facilities.

## Troubleshooting

| Problem | Check |
| --- | --- |
| Dashboard targets look wrong | Load `.env`, restart the process, inspect `profile`, and open “Your targets, explained” |
| Stats and dashboard disagree about weight | Update both profile `weight_kg` and `FOODLOG_WEIGHT_KG` |
| Fiber is incomplete | Older meals may lack values; correct them using actual nutrition information |
| Saved-food logging rejects missing fiber | Re-save that food with its complete serving information and fiber |
| Dashboard returns 404 or 405 | Start `api.index:app` and use the configured path plus `/dashboard` |
| Refresh cannot update a file | Open the served dashboard or regenerate the HTML snapshot |
| A different client shows an empty journal | Check that both processes use the same database configuration |
| Import or command not found | Use the checkout's `.venv/bin/python` and install `requirements.txt` |

## Project files

| File | Purpose |
| --- | --- |
| `server.py` | MCP tools, database schema, scoring, and statistics |
| `dashboard.py` | Self-contained dashboard, aggregation, and calculated targets |
| `test_server.py` | Regression tests using temporary SQLite and libSQL databases |
| `.env.example` | Commented sample configuration |
| `api/index.py`, `vercel.json` | ASGI hosting entrypoint and Vercel routing |
| `PRODUCT.md` | Product background |
| `CLAUDE.md` | Contributor instructions and invariants |

Contributors: keep grading server-side, preserve ingredient-level data, and never silently
turn missing nutrients into zero. Run `./.venv/bin/python test_server.py` before sharing changes.

### Scoring reference for contributors

Scoring constants live in `SCORING` and `NOVA_DEFAULTS` in `server.py`. The current model starts
at 65 points, adds capped protein and fiber density contributions, subtracts sugar, saturated
fat, and sodium penalties, and applies a processing adjustment. Fiber offsets part of the sugar
penalty. Grades use thresholds A ≥ 80, B ≥ 65, C ≥ 50, D ≥ 35, otherwise F.

The existing calibration examples are retained here for changes to that model:

```text
A 88.8  chicken + broccoli + rice      C 62.4  sourdough toast + butter
A 80.0  plain greek yogurt             C 54.2  croissant
B 76.5  banana                         D 39.8  orange juice
B 74.2  turkish eggs + bread           F 23.0  coke
B 72.5  protein bar
```

These examples describe the project's scoring calibration. The test suite checks selected
fixtures and relative ordering; update the applicable assertions and this reference together
when deliberately changing the scoring model.