foodlog
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@foodlogLog my lunch: 170g chicken breast, 100g rice, 50g avocado."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
foodlog MCP
A calorie/macro/quality journal your agents read and write. One server.py, one SQLite file,
stdlib only apart from the MCP SDK.
server.py server, schema, scoring, KPIs — the whole thing
test_server.py self-check — python test_server.py
foodlog.db your data (created on first log; override with $FOODLOG_DB)
api/index.py serverless entrypoint, inert unless you deploy
PRODUCT.md what it does and why, without the code
CLAUDE.md rules for agents working on the repoSetup
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
./.venv/bin/python test_server.py # should print "all good"Claude Code (stdio, local — the fast path):
claude mcp add foodlog -s user -- "$PWD/.venv/bin/python" "$PWD/server.py"ChatGPT (needs a remote HTTPS URL — it can't spawn a local process). Cheapest version, no deploy, no cloud database:
FOODLOG_TRANSPORT=http ./.venv/bin/python server.py # serves /mcp on :8000
cloudflared tunnel --url http://localhost:8000 # or: ngrok http 8000Settings → Connectors → Advanced → Developer mode → Add, URL https://<tunnel>/mcp. Your data
stays on your laptop; the tunnel dies when you close the terminal. For an always-on URL see
Deploying.
Both clients can point at the same foodlog.db (SQLite handles the concurrency at this volume).
Related MCP server: Kitchen MCP Server
Tools
tool | does |
| write a meal; grades it server-side |
| fix a mislog; re-grades automatically |
| read the journal |
| fuzzy lookup over staples then history — resolves "the usual X" |
| remember a repeat food, per serving |
| the KPI report + a rendered markdown table |
| targets: |
Set your targets once, or half the KPIs stay null:
profile({"kcal_target": 2000, "protein_target_g": 120, "weight_kg": 60, "late_hour": 20})How logging works
log_meal takes items, not plates. Each item is one ingredient with its own weight, macros and
processing level; the server sums them, computes the grade, and stores the breakdown. The estimation
protocol lives in the tool's docstring, so every agent that loads the server gets the same
instructions:
one item per ingredient — never one number for the whole plate
grams from visual anchors (chicken breast ~170 g, bread slice ~35 g, tbsp oil ~14 g, egg ~50 g)
macros per ingredient at that weight
a NOVA level per ingredient
fiber_gis required for each ingredient and saved staple, including explicit0for fiber-free foods. If unavailable, obtain the value before logging.sugar_g/sat_fat_g/sodium_mgremain optional."the usual X" →
find_foodfirst, then{"staple": "...", "portions": n}; fiber is scaled from the saved serving. Older staples without fiber must be updated before reuse. Existing meals remain readable, with missing fiber shown as incomplete.
So the three phrasings you wanted all land on the same path:
you say | agent does |
"the usual coffee I have in the morning" |
|
"point five ounces of condensed milk" | converts to 14 g, looks up macros, one item, nova 3 |
[photo of turkish eggs] | eggs / yogurt / butter / chili oil / sourdough as five items with grams |
Whatever the server had to guess comes back in estimated_fields, and confidence is
high/medium/low by how many micros you supplied. That's your signal to ask a follow-up
("was there butter on that?") instead of silently grading on defaults.
Nutritional grade — the framework
The agent never picks the letter. It reports numbers; score_meal() computes the grade. That is
the whole point — ChatGPT and Claude will disagree about whether a croissant is a B or a D, but they
can't disagree about 33 g of carbs. Every constant lives in the SCORING dict at the top of
server.py; tune those, not the code.
Everything is measured per 100 kcal (density), so a 300 kcal snack and a 900 kcal dinner are judged on the same axis. Grade = quality only. How much you ate is the calorie KPIs' job — keeping them separate is what stops a large healthy dinner from being punished twice.
score = 65
+ min(18, protein_density × 1.6) protein is the one macro worth chasing
+ min(15, fiber_density × 5.0)
− min(20, max(0, effective_sugar − free) × 3.0)
− min(15, max(0, satfat_density − 1.5) × 4.0)
− min(15, max(0, sodium_density − 200)/100 × 1.5)
+ processing: NOVA 1 +2 · 2 ±0 · 3 −10 · 4 −22
effective_sugar = sugar_density − 2 × fiber_density fiber cancels intrinsic sugar
free sugar allowance = 5 (NOVA 1–2, i.e. fruit and dairy) · 2 (NOVA 3–4)
A ≥ 80 B ≥ 65 C ≥ 50 D ≥ 35 F < 35The fiber offset is the load-bearing rule: it's what keeps a banana (14 g sugar, 3 g fiber) a B while orange juice (21 g sugar, 0.5 g fiber) lands at D and a coke at F, without special-casing anything.
NOVA processing level, the one judgement call the agent does make:
1 whole | egg, rice, spinach, chicken, milk, fruit |
2 culinary ingredient | oil, butter, sugar, honey, flour, salt |
3 processed | bread, cheese, canned beans, cured meat, tinned fish |
4 ultra-processed | soda, packaged snacks, nuggets, protein bars, most cereal |
Missing micros are filled from NOVA-based per-100-kcal defaults, so a photo-logged meal with only
macros still gets a comparable grade — flagged confidence: "low".
Calibration as it stands:
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 barStats — the KPIs and why each one is there
stats(period="today"|"yesterday"|"week"|"month"|"all"), or explicit start/end. Returns five
structured blocks plus table, a ready-to-paste markdown summary + a 14-day day-by-day grid + top
contributors. Show the table, then talk about what moved.
energy — avg_per_day, delta_vs_target, days_within_10pct_of_target, swing_stdev,
biggest/lightest day.
swing_stdevis the one people never track. An average of 2000 hides "1400 Monday, 2800 Friday", and the swing predicts how a week felt far better than the mean does.
macros — averages, split_pct (P/C/F by calories), protein_per_kg, protein_per_100kcal,
protein_goal_hit_rate_pct.
Hit rate beats average: averaging 120 g over a week can mean seven decent days or two huge ones and five bad ones, and only one of those builds muscle.
protein_per_100kcalis the diet-independent version — it says whether your food is protein-dense regardless of how much you ate.
quality — gpa (calorie-weighted!), letter, grade_counts, pct_kcal_from_ab,
pct_kcal_from_df, avg_nova, best/worst day, low_confidence_meals.
Weighting by calories is the honest way to average grades: five A-grade black coffees shouldn't cancel one 900 kcal F.
pct_kcal_from_dfis the actionable number — it's the slice of your week where the wins actually are.low_confidence_mealstells you how much of the report to trust.
timing — median_first_bite, median_last_bite, median_eating_window_h,
pct_kcal_after_20h, kcal_share_by_meal_type, meals_per_day.
Eating window and late-calorie share are the habits most invisible from a food list and most connected to sleep and next-morning hunger. "42% of your calories land after 8pm" is a sentence that changes behaviour; "you ate 2100 calories" isn't.
habits — log_streak_days, distinct_foods, top_calorie_contributors (with % of intake),
pct_kcal_from_staples, weekday vs weekend averages and weekend_lift_pct.
top_calorie_contributorsis the single most actionable output here — it names the three foods that are your diet, and changing one of them beats any amount of willpower.weekend_lift_pctis the classic blind spot: a perfect Mon–Fri and +40% Sat–Sun nets out to no deficit at all.pct_kcal_from_staples×distinct_foodstells you whether you're on autopilot or improvising.
Deploying
Read this before you deploy anything
You are one person logging four meals a day. Hosting buys you an always-on URL and costs you a network database, cold starts, a public endpoint to secure, and a diary that now lives on someone else's disk. In rough order of laziness:
keeps SQLite | always-on URL | effort | |
stdio + tunnel when ChatGPT needs it | ✅ on your laptop | ❌ | zero — it already works |
Fly.io / Railway with a volume | ✅ one file on a mounted disk | ✅ | a Dockerfile, ~$0–5/mo |
Vercel + Turso | ⚠️ same SQL, remote engine | ✅ | this section |
Vercel is the only one of the three that forces the database question, because Vercel functions have no persistent disk. If you don't specifically want Vercel, options 1 and 2 are strictly less work. Everything below is for when you do.
Why the database has to change
foodlog.db is a file. Vercel runs your code as serverless functions: no disk that survives a
request, several instances running at once, /tmp wiped between invocations. Write a meal on one
invocation and the next one starts from an empty file. Nothing about the code is wrong — the
storage assumption just doesn't hold there.
What was considered:
option | verdict |
Turso (libSQL) | ✅ chosen. SQLite's engine, exposed over HTTP. Same SQL, same schema, same aggregation queries — one ~10-line shim (below) and it runs. Generous free tier (500 DBs, 9 GB, 1B row reads/month), which for one person is effectively infinite. |
Neon / Vercel Postgres | Rewrites every |
Supabase | Same as Neon, plus auth and storage you don't need. |
Vercel KV / Upstash Redis | Wrong shape. |
Vercel Blob / a JSON file in the repo | Not a database. No concurrent writes, no queries, and a redeploy overwrites your history. |
The deciding argument is that stats() is ~80% of the interesting code and it's all relational
aggregation over one table. Whatever it runs on should speak SQL, and the closest thing to the
SQLite it already speaks is SQLite.
The database swap — already done
_con() branches on one environment variable. No TURSO_DATABASE_URL and it's stdlib sqlite3 on
your local file, exactly as before; set it and the same SQL runs against Turso instead:
url = os.environ.get("TURSO_DATABASE_URL")
if url:
c = _Conn(libsql.connect(url, auth_token=os.environ.get("TURSO_AUTH_TOKEN", "")))
else:
c = sqlite3.connect(DB); c.row_factory = sqlite3.Row_Conn/_Cursor are the whole shim, ~20 lines: libSQL hands back plain tuples and its cursors
aren't iterable, while this file does dict(row), row["name"] and for r in con.execute(...)
everywhere. Zipping cursor.description against each tuple closes the gap. Everything else —
execute, executescript, commit, lastrowid, ? placeholders, ON CONFLICT — is identical,
which is the entire argument for Turso over Postgres.
The schema runs once per process rather than per connection. Locally that saved nothing; on Turso
every statement is an HTTP round trip, so it's the difference between a fast tool call and six
pointless ones. Cold starts re-run it and CREATE IF NOT EXISTS makes that free.
test_server.py runs its assertions against both backends, so the shim can't rot silently.
Create the database (the schema builds itself on first write):
turso db create foodlog
turso db show foodlog --url # libsql://foodlog-<you>.turso.io
turso db tokens create foodlogFree tier is 500 databases, 9 GB and 1B row reads a month. You will use roughly none of it.
Vercel
api/index.py and vercel.json are already in the repo. Two things make it serverless-safe:
stateless mode —
stateless_http=True, json_response=True. Sessions and held-open SSE streams assume one long-lived process; serverless has none. Each request stands alone.the secret is in the URL path — ChatGPT connectors can only send "no auth" or full OAuth, so a bearer header isn't available.
FOODLOG_PATH=/mcp-<random>is a token in a URL over TLS. Honest assessment: obscurity. Adequate for a food log on a URL you don't paste anywhere; not adequate for anything you'd mind a stranger writing to. Real fix isMCPServer(auth=...)with OAuth.
vercel link
vercel env add TURSO_DATABASE_URL # libsql://foodlog-<you>.turso.io
vercel env add TURSO_AUTH_TOKEN
vercel env add FOODLOG_PATH # /mcp-$(openssl rand -hex 12)
vercel deploy --prodThen point ChatGPT at https://<project>.vercel.app/mcp-<your-random>, and Claude Code at the same
URL with claude mcp add --transport http foodlog <url>.
Verify locally first — this is the exact code path Vercel runs:
FOODLOG_PATH=/mcp-test ./.venv/bin/python -m uvicorn api.index:app --port 8123
curl -s -X POST localhost:8123/mcp-test -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Three things bite on Vercel, all of them fixed in api/index.py and vercel.json, none of them
obvious from a 404:
The catch-all rewrite replaces the request path. "/(.*)" -> "/api/index" means the ASGI app
receives /api/index, not the URL you asked for, so the mount — and the secret inside it — never
matches and every path 404s. vercel.json carries the original through as ?__p= and the entrypoint
puts it back before the MCP app sees it.
The SDK's DNS-rebinding guard allowlists localhost only, so the deployed hostname comes back
421 Invalid Host header once routing works. The entrypoint allowlists VERCEL_PROJECT_PRODUCTION_URL
and VERCEL_URL, which Vercel injects; locally both are unset and the SDK default applies unchanged.
Remote libSQL upper-cases column names that are SQL keywords — key comes back as KEY, where
the local engine leaves it alone. That breaks every settings read on the server while passing every
test on your laptop. _Cursor folds names to lower case; test_server.py pins that behaviour.
Also: TURSO_AUTH_TOKEN must come from turso db tokens create <db>, not turso auth token. The
latter is your account token and the database rejects it with invalid JWT token.
Watch the Python version (pin it in Project Settings → General if the libsql wheel fails to
install) and the 10-second function limit on Hobby — vercel.json deliberately doesn't set
maxDuration, since asking for more than 10s fails the build there.
Data
Plain SQLite at foodlog.db — sqlite3 foodlog.db "select * from meals" any time, and back it up
by copying the file. Times are local and naive; that's correct for a personal food log and wrong the
day you move timezones mid-week.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Food logging, nutrition summaries, and meal photo calorie and macro estimates.
Manage your Health Partner account, log food, water, workouts, body using agent
Log meals, check calories and macros, set up a nutrition plan, and search foods.
Use TrueCal from AI agents to review progress, meals, targets, trends, and supported updates.
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceEnables tracking food intake and nutrition using the USDA FoodData Central database. Supports logging meals, setting daily nutrition goals, viewing food diaries, and analyzing nutrition trends over time with local SQLite storage.141MIT
- FlicenseNot gradedqualityDmaintenanceEnables querying food nutritional information, discovering recipes by ingredients or diet type, getting ingredient substitutions, and receiving personalized food recommendations based on mood and season.-
- AlicenseAqualityBmaintenanceEnables nutrition tracking with Cronometer, including food logging, food search, diary management, and nutrition data retrieval via natural language.13MIT
- FlicenseNot gradedqualityDmaintenanceEnables AI assistants to search recipes, compose nutritionally balanced meals, optimize weekly meal plans based on macro targets for family members, and generate consolidated grocery lists from a personal recipe database.-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/lucia-urcuyo/food-log-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server