Skip to content

fix: use modern /food/add endpoint for diary writes#5

Open
sverrejoh wants to merge 3 commits into
AdamWalt:mainfrom
sverrejoh:fix/use-modern-food-add-endpoint
Open

fix: use modern /food/add endpoint for diary writes#5
sverrejoh wants to merge 3 commits into
AdamWalt:mainfrom
sverrejoh:fix/use-modern-food-add-endpoint

Conversation

@sverrejoh

Copy link
Copy Markdown

Fix: use modern /food/add endpoint for diary writes

Summary

add_food_to_diary() (and therefore the mfp_add_food_to_diary MCP tool)
currently writes nothing. The legacy endpoint it targets — POST /food/diary/{username}/add
was removed from MyFitnessPal and now returns 404. Every write attempt looks
successful (no exception) but the diary never receives the entry.

This PR rewrites the function to use MFP's current POST /food/add endpoint,
discovered by capturing a real "Add Food to Diary" browser submission via Playwright.

The bug, reproduced

Tested on June 4, 2026 with a freshly authenticated session
(__Secure-next-auth.session-token cookie, fully populated Rails session cookies
including _mfp_session, remember_me, known_user):

GET  https://www.myfitnesspal.com/food/diary/{user}/add  → 404
POST https://www.myfitnesspal.com/food/diary/{user}/add  → 404

The MCP tool returns generic "Failed to add food to diary" but the diary on the
subsequent read has zero matching entries.

The new endpoint

Captured by Playwright submitting the visible Add Food form:

POST /food/add
  authenticity_token=<from search-results page>
  food_entry[food_id]=<data-original-id, NOT mfp_id>
  food_entry[date]=YYYY-MM-DD
  food_entry[quantity]=<servings>
  food_entry[weight_id]=<first data-weight-ids value>
  food_entry[meal_id]=0|1|2|3

Headers:
  X-CSRF-Token: <meta name="csrf-token" content="...">
  Referer:      https://www.myfitnesspal.com/food/search
  (X-Requested-With must NOT be set; that triggers 302 → /account/login)

Two key differences from the legacy contract:

  1. food_entry[food_id] is data-original-id, not mfp_id. They are different IDs.
    The MFP search results expose both:
    • data-external-id="23647715544933" ← matches mfp_id from mfp_search_food
    • data-original-id="1501419233" ← the value /food/add expects
  2. food_entry[meal_id], not meal is the field name for meal selection.

How the rewritten function works

  1. Warm the session: visit /food/diary (populates Rails session cookies).
  2. GET /food/add_to_diary?meal=<index> to get an initial authenticity_token.
  3. Resolve the food's name via client.get_food_item_details(mfp_id),
    then POST /food/search to surface a result row with data-original-id and
    data-weight-ids.
  4. POST /food/add with the modern field structure, the meta X-CSRF-Token
    header, and Referer: /food/search.

A 302 to /food/diary/* confirms success. A 302 to /account/login indicates
the session is missing/expired, and a non-redirect response is treated as
failure.

Additional fix

AddFoodToDiaryInput.quantity previously had le=100, which blocked any
gram-quantity entry larger than 100 g — e.g. "200 g cottage cheese",
"109 g ham", "150 g rice" — all common cases. Raised to le=10000.

Verification

Adding 5 foods (109 g ham, 57 g bread, 5 g butter, 43 g cucumber,
200 g cottage cheese, 1 medium orange) via the patched mfp_add_food_to_diary:

  • All 5 calls return {"success": true, ...}
  • Subsequent mfp_get_diary shows all 5 entries with correct quantities,
    units, and nutrition totals
  • Day total moves from 474 cal → 1148 cal as expected

The original code on the same session and food IDs persists 0 entries.


🤖 This patch was developed and verified by Claude by reverse-engineering the live site
behavior with Playwright. The original test transcript (failure proofs + new
endpoint discovery + post-fix verification) is available on request.

The legacy /food/diary/{username}/add endpoint that
add_food_to_diary() and mfp_add_food_to_diary tool target was
removed by MyFitnessPal. All write attempts silently fail:

  GET  https://www.myfitnesspal.com/food/diary/{user}/add → 404
  POST https://www.myfitnesspal.com/food/diary/{user}/add → 404

(Reproducible today with a fresh authenticated session, fully
populated Rails session cookies, and the exact post_data the
function constructs.)

The "Add Food to Diary" form on the live site now POSTs to a
different endpoint with a substantially different field
contract — discovered by capturing a real browser submission
via Playwright:

  POST /food/add
    authenticity_token=<from search-results page>
    food_entry[food_id]=<data-original-id, NOT mfp_id>
    food_entry[date]=YYYY-MM-DD
    food_entry[quantity]=<servings>
    food_entry[weight_id]=<first data-weight-ids value>
    food_entry[meal_id]=0|1|2|3

  Required headers:
    X-CSRF-Token: <meta name="csrf-token" content>
    Referer:      https://www.myfitnesspal.com/food/search
    (NO X-Requested-With — that triggers a 302 to /account/login)

The new flow rewrites add_food_to_diary to:

  1. Warm the session by visiting /food/diary
  2. Fetch /food/add_to_diary?meal=X (for initial authenticity_token)
  3. POST /food/search with the food's name (resolved via
     get_food_item_details) to surface the result row, which
     exposes data-original-id and data-weight-ids
  4. POST /food/add with the modern field structure and the
     X-CSRF-Token header

Also raises AddFoodToDiaryInput.quantity upper bound from 100
to 10000 — the previous cap blocked common gram-based entries
like "200 g cottage cheese" or "109 g ham".

Verified end-to-end: adding 5 foods via mfp_add_food_to_diary
produces 5 entries in the diary on the subsequent
mfp_get_diary read, with correct quantities, units, and
nutrition totals. The original code on the same session,
inputs, and food IDs writes 0 entries (silent 404 from the
dead endpoint).

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
@sverrejoh sverrejoh requested a review from AdamWalt as a code owner June 4, 2026 12:53
Adds delete support for diary entries — the missing inverse of
mfp_add_food_to_diary.

Endpoint:
  DELETE /food/remove/{food_entry_id}
  Headers:
    X-CSRF-Token: <meta name="csrf-token" content>
    Referer:      https://www.myfitnesspal.com/food/diary
    X-Requested-With: XMLHttpRequest
  Success = 200/204/302.

The food_entry_id is the internal entry identifier (different from
mfp_id of the food itself). It can be scraped from the rendered
diary page on each <a data-food-entry-id="..."
class="js-show-edit-food">.

The tool takes one of two inputs:

  1. entry_id — precise deletion of a known entry.
  2. name_contains — fuzzy substring match against entry names,
     optionally filtered by meal, capped at max_matches (default 1)
     for safety.

Two internal helpers added:
  list_diary_entries(client, date)  — scrape entry IDs/names/meals
  remove_food_entry(client, eid)    — perform the DELETE

Verified end-to-end:
  add → get_diary (visible) → remove → get_diary (gone)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
@sverrejoh

Copy link
Copy Markdown
Author

Added a second commit: feat: add mfp_remove_food_from_diary tool.

Same investigation pattern as the write fix — used Playwright to find that each diary entry row exposes its food_entry_id via <a data-food-entry-id="…" class="js-show-edit-food">, and the delete is:

DELETE /food/remove/{food_entry_id}
  X-CSRF-Token: <meta name="csrf-token" content>
  Referer: https://www.myfitnesspal.com/food/diary

The new MCP tool supports two modes:

  • By entry_id — precise deletion when the ID is known
  • By name_contains + optional meal — fuzzy substring match, capped by max_matches (default 1)

Round-trip verified through the MCP tools: add → get_diary (visible) → remove → get_diary (gone).

Happy to split this into a separate PR if you'd prefer the fix and the feature reviewed independently — just say the word.

Comment thread src/mfp_mcp/server.py Outdated
Comment thread src/mfp_mcp/server.py Outdated
Addresses two review comments on AdamWalt#5:

1. **No silent fallback to first search result.**
   Previously, when the food's data-external-id wasn't found in the
   first page of /food/search results, add_food_to_diary would
   substitute the first result. That risked silently writing the
   wrong food to a user's diary — a much worse failure than failing
   the call.

   The fallback is removed. add_food_to_diary now raises with an
   actionable message that distinguishes "id not on this page of
   results" from "no results at all", and tells the caller to use
   mfp_search_food to disambiguate.

   get_food_item_details is wrapped in try/except so a bogus id
   degrades gracefully into the search-step error, rather than
   throwing an empty lxml/HTTP exception earlier in the pipeline.

2. **Remove `unit` from AddFoodToDiaryInput.**
   The rewritten add_food_to_diary always uses the food's first
   weight_id and treats quantity as servings-of-default-unit. The
   public schema still exposed `unit`, which created the surprising
   contract "you can pass '1 cup' but it'll be ignored".

   `unit` is dropped from the model entirely and the schema now sets
   `extra="forbid"`, so a caller passing `unit="1 cup"` gets a clear
   ValidationError at the boundary. The docs for `quantity` now
   point the caller to mfp_get_food_details to inspect what the
   default unit actually is for any given food.

Verified:
 - Schema rejects unit='1 cup' with extra_forbidden ValidationError
 - Happy-path add still works (Bama agurk x0.5)
 - Bogus mfp_id 999999999999999 raises "No search results returned
   for food 999999999999999 ... Try mfp_search_food directly"

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants