Add legacy MCP tools for recent and saved food collections#1
Conversation
|
~1,000 lines of new logic covering HTML parsing, write operations, and entry ID matching with no tests. The PR description mentions manual verification against a live session, which is great for smoke testing, but I'd like to see at least unit tests for the pure functions before merging:
These don't need a live MFP session and would catch regressions if MFP changes their HTML structure. |
|
Thanks for the contribution — the code quality here is solid. The A few things I'd like addressed before approving:
Happy to re-review once those are addressed. |
Port the legacy add-page AJAX collections (PR AdamWalt#1 by tomerh2001, adapted): - mfp_get_recent_foods, mfp_get_frequent_foods, mfp_get_my_foods - Fetch via POST /food/load_{recent,most_used,my_foods} with meal/base_index/ page params; these endpoints require X-Requested-With plus the page csrf-token meta as X-CSRF-Token (without the params the server returns 406). - Parse the favorite rows into {name, food_id, default_serving, weight_id}. food_id is the classic /food/add id; My Foods are private and do not appear in mfp_search_food, so this is the only way to reach them. Also ignore .DS_Store.
AdamWalt
left a comment
There was a problem hiding this comment.
Review summary
This is the most ambitious of the open PRs and, in pure engineering terms, the highest-quality in places: it uses proper lxml/xpath parsing (not regex), confirms writes by diffing before/after diary snapshots, resolves real serving sizes, and richly normalizes the legacy AJAX payloads. The three read tools (mfp_get_recent_foods / frequent / my_foods) are clean, well-annotated, and match the stated purpose.
But it's also the riskiest to merge as written, for reasons that are mostly about scope and verification rather than code quality:
Blocking-ish concerns
- Global cloudscraper monkeypatch (
create_mfp_client) changes the HTTP transport for every request and disables Cloudflare bypass to fix a host-specific 403. This can regress users who actually need cloudscraper. Needs a more targeted fix or, at minimum, opt-in behavior + disclosure. (inline) - Undisclosed scope + no verification for the write/delete paths. The description and test plan cover only the three read tools, but the PR also rewrites
add_food_to_diary, addsmfp_update_food_entry, and adds the destructivemfp_delete_food_entry. (inline ×2) mfp_get_diarynow double-fetches on every read and zips entry_ids positionally; on a count mismatch it can attach the wrong id, which then targets update/delete at the wrong row. (inline)
Cross-PR coordination (important)
- This PR and #5 both rewrite
add_food_to_diaryto/food/addand will hard-conflict. They disagree on theunitcontract (#1 keeps serving resolution; #5 dropsunit). Pick one as the basis for the write rewrite. - This PR also touches
get_mfp_client/ client construction, overlapping with #3's auth changes. - If you want the read-only food-collection tools, those are the cleanly-separable, low-risk part of this PR and could be landed on their own quickly.
Verdict
Not mergeable as-is. The read tools are close to ready and could be split out; the transport monkeypatch and the unverified write/delete rewrite need rework and verification before they should land — and the add_food_to_diary rewrite has to be reconciled with #5 either way.
Recap across all four open PRs
- #2 (gitignore chore): ✅ Mergeable as-is. Subset of #3 — consider merging this first and trimming #3, or closing in favor of #3.
- #3 (encryption + Chromium auto-discovery):
⚠️ Request minor changes — Windows keychain round-trip mismatch + contradictory README "recommended" wording. Solid otherwise. - #5 (
/food/addfix):⚠️ Add path is verified and good; bundled unverified destructive remove tool + brittle regex need resolving. Conflicts with #1. - #1 (food collections + write/delete + transport patch): ❌ Not mergeable as-is — read tools are nearly ready and splittable; monkeypatch + unverified writes need rework. Conflicts with #1/#5/#3.
Suggested order: land #2, then #3 (after its fixes), then decide #1-vs-#5 for the diary-write rewrite (I'd lean toward #5's leaner verified add + #1's read-only collection tools split out), saving update/delete for a follow-up with real verification.
Generated by Claude Code
| return jar | ||
|
|
||
|
|
||
| def create_mfp_client(cookiejar: Optional[CookieJar] = None): |
There was a problem hiding this comment.
This temporarily replaces myfitnesspal_client.cloudscraper.create_scraper at the module level and restores it in a finally block. Since this is an async MCP server, concurrent tool calls could race here — one call restores the original while another is mid-construction.
Could you either:
- Guard this with a
threading.Lock, or - Avoid the global mutation entirely (e.g., construct the session directly and pass it into the client, or subclass the client)
This is the main blocker for me before approving.
| return jar | ||
|
|
||
|
|
||
| def create_mfp_client(cookiejar: Optional[CookieJar] = None): |
There was a problem hiding this comment.
This global monkeypatch is the riskiest thing in the PR. create_mfp_client reaches into myfitnesspal.client.cloudscraper and swaps create_scraper for a plain requests.Session, then restores it in finally. Two concerns:
- It disables Cloudflare bypass for everyone.
cloudscraperexists specifically to get past Cloudflare challenges. Replacing it fixes the403 on /user/auth_tokenyou saw on this host, but for users who actually hit a Cloudflare interstitial, the plain session will now fail where it previously worked. That's trading a fix on one environment for a regression on another. - It mutates a module global. Even with the
finallyrestore, it's not concurrency-safe — if two clients are created concurrently (or the lambda leaks), behavior is undefined.
If the 403 is really about the scraper, a more targeted fix (e.g. passing a custom session only on the specific failing request, or making the plain-session fallback opt-in / try-cloudscraper-then-fallback) would avoid globally defeating cloudscraper. At minimum this deserves a prominent comment and a mention in the PR description, since it changes the transport for every request, not just the new tools.
Generated by Claude Code
| "openWorldHint": True, | ||
| }, | ||
| ) | ||
| async def mfp_delete_food_entry(params: DeleteFoodEntryInput) -> str: |
There was a problem hiding this comment.
Scope vs. description + verification gap. The PR description says it adds "recent foods, frequent foods, and My Foods" tools, and the verification section only exercises those three read tools. But the diff also: rewrites add_food_to_diary end-to-end to the /food/add endpoint, adds a write tool (mfp_update_food_entry), adds a destructive tool (mfp_delete_food_entry), and globally swaps the HTTP transport. None of the write/delete/add-rewrite paths appear in the test plan.
The delete here (/food/remove/{id} with _method=delete) does at least confirm by re-reading the diary and asserting the entry is gone — good — but a destructive operation with no stated verification is exactly what I'd want proven before merge. Recommend either splitting the write/delete work into its own PR or adding explicit before/after verification for update and delete to the test plan.
Generated by Claude Code
| def get_diary_entry_snapshots(client, target_date: date) -> Dict[str, Dict[str, Any]]: | ||
| """Return diary entries keyed by stable entry ID for a given date.""" | ||
| day = client.get_date(target_date) | ||
| entry_ids_by_meal = extract_diary_entry_ids(client, target_date) |
There was a problem hiding this comment.
mfp_get_diary now does an extra full page fetch + parse (extract_diary_entry_ids) on every read to attach entry_ids. That's a real cost added to the hottest read path, and it's only needed when the caller intends to edit/delete. The count-mismatch warning also implies the id↔entry alignment is positional and can silently drift (the diary parse and the library's meal.entries ordering must agree). Two thoughts: (a) consider making the entry-id enrichment opt-in via a flag on GetDiaryInput so plain reads stay single-fetch; (b) when the counts mismatch you currently still zip them positionally — that can attach the wrong entry_id to an entry, which then drives update/delete against the wrong row. Safer to attach None for the whole meal on mismatch rather than risk a misaligned id.
Generated by Claude Code
| "openWorldHint": True, | ||
| }, | ||
| ) | ||
| async def mfp_update_food_entry(params: UpdateFoodEntryInput) -> str: |
There was a problem hiding this comment.
Nice touch: update_food_entry snapshots the diary before/after and uses find_replacement_entry to re-locate the row when MFP rotates the entry_id. That heuristic (meal match → short_name → name substring → single-new-entry fallback) is reasonable, but it's doing fuzzy matching to confirm a write succeeded, and the fallbacks can mis-identify when a meal has multiple similar items edited in quick succession. Given how much inference is happening here, this is another reason update/delete deserve dedicated verification rather than riding in on a "food collections" PR.
Generated by Claude Code
Summary
Why
Newer account routes such as
/food/mine,/meal/mine, and/food/newcan redirect to/account/logoutin this environment even while diary reads and authenticated API token fetches still work. That made simple account-level food list requests slow and brittle to answer manually.This change gives the MCP first-class tools for those queries and documents the non-obvious legacy path that still behaves reliably.
Notes
I attempted to create a tracking issue in the fork first to follow the normal fork flow, but GitHub Issues are disabled on this repository, so there was no issue to link.
Verification
python -m py_compile src/mfp_mcp/server.pymfp_get_recent_foods,mfp_get_frequent_foods, andmfp_get_my_foodslocally against a live authenticated session