Skip to main content
Glama
delize

MyFitnessPal MCP Server

by delize
README.md
# MyFitnessPal MCP Service

A deployable [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for MyFitnessPal that works as a **remote Claude connector**: food diary, food search, exercises, body measurements, nutrition goals, water intake, and nutrition reports.

This is a fork of [AdamWalt/myfitnesspal-mcp-python](https://github.com/AdamWalt/myfitnesspal-mcp-python) (MIT, tool implementations) restructured for remote deployment with the OAuth 2.1 / streamable-http transport skeleton from [garmin-mcp-service](https://github.com/delize/garmin-mcp-service). MyFitnessPal access is via [coddingtonbear/python-myfitnesspal](https://github.com/coddingtonbear/python-myfitnesspal).

## Tools

| Tool | Type | Description |
|------|------|-------------|
| `mfp_get_diary` | Read | Food diary (meals, entries, nutrition, totals, goals) for a date |
| `mfp_search_food` | Read | Search the MyFitnessPal food database |
| `mfp_get_food_details` | Read | Full nutrition breakdown for a food by MFP ID |
| `mfp_get_recent_foods` | Read | Recently used foods from the authenticated account |
| `mfp_get_frequent_foods` | Read | Most-used foods from the authenticated account |
| `mfp_get_my_foods` | Read | Foods created or saved by the authenticated account |
| `mfp_get_measurements` | Read | Body measurement history (Weight, Body Fat, ...) |
| `mfp_set_measurement` | Write | Log a body measurement for today |
| `mfp_get_exercises` | Read | Logged cardio/strength exercises for a date |
| `mfp_get_goals` | Read | Daily nutrition goals |
| `mfp_set_goals` | Write | Update daily nutrition goals |
| `mfp_get_water` | Read | Water intake for a date |
| `mfp_set_water` | Write | Log water intake for a date |
| `mfp_add_food_to_diary` | Write | Add a food entry to a meal |
| `mfp_create_food` | Write | Create a new custom food in the MyFitnessPal database |
| `mfp_update_food_entry` | Write | Update an existing diary entry by `entry_id` |
| `mfp_delete_food_entry` | Write | Delete an existing diary entry by `entry_id` |
| `mfp_get_report` | Read | Nutrition report (e.g. Net Calories) over a date range |

### Food collections (recent / frequent / my foods)

`mfp_get_recent_foods`, `mfp_get_frequent_foods`, and `mfp_get_my_foods` each take an optional `limit` (recent/frequent default 10, my-foods default 100, max 100) and `response_format` (`markdown` or `json`). They intentionally use the **legacy add-to-diary AJAX endpoints** (`/food/load_recent`, `/food/load_most_used`, `/food/load_my_foods`) rather than the newer `/food/mine`, `/meal/mine`, or `/food/new` pages, which can redirect to `/account/logout` even when diary reads and API-token fetches still work.

### Editing diary entries

`mfp_get_diary` with `response_format=json` now surfaces an `entry_id` for each meal entry. Pass that id to:

- `mfp_update_food_entry` - change `meal`, `quantity`, `unit` (serving-size label, e.g. `"350 ml"`), or `weight_id` (raw MFP serving-size option id, overrides `unit`) for an entry; requires `date` for historical entries. MyFitnessPal can rewrite an entry during edit, so the response reports `current_entry_id` and `entry_id_changed` so you can keep tracking the right row.
- `mfp_delete_food_entry` - delete an entry by `entry_id` (requires `date` for historical entries).

### Creating a custom food

`mfp_create_food` submits a brand-new food to the MyFitnessPal database when `mfp_search_food` turns up nothing suitable. Required fields are `description` and the per-serving core macros (`calories`, `fat`, `carbs`, `protein`); `brand`, `serving_size`, `servings_per_container`, `share_public`, and the optional micronutrients (fiber, sugar, sodium, cholesterol, vitamins, etc.) round it out. All nutrition values are entered **per single serving** as defined by `serving_size` (e.g. `serving_size="125 g"` with the numbers for a 125 g portion). When the serving unit is a mass unit (`g`, `mg`, `kg`, `oz`, `lb`) the gram weight is recorded so MFP's gram-based scaling stays correct.

On success it returns the new food's `mfp_id`; pass that to `mfp_add_food_to_diary` to log it (it may take a short moment to also surface in `mfp_search_food`). Re-running with the same details creates duplicate foods.

**`share_public=True` is irreversible** — it submits the food to MyFitnessPal's shared public database, and public foods can no longer be edited or deleted. Leave it `False` (default) to create a private food you can still delete.

> Implementation note: the [`python-myfitnesspal`](https://github.com/coddingtonbear/python-myfitnesspal) library's `set_new_food()` no longer works — MyFitnessPal replaced the server-rendered `/food/new` Rails form with a client-side SPA that has no `authenticity_token` input, so the library's HTML scrape raises `IndexError: list index out of range`. `mfp_create_food` instead POSTs to the `v2/foods` API with the account's bearer token (the same mechanism the library still uses for goals).

## Authentication: the cookie strategy

MyFitnessPal's login page is **captcha-protected**, so headless password login is dead - this server never asks for your MFP password. Instead it reads session cookies from one of:

1. **Firefox profile sidecar (recommended):** log into myfitnesspal.com once, interactively, in a Firefox profile; mount that profile directory read-only into the container at `/profile`. The server copies `cookies.sqlite` (and its WAL) to a temp file on each refresh - Firefox's locks don't matter - and extracts the `myfitnesspal.com` cookies. The copy is cached and only re-read when the file changes, so Firefox can keep running (e.g. a headless Firefox sidecar container you occasionally VNC into to re-login).
2. **JSON cookies file:** `MFP_COOKIES_FILE` pointing at `{"cookies": {name: value}}` (AdamWalt's `~/.mfp_mcp/cookies.json` format) or a plain `{name: value}` dict.

Session cookies expire eventually (~30 days); when tools start failing with auth errors, log into MFP again in that Firefox profile.

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `MFP_FIREFOX_PROFILE_DIR` | `/profile` (Docker) | Firefox profile dir (or parent dir of profiles) containing `cookies.sqlite` with a logged-in MFP session |
| `MFP_COOKIES_FILE` | - | JSON cookies file; used if set and no `cookies.sqlite` is found |
| `MCP_TRANSPORT` | `stdio` (`streamable-http` in Docker) | Transport: `stdio` or `streamable-http` |
| `MCP_HOST` | `127.0.0.1` (`0.0.0.0` in Docker) | Bind address for HTTP mode |
| `MCP_PORT` | `8000` | Port for HTTP mode |
| `MCP_ALLOWED_HOSTS` | - | Comma-separated allowed `Host` headers (reverse proxy domains). Enables DNS-rebinding protection; if unset, protection is disabled in HTTP mode |
| `MCP_OAUTH_PASSCODE` | - | Shared passcode for the OAuth login page (remote connectors). Omit for unauthenticated LAN-only use |
| `MCP_RESOURCE_URL` | - | Exact public URL clients use (no path), e.g. `https://mfp.example.com`. Required together with the passcode |
| `MCP_ACCESS_TOKEN_TTL` | `2592000` (30 days) | Access-token lifetime in **seconds**. When the token expires the connector re-authorizes, which means re-entering the passcode; a short value (the old 24h default) forces a daily re-login if the client doesn't silently refresh. Lower it (e.g. `86400`) for tighter tokens |

## Docker

```bash
docker build -t myfitnesspal-mcp-service .

docker run -d -p 8000:8000 \
  -v ~/.mozilla/firefox/abcd1234.default-release:/profile:ro \
  -e MCP_ALLOWED_HOSTS=mfp.example.com \
  -e MCP_RESOURCE_URL=https://mfp.example.com \
  -e MCP_OAUTH_PASSCODE="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')" \
  ghcr.io/delize/myfitnesspal-mcp-service:latest
```

CI builds and pushes `ghcr.io/delize/myfitnesspal-mcp-service` (amd64 + arm64) on pushes to `main` and `v*` tags.

## Claude Connector Setup

Same flow as garmin-mcp-service:

1. Deploy behind HTTPS (reverse proxy) with `MCP_TRANSPORT=streamable-http`, `MCP_RESOURCE_URL`, and `MCP_OAUTH_PASSCODE` set.
2. In Claude, add a custom connector with URL `https://mfp.example.com/mcp`. Leave OAuth Client ID/Secret blank - the server supports dynamic client registration (`/register`), authorization + PKCE (`/authorize`, `/token`).
3. Claude redirects you to the `/login` passcode page once; enter `MCP_OAUTH_PASSCODE`. After that the client holds and refreshes its own token.

The passcode proves "the caller knows the passcode", not identity - keep network-level access control (IP allowlist, VPN) in front of any internet-facing deployment. Omitting `MCP_OAUTH_PASSCODE`/`MCP_RESOURCE_URL` runs the HTTP server unauthenticated (a warning is logged); only do that on a trusted network.

For local stdio use (Claude Desktop):

```json
{
  "mcpServers": {
    "myfitnesspal": {
      "command": "python",
      "args": ["-m", "myfitnesspal_mcp.server"],
      "env": {
        "MFP_FIREFOX_PROFILE_DIR": "/home/you/.mozilla/firefox/abcd1234.default-release"
      }
    }
  }
}
```

## Troubleshooting

### Tools don't appear even though the connector shows "Connected"

**Problem**: The connector authorizes and shows as Connected, but its tools never
surface in a conversation - asking the model to use them, or searching for them,
turns up nothing. No error is shown.

**Cause**: This is almost always a **client-side tool-budget limit, not a problem
with this server**. Claude caps how many tools can be active in a single
conversation across *all* connected servers combined. If another connector exposes
a very large tool set, it can consume that budget and silently crowd this server's
tools out of the conversation. (Seen in practice with a connector exposing ~170
tools starving this server's handful.)

**Confirm / fix**:
1. In a **fresh conversation**, disable the other large connector(s) and check
   whether these tools now appear. If they do, it was the budget.
2. Keep high-tool-count connectors in **separate conversations**, or trim their
   active tools if the client supports per-tool toggles.
3. This server always returns its full tool list regardless - you can verify
   independently with an authenticated `tools/list` call against `/mcp`. If that
   returns the tools but the client doesn't show them, the gap is on the client
   side, not here.

## Attribution

- Tool implementations: [AdamWalt/myfitnesspal-mcp-python](https://github.com/AdamWalt/myfitnesspal-mcp-python) (MIT, this repo is a fork - full history preserved)
- MyFitnessPal client library: [coddingtonbear/python-myfitnesspal](https://github.com/coddingtonbear/python-myfitnesspal)
- OAuth/transport skeleton: garmin-mcp-service

## License

MIT - see [LICENSE](LICENSE) (preserves the original copyright).

TDQS

A3.9/5.0

Scored across 18 tools

Disambiguation4/5

Most tools have clearly distinct purposes: read/write pairs (get_water/set_water, get_goals/set_goals) are unambiguous, and diary CRUD operations are well-separated. The closest overlap is among the four food-list tools (search, recent, frequent, my_foods) and between create_food and add_food_to_diary, but descriptions clarify the source and intent of each.

Naming Consistency5/5

All 18 tools follow a uniform mfp_ prefix with snake_case verb_noun naming (get_, set_, add_, update_, delete_, search_, create_). The verb-noun pattern is predictable and consistent throughout, making tool selection straightforward for agents.

Tool Count4/5

18 tools is slightly above the ideal 3-15 range, but the domain is broad: diary entries, food database, measurements, goals, water, exercises, and reports each justify several tools. The count feels earned rather than bloated, though it leans heavy.

Completeness4/5

Core nutrition workflows are complete: diary entries have full CRUD, goals and water have read/write pairs, measurements have set/get, and the food database supports search, details, and creation. Notable gaps are the lack of exercise logging (get_exercises has no set/add counterpart) and no edit/delete tools for custom foods despite create_food implying they can be deleted.

Maintenance

ActivityInactive
ResponsivenessNo issues