Skip to main content
Glama
README.md
# ๐Ÿฝ๏ธ foodos-mcp

### Big batch. Different appetites. Macros that add up. โœจ

Turn recipes into **traceable nutrition numbers, cooked-weight calculations, and portions that fit each eater** โ€” all through the Model Context Protocol (MCP).

Your AI assistant handles the conversation. **foodos-mcp handles the math.** ๐Ÿงฎ

LLMs can help you find dinner, but they can also guess gram weights, invent macros, or split a batch in half when everyone needs a different portion. This server makes those calculations deterministic and auditable, from the first ingredient to the last lunchbox.

## โœจ Meet your meal-prep math engine

- ๐Ÿ”Ž **Know where every number came from.** Macro values trace to a USDA food ID, an Open Food Facts barcode, or data you supplied.
- โš–๏ธ **Weigh it cooked. Count it correctly.** Turn raw ingredient totals into macros per 100 g of the finished batch.
- ๐Ÿ› **One batch, different portions.** Size a serving by a target, a fixed weight, or whatever is left.
- ๐Ÿ“ฆ **Give leftovers a plan.** Keep track of portions for tonight and boxes for later.
- ๐Ÿ›‘ **Keep guesses off the plate.** Ambiguous food lookups return candidates for you to choose from.

```text
๐Ÿฅ• Raw ingredients โ†’ ๐Ÿงฎ Batch macros โ†’ ๐Ÿณ Cooked weight
                                            โ†“
                                  Macros per 100 g cooked
                                            โ†“
                              ๐Ÿฝ๏ธ Portions sized for each eater
                                            โ†“
                                  ๐Ÿ“ฆ Leftovers & meal prep
```

One eater needs enough protein to finish their daily target. Another takes 180 g off the scale. A third takes what is left. Tuesday's lunch gets a box, too. **Equal portions are optional. Getting the math right is the whole point.** ๐Ÿ™Œ

[๐Ÿš€ Get started](#-quick-start) ยท [๐Ÿ— See the math](#-a-batch-in-action) ยท [๐Ÿงฐ Explore the tools](#-the-toolbox) ยท [๐Ÿ› ๏ธ Contribute](#-build-with-us)

## ๐Ÿš€ Quick start

```bash
npx foodos-mcp            # stdio, for a local client
npx foodos-mcp --transport http --port 3000
```

Node 22.12 or newer. You need a free USDA API key, which takes about a minute to get at [api.data.gov/signup](https://api.data.gov/signup/).

### ๐Ÿ–ฅ๏ธ Connect to Claude Desktop

Add this to `claude_desktop_config.json`, then restart Claude Desktop.

```json
{
  "mcpServers": {
    "foodos": {
      "command": "npx",
      "args": ["-y", "foodos-mcp"],
      "env": {
        "FDC_API_KEY": "your-key-here",
        "FOODOS_CONTACT": "you@example.com"
      }
    }
  }
}
```

`FOODOS_CONTACT` goes in the User-Agent when the server reads Open Food Facts, which their terms ask for. Everything works without it; you are just anonymous.

## ๐Ÿ— A batch in action

Letโ€™s roast 800 g of chicken and follow the numbers all the way to the plate. Every step is checkable. ๐Ÿ”

**1. ๐Ÿ”Ž Find the food.** `searchFood` with "chicken breast raw" returns candidates from USDA. Pick one; the model should show you the list rather than choose. Here it is fdcId **2646170**, "Chicken, breast, boneless, skinless, raw".

**2. ๐Ÿงฌ Get its macros.** `getFoodMacros` returns, per 100 g:

| | per 100 g |
|---|---|
| Protein | 22.525 g |
| Fat | 1.934 g |
| Carbohydrate | 0 g |
| Energy | 106.034 kcal |

It also reports `energySource: "atwater_general_2047"`. Foundation foods carry no plain energy nutrient at all, only the two Atwater values, and the server tells you which one it used rather than leaving you to wonder.

**3. ๐Ÿงฎ Total the batch.** `computeBatchMacros` with 800 g of it: 180.2 g protein, 15.5 g fat, 848 kcal.

**4. ๐Ÿณ Weigh it after cooking.** You roast it and the tray comes out at 600 g. `setCookedYield` with `rawG: 800, cookedG: 600` gives a yield factor of 0.75 and, per 100 g cooked: 30 g protein, 2.6 g fat, 141 kcal.

**5. ๐Ÿฝ๏ธ Serve it up.** `portionBatch` with two rules, one fixed weight and one remainder:

| Eater | Rule | Weight | Protein | Energy |
|---|---|---|---|---|
| A | fixed 180 g | 180 g | 54.1 g | 254 kcal |
| B | remainder | 420 g | 126.1 g | 594 kcal |

54.06 plus 126.14 is 180.2, which is the batch. The arithmetic runs at full precision from end to end and only the presentation is rounded, so the reconciliation is exact even where two rounded figures are each a tenth off.

๐ŸŽฏ **Want a portion that fills a target?** Change A's rule to `solveForRemaining` and give A a daily protein target of 150 g with 100 g already eaten. A gets the 166 g that carries the remaining 50 g of protein, and the response says `bindingConstraint: "proteinG"` along with where fat and energy landed. If that portion blows A's fat target, the response says so in the residuals rather than leaving you to notice.

๐Ÿƒ Try it yourself: [`examples/worked-example.ts`](examples/worked-example.ts) runs this against the live API with your own key.

## ๐Ÿงฐ The toolbox

**๐Ÿ“– Read a recipe.** `parseRecipe` pulls the schema.org data a page publishes for machines. `parseIngredientLine` splits one line into its parts and flags what is ambiguous, so "1 medium onion" comes back as a size descriptor rather than as 150 g.

**๐Ÿ”Ž Look up nutrition.** `searchFood`, `getFoodMacros`, `lookupBarcode`, `toGrams`.

**๐Ÿงฎ Calculate the batch.** `computeBatchMacros`, `setCookedYield`.

**๐Ÿฝ๏ธ Portion your plates.** `portionBatch`, the reason this repository exists.

**๐Ÿ›’ Plan the next batch.** `planBatchSize`, `scaleRecipe`, `shoppingList`.

**๐Ÿ“š Peek under the hood.** `foodos://yield-factors` and `foodos://densities` expose the bundled tables so you can see exactly what a calculation was based on. `foodos://rate-limit-status` shows how much of the hourly budget is left.

**๐Ÿ’ฌ Follow the guided workflow.** `portionABatch` walks a client through the whole chain, from a URL to portions on plates.

## ๐ŸŒ Ingredients deserve sources. So do numbers.

Hereโ€™s where the data comes from, along with the licensing and caching considerations behind the integrations.

| Source | Verdict | Why |
|---|---|---|
| **USDA FoodData Central** | Primary nutrition source | US public domain under CC0 1.0. Free key from api.data.gov. No caching restriction, so this server caches aggressively. |
| **Open Food Facts** | Secondary, for packaged goods and barcodes | Open data under the ODbL, contents under the DbCL. Their terms ask for a descriptive User-Agent with a contact address, which this server sends. |
| **Spoonacular** | Not integrated | Their terms cap caching at about an hour and then require deletion. That is incompatible with a local store, and a local store is what makes the rate limit survivable. |
| **Edamam** | Not integrated | Their terms prohibit automated programmatic requests intended to collect or save data. An MCP server is exactly that. |

Cooking yields come from the [USDA Table of Cooking Yields for Meat and Poultry, Release 2](https://agdatacommons.nal.usda.gov/articles/dataset/USDA_Table_of_Cooking_Yields_for_Meat_and_Poultry/24660864) (CC0), plus rows transcribed by hand from [Agriculture Handbook 102](https://www.ars.usda.gov/ARSUserFiles/80400530/pdf/ah102.pdf) (1975, public domain) for rice, pasta, lentils and oats, each citing its page. Densities are derived from the USDA SR Legacy bulk export, a 6 MB public-domain download that needs no API key, with each row citing the food and portion it came from. A cup of flour is 125 g and a cup of honey is 339 g, which is why volume never converts to weight without knowing the ingredient.

Rice roughly triples in weight when cooked and lentils nearly do. A yield factor is not always below 1, and treating it as though it were would be wrong for half a kitchen.

## ๐Ÿšฆ Keep your API budget happy

FoodData Central runs behind api.data.gov: **1,000 requests an hour per key**, on a rolling window, with a 429 when you go over. A single recipe with fifteen ingredients can burn thirty calls between searching and looking up, so this is a real ceiling rather than a theoretical one.

Hereโ€™s how the server keeps the budget visible:

- A local counter refuses the call before it leaves the process when the budget is spent, and tells you when it frees up.
- Response headers are authoritative. If USDA says 40 requests remain, the local count is corrected to match, because a key shared with another client is further along than this process can know.
- The default budget is **800**, deliberately under the real 1,000, so a second client does not push you into a 429.
- Nothing queues, sleeps or retries. Only you know whether the rest of the work is worth what is left.
- Cache hits never count. Lookups are cached for a month, which is allowed without reservation because the data are public domain.

Check `foodos://rate-limit-status` before a big batch. Set `FDC_HOURLY_BUDGET` to change the ceiling.

The real answer, when this becomes a problem, is that USDA publishes complete monthly exports of about 6 GiB. A local copy takes the API out of the hot path entirely. That is the v0.2 plan; there is a `TODO(bulk-import)` in the client pointing at it.

`DEMO_KEY` works for a first smoke test and is capped at ten requests an hour, which you will hit almost immediately. It is never used as a fallback in code.

## โš™๏ธ Make it yours

| Variable | Default | What it does |
|---|---|---|
| `FDC_API_KEY` | none | Required for anything that reads USDA data. No default key ships in this repository. |
| `FDC_HOURLY_BUDGET` | `800` | Requests an hour before the server refuses to make more. |
| `FOODOS_CONTACT` | repository URL | Contact address sent in the User-Agent, as Open Food Facts asks. |
| `FOODOS_CACHE_DIR` | platform cache directory | Where cached lookups live. |
| `FOODOS_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` or `silent`. Always goes to stderr, because stdout carries the protocol. |

## โ˜๏ธ Run it remotely

```bash
foodos-mcp --transport http --host 127.0.0.1 --port 3000 --allowed-hosts foodos.example.com
```

The endpoint is `POST /mcp`, and `GET /healthz` answers for a load balancer. It runs stateless, so there is no session affinity to arrange.

There is no authentication in this server, on purpose. Terminate TLS and auth at the reverse proxy and keep the server bound to loopback:

```nginx
location /mcp {
    proxy_pass http://127.0.0.1:3000/mcp;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Connection "";
    proxy_buffering off;          # streamable HTTP sends server-sent events
    proxy_read_timeout 300s;
}
```

Passing `--allowed-hosts` turns on DNS rebinding protection for the hosts you name.

## ๐Ÿงญ What belongs on the menu

foodos-mcp focuses on recipe and portion calculations. It provides numbers, with no health advice, diet recommendations, or judgments about your plate.

There are no accounts or built-in authentication. The server reads two public food data sources, caches lookups locally, and does the arithmetic.

## ๐Ÿ› ๏ธ Build with us

```bash
pnpm install
pnpm check        # biome, tsc and vitest
pnpm test:coverage
pnpm build
```

The test suite runs entirely offline. Every HTTP request is intercepted, and an unhandled one fails the run, so a forgotten network call cannot reach USDA from CI.

Ready to dig in? Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. ๐Ÿง‘โ€๐Ÿณ

## ๐Ÿ“œ License

**MIT licensed.** Fork it, explore it, and build something delicious. See [LICENSE](LICENSE).

USDA FoodData Central data are in the public domain. U.S. Department of Agriculture, Agricultural Research Service. FoodData Central, [fdc.nal.usda.gov](https://fdc.nal.usda.gov). Open Food Facts data are available under the [Open Database License](https://opendatacommons.org/licenses/odbl/1-0/).

TDQS

A4.1/5.0

Scored across 12 tools

Disambiguation4/5

Most tools target distinct pipeline stagesโ€”food lookup, parsing, conversion, batch math, portioning, scaling, and shoppingโ€”so an agent can generally select by input and output. The only mild ambiguities are getFoodMacros vs lookupBarcode (both expose per-100g macros) and planBatchSize vs portionBatch (both reason about eaters), but the descriptions state the identifier/source and pre-cook/post-cook differences clearly.

Naming Consistency4/5

All tool names use camelCase and most follow a clear verb+object convention: parseRecipe, searchFood, computeBatchMacros, scaleRecipe. shoppingList is a bare noun and toGrams uses a preposition rather than a verb, so the pattern is consistent in style but not perfectly uniform.

Tool Count5/5

Twelve tools is an appropriate, well-scoped size for a food/macro meal-planning server. Each tool covers one distinct operation with no redundant clusters, and the set is large enough to span a full workflow without feeling bloated.

Completeness5/5

The server covers the complete recipe-to-shopping lifecycle: finding foods, parsing recipe and ingredient input, converting to grams, computing batch macros, accounting for cooked yield, portioning, scaling, and aggregating a shopping list. Dependent tools explicitly reference the outputs they need, and unresolved results return candidates instead of dead-ending.

Maintenance

ActivityMaintained
ResponsivenessNo issues