Wrapping USDA FoodData Central in MCP: What Breaks
Published September 21, 2026
This post is not a guide to the USDA FoodData Central API. We already have one of those, USDA FoodData Central API: it is free, but here is why developers switch, and it covers getting a key, the endpoints, the data types and the rate limits. Start there if that is what you need.
This is the narrower thing: what specifically goes wrong when FDC data ends up behind an agent's tool call, and how to detect each failure. Different problem, because an agent will confidently report a wrong number in prose where a developer reading raw JSON would have spotted it.
FDC is the most-wrapped dataset in the nutrition MCP ecosystem, for good reasons: free, authoritative, a simple API. Which means these traps are widely distributed.
Trap 1: two energy nutrients, and one of them is 4.184x the other
The one that does the most damage.
FDC's nutrient model is numeric identifiers rather than names, and energy appears more than once. There is an energy value in kilocalories and, in some datasets and records, an energy value in kilojoules. One kilocalorie is about 4.184 kilojoules.
So an integration that picks the energy nutrient by position, or by the first match on a name containing "Energy", can end up reading the kilojoule figure and labelling it kcal. The result is a number inflated by roughly 4.18x. Chicken breast at 690 "calories" per 100 g. Lettuce at 60.
Why this is worse inside an agent: a developer inspecting JSON sees 690 and knows immediately something is wrong. An agent puts it in a sentence, sums it with three other foods, and reports a plausible-looking day total that is out by a factor of four. Nothing in the reply says which nutrient identifier it read.
Detection. Two checks, both cheap and both worth running against any nutrition dataset, including a commercial one.
The 4/4/9 cross-check: protein and carbohydrate at roughly 4 kcal per gram, fat at roughly 9, multiplied out and compared to the stated calorie figure. A kJ-in-a-kcal-field error shows up as a gap of roughly 4x, which is unmissable.
A plausibility range: essentially no whole food exceeds about 900 kcal per 100 g, because pure fat is around 900 and nothing is more energy-dense than pure fat. Any row above that is wrong. Any vegetable above roughly 100 is wrong. This is the sort of check worth enforcing at the database level rather than hoping to catch by eye, which is why this catalog carries a kcal plausibility constraint on ingest.
Fix. Select energy by its specific nutrient identifier and unit, and assert the unit. Never by name matching, never by position.
Trap 2: label nutrients and analysed nutrients are different things
FDC's branded-food records can carry manufacturer label values, while foundation and legacy records carry laboratory-analysed nutrient data. These are different fields with different completeness and different rounding.
Label values are rounded according to labelling rules, which permit substantial rounding at low values, and they are per the manufacturer's declared serving as well as per 100 g. Analysed values are measured and carry more decimal places and more nutrients.
An integration that reads one field and falls back to the other without saying so produces a dataset where two rows that look comparable are not. Sum a day from a mix and the totals are internally inconsistent in ways you cannot see.
Detection. For any row, ask which field the number came from. If the answer is "whichever was present", you have this problem.
Trap 3: sparse nutrients look like zeroes
An FDC record may simply not contain a nutrient. Not zero, but absent.
The failure is an integration that coalesces missing to zero. Now a food that has never been analysed for sodium reports 0 mg of sodium, and an agent summing a day reports a sodium total that is confidently far too low. This matters most for exactly the people who care most: anyone tracking sodium for blood pressure, potassium on a renal diet, or iron for a deficiency.
Absent and zero are different facts and the difference is clinically relevant.
Detection. Ask for a micronutrient on an obscure food. If it returns 0 rather than "not available", the mapping is lossy. The broader coverage picture is in the micronutrient guide.
Fix. Preserve null. An agent that says "sodium is not recorded for this food" is more useful than one that says zero, because the first prompts you to find the label and the second does not.
Trap 4: the same food many times, in different states
Search FDC for chicken breast and you get a lot of rows: raw, cooked, roasted, with skin, without, plus branded products. Rice returns dry and cooked. Oats likewise.
They differ enormously per gram, because cooking changes water content. Cooked rice is roughly a third the calories per gram of dry rice, since it absorbed water. Get this wrong and you are out by a factor of three on a staple.
Why agents specifically struggle: the model picks the row whose name is closest to what you said. You said "rice". It picks something. Neither of you established whether the 200 g you weighed was dry or cooked, and the arithmetic proceeds confidently either way.
Detection. Ask for "100 g of rice" and see whether you get a question or a number. A number means the ambiguity was resolved silently, by guessing.
Fix, on the data side: expose the state in the tool's result so the model has to choose visibly. Fix, on your side: say dry or cooked, every time. This is the highest-frequency error in day-to-day food logging and no database removes it, and calorie tracking in Claude covers how it plays out in practice.
Trap 5: household measures are not weights
FDC records can carry serving descriptions in household terms (a cup, a tablespoon, a medium item, a piece) alongside a gram equivalent.
Two things go wrong with these inside an agent. First, household measures are volume for foods that are priced and analysed by mass, and the conversion depends on how the food is packed. A cup of chopped spinach and a cup of packed spinach are very different masses. Second, an agent asked for "a cup of rice" will happily use whatever gram equivalent it finds, without establishing whether your cup is a US cup, a metric cup, or the mug you actually used.
The gram equivalent in the data is a reasonable average. Your serving is not an average.
Detection. Ask for "a cup of cooked rice" and see whether the reply states the gram figure it used. If it does not, you cannot tell what was assumed.
Fix. Work in grams and treat household measures as a last resort. A kitchen scale removes this entire category of error, which is why it is the first thing recommended in calorie tracking in Claude.
If you are writing your own wrapper
Five things to build in, none of them hard, all of them commonly skipped:
Select nutrients by identifier, and assert the unit. Never by name match, never by array position. Fail loudly on an unexpected unit rather than passing the number through.
Enforce a plausibility range on ingest. Reject or quarantine anything above roughly 900 kcal per 100 g. This one check catches the entire class of unit errors.
Preserve null. Do not coalesce a missing nutrient to zero. Return "not recorded" and let the agent say so.
Put the food's state in the name the model sees. Raw, cooked, dry, with skin. If the model cannot see the distinction in the result, it cannot ask you about it.
Compact the response. Macros plus an identifier on search; the full panel behind a second call. Otherwise a single search eats the context the conversation needs, as discussed in giving an AI agent a 4M-food nutrition database.
The general shape of writing tools a model uses correctly (names, descriptions, argument schemas) is covered in designing MCP tools an LLM actually calls correctly, and the server mechanics in how to build a remote MCP server in Python.
The general lesson
These traps share a structure: FDC is accurate data with a model that requires care, and an agent removes the step where a human would have noticed the number was absurd.
A developer reading JSON has a sanity-check loop built in. An agent writing prose does not. So the checks have to move into the data layer: assert units, preserve nulls, enforce plausibility ranges, make ambiguity visible in the result. They will not happen downstream.
That is the argument for a maintained catalog over a thin wrapper, and it is an argument about engineering rather than about data ownership. Provenance tracking, plausibility constraints and a review queue exist in this catalog for precisely these failure modes, and none of them is exotic, and anyone wrapping FDC could implement all four. Many have not.
We are not claiming immunity here. A large catalog assembled from multiple sources is subject to the same class of errors, which is why the checks exist and why the verified foods filter and the plausibility constraint are part of the ingest path rather than an afterthought. Run the 4/4/9 check on our rows too.
If you are choosing between FDC and a catalog
Not a close call in either direction; it depends entirely on what you eat.
FDC is the better choice if your food is mostly whole and generic, meaning meat, vegetables, grains and dairy staples, you are in the US, and you want free and authoritative with real provenance. Several free MCP wrappers give you exactly that.
A maintained catalog is the better choice if you eat branded packaged food, you are outside the US, you want barcode lookup with an international fallback, or you want the four traps above already handled. That is what this plan is, at $29 a month or $228 a year with a 7-day trial, with the tool reference in the docs and the overview on the MCP page.
claude mcp add --transport http calorie-api \
https://calorieapiadmin.com/mcp \
--header "X-API-Key: YOUR_KEY"
Claude Code and Cursor, with the header. Browser sign-in for Claude apps is off until OAuth is enabled.
What we have not measured
We have not audited a sample of FDC records and counted how often each trap appears, and we have not surveyed the public FDC MCP wrappers to see which ones handle which trap. Those would be genuinely useful measurements and we have not run them, so there are no frequencies in this post.
The traps themselves are structural properties of the dataset and its API, checkable in the USDA's own documentation. The 4.184 conversion factor is arithmetic. The plausibility bounds follow from the energy density of fat. The detection methods are things you can run in a minute against any server, ours included.
Related
Frequently Asked Questions
What is the kJ and kcal trap in FoodData Central?
Energy appears more than once in the nutrient model, and one kilocalorie is about 4.184 kilojoules. An integration that picks energy by position or by a name containing Energy can read the kilojoule figure and label it kcal, inflating everything by roughly 4.18x. Select by nutrient identifier and assert the unit.
Why is this worse inside an agent than in normal code?
A developer reading raw JSON sees 690 calories for chicken breast and knows something is wrong. An agent puts the number in a sentence, sums it with three other foods, and reports a plausible day total that is out by a factor of four, with nothing in the reply naming the field it read.
How do I detect a bad energy value?
Two checks. The 4/4/9 cross-check: protein and carbs at roughly 4 kcal per gram, fat at roughly 9, compared to the stated calories, where a unit error shows up as a roughly fourfold gap. And a plausibility bound: almost nothing exceeds about 900 kcal per 100 g, because pure fat is around 900.
What is wrong with treating a missing nutrient as zero?
Absent and zero are different facts. A food never analysed for sodium is not a food with no sodium, and coalescing to zero produces day totals that are confidently far too low. This matters most for the people tracking sodium, potassium or iron for a medical reason.
Should I use a free USDA wrapper or a commercial catalog?
USDA is the better choice for mostly whole and generic foods in the US, free and authoritative with real provenance. A maintained catalog is better for branded packaged food, outside the US, when you want barcode lookup with an international fallback, or when you want these traps already handled.
