@cyanheads/openfoodfacts-mcp-server
This server has a remotely hosted endpoint. Open its connector listing for connection details, authentication requirements, and setup instructions:
Public Hosted Server: https://openfoodfacts.caseyjhand.com/mcp
Overview
Packaged-food data from Open Food Facts, a crowd-sourced database of 3M+ products. Look up items by barcode, search by text, tags, allergens, and nutrient thresholds, compare products side by side, and resolve everyday terms to the canonical tags the filters take.
Tools
Tool | Description |
| Fetch one product by barcode: ingredients, allergens and traces, additives, scores, nutrition, and tags |
| Search by text, tag filters, allergen exclusions, and nutrient thresholds (per 100 g, or per 100 ml for liquids); returns summary rows with barcodes |
| Compare up to 10 products per call by barcode on nutrition (per 100 g, or per 100 ml for liquids), Nutri-Score, NOVA, and Green-Score |
| Resolve a human term to the canonical tag ID that |
Skills
Skill | Description |
| Judgment for calling the tools: allergy checks that never overclaim, choosing among a product's barcodes, where tag IDs come from, grade ranges, free-from searches, and the shared search budget. Served over MCP as |
Related MCP server: openfoodfacts-mcp
Capability reference
off_get_product tool
barcodeis digits only, 4–40 digits after any leading zeros; optionalfieldstrims the response and pulls in what a field depends on (nutrimentsbrings the serving size, quantity, and unit, andserving_quantityandserving_quantity_uniteach bring the other plus the serving size), withrequested_fieldsechoing what was fetchedReturns
product_nameandgeneric_name(what the product is, in its main language), ingredients (raw text plus a parsed tree up to three levels deep withpercent_estimate),allergens_tags,traces_tags,additives_tags,ingredients_analysis_tags(vegan, vegetarian, palm-oil verdicts),nutriscore_grade,nova_group,ecoscore_grade(Green-Score),nutrient_levels(Open Food Facts' low, moderate, or high for fat, saturated fat, sugars, and salt, with abasisofas_soldorpreparednaming the figures it judged;fields: ["nutrient_levels"]bringsnutrimentsfor that reason),nutriments(per serving, and per 100 g, or per 100 ml for liquids), category/label/packaging/origin/country tags,image_url, andcompleteness(0–1)traces_tags: ["en:none"]means the label states no traces, while an empty array means none entered yet; a barcode no contributor has recorded fails asnot_foundOpen Food Facts returns one preparation state of the nutrition per read. A read of prepared-product figures puts them under keys ending in
_preparedinadditional_100gandadditional_serving, and prints them in "as prepared" sections. When a read returns only prepared figures, one more product read fetches the single as-sold set the record stores (/api/v3.5fields=nutrition.input_sets) and fills the named as-sold fields per 100 g, or per 100 ml for liquids, with no as-sold per-serving figure;nutriments.stored_as_soldreportsread,none(the record stores no usable set), orunreadwith areason, anerror, and, when the failure named a wait,retryAfterin whole seconds (the call itself still succeeds). Other reads send nothing extrasource(off_apioroff_mirror) names where the product was read; a barcode a local mirror holds is read from it with no upstream request, withmirror_as_of(the time up to which the copy reflects every product contributed or edited on Open Food Facts) and amirror_snapshotnote, and the stored as-sold set comes from the same stored record
off_search_products tool
query(up to 24 words, all of which must match the name, generic name, categories, labels, or brand; punctuation separates words, somilk-chocolaterequires both) plus the tag filterscategories_tag,brands_tag,labels_tag(up to 10),allergens_tag,traces_tag,ingredients_analysis_tag,ingredients_tag(up to 10, all of which must appear),additives_tag,nutrition_grade,nova_group,ecoscore_grade(Green-Score),countries_tag, theexclude_allergens/exclude_traceslists (up to 14 each), and up to 18nutrient_filters(per 100 g, or per 100 ml for liquids), all combined as AND; optionalsort_bynutrition_grade,nova_group, andecoscore_gradeeach take one grade or an array matching any of them (["a", "b"]for Nutri-Score B or better), where alabels_tagarray requires all of its values;ecoscore_gradeaexcludesa-plus. A grade filter never changes which backend answers; on a text search it matches the grades the index holds, which each row shows. A tag-onlyecoscore_gradesearch matches the grade Open Food Facts files for France, while each row shows the world gradepage_sizedefaults to 20 and is served at up to 100 per call; a larger value, with no ceiling, is served at 100 and flagged inpage_size_capped, andpagecounts pages of 100. Rows carrybarcode,product_name,brands,nutriscore_grade,nova_group,ecoscore_grade, andcategories_tags, plustotal,total_is_lower_bound,last_page,omitted, andtruncated(more matches beyond this page). Exclusions addexclusion_coverage, since products with no allergen data entered pass them;nutrient_filtersaddindexed_nutriments(the index values the filters checked, which can differ fromoff_get_product)The text index compares
energy-kcal,fat,saturated-fat,carbohydrates, andsugarsas whole numbers (sugars lte 3returns a row at 3.53). Anutrient_filterssearch sendsgtandgteon those five as[floor(value) TO *]so no match is lost, then checks every returned row against every constraint at the index's 32-bit precision. Rows that fail leave the page and count inoutside_constraints, so a page can come back short or empty while later pages hold matches.total,total_is_lower_bound, andlast_pagedescribe the index's whole-number answer and still count those rows. A range inside one whole number (sugars gt 3.9andlt 4) can pass few of the 10,000 reachable rows; addqueryor tag filters to narrow itingredients_tagtakes canonical ingredient IDs (en:inulin; resolve one withoff_browse_taxonomyfacetingredients, where IDs are singular), and each parsed ingredient also carries its taxonomy parents, soen:vegetablematches a product listing carrot. It is answered only by the text index, with or withoutquery, since the mirror indexes no ingredients and the live tag path stops at page 10query,nutrient_filters, oringredients_tagsearches a text index that lags the live database (flagged intext_index_snapshot), reaches only the first 10,000 results, and rejectsadditives_tagasadditives_filter_needs_tag_search; tag-only searches read the live database through page 10, unless a local mirror serves them: with every tag value confirmed, a mirror-served search has an exacttotaland reaches its first 10,000 results, and one carryingecoscore_gradeis served only once a full reload of the mirror has indexed Green-Score. A page past any of these limits fails aspage_out_of_rangesource(off_apioroff_mirror) names where the answer was read; from the mirror it also carriesmirror_as_ofand amirror_snapshotnote, and the default order andsort_bypopularity_keyandunique_scans_nfollow the values stored with each record, which can differ from a live search, which ranks by its current scan counts
off_compare_products tool
2–100
barcodes, one row each in input order; repeated barcodes are compared once, and those past the 10th distinct one are listed inunprocessedwithout being fetchedRows carry
found,nutriscore_grade,nova_group,ecoscore_grade, energy, fat, saturated fat, carbohydrates, sugars, salt, protein, and fiber (per 100 g, or per 100 ml for liquids), andcompleteness; missing values stay absent, never imputedA read of prepared-product figures adds a
preparedobject on the row with the same columns, read from the*_prepared_100gkeys and never substituted for an as-sold figure. A row whose read returned only prepared figures gets the same stored as-sold read asoff_get_product, once every barcode's product read has settled, and reports it asstored_as_sold(read,none, orunread, which never moves the barcode intofailed). A row with as-sold figures appears in the as-sold table, a row withpreparedin the "as prepared" table, and a row with both appears in bothEach row carries
source(off_apioroff_mirror), andmirror_as_offor a row read from a local mirror, so one comparison can mix the two; amirror_snapshotnote appears when any row came from itnot_foundlists barcodes with no contributor record andfailedlists fetches that failed, each with areasonand, when the failure named a wait,retryAfterin whole seconds; a failed barcode gets no row and never blocks the rest
off_browse_taxonomy tool
facetis one ofcategories,labels,allergens,additives,countries,ingredients,nova_groups,nutrition_grades;searchmatches a substring of the tag ID, name, or a synonym ("shellfish" →en:crustaceans), and the tag the term itself names (US→en:united-states) is listed first;limit(default 20) returns up to 200 per call, with no input maximum (a larger value returns the first 200); a cut result says how many matched; no paging. Each lookup asks for up to 2,000 suggestions, above the largest answer measured (1,313), so an E-number term such ase314resolves toen:e314and the match count is exact below that sizeReturns
tags[]ofidandname, whereidgoes tooff_search_productsunchanged;nova_groupsandnutrition_gradescome back complete withtotal_in_facetThe six open facets resolve against the live Open Food Facts taxonomy. Five fall back to a small offline sample, with a
notice, when it is unavailable, and omittingsearchreturns only that sample.ingredientshas no sample: it lists nothing without asearchor when the lookup fails, and its IDs are singular (oatresolves toen:oat,oatsto nothing)
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
Open Food Facts-specific:
No API key; the identifying
User-AgentOpen Food Facts' terms require is built inPer-endpoint request budgets (product 15/min, search 10/min, taxonomy 10/min); transient failures retry up to 4 attempts within a total deadline of 35 s for a product or search read and 20 s for a taxonomy lookup (one caller's search that canonicalizes a tag first can spend 55 s), after which the read fails as
upstream_timeout, orrate_limitedwhen its last finished attempt answered 429The stored as-sold read for a prepared-only product spends the product budget, so a barcode can take two product reads, and has its own 20 s deadline: a call settles within 35 s + 20 s = 55 s, and an unread set never fails the call or the row
Optional local export mirror (
OFF_MIRROR_PATH): barcode reads and confirmed tag-only searches served from a SQLite copy of the export, kept current by daily deltas; a result names itssourceand, from the mirror, the time it reflectsNutriments normalized from hyphenated keys (
energy-kcal_100g→energy_kcal_100g), withadditional_100g/additional_servingmaps that label each nutrient with its unitTag filters take canonical IDs (
en:organic,en:no-gluten); on text searches a case variant, synonym, or brand name is canonicalized first (US→en:united-states)Every field is contributor-entered, so a missing one means not yet recorded, except that a product read returning only prepared-product nutrition is followed by a read of the as-sold figures the record stores, reported as
stored_as_sold; Nutri-Score, NOVA, and Green-Score are Open Food Facts' own computed grades, returned as-is, and carry regional formula caveats
Agent-friendly output:
Per-serving nutrition carries its denominator:
serving_size,serving_quantity,serving_quantity_unit, and a note when none is recordedGraceful partial failure:
off_compare_productsreturns resolved rows alongsidenot_foundandfailedExplicit count bounds:
total_is_lower_bound,last_page,omitted, andoutside_constraintson searchesTyped failures: each carries a declared
reason(upstream_error,upstream_timeout,upstream_rejected,rate_limited, plus per-tool input reasons) and a recovery hint; an incomplete upstream response is a retryableupstream_error, orupstream_timeoutwhen search flags a timeout, never an empty result or a not-found
Getting started
Connect to the hosted instance, no install:
claude mcp add --transport http openfoodfacts-mcp-server https://openfoodfacts.caseyjhand.com/mcpOr run it locally:
claude mcp add openfoodfacts-mcp-server -- npx -y @cyanheads/openfoodfacts-mcp-serverFor other clients, add this to the MCP config file:
{
"mcpServers": {
"openfoodfacts-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/openfoodfacts-mcp-server"]
}
}
}No API key or account is needed: the server identifies itself to Open Food Facts with its own User-Agent. For Docker or a source build, see Running the server.
Configuration
Variable | Description | Default |
| Open Food Facts API base URL. |
|
| Product read budget (requests/min). The default is Open Food Facts' published per-IP limit; lower it on a shared outbound IP. |
|
| Search budget (requests/min). The default is Open Food Facts' published per-IP limit. |
|
| Taxonomy lookup budget (requests/min), shared by |
|
| Path of the local export mirror's SQLite file. Unset disables the mirror. Works from a source checkout or the Docker image only. | unset |
| Transport: |
|
| HTTP server port. |
|
| Auth mode: |
|
| Log level ( |
|
| Log each failed tool call's arguments and result, redacted by key name and capped at |
|
| Enable OpenTelemetry instrumentation. |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.4 or higher (or Node.js v24+).
Clone and install:
git clone https://github.com/cyanheads/openfoodfacts-mcp-server.git cd openfoodfacts-mcp-server && bun install cp .env.example .env # optional: override rate limits or the base URLBuild and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker run --rm -p 3010:3010 ghcr.io/cyanheads/openfoodfacts-mcp-server:latestThe image defaults to HTTP transport, stateless session mode, and logs to /var/log/openfoodfacts-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/openfoodfacts-mcp-server:latest"]. To build locally, docker build -t openfoodfacts-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Local export mirror
Setting OFF_MIRROR_PATH keeps a local SQLite copy of the Open Food Facts export. off_get_product and off_compare_products read it first by barcode and read live for a barcode it lacks. A search by tag filters alone is served from it when every value is a confirmed tag or brand slug, with an exact total through its first 10,000 results, a search carrying ecoscore_grade only once a full reload has indexed Green-Score; every other search is live. A full product read of a row not yet rewritten by that reload is live too, since the row lacks generic_name and nutrient_levels; should that read fail or find no product, the row answers without them and mirror_fields_unread says why. A store error falls through to a live read. The mirror scripts need a checkout or the Docker image, so the npm install and .mcpb bundle stay live-only.
In the image, mount a volume at the store directory, /usr/src/app/.mirror, set OFF_MIRROR_PATH to a file in it, and build the store once with docker exec:
docker run -d --name openfoodfacts-mcp-server -p 3010:3010 \
-v off-mirror:/usr/src/app/.mirror \
-e OFF_MIRROR_PATH=/usr/src/app/.mirror/openfoodfacts.sqlite \
ghcr.io/cyanheads/openfoodfacts-mcp-server:latest
docker exec openfoodfacts-mcp-server bun run mirror:init # full load of the export, then the deltas since; re-run after an interrupt to resume
docker exec openfoodfacts-mcp-server bun run mirror:status # health, product count, and the as-of time answers cite
docker exec openfoodfacts-mcp-server bun run mirror:verify # status plus SQLite's integrity check
docker exec openfoodfacts-mcp-server bun run mirror:refresh # one delta pass by handA full mirror:init of the 4.9M-product export took about 1 h 48 min in testing, peaked at about 27 GB on the volume while the 13.4 GB download sat beside the store, left a 14.3 GB store, and used under 350 MB of memory. Reads stay live until mirror:init completes. Once OFF_MIRROR_PATH is set the server refreshes the mirror itself every three hours, at minute 30 of the server's local time: it applies each new daily delta, and reloads the whole export when the last full load is 30 days old and the export has changed since, because deltas never report deleted products, and whenever the last full load was built by an older record version, which a release changing what a record holds bumps. An export or delta line that is not valid JSON, or runs past 16 MiB, is skipped and logged by line number, and mirror:status prints how many the last full load skipped. An export that builds no product at all is refused before it deletes anything (export_no_products): the mirror keeps serving what it holds, and each refresh downloads the export again, applying no delta, until an export that lists products replaces it. While a reload runs, mirror_as_of can drop below the last delta's end, to a time the rewritten products reflect, until the load completes and the deltas re-apply. mirror:status and mirror:verify exit non-zero unless a sync has completed and the store holds products. From a checkout, run the same bun run mirror:* scripts with OFF_MIRROR_PATH set in .env.
Project structure
Directory | Purpose |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Open Food Facts API client — HTTP, rate limiting, retry, field normalization. |
| Local export mirror — SQLite store, record builder, export and delta ingester, tag-search queries, and the refresh schedule. |
|
|
| Tag vocabulary — live resolution, offline sample, and tag-value canonicalization. |
| Markdown escaping for contributor-entered values in text output. |
| Published Agent Skills for the agents that call the tools. |
| Unit and integration tests mirroring |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storageRegister new tools via the barrel in
src/mcp-server/tools/definitions/index.tsWrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
Contributing
Issues are welcome — see CONTRIBUTING.md for the forms and what makes a report actionable. Report vulnerabilities through private disclosure, never a public issue. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details. Open Food Facts data is released under the Open Database License (ODbL) 1.0; downstream use must cite Open Food Facts.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Look up a packaged food by barcode, search and compare products, and check listed allergens.
USDA FoodData Central nutrient lookup, FDA recall watch, EU FMCG labelling via remote MCP
Unlock the power of food transparency with our Open Food Facts MCP server. Easily look up any food
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to access the Open Food Facts database to query detailed food product information, nutritional data, and environmental scores. Supports product lookup by barcode, smart search with filtering, nutritional analysis, product comparison, and dietary recommendations to help users make informed food choices.51MIT
- AlicenseAqualityBmaintenanceAn MCP server for the Open Food Facts API that allows users to search, read, and contribute to a global food database. It enables looking up nutrition data by barcode or name and managing product information through natural language.10341 npm4MIT
- AlicenseAqualityDmaintenanceMCP server for Open Food Facts, enabling food product lookup by barcode, search, nutrition facts, allergen checks, and eco-scores without an API key.812 PyPIMIT
- AlicenseAqualityCmaintenanceProduct evaluation MCP server for US packaged food. Health scores, ingredient safety, regulatory flags, recall history, corporate ownership.21MIT