@cyanheads/openfoodfacts-mcp-server
Look up food products by barcode, search by keyword, tag, allergen, or nutrition filter, compare products side-by-side, and browse the canonical tag vocabulary via MCP. STDIO or Streamable HTTP.
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 |
|---|---|
off_get_product | Fetch one product by barcode: ingredients, allergens and traces, additives, scores, nutrition, and tags |
off_search_products | Search by text, tag filters, allergen exclusions, and nutrient thresholds (per 100 g, or per 100 ml for liquids); returns summary rows with barcodes |
off_compare_products | 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 |
off_browse_taxonomy | Resolve a human term to the canonical tag ID that off_search_products filters on |
Skills
| Skill | Description |
|---|---|
openfoodfacts-guide | 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 skill://openfoodfacts-guide/SKILL.md and shipped in the plugin, npm package, and .mcpb bundle. |
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), withrequested_fieldsechoing what was fetched- Returns ingredients (raw text plus a parsed tree up to three levels deep with
percent_estimate),allergens_tags,traces_tags,additives_tags,ingredients_analysis_tags(vegan, vegetarian, palm-oil verdicts),nutriscore_grade,nova_group,ecoscore_grade(Green-Score),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_found
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,additives_tag,nutrition_grade,nova_group,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_bypage_sizedefaults to 20 and is served at up to 50 per call; a larger value (up to 10,000) is served at 50 and flagged inpage_size_capped, andpagecounts pages of 50. 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)queryornutrient_filterssearches 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. A page past either limit fails aspage_out_of_range
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 fetched - Rows carry
found,nutriscore_grade,nova_group,ecoscore_grade, energy, fat, saturated fat, sugars, salt, protein, and fiber (per 100 g, or per 100 ml for liquids), andcompleteness; missing values stay absent, never imputed not_foundlists barcodes with no contributor record andfailedlists fetches that failed, each with areason; a failed barcode gets no row and never blocks the rest
off_browse_taxonomy tool
facetis one ofcategories,labels,allergens,additives,countries,nova_groups,nutrition_grades;searchmatches a substring of the tag ID, name, or a synonym ("shellfish" →en:crustaceans);limit(default 20) returns up to 100 per call (a value up to 200 still returns 100); a cut result says how many matched; no paging- Returns
tags[]ofidandname, whereidgoes tooff_search_productsunchanged;nova_groupsandnutrition_gradescome back complete withtotal_in_facet - The five open facets resolve against the live Open Food Facts taxonomy and fall back to a small offline sample, with a
notice, when it is unavailable; omittingsearchreturns only that sample
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 in - Per-endpoint request budgets (product 15/min, search 10/min, taxonomy 10/min); transient failures retry up to 4 attempts
- Nutriments normalized from hyphenated keys (
energy-kcal_100g→energy_kcal_100g), withadditional_100g/additional_servingmaps that label each nutrient with its unit - Tag 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; 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 recorded - Graceful partial failure:
off_compare_productsreturns resolved rows alongsidenot_foundandfailed - Explicit count bounds:
total_is_lower_bound,last_page, andomittedon searches - Typed 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, 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/mcp
Or run it locally:
claude mcp add openfoodfacts-mcp-server -- npx -y @cyanheads/openfoodfacts-mcp-server
For 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 |
|---|---|---|
OFF_BASE_URL | Open Food Facts API base URL. | https://world.openfoodfacts.org |
OFF_RATE_LIMIT_PRODUCT | Product read budget (requests/min). The default is Open Food Facts' published per-IP limit; lower it on a shared outbound IP. | 15 |
OFF_RATE_LIMIT_SEARCH | Search budget (requests/min). The default is Open Food Facts' published per-IP limit. | 10 |
OFF_RATE_LIMIT_TAXONOMY | Taxonomy lookup budget (requests/min), shared by off_browse_taxonomy, tag canonicalization on text searches, and exclusion checks. | 10 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | HTTP server port. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (debug, info, warning, error). | info |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 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 URL -
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http -
Run 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:latest
The 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.
Project structure
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools and the published skills, and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). |
src/services/openfoodfacts | Open Food Facts API client — HTTP, rate limiting, retry, field normalization. |
src/services/taxonomy | Tag vocabulary — live resolution, offline sample, and tag-value canonicalization. |
src/utils | Markdown escaping for contributor-entered values in text output. |
skills/ | Published Agent Skills for the agents that call the tools. |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools via the barrel in
src/mcp-server/tools/definitions/index.ts - Wrap 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 test
License
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.