@cyanheads/openfoodfacts-mcp-server 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +8 -3
  2. package/CLAUDE.md +8 -3
  3. package/Dockerfile +18 -0
  4. package/README.md +34 -6
  5. package/changelog/0.4.x/0.4.2.md +33 -0
  6. package/dist/config/server-config.d.ts +1 -0
  7. package/dist/config/server-config.d.ts.map +1 -1
  8. package/dist/config/server-config.js +11 -0
  9. package/dist/config/server-config.js.map +1 -1
  10. package/dist/index.js +9 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.js +1 -1
  13. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts +62 -1
  15. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/compare-products.tool.js +211 -23
  17. package/dist/mcp-server/tools/definitions/compare-products.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts +18 -1
  19. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/get-product.tool.js +68 -30
  21. package/dist/mcp-server/tools/definitions/get-product.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/index.d.ts +69 -28
  23. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts +8 -2
  25. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/search-products.tool.js +285 -74
  27. package/dist/mcp-server/tools/definitions/search-products.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/read-source.d.ts +31 -0
  29. package/dist/mcp-server/tools/read-source.d.ts.map +1 -0
  30. package/dist/mcp-server/tools/read-source.js +45 -0
  31. package/dist/mcp-server/tools/read-source.js.map +1 -0
  32. package/dist/services/mirror/mirror-feeds.d.ts +58 -0
  33. package/dist/services/mirror/mirror-feeds.d.ts.map +1 -0
  34. package/dist/services/mirror/mirror-feeds.js +95 -0
  35. package/dist/services/mirror/mirror-feeds.js.map +1 -0
  36. package/dist/services/mirror/mirror-ingester.d.ts +112 -0
  37. package/dist/services/mirror/mirror-ingester.d.ts.map +1 -0
  38. package/dist/services/mirror/mirror-ingester.js +564 -0
  39. package/dist/services/mirror/mirror-ingester.js.map +1 -0
  40. package/dist/services/mirror/mirror-job.d.ts +9 -0
  41. package/dist/services/mirror/mirror-job.d.ts.map +1 -0
  42. package/dist/services/mirror/mirror-job.js +48 -0
  43. package/dist/services/mirror/mirror-job.js.map +1 -0
  44. package/dist/services/mirror/mirror-lock.d.ts +45 -0
  45. package/dist/services/mirror/mirror-lock.d.ts.map +1 -0
  46. package/dist/services/mirror/mirror-lock.js +128 -0
  47. package/dist/services/mirror/mirror-lock.js.map +1 -0
  48. package/dist/services/mirror/mirror-queries.d.ts +76 -0
  49. package/dist/services/mirror/mirror-queries.d.ts.map +1 -0
  50. package/dist/services/mirror/mirror-queries.js +281 -0
  51. package/dist/services/mirror/mirror-queries.js.map +1 -0
  52. package/dist/services/mirror/mirror-scheduler.d.ts +41 -0
  53. package/dist/services/mirror/mirror-scheduler.d.ts.map +1 -0
  54. package/dist/services/mirror/mirror-scheduler.js +131 -0
  55. package/dist/services/mirror/mirror-scheduler.js.map +1 -0
  56. package/dist/services/mirror/mirror-store.d.ts +150 -0
  57. package/dist/services/mirror/mirror-store.d.ts.map +1 -0
  58. package/dist/services/mirror/mirror-store.js +281 -0
  59. package/dist/services/mirror/mirror-store.js.map +1 -0
  60. package/dist/services/mirror/mirror-sync.d.ts +62 -0
  61. package/dist/services/mirror/mirror-sync.d.ts.map +1 -0
  62. package/dist/services/mirror/mirror-sync.js +226 -0
  63. package/dist/services/mirror/mirror-sync.js.map +1 -0
  64. package/dist/services/mirror/record-builder.d.ts +78 -0
  65. package/dist/services/mirror/record-builder.d.ts.map +1 -0
  66. package/dist/services/mirror/record-builder.js +957 -0
  67. package/dist/services/mirror/record-builder.js.map +1 -0
  68. package/dist/services/openfoodfacts/nutrition-sets.d.ts +132 -0
  69. package/dist/services/openfoodfacts/nutrition-sets.d.ts.map +1 -0
  70. package/dist/services/openfoodfacts/nutrition-sets.js +507 -0
  71. package/dist/services/openfoodfacts/nutrition-sets.js.map +1 -0
  72. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts +191 -13
  73. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts.map +1 -1
  74. package/dist/services/openfoodfacts/openfoodfacts-service.js +389 -37
  75. package/dist/services/openfoodfacts/openfoodfacts-service.js.map +1 -1
  76. package/dist/services/openfoodfacts/user-agent.d.ts +11 -0
  77. package/dist/services/openfoodfacts/user-agent.d.ts.map +1 -0
  78. package/dist/services/openfoodfacts/user-agent.js +17 -0
  79. package/dist/services/openfoodfacts/user-agent.js.map +1 -0
  80. package/dist/services/taxonomy/taxonomy-service.d.ts +49 -14
  81. package/dist/services/taxonomy/taxonomy-service.d.ts.map +1 -1
  82. package/dist/services/taxonomy/taxonomy-service.js +114 -27
  83. package/dist/services/taxonomy/taxonomy-service.js.map +1 -1
  84. package/package.json +7 -1
  85. package/server.json +3 -3
  86. package/skills/openfoodfacts-guide/SKILL.md +4 -2
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** openfoodfacts-mcp-server
4
- **Version:** 0.4.1
4
+ **Version:** 0.4.2
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
@@ -206,16 +206,20 @@ See framework CLAUDE.md and the `api-errors` skill for the full auto-classificat
206
206
  src/
207
207
  index.ts # createApp() entry point
208
208
  config/
209
- server-config.ts # Server-specific env vars (OFF_BASE_URL, rate limits)
209
+ server-config.ts # Server-specific env vars (OFF_BASE_URL, rate limits, OFF_MIRROR_PATH)
210
210
  services/
211
+ mirror/ # Optional local export mirror (OFF_MIRROR_PATH): store, record builder, ingester, queries, refresh schedule
211
212
  openfoodfacts/
212
- openfoodfacts-service.ts # Open Food Facts API client (HTTP, rate limiting, retry)
213
+ openfoodfacts-service.ts # Open Food Facts API client (HTTP, rate limiting, retry); reads the mirror first when one is ready
214
+ user-agent.ts # Identifying User-Agent shared by the client and the mirror's downloads
215
+ nutrition-sets.ts # Stored as-sold nutrition sets: choosing one for a prepared-only read
213
216
  types.ts # Domain types
214
217
  taxonomy/
215
218
  taxonomy-service.ts # Tag vocabulary — live resolution, offline sample, merge/fallback policy, tag-value canonicalization
216
219
  utils/
217
220
  markdown.ts # Contextual Markdown escaping for crowd-sourced values in content[]
218
221
  mcp-server/
222
+ tools/read-source.ts # How tool text names an answer's source (off_api / off_mirror) and the mirror freshness note
219
223
  tools/definitions/
220
224
  get-product.tool.ts # off_get_product
221
225
  search-products.tool.ts # off_search_products
@@ -316,6 +320,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
316
320
  | `bun run format` | Auto-fix formatting (safe fixes only) |
317
321
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
318
322
  | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
323
+ | `bun run mirror:init` / `mirror:refresh` / `mirror:status` / `mirror:verify` | Build, refresh, inspect, and integrity-check the local export mirror at `OFF_MIRROR_PATH` (checkout or Docker image) |
319
324
  | `bun run start:stdio` | Production mode (stdio) |
320
325
  | `bun run start:http` | Production mode (HTTP) |
321
326
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** openfoodfacts-mcp-server
4
- **Version:** 0.4.1
4
+ **Version:** 0.4.2
5
5
  **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
7
  **MCP SDK:** `@modelcontextprotocol/server` ^2.4.0
@@ -206,16 +206,20 @@ See framework CLAUDE.md and the `api-errors` skill for the full auto-classificat
206
206
  src/
207
207
  index.ts # createApp() entry point
208
208
  config/
209
- server-config.ts # Server-specific env vars (OFF_BASE_URL, rate limits)
209
+ server-config.ts # Server-specific env vars (OFF_BASE_URL, rate limits, OFF_MIRROR_PATH)
210
210
  services/
211
+ mirror/ # Optional local export mirror (OFF_MIRROR_PATH): store, record builder, ingester, queries, refresh schedule
211
212
  openfoodfacts/
212
- openfoodfacts-service.ts # Open Food Facts API client (HTTP, rate limiting, retry)
213
+ openfoodfacts-service.ts # Open Food Facts API client (HTTP, rate limiting, retry); reads the mirror first when one is ready
214
+ user-agent.ts # Identifying User-Agent shared by the client and the mirror's downloads
215
+ nutrition-sets.ts # Stored as-sold nutrition sets: choosing one for a prepared-only read
213
216
  types.ts # Domain types
214
217
  taxonomy/
215
218
  taxonomy-service.ts # Tag vocabulary — live resolution, offline sample, merge/fallback policy, tag-value canonicalization
216
219
  utils/
217
220
  markdown.ts # Contextual Markdown escaping for crowd-sourced values in content[]
218
221
  mcp-server/
222
+ tools/read-source.ts # How tool text names an answer's source (off_api / off_mirror) and the mirror freshness note
219
223
  tools/definitions/
220
224
  get-product.tool.ts # off_get_product
221
225
  search-products.tool.ts # off_search_products
@@ -316,6 +320,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
316
320
  | `bun run format` | Auto-fix formatting (safe fixes only) |
317
321
  | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
318
322
  | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
323
+ | `bun run mirror:init` / `mirror:refresh` / `mirror:status` / `mirror:verify` | Build, refresh, inspect, and integrity-check the local export mirror at `OFF_MIRROR_PATH` (checkout or Docker image) |
319
324
  | `bun run start:stdio` | Production mode (stdio) |
320
325
  | `bun run start:http` | Production mode (HTTP) |
321
326
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
package/Dockerfile CHANGED
@@ -143,12 +143,30 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
143
143
  COPY --from=build /usr/src/app/dist ./dist
144
144
  COPY --from=build /usr/src/app/skills ./skills
145
145
 
146
+ # Mirror lifecycle scripts (`bun run mirror:init|refresh|status|verify` via
147
+ # `docker exec <container> …`). The shared context shim is imported by the
148
+ # three named scripts, so it travels with them. Bun honors tsconfig `paths` at
149
+ # runtime: a checkout's tsconfig.json maps `@/*` to `./src/*`, and this emitted
150
+ # one maps it to the compiled `./dist/*`, so the same package scripts run in both.
151
+ COPY --from=build /usr/src/app/scripts/mirror-init.ts \
152
+ /usr/src/app/scripts/mirror-refresh.ts \
153
+ /usr/src/app/scripts/mirror-status.ts \
154
+ /usr/src/app/scripts/_mirror-context.ts \
155
+ ./scripts/
156
+ RUN echo '{"compilerOptions":{"baseUrl":".","paths":{"@/*":["./dist/*"]}}}' > tsconfig.json
157
+
146
158
  # The 'oven/bun' image already provides a non-root user named 'bun'.
147
159
  # We will use this existing user for enhanced security.
148
160
 
149
161
  # Create and set permissions for the log directory, assigning ownership to the 'bun' user.
150
162
  RUN mkdir -p /var/log/openfoodfacts-mcp-server && chown -R bun:bun /var/log/openfoodfacts-mcp-server
151
163
 
164
+ # Writable home for the export mirror (OFF_MIRROR_PATH, e.g. .mirror/openfoodfacts.sqlite),
165
+ # owned by the runtime user so a volume mounted over it is writable. A full init of the
166
+ # 4.9M-product export peaks at about 27 GB there while the export download sits beside the
167
+ # store, and leaves a store of about 14.3 GB.
168
+ RUN mkdir -p /usr/src/app/.mirror && chown -R bun:bun /usr/src/app/.mirror
169
+
152
170
  # Switch to the non-root user
153
171
  USER bun
154
172
 
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.4.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/openfoodfacts-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/openfoodfacts-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/openfoodfacts-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.4.2-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/openfoodfacts-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/openfoodfacts-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/openfoodfacts-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -53,7 +53,8 @@ Packaged-food data from Open Food Facts, a crowd-sourced database of 3M+ product
53
53
  - `barcode` is digits only, 4–40 digits after any leading zeros; optional `fields` trims the response and pulls in what a field depends on (`nutriments` brings the serving size, quantity, and unit, and `serving_quantity` and `serving_quantity_unit` each bring the other plus the serving size), with `requested_fields` echoing what was fetched
54
54
  - 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`, and `completeness` (0–1)
55
55
  - `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 as `not_found`
56
- - Open Food Facts returns one preparation state of the nutrition per read. A read of prepared-product figures puts them under keys ending in `_prepared` in `additional_100g` and `additional_serving`, prints them in "as prepared" sections, and leaves the named as-sold nutrient fields absent; the text says the read returned only prepared figures, not that the record holds no as-sold ones
56
+ - Open Food Facts returns one preparation state of the nutrition per read. A read of prepared-product figures puts them under keys ending in `_prepared` in `additional_100g` and `additional_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.5` `fields=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_sold` reports `read`, `none` (the record stores no usable set), or `unread` with a `reason`, an `error`, and, when the failure named a wait, `retryAfter` in whole seconds (the call itself still succeeds). Other reads send nothing extra
57
+ - `source` (`off_api` or `off_mirror`) names where the product was read; a barcode a [local mirror](#local-export-mirror) holds is read from it with no upstream request, with `mirror_as_of` (the time of the newest Open Food Facts change the copy reflects) and a `mirror_snapshot` note, and the stored as-sold set comes from the same stored record
57
58
 
58
59
  ---
59
60
 
@@ -62,7 +63,8 @@ Packaged-food data from Open Food Facts, a crowd-sourced database of 3M+ product
62
63
  - `query` (up to 24 words, all of which must match the name, generic name, categories, labels, or brand; punctuation separates words, so `milk-chocolate` requires both) plus the tag filters `categories_tag`, `brands_tag`, `labels_tag` (up to 10), `allergens_tag`, `traces_tag`, `ingredients_analysis_tag`, `additives_tag`, `nutrition_grade`, `nova_group`, `countries_tag`, the `exclude_allergens` / `exclude_traces` lists (up to 14 each), and up to 18 `nutrient_filters` (per 100 g, or per 100 ml for liquids), all combined as AND; optional `sort_by`
63
64
  - `page_size` defaults to 20 and is served at up to 50 per call; a larger value, with no ceiling, is served at 50 and flagged in `page_size_capped`, and `page` counts pages of 50. Rows carry `barcode`, `product_name`, `brands`, `nutriscore_grade`, `nova_group`, `ecoscore_grade`, and `categories_tags`, plus `total`, `total_is_lower_bound`, `last_page`, `omitted`, and `truncated` (more matches beyond this page). Exclusions add `exclusion_coverage`, since products with no allergen data entered pass them; `nutrient_filters` add `indexed_nutriments` (the index values the filters checked, which can differ from `off_get_product`)
64
65
  - The text index compares `energy-kcal`, `fat`, `saturated-fat`, `carbohydrates`, and `sugars` as whole numbers (`sugars lte 3` returns a row at 3.53). A `nutrient_filters` search sends `gt` and `gte` on 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 in `outside_constraints`, so a page can come back short or empty while later pages hold matches. `total`, `total_is_lower_bound`, and `last_page` describe the index's whole-number answer and still count those rows. A range inside one whole number (`sugars gt 3.9` and `lt 4`) can pass few of the 10,000 reachable rows; add `query` or tag filters to narrow it
65
- - `query` or `nutrient_filters` searches a text index that lags the live database (flagged in `text_index_snapshot`), reaches only the first 10,000 results, and rejects `additives_tag` as `additives_filter_needs_tag_search`; tag-only searches read the live database through page 10. A page past either limit fails as `page_out_of_range`
66
+ - `query` or `nutrient_filters` searches a text index that lags the live database (flagged in `text_index_snapshot`), reaches only the first 10,000 results, and rejects `additives_tag` as `additives_filter_needs_tag_search`; tag-only searches read the live database through page 10, unless a [local mirror](#local-export-mirror) serves them: with every tag value confirmed, a mirror-served search has an exact `total` and reaches its first 10,000 results. A page past any of these limits fails as `page_out_of_range`
67
+ - `source` (`off_api` or `off_mirror`) names where the answer was read; from the mirror it also carries `mirror_as_of` and a `mirror_snapshot` note, and the default order and `sort_by` `popularity_key` and `unique_scans_n` follow the values stored with each record, which can differ from a live search, which ranks by its current scan counts
66
68
 
67
69
  ---
68
70
 
@@ -70,8 +72,9 @@ Packaged-food data from Open Food Facts, a crowd-sourced database of 3M+ product
70
72
 
71
73
  - 2–100 `barcodes`, one row each in input order; repeated barcodes are compared once, and those past the 10th distinct one are listed in `unprocessed` without being fetched
72
74
  - Rows 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), and `completeness`; missing values stay absent, never imputed
73
- - A read of prepared-product figures adds a `prepared` object on the row with the same columns, read from the `*_prepared_100g` keys and never substituted for an as-sold figure. A row with as-sold figures appears in the as-sold table, a row with `prepared` in the "as prepared" table, and a row with both appears in both
74
- - `not_found` lists barcodes with no contributor record and `failed` lists fetches that failed, each with a `reason`; a failed barcode gets no row and never blocks the rest
75
+ - A read of prepared-product figures adds a `prepared` object on the row with the same columns, read from the `*_prepared_100g` keys and never substituted for an as-sold figure. A row whose read returned only prepared figures gets the same stored as-sold read as `off_get_product`, once every barcode's product read has settled, and reports it as `stored_as_sold` (`read`, `none`, or `unread`, which never moves the barcode into `failed`). A row with as-sold figures appears in the as-sold table, a row with `prepared` in the "as prepared" table, and a row with both appears in both
76
+ - Each row carries `source` (`off_api` or `off_mirror`), and `mirror_as_of` for a row read from a [local mirror](#local-export-mirror), so one comparison can mix the two; a `mirror_snapshot` note appears when any row came from it
77
+ - `not_found` lists barcodes with no contributor record and `failed` lists fetches that failed, each with a `reason` and, when the failure named a wait, `retryAfter` in whole seconds; a failed barcode gets no row and never blocks the rest
75
78
 
76
79
  ---
77
80
 
@@ -89,9 +92,11 @@ Open Food Facts-specific:
89
92
 
90
93
  - No API key; the identifying `User-Agent` Open Food Facts' terms require is built in
91
94
  - Per-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`, or `rate_limited` when its last finished attempt answered 429
95
+ - The 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
96
+ - Optional [local export mirror](#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 its `source` and, from the mirror, the time it reflects
92
97
  - Nutriments normalized from hyphenated keys (`energy-kcal_100g` → `energy_kcal_100g`), with `additional_100g` / `additional_serving` maps that label each nutrient with its unit
93
98
  - 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`)
94
- - Every field is contributor-entered, so a missing one means not yet recorded, except that a read of prepared-product nutrition omits as-sold figures the record can still hold; Nutri-Score, NOVA, and Green-Score are Open Food Facts' own computed grades, returned as-is, and carry regional formula caveats
99
+ - 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
95
100
 
96
101
  Agent-friendly output:
97
102
 
@@ -137,6 +142,7 @@ No API key or account is needed: the server identifies itself to Open Food Facts
137
142
  | `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` |
138
143
  | `OFF_RATE_LIMIT_SEARCH` | Search budget (requests/min). The default is Open Food Facts' published per-IP limit. | `10` |
139
144
  | `OFF_RATE_LIMIT_TAXONOMY` | Taxonomy lookup budget (requests/min), shared by `off_browse_taxonomy`, tag canonicalization on text searches, and exclusion checks. | `10` |
145
+ | `OFF_MIRROR_PATH` | Path of the [local export mirror](#local-export-mirror)'s SQLite file. Unset disables the mirror. Works from a source checkout or the Docker image only. | unset |
140
146
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
141
147
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
142
148
  | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
@@ -188,6 +194,26 @@ docker run --rm -p 3010:3010 ghcr.io/cyanheads/openfoodfacts-mcp-server:latest
188
194
 
189
195
  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.
190
196
 
197
+ #### Local export mirror
198
+
199
+ 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; every other search is live. 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.
200
+
201
+ 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`:
202
+
203
+ ```sh
204
+ docker run -d --name openfoodfacts-mcp-server -p 3010:3010 \
205
+ -v off-mirror:/usr/src/app/.mirror \
206
+ -e OFF_MIRROR_PATH=/usr/src/app/.mirror/openfoodfacts.sqlite \
207
+ ghcr.io/cyanheads/openfoodfacts-mcp-server:latest
208
+
209
+ 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
210
+ docker exec openfoodfacts-mcp-server bun run mirror:status # health, product count, and the as-of time answers cite
211
+ docker exec openfoodfacts-mcp-server bun run mirror:verify # status plus SQLite's integrity check
212
+ docker exec openfoodfacts-mcp-server bun run mirror:refresh # one delta pass by hand
213
+ ```
214
+
215
+ A 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. 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. `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`.
216
+
191
217
  ## Project structure
192
218
 
193
219
  | Directory | Purpose |
@@ -196,6 +222,8 @@ The image defaults to HTTP transport, stateless session mode, and logs to `/var/
196
222
  | `src/config` | Server-specific environment variable parsing and validation with Zod. |
197
223
  | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). |
198
224
  | `src/services/openfoodfacts` | Open Food Facts API client — HTTP, rate limiting, retry, field normalization. |
225
+ | `src/services/mirror` | Local export mirror — SQLite store, record builder, export and delta ingester, tag-search queries, and the refresh schedule. |
226
+ | `scripts/mirror-*.ts` | `mirror:init`, `mirror:refresh`, `mirror:status`, and `mirror:verify`. |
199
227
  | `src/services/taxonomy` | Tag vocabulary — live resolution, offline sample, and tag-value canonicalization. |
200
228
  | `src/utils` | Markdown escaping for contributor-entered values in text output. |
201
229
  | `skills/` | Published Agent Skills for the agents that call the tools. |
@@ -0,0 +1,33 @@
1
+ ---
2
+ summary: "Adds an optional local mirror of the Open Food Facts export that serves barcode reads and tag-only searches, reads a record's stored as-sold figures when a product read returns only prepared ones, and names why a taxonomy lookup failed, with the wait in whole seconds."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.4.2 — 2026-11-01
8
+
9
+ ## Added
10
+
11
+ - **Local export mirror** ([#79](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/79)): setting `OFF_MIRROR_PATH` keeps a 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. `bun run mirror:init`, `mirror:refresh`, `mirror:status`, and `mirror:verify` build and inspect the store from a checkout or the Docker image, and the server refreshes it every three hours. A refresh reloads the whole export every 30 days, or when the listed deltas no longer reach the store's checkpoint, unless one HEAD request finds the export's ETag unchanged since the last load. 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. Only `mirror:init` creates the store: the serving process reads live while the path names no file, and a refresh there is refused as `mirror_not_initialized`. Opening the store waits at most 250 ms on a sync's write lock before the call reads live, and a `mirror:*` script that meets that lock names the sync holding it. With the variable unset nothing opens or schedules.
12
+ - **Mirror-served tag-only searches** ([#79](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/79)): `off_search_products` with tag filters alone is served from the mirror when every tag value is confirmed, with an exact `total` through its first 10,000 results. Under the default order, `popularity_key`, and `unique_scans_n` the mirror sorts by the values stored with each record, so the order can differ from a live search.
13
+ - **`source`, `mirror_as_of`, `mirror_snapshot`** ([#79](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/79)): product, compare (per row), and search results name `off_api` or `off_mirror`; a mirror answer also carries when the copy was last updated and a note saying so.
14
+ - **`stored_as_sold`** ([#73](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/73)): when an `off_get_product` or `off_compare_products` read returns only prepared figures, one follow-up read fetches the record's stored as-sold set and fills the as-sold fields. The field reports `read`, `none`, or `unread` with a `reason`; an unread set never fails the call.
15
+ - **`cause` on `lookup_failed`** ([#77](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/77)): a failed taxonomy lookup reports `budget_spent`, `rate_limited`, or `unavailable`, and the exclusion refusal, the zero-result notice, and the `off_browse_taxonomy` notice name it, blaming the vocabulary only for `unavailable`.
16
+ - **`unconfirmed[]` on `page_out_of_range`** ([#79](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/79)): a tag-only page past 10 that the ready mirror cannot take because a value went unconfirmed is refused with those values named, and is retryable when each failed only at lookup.
17
+
18
+ ## Changed
19
+
20
+ - **`retryAfter`** ([#76](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/76)): published in whole seconds on every failure that knows its wait. Each `failed[]` entry of `off_compare_products` carries it, and a retryable `unrecognized_exclusion` refusal carries the longest wait among its unchecked values.
21
+ - **`off_compare_products` stored sets** ([#73](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/73)): a row whose stored as-sold set was read stays in the as-sold table even when that set fills none of the compared columns.
22
+ - **Docs and skill**: tool descriptions, the server instructions, the `openfoodfacts-guide` skill, and the README describe the mirror, stored sets, and failure causes.
23
+
24
+ ## Fixed
25
+
26
+ - **Shared taxonomy lookups** ([#75](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/75)): a caller that joined a lookup whose starter cancelled restarted it with a fresh 20 s deadline and could outlast 55 s. It now restarts with what is left of its own, so a single call settles within 55 s. A joined caller that is itself cancelled now ends at once instead of waiting for the shared answer.
27
+ - **Brand values with a language prefix** ([#79](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/79)): `xx:nutella` and `en:nutella` now drop the prefix before slugging. The text-search path had sent `xx-nutella` since 0.3.6.
28
+
29
+ ## Dependencies
30
+
31
+ - **`node-cron`** ^4.6.0 (added, runs the scheduled refresh).
32
+ - **`better-sqlite3`** ^13.0.3 (added, dev; SQLite for the Node test lane).
33
+ - **`tsc-alias`** held at ^1.9.9 on the `devcheck.config.json` `outdated.allowlist`: installing 1.9.10 fails because the install's Socket security scanner gets a 403 from its upstream.
@@ -26,6 +26,7 @@ declare const ServerConfigSchema: z.ZodObject<{
26
26
  rateLimitProduct: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
27
27
  rateLimitSearch: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
28
28
  rateLimitTaxonomy: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
29
+ mirrorPath: z.ZodOptional<z.ZodString>;
29
30
  }, z.core.$strip>;
30
31
  export type ServerConfig = z.infer<typeof ServerConfigSchema>;
31
32
  /** Lazy-parsed server config from environment variables. */
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C;;;;;;;;;;;;;;;;;GAiBG;AACH,QAAA,MAAM,kBAAkB;;;;;iBAuBtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,4DAA4D;AAC5D,wBAAgB,eAAe,IAAI,YAAY,CAQ9C"}
1
+ {"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C;;;;;;;;;;;;;;;;;GAiBG;AACH,QAAA,MAAM,kBAAkB;;;;;;iBAiCtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,4DAA4D;AAC5D,wBAAgB,eAAe,IAAI,YAAY,CAS9C"}
@@ -45,6 +45,16 @@ const ServerConfigSchema = z.object({
45
45
  .min(1)
46
46
  .default(10)
47
47
  .describe('Taxonomy resolution rate limit (requests/min)'),
48
+ /**
49
+ * Unset means no mirror: no store opens and no refresh is scheduled. The `mirror:*` scripts build
50
+ * the store, and they run only from a checkout or the Docker image, so the variable appears in no
51
+ * npm-facing manifest.
52
+ */
53
+ mirrorPath: z
54
+ .string()
55
+ .min(1)
56
+ .optional()
57
+ .describe('Local export mirror store path (SQLite file); unset disables the mirror'),
48
58
  });
49
59
  let _config;
50
60
  /** Lazy-parsed server config from environment variables. */
@@ -54,6 +64,7 @@ export function getServerConfig() {
54
64
  rateLimitProduct: 'OFF_RATE_LIMIT_PRODUCT',
55
65
  rateLimitSearch: 'OFF_RATE_LIMIT_SEARCH',
56
66
  rateLimitTaxonomy: 'OFF_RATE_LIMIT_TAXONOMY',
67
+ mirrorPath: 'OFF_MIRROR_PATH',
57
68
  });
58
69
  return _config;
59
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,OAAO,CAAC,iCAAiC,CAAC;SAC1C,QAAQ,CAAC,8BAA8B,CAAC;IAC3C,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,wCAAwC,CAAC;IACrD,eAAe,EAAE,CAAC,CAAC,MAAM;SACtB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,kCAAkC,CAAC;IAC/C,iBAAiB,EAAE,CAAC,CAAC,MAAM;SACxB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,+CAA+C,CAAC;CAC7D,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,4DAA4D;AAC5D,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,cAAc;QACvB,gBAAgB,EAAE,wBAAwB;QAC1C,eAAe,EAAE,uBAAuB;QACxC,iBAAiB,EAAE,yBAAyB;KAC7C,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,OAAO,EAAE,CAAC;SACP,MAAM,EAAE;SACR,OAAO,CAAC,iCAAiC,CAAC;SAC1C,QAAQ,CAAC,8BAA8B,CAAC;IAC3C,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,wCAAwC,CAAC;IACrD,eAAe,EAAE,CAAC,CAAC,MAAM;SACtB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,kCAAkC,CAAC;IAC/C,iBAAiB,EAAE,CAAC,CAAC,MAAM;SACxB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,EAAE,CAAC;SACX,QAAQ,CAAC,+CAA+C,CAAC;IAC5D;;;;OAIG;IACH,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,QAAQ,EAAE;SACV,QAAQ,CAAC,yEAAyE,CAAC;CACvF,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,4DAA4D;AAC5D,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,OAAO,EAAE,cAAc;QACvB,gBAAgB,EAAE,wBAAwB;QAC1C,eAAe,EAAE,uBAAuB;QACxC,iBAAiB,EAAE,yBAAyB;QAC5C,UAAU,EAAE,iBAAiB;KAC9B,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/dist/index.js CHANGED
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { createApp } from '@cyanheads/mcp-ts-core';
7
7
  import { allToolDefinitions } from './mcp-server/tools/definitions/index.js';
8
+ import { startMirrorRefreshSchedule, stopMirrorRefreshSchedule, } from './services/mirror/mirror-scheduler.js';
9
+ import { closeMirrorStore } from './services/mirror/mirror-store.js';
8
10
  import { initOpenFoodFactsService } from './services/openfoodfacts/openfoodfacts-service.js';
9
11
  import { initTaxonomyService } from './services/taxonomy/taxonomy-service.js';
10
12
  await createApp({
@@ -12,11 +14,16 @@ await createApp({
12
14
  title: 'openfoodfacts-mcp-server',
13
15
  tools: allToolDefinitions,
14
16
  skills: 'skills',
15
- instructions: 'Query Open Food Facts, a free crowd-sourced database of 3M+ packaged foods: off_get_product looks up one product by barcode (4–40 digits), off_search_products finds products by text or tag filters, off_compare_products puts several side by side on nutrition, and off_browse_taxonomy resolves a human term to the canonical tag ID the other tools filter on — so start there when you have a category or label rather than a barcode. Every field is contributor-entered, so a missing one means "not yet recorded" rather than absent, except that a read of prepared-product nutrition omits as-sold figures the record can still hold, and the computed Nutri-Score, NOVA, and Green-Score grades carry regional caveats. No API key is required; the data is ODbL 1.0, so cite Open Food Facts wherever you pass it on.',
17
+ instructions: 'Query Open Food Facts, a free crowd-sourced database of 3M+ packaged foods: off_get_product looks up one product by barcode (4–40 digits), off_search_products finds products by text or tag filters, off_compare_products puts several side by side on nutrition, and off_browse_taxonomy resolves a human term to the canonical tag ID the other tools filter on — so start there when you have a category or label rather than a barcode. Every field is contributor-entered, so a missing one means "not yet recorded" rather than absent, 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, and the computed Nutri-Score, NOVA, and Green-Score grades carry regional caveats. No API key is required; the data is ODbL 1.0, so cite Open Food Facts wherever you pass it on.',
16
18
  sessionMode: 'stateless',
17
- setup() {
19
+ async setup() {
18
20
  initOpenFoodFactsService();
19
21
  initTaxonomyService();
22
+ await startMirrorRefreshSchedule();
23
+ },
24
+ async teardown() {
25
+ await stopMirrorRefreshSchedule();
26
+ await closeMirrorStore();
20
27
  },
21
28
  });
22
29
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,KAAK,EAAE,kBAAkB;IACzB,MAAM,EAAE,QAAQ;IAChB,YAAY,EACV,oyBAAoyB;IACtyB,WAAW,EAAE,WAAW;IACxB,KAAK;QACH,wBAAwB,EAAE,CAAC;QAC3B,mBAAmB,EAAE,CAAC;IACxB,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAC;AAC7F,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAE9E,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,0BAA0B;IACjC,KAAK,EAAE,kBAAkB;IACzB,MAAM,EAAE,QAAQ;IAChB,YAAY,EACV,m2BAAm2B;IACr2B,WAAW,EAAE,WAAW;IACxB,KAAK,CAAC,KAAK;QACT,wBAAwB,EAAE,CAAC;QAC3B,mBAAmB,EAAE,CAAC;QACtB,MAAM,0BAA0B,EAAE,CAAC;IACrC,CAAC;IACD,KAAK,CAAC,QAAQ;QACZ,MAAM,yBAAyB,EAAE,CAAC;QAClC,MAAM,gBAAgB,EAAE,CAAC;IAC3B,CAAC;CACF,CAAC,CAAC"}
@@ -69,7 +69,7 @@ export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
69
69
  notice: z
70
70
  .string()
71
71
  .optional()
72
- .describe(`Caveat about the answer — that the listing is a limited reference list rather than the full vocabulary, that the live vocabulary could not be reached or gave no usable answer, that nothing matched and why, or that the limit cut matches, naming how many matched and the next call: a larger limit while one would list more, otherwise a narrower term, which is the only route past the first ${TAXONOMY_LIMIT_CAP} (a limit above ${TAXONOMY_LIMIT_CAP} is named too).`),
72
+ .describe(`Caveat about the answer — that the listing is a limited reference list rather than the full vocabulary, that the live vocabulary went unchecked and why (unreachable or no usable answer, Open Food Facts rate-limiting this server, or this server's own taxonomy budget spent, with the wait in seconds when one is known), that nothing matched and why, or that the limit cut matches, naming how many matched and the next call: a larger limit while one would list more, otherwise a narrower term, which is the only route past the first ${TAXONOMY_LIMIT_CAP} (a limit above ${TAXONOMY_LIMIT_CAP} is named too).`),
73
73
  truncated: z.boolean().optional().describe('True when more tags exist beyond the limit.'),
74
74
  shown: z.number().optional().describe('Number of tags returned.'),
75
75
  cap: z
@@ -1 +1 @@
1
- {"version":3,"file":"browse-taxonomy.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/browse-taxonomy.tool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browse-taxonomy.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/browse-taxonomy.tool.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,48 @@
4
4
  */
5
5
  import { z } from '@cyanheads/mcp-ts-core';
6
6
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
7
+ import { type StoredAsSoldRead } from '../../../services/openfoodfacts/openfoodfacts-service.js';
8
+ import { type RawProduct } from '../../../services/openfoodfacts/types.js';
9
+ /** The failure reasons this tool declares — the four the service raises for a product fetch. */
10
+ declare const FAILURE_REASONS: readonly ['upstream_error', 'upstream_timeout', 'upstream_rejected', 'rate_limited'];
11
+ type FailureReason = (typeof FAILURE_REASONS)[number];
12
+ /**
13
+ * How the read of a record's stored as-sold set went, on a product or row whose read returned only
14
+ * prepared figures. `unread` is shaped like a `failed` entry, less the barcode its holder carries.
15
+ */
16
+ export type StoredAsSold = {
17
+ status: 'read';
18
+ } | {
19
+ status: 'none';
20
+ } | {
21
+ status: 'unread';
22
+ reason: FailureReason;
23
+ error: string;
24
+ retryAfter?: number;
25
+ };
26
+ /**
27
+ * Settle a stored as-sold read onto the product read it follows: a chosen set joins the product's
28
+ * `nutriments`, and a failure leaves the product as it was beside an `unread` entry built the way
29
+ * `describeFailure` builds a `failed` one.
30
+ */
31
+ export declare function settleStoredAsSold(barcode: string, product: RawProduct, read: StoredAsSoldRead): {
32
+ product: RawProduct;
33
+ stored_as_sold: StoredAsSold;
34
+ };
35
+ /**
36
+ * The `stored_as_sold` output field, one schema for both product tools. `subject` names what
37
+ * carries it ("this product", "this row").
38
+ */
39
+ export declare function storedAsSoldSchema(subject: string): z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
40
+ status: z.ZodLiteral<"read">;
41
+ }, z.core.$strip>, z.ZodObject<{
42
+ status: z.ZodLiteral<"none">;
43
+ }, z.core.$strip>, z.ZodObject<{
44
+ status: z.ZodLiteral<"unread">;
45
+ reason: z.ZodString;
46
+ error: z.ZodString;
47
+ retryAfter: z.ZodOptional<z.ZodNumber>;
48
+ }, z.core.$strip>], "status">>;
7
49
  export declare const offCompareProductsTool: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
8
50
  barcodes: z.ZodArray<z.ZodString>;
9
51
  }, z.core.$strip>, z.ZodObject<{
@@ -25,7 +67,22 @@ export declare const offCompareProductsTool: import("@cyanheads/mcp-ts-core").To
25
67
  saturated_fat_100g: z.ZodOptional<z.ZodNumber>;
26
68
  sugars_100g: z.ZodOptional<z.ZodNumber>;
27
69
  }, z.core.$strip>>;
70
+ stored_as_sold: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
71
+ status: z.ZodLiteral<"read">;
72
+ }, z.core.$strip>, z.ZodObject<{
73
+ status: z.ZodLiteral<"none">;
74
+ }, z.core.$strip>, z.ZodObject<{
75
+ status: z.ZodLiteral<"unread">;
76
+ reason: z.ZodString;
77
+ error: z.ZodString;
78
+ retryAfter: z.ZodOptional<z.ZodNumber>;
79
+ }, z.core.$strip>], "status">>;
28
80
  completeness: z.ZodOptional<z.ZodNumber>;
81
+ source: z.ZodOptional<z.ZodEnum<{
82
+ off_api: "off_api";
83
+ off_mirror: "off_mirror";
84
+ }>>;
85
+ mirror_as_of: z.ZodOptional<z.ZodString>;
29
86
  carbohydrates_100g: z.ZodOptional<z.ZodNumber>;
30
87
  energy_kcal_100g: z.ZodOptional<z.ZodNumber>;
31
88
  fat_100g: z.ZodOptional<z.ZodNumber>;
@@ -41,6 +98,7 @@ export declare const offCompareProductsTool: import("@cyanheads/mcp-ts-core").To
41
98
  barcode: z.ZodString;
42
99
  reason: z.ZodString;
43
100
  error: z.ZodString;
101
+ retryAfter: z.ZodOptional<z.ZodNumber>;
44
102
  }, z.core.$strip>>>;
45
103
  unprocessed: z.ZodOptional<z.ZodArray<z.ZodString>>;
46
104
  }, z.core.$strip>, readonly [{
@@ -71,5 +129,8 @@ export declare const offCompareProductsTool: import("@cyanheads/mcp-ts-core").To
71
129
  readonly retryable: true;
72
130
  readonly thrownBy: "service";
73
131
  readonly recovery: "Wait the seconds the failed entry names, or about a minute when it names none, then retry only those barcodes. Compare fewer barcodes per call to stay inside the budget.";
74
- }], undefined>;
132
+ }], {
133
+ readonly mirror_snapshot: z.ZodOptional<z.ZodString>;
134
+ }>;
135
+ export {};
75
136
  //# sourceMappingURL=compare-products.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compare-products.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/compare-products.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAuP3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuTjC,CAAC"}
1
+ {"version":3,"file":"compare-products.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/compare-products.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAW3E,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,mDAAmD,CAAC;AAC3D,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,mCAAmC,CAAC;AAiI3C,gGAAgG;AAChG,QAAA,MAAM,eAAe,YACnB,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,CACN,CAAC;AAEX,KAAK,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAqCtD;;;GAGG;AACH,MAAM,MAAM,YAAY,GACpB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAkBpF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,UAAU,EACnB,IAAI,EAAE,gBAAgB,GACrB;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,cAAc,EAAE,YAAY,CAAA;CAAE,CAgBvD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM;;;;;;;;;+BAkDjD;AA4HD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsajC,CAAC"}