@cyanheads/openfoodfacts-mcp-server 0.4.0 → 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 (92) hide show
  1. package/AGENTS.md +8 -3
  2. package/CLAUDE.md +8 -3
  3. package/Dockerfile +18 -0
  4. package/README.md +42 -11
  5. package/changelog/0.4.x/0.4.1.md +30 -0
  6. package/changelog/0.4.x/0.4.2.md +33 -0
  7. package/dist/config/server-config.d.ts +1 -0
  8. package/dist/config/server-config.d.ts.map +1 -1
  9. package/dist/config/server-config.js +11 -0
  10. package/dist/config/server-config.js.map +1 -1
  11. package/dist/index.js +9 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.js +13 -8
  15. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts +79 -7
  17. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/compare-products.tool.js +298 -75
  19. package/dist/mcp-server/tools/definitions/compare-products.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts +20 -3
  21. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/get-product.tool.js +123 -40
  23. package/dist/mcp-server/tools/definitions/get-product.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/index.d.ts +91 -38
  25. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts +11 -4
  27. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/search-products.tool.js +386 -114
  29. package/dist/mcp-server/tools/definitions/search-products.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/read-source.d.ts +31 -0
  31. package/dist/mcp-server/tools/read-source.d.ts.map +1 -0
  32. package/dist/mcp-server/tools/read-source.js +45 -0
  33. package/dist/mcp-server/tools/read-source.js.map +1 -0
  34. package/dist/services/mirror/mirror-feeds.d.ts +58 -0
  35. package/dist/services/mirror/mirror-feeds.d.ts.map +1 -0
  36. package/dist/services/mirror/mirror-feeds.js +95 -0
  37. package/dist/services/mirror/mirror-feeds.js.map +1 -0
  38. package/dist/services/mirror/mirror-ingester.d.ts +112 -0
  39. package/dist/services/mirror/mirror-ingester.d.ts.map +1 -0
  40. package/dist/services/mirror/mirror-ingester.js +564 -0
  41. package/dist/services/mirror/mirror-ingester.js.map +1 -0
  42. package/dist/services/mirror/mirror-job.d.ts +9 -0
  43. package/dist/services/mirror/mirror-job.d.ts.map +1 -0
  44. package/dist/services/mirror/mirror-job.js +48 -0
  45. package/dist/services/mirror/mirror-job.js.map +1 -0
  46. package/dist/services/mirror/mirror-lock.d.ts +45 -0
  47. package/dist/services/mirror/mirror-lock.d.ts.map +1 -0
  48. package/dist/services/mirror/mirror-lock.js +128 -0
  49. package/dist/services/mirror/mirror-lock.js.map +1 -0
  50. package/dist/services/mirror/mirror-queries.d.ts +76 -0
  51. package/dist/services/mirror/mirror-queries.d.ts.map +1 -0
  52. package/dist/services/mirror/mirror-queries.js +281 -0
  53. package/dist/services/mirror/mirror-queries.js.map +1 -0
  54. package/dist/services/mirror/mirror-scheduler.d.ts +41 -0
  55. package/dist/services/mirror/mirror-scheduler.d.ts.map +1 -0
  56. package/dist/services/mirror/mirror-scheduler.js +131 -0
  57. package/dist/services/mirror/mirror-scheduler.js.map +1 -0
  58. package/dist/services/mirror/mirror-store.d.ts +150 -0
  59. package/dist/services/mirror/mirror-store.d.ts.map +1 -0
  60. package/dist/services/mirror/mirror-store.js +281 -0
  61. package/dist/services/mirror/mirror-store.js.map +1 -0
  62. package/dist/services/mirror/mirror-sync.d.ts +62 -0
  63. package/dist/services/mirror/mirror-sync.d.ts.map +1 -0
  64. package/dist/services/mirror/mirror-sync.js +226 -0
  65. package/dist/services/mirror/mirror-sync.js.map +1 -0
  66. package/dist/services/mirror/record-builder.d.ts +78 -0
  67. package/dist/services/mirror/record-builder.d.ts.map +1 -0
  68. package/dist/services/mirror/record-builder.js +957 -0
  69. package/dist/services/mirror/record-builder.js.map +1 -0
  70. package/dist/services/openfoodfacts/nutrition-sets.d.ts +132 -0
  71. package/dist/services/openfoodfacts/nutrition-sets.d.ts.map +1 -0
  72. package/dist/services/openfoodfacts/nutrition-sets.js +507 -0
  73. package/dist/services/openfoodfacts/nutrition-sets.js.map +1 -0
  74. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts +202 -24
  75. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts.map +1 -1
  76. package/dist/services/openfoodfacts/openfoodfacts-service.js +583 -104
  77. package/dist/services/openfoodfacts/openfoodfacts-service.js.map +1 -1
  78. package/dist/services/openfoodfacts/types.d.ts +27 -4
  79. package/dist/services/openfoodfacts/types.d.ts.map +1 -1
  80. package/dist/services/openfoodfacts/types.js +15 -0
  81. package/dist/services/openfoodfacts/types.js.map +1 -1
  82. package/dist/services/openfoodfacts/user-agent.d.ts +11 -0
  83. package/dist/services/openfoodfacts/user-agent.d.ts.map +1 -0
  84. package/dist/services/openfoodfacts/user-agent.js +17 -0
  85. package/dist/services/openfoodfacts/user-agent.js.map +1 -0
  86. package/dist/services/taxonomy/taxonomy-service.d.ts +49 -14
  87. package/dist/services/taxonomy/taxonomy-service.d.ts.map +1 -1
  88. package/dist/services/taxonomy/taxonomy-service.js +124 -35
  89. package/dist/services/taxonomy/taxonomy-service.js.map +1 -1
  90. package/package.json +9 -3
  91. package/server.json +3 -3
  92. package/skills/openfoodfacts-guide/SKILL.md +5 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** openfoodfacts-mcp-server
4
- **Version:** 0.4.0
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.0
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.0-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
 
@@ -50,31 +50,37 @@ Packaged-food data from Open Food Facts, a crowd-sourced database of 3M+ product
50
50
 
51
51
  ### `off_get_product` <sub>tool</sub>
52
52
 
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), with `requested_fields` echoing what was fetched
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`, 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
56
58
 
57
59
  ---
58
60
 
59
61
  ### `off_search_products` <sub>tool</sub>
60
62
 
61
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`
62
- - `page_size` defaults to 20 and is served at up to 50 per call; a larger value (up to 10,000) 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`)
63
- - `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`
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`)
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
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
64
68
 
65
69
  ---
66
70
 
67
71
  ### `off_compare_products` <sub>tool</sub>
68
72
 
69
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
70
- - 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), and `completeness`; missing values stay absent, never imputed
71
- - `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
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
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
72
78
 
73
79
  ---
74
80
 
75
81
  ### `off_browse_taxonomy` <sub>tool</sub>
76
82
 
77
- - `facet` is one of `categories`, `labels`, `allergens`, `additives`, `countries`, `nova_groups`, `nutrition_grades`; `search` matches 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
83
+ - `facet` is one of `categories`, `labels`, `allergens`, `additives`, `countries`, `nova_groups`, `nutrition_grades`; `search` matches a substring of the tag ID, name, or a synonym ("shellfish" → `en:crustaceans`); `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 as `e314` resolves to `en:e314` and the match count is exact below that size
78
84
  - Returns `tags[]` of `id` and `name`, where `id` goes to `off_search_products` unchanged; `nova_groups` and `nutrition_grades` come back complete with `total_in_facet`
79
85
  - 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; omitting `search` returns only that sample
80
86
 
@@ -85,17 +91,19 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
85
91
  Open Food Facts-specific:
86
92
 
87
93
  - No API key; the identifying `User-Agent` Open Food Facts' terms require is built in
88
- - Per-endpoint request budgets (product 15/min, search 10/min, taxonomy 10/min); transient failures retry up to 4 attempts
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
89
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
90
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`)
91
- - 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
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
92
100
 
93
101
  Agent-friendly output:
94
102
 
95
103
  - Per-serving nutrition carries its denominator: `serving_size`, `serving_quantity`, `serving_quantity_unit`, and a note when none is recorded
96
104
  - Graceful partial failure: `off_compare_products` returns resolved rows alongside `not_found` and `failed`
97
- - Explicit count bounds: `total_is_lower_bound`, `last_page`, and `omitted` on searches
98
- - 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 retryable `upstream_error`, never an empty result or a not-found
105
+ - Explicit count bounds: `total_is_lower_bound`, `last_page`, `omitted`, and `outside_constraints` on searches
106
+ - 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 retryable `upstream_error`, or `upstream_timeout` when search flags a timeout, never an empty result or a not-found
99
107
 
100
108
  ## Getting started
101
109
 
@@ -134,6 +142,7 @@ No API key or account is needed: the server identifies itself to Open Food Facts
134
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` |
135
143
  | `OFF_RATE_LIMIT_SEARCH` | Search budget (requests/min). The default is Open Food Facts' published per-IP limit. | `10` |
136
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 |
137
146
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
138
147
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
139
148
  | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
@@ -185,6 +194,26 @@ docker run --rm -p 3010:3010 ghcr.io/cyanheads/openfoodfacts-mcp-server:latest
185
194
 
186
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.
187
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
+
188
217
  ## Project structure
189
218
 
190
219
  | Directory | Purpose |
@@ -193,6 +222,8 @@ The image defaults to HTTP transport, stateless session mode, and logs to `/var/
193
222
  | `src/config` | Server-specific environment variable parsing and validation with Zod. |
194
223
  | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). |
195
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`. |
196
227
  | `src/services/taxonomy` | Tag vocabulary — live resolution, offline sample, and tag-value canonicalization. |
197
228
  | `src/utils` | Markdown escaping for contributor-entered values in text output. |
198
229
  | `skills/` | Published Agent Skills for the agents that call the tools. |
@@ -0,0 +1,30 @@
1
+ ---
2
+ summary: "Checks every nutrient-filtered row against the caller bounds, caps each read with a total retry deadline, treats a flagged search timeout and a dropped connection as declared failures, labels prepared figures on product reads, adds prepared and carbohydrate figures to comparisons, and returns up to 200 taxonomy tags."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.4.1 — 2026-10-30
8
+
9
+ ## Changed
10
+
11
+ - **`off_get_product` prepared figures** ([#59](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/59)): Open Food Facts returns one preparation state of the nutrition per read, so a read of prepared-product figures carries `_prepared` keys and no named as-sold field. The text now prints those keys in "as prepared" sections and states that the read returned only prepared figures, not that the record holds no as-sold ones; `structuredContent` values are unchanged and the field descriptions name `_prepared` keys.
12
+ - **`off_compare_products` prepared figures** ([#59](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/59)): a row gains an optional `prepared` object with the same columns, read from `*_prepared_100g` keys and never substituted for an as-sold figure. The text adds an "as prepared" table, and a row with prepared figures only leaves the as-sold table. As-sold figures are not read for prepared products yet ([#73](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/73)).
13
+ - **`off_compare_products` carbohydrates** ([#71](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/71)): rows gain an optional `carbohydrates_100g`, and the text table a Carbohydrates column between saturated fat and sugars.
14
+ - **`off_browse_taxonomy` returns up to 200 tags** ([#70](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/70)), up from 100. `limit` has no maximum: a value above 200 returns the first 200 and, when more matched, says how many did, instead of failing validation. Each lookup now asks for up to 2,000 suggestions, above the largest answer measured (1,313), so the match count is exact below that size; an answer that reached it would be cut with nothing flagging it.
15
+ - **`off_search_products` `page_size`** ([#70](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/70)) has no maximum: a value over 10,000, previously rejected, is served at 50 and reported in `page_size_capped`.
16
+ - **Retry deadlines** ([#72](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/72)): all attempts and backoff of one read share a deadline, 35 s for a product or search read and 20 s for a taxonomy lookup, and the request in flight is cut off at expiry. A read that would have finished later now fails as the retryable `upstream_timeout`, or as `rate_limited` when its last finished attempt answered 429. A taxonomy lookup that expires does not fail its tool by itself: `off_browse_taxonomy` falls back to its offline sample with a notice, a tag filter is sent normalized, and an exclusion ends as the retryable `unrecognized_exclusion`. A search that canonicalizes a tag first can spend 55 s for one caller; one joined to a lookup whose first caller cancels can run longer ([#75](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/75)).
17
+ - **Docs and skill**: tool descriptions, the server instructions, the `openfoodfacts-guide` skill, and the README describe the row check, the deadlines, and prepared figures.
18
+
19
+ ## Fixed
20
+
21
+ - **`off_search_products` `nutrient_filters`** ([#64](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/64)): the text index compares `energy-kcal`, `fat`, `saturated-fat`, `carbohydrates`, and `sugars` as whole numbers, so `sugars lte 3` returned a row at 3.53 and `sugars gt 3` dropped matches. `gt` and `gte` on those five now widen to the whole number below, and every returned row is checked against every constraint at the index's 32-bit precision. Rows 0.4.0 returned outside the constraints are gone.
22
+ - **`outside_constraints`** ([#64](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/64)): new optional count of the rows left off a page by that check. A page can come back short or empty while later pages hold matches, and `total`, `total_is_lower_bound`, and `last_page` still describe the index's whole-number answer, which the text now says.
23
+ - **Flagged search timeouts** ([#68](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/68)): a text-search or autocomplete answer carrying `timed_out: true` was read as complete, reporting a partial page as exact and caching an empty autocomplete as `no_match`. The read now fails as the retryable `upstream_timeout`. A text search reports that reason, which `off_search_products` declares for it. A failed autocomplete is not cached: `off_browse_taxonomy` falls back to its offline sample with a notice, a tag filter is sent normalized, and an exclusion ends as the retryable `unrecognized_exclusion`.
24
+ - **`off_get_product` `serving_quantity`** ([#69](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/69)): requesting `serving_quantity` now also fetches `serving_quantity_unit`, so a unit the record holds is no longer reported as not recorded. A record with no unit still reads "unit not recorded".
25
+ - **Dropped connections** ([#74](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/74)): `off_get_product` and `off_search_products` failed with no `reason` when Open Food Facts closed the connection mid-body. They now fail as the retryable `upstream_error`. `off_compare_products` already listed such a barcode in `failed` as `upstream_error`, and `off_browse_taxonomy` already fell back to its offline sample with a notice; both now name the dropped connection instead of the runtime's message.
26
+
27
+ ## Dependencies
28
+
29
+ - **`@biomejs/biome`** ^2.6.3 → ^2.6.4 (dev).
30
+ - **`@types/node`** ^26.6.5 → ^26.6.6 (dev).
@@ -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, 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,ksBAAksB;IACpsB,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"browse-taxonomy.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/browse-taxonomy.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAWjD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EAuKhC,CAAC"}
1
+ {"version":3,"file":"browse-taxonomy.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/browse-taxonomy.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAUjD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EA6KhC,CAAC"}
@@ -3,14 +3,13 @@
3
3
  * @module mcp-server/tools/definitions/browse-taxonomy
4
4
  */
5
5
  import { tool, z } from '@cyanheads/mcp-ts-core';
6
- import { MAX_AUTOCOMPLETE_SIZE } from '../../../services/openfoodfacts/openfoodfacts-service.js';
7
6
  import { getTaxonomyService } from '../../../services/taxonomy/taxonomy-service.js';
8
7
  import { mdInline, mdInlineCode } from '../../../utils/markdown.js';
9
8
  /**
10
- * Most tags one call returns. A larger `limit`, up to the suggestions one live lookup reads
11
- * (`MAX_AUTOCOMPLETE_SIZE`), returns this many and discloses the cut when more matched.
9
+ * Most tags one call returns. A larger `limit` returns this many and discloses the cut when more
10
+ * matched.
12
11
  */
13
- const TAXONOMY_LIMIT_CAP = 100;
12
+ const TAXONOMY_LIMIT_CAP = 200;
14
13
  export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
15
14
  title: 'Browse Food Facts Taxonomy',
16
15
  description: 'Resolve a human term to the canonical Open Food Facts tag ID that off_search_products filters on. Covers categories, labels/certifications, allergens, additives, countries, NOVA groups, and Nutri-Score grades. Pass a search term to resolve against the Open Food Facts vocabulary, which holds tens of thousands of tags; omitting it returns only a small reference list for each facet except NOVA groups and Nutri-Score grades, which are complete. Most tag IDs use the "en:" prefix (e.g. "en:organic", "en:no-gluten", "en:crustaceans"); NOVA groups return bare digits "1"-"4" and Nutri-Score grades bare letters "a"-"e". Pass the id through to off_search_products exactly as returned. Category tags are frequently plural ("kombucha" resolves to "en:kombuchas"), so use the returned id rather than constructing one.',
@@ -35,13 +34,19 @@ export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
35
34
  .string()
36
35
  .optional()
37
36
  .describe('Term to resolve. Matched case-insensitively as a substring of the tag ID, the display name, or a common synonym of either ("shellfish" resolves to en:crustaceans, "gluten free" to en:no-gluten). A single word works best ("hummus", not "hummus dip"). Omit only to see a small reference list — Open Food Facts cannot list the full vocabulary without a term, so an unfiltered call is not a view of the full facet.'),
37
+ // `.int()` would advertise a `maximum` of 2^53 - 1 for a value the handler clamps to the cap.
38
38
  limit: z
39
39
  .number()
40
- .int()
40
+ .multipleOf(1, 'Send limit as a whole number, not a fractional one.')
41
+ // multipleOf tolerates float noise (20.000000000000004). It stays for what it advertises; this
42
+ // closes the gap, only where it passed, so 1.5 reports once.
43
+ .refine(Number.isInteger, {
44
+ error: 'Send limit as a whole number, not a fractional one.',
45
+ when: ({ issues }) => issues.length === 0,
46
+ })
41
47
  .min(1)
42
- .max(MAX_AUTOCOMPLETE_SIZE)
43
48
  .default(20)
44
- .describe(`Maximum entries to return (default 20). One call returns at most ${TAXONOMY_LIMIT_CAP}: a larger value, up to ${MAX_AUTOCOMPLETE_SIZE} (the most suggestions one lookup reads), returns the first ${TAXONOMY_LIMIT_CAP} and, when more matched, says how many did. There is no offset or page input: Open Food Facts offers no cursor for this lookup, so narrow the search term rather than paging. The tag spelling the term itself (e.g. "lentil" → en:lentils) is listed first among the live matches, so it is not the one a small limit cuts.`),
49
+ .describe(`Maximum entries to return (default 20). One call returns at most ${TAXONOMY_LIMIT_CAP}: a larger value returns the first ${TAXONOMY_LIMIT_CAP} and, when more matched, says how many did. There is no offset or page input: Open Food Facts offers no cursor for this lookup, so narrow the search term rather than paging. The tag spelling the term itself (e.g. "lentil" → en:lentils) is listed first among the live matches, so it is not the one a small limit cuts.`),
45
50
  }),
46
51
  output: z.object({
47
52
  facet: z.string().describe('The facet name that was queried (echoes the input).'),
@@ -64,7 +69,7 @@ export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
64
69
  notice: z
65
70
  .string()
66
71
  .optional()
67
- .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).`),
68
73
  truncated: z.boolean().optional().describe('True when more tags exist beyond the limit.'),
69
74
  shown: z.number().optional().describe('Number of tags returned.'),
70
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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"}