@cyanheads/openfoodfacts-mcp-server 0.3.0 → 0.3.1
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.
- package/AGENTS.md +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +3 -3
- package/changelog/0.3.x/0.3.1.md +16 -0
- package/dist/config/server-config.d.ts +12 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +13 -1
- package/dist/config/server-config.js.map +1 -1
- package/package.json +1 -1
- package/server.json +7 -7
package/AGENTS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openfoodfacts-mcp-server
|
|
4
|
-
**Version:** 0.3.
|
|
4
|
+
**Version:** 0.3.1
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
|
|
@@ -79,7 +79,7 @@ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
|
79
79
|
|
|
80
80
|
const ServerConfigSchema = z.object({
|
|
81
81
|
baseUrl: z.string().default('https://world.openfoodfacts.org').describe('Open Food Facts API base URL'),
|
|
82
|
-
rateLimitProduct: z.coerce.number().int().min(1).default(
|
|
82
|
+
rateLimitProduct: z.coerce.number().int().min(1).default(15).describe('Product read rate limit (requests/min)'),
|
|
83
83
|
rateLimitSearch: z.coerce.number().int().min(1).default(10).describe('Search rate limit (requests/min)'),
|
|
84
84
|
});
|
|
85
85
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** openfoodfacts-mcp-server
|
|
4
|
-
**Version:** 0.3.
|
|
4
|
+
**Version:** 0.3.1
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.11.0`
|
|
6
6
|
**Engines:** Bun ≥1.3.0, Node ≥24.0.0
|
|
7
7
|
**MCP SDK:** `@modelcontextprotocol/sdk` ^1.29.0
|
|
@@ -79,7 +79,7 @@ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
|
79
79
|
|
|
80
80
|
const ServerConfigSchema = z.object({
|
|
81
81
|
baseUrl: z.string().default('https://world.openfoodfacts.org').describe('Open Food Facts API base URL'),
|
|
82
|
-
rateLimitProduct: z.coerce.number().int().min(1).default(
|
|
82
|
+
rateLimitProduct: z.coerce.number().int().min(1).default(15).describe('Product read rate limit (requests/min)'),
|
|
83
83
|
rateLimitSearch: z.coerce.number().int().min(1).default(10).describe('Search rate limit (requests/min)'),
|
|
84
84
|
});
|
|
85
85
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/openfoodfacts-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/openfoodfacts-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -109,7 +109,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp
|
|
|
109
109
|
Open Food Facts-specific:
|
|
110
110
|
|
|
111
111
|
- No API key required — the identifying `User-Agent` header (required by OFF terms) is baked into the service layer
|
|
112
|
-
- Token-bucket rate limiting per endpoint class: product reads (~
|
|
112
|
+
- Token-bucket rate limiting per endpoint class: product reads (~15/min), search (~10/min), taxonomy resolution (~10/min). The product and search defaults are the per-IP ceilings Open Food Facts publishes; lower them on a shared outbound IP. A local refusal says so — it never reports itself as an Open Food Facts rate limit
|
|
113
113
|
- Automatic retry (3 attempts, 500ms base) for transient failures only — 5xx, timeouts, and 429 (honoring `Retry-After`), with HTML error page detection for 503 during high load. A 4xx is never retried; the upstream's own explanation is surfaced instead
|
|
114
114
|
- Nutriments normalized from raw hyphenated keys (`energy-kcal_100g`) to underscore form — the `_100g` and `_serving` variants of every nutrient on the record, with the macros as named fields and the rest in an open map that carries each nutrient's own unit (micronutrients are reported in grams, so calcium `0.071` is 71 mg)
|
|
115
115
|
- Live tag resolution for `off_browse_taxonomy` against the Open Food Facts taxonomy, merged behind a small in-process sample that covers offline operation and is authoritative for E-number lookups, which the upstream suggester answers poorly
|
|
@@ -241,7 +241,7 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
|
|
|
241
241
|
| Variable | Description | Default |
|
|
242
242
|
|:---------|:------------|:--------|
|
|
243
243
|
| `OFF_BASE_URL` | Open Food Facts API base URL. Override for local testing against a mock server. | `https://world.openfoodfacts.org` |
|
|
244
|
-
| `OFF_RATE_LIMIT_PRODUCT` | Product read rate limit (requests/min). | `
|
|
244
|
+
| `OFF_RATE_LIMIT_PRODUCT` | Product read rate limit (requests/min). Matches the 15 req/min/IP Open Food Facts documents for product reads. | `15` |
|
|
245
245
|
| `OFF_RATE_LIMIT_SEARCH` | Search rate limit (requests/min). | `10` |
|
|
246
246
|
| `OFF_RATE_LIMIT_TAXONOMY` | Taxonomy resolution rate limit (requests/min). A spent budget falls back to the offline sample rather than failing. | `10` |
|
|
247
247
|
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "OFF_RATE_LIMIT_PRODUCT now defaults to 15/min, the per-IP ceiling Open Food Facts publishes for product reads, down from a default that permitted 6.6x that rate."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.3.1 — 2026-07-26
|
|
8
|
+
|
|
9
|
+
## Fixed
|
|
10
|
+
|
|
11
|
+
- **`OFF_RATE_LIMIT_PRODUCT`** — default lowered from `100` to `15` requests/min, matching the 15 req/min/IP Open Food Facts documents for `GET /api/v*/product`. The prior default authorized exceeding the published ceiling, which Open Food Facts answers with an IP ban rather than a throttle. Operators running their own Product Opener instance can still raise it via the env var. ([#21](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/21))
|
|
12
|
+
- **README.md, docs/design.md, docs/idea.md, `server.json`** — corrected the four documentation references that still stated the old `100`/min product-read default.
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **`docs/design.md`** — documents that the taxonomy tier calls `search.openfoodfacts.org` (search-a-licious), a separate deployment the published Open Food Facts limits do not name and for which no limit is documented, so its `10`/min default is a conservative local choice rather than a mirror of a published figure. The text and tag search paths share one budget, so requests reaching the endpoint the documented 10/min covers stay within it regardless of how a query routes.
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
* @module config/server-config
|
|
4
4
|
*/
|
|
5
5
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
/**
|
|
7
|
+
* Rate-limit defaults sit at the per-IP ceilings Open Food Facts publishes, not above them: 15
|
|
8
|
+
* req/min for product reads (`GET /api/v2/product/…`) and 10 req/min for search
|
|
9
|
+
* (`GET /api/v2/search`). The documented consequence of exceeding either is an IP ban rather than a
|
|
10
|
+
* throttle, so there is no headroom to spend. A shared outbound IP needs a lower number than the
|
|
11
|
+
* published one — that is what the env vars are for, alongside operators running their own Product
|
|
12
|
+
* Opener instance who can raise them.
|
|
13
|
+
*
|
|
14
|
+
* The taxonomy tier is covered by neither published figure: it calls search.openfoodfacts.org
|
|
15
|
+
* (search-a-licious), a separate deployment the published limits do not name and for which no limit
|
|
16
|
+
* is documented. Its default matches the search tier as a conservative stand-in.
|
|
17
|
+
*/
|
|
6
18
|
declare const ServerConfigSchema: z.ZodObject<{
|
|
7
19
|
baseUrl: z.ZodDefault<z.ZodString>;
|
|
8
20
|
rateLimitProduct: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
@@ -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,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;;;;;;;;;;;GAWG;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"}
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
6
|
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
7
|
+
/**
|
|
8
|
+
* Rate-limit defaults sit at the per-IP ceilings Open Food Facts publishes, not above them: 15
|
|
9
|
+
* req/min for product reads (`GET /api/v2/product/…`) and 10 req/min for search
|
|
10
|
+
* (`GET /api/v2/search`). The documented consequence of exceeding either is an IP ban rather than a
|
|
11
|
+
* throttle, so there is no headroom to spend. A shared outbound IP needs a lower number than the
|
|
12
|
+
* published one — that is what the env vars are for, alongside operators running their own Product
|
|
13
|
+
* Opener instance who can raise them.
|
|
14
|
+
*
|
|
15
|
+
* The taxonomy tier is covered by neither published figure: it calls search.openfoodfacts.org
|
|
16
|
+
* (search-a-licious), a separate deployment the published limits do not name and for which no limit
|
|
17
|
+
* is documented. Its default matches the search tier as a conservative stand-in.
|
|
18
|
+
*/
|
|
7
19
|
const ServerConfigSchema = z.object({
|
|
8
20
|
baseUrl: z
|
|
9
21
|
.string()
|
|
@@ -13,7 +25,7 @@ const ServerConfigSchema = z.object({
|
|
|
13
25
|
.number()
|
|
14
26
|
.int()
|
|
15
27
|
.min(1)
|
|
16
|
-
.default(
|
|
28
|
+
.default(15)
|
|
17
29
|
.describe('Product read rate limit (requests/min)'),
|
|
18
30
|
rateLimitSearch: z.coerce
|
|
19
31
|
.number()
|
|
@@ -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,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,
|
|
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;;;;;;;;;;;GAWG;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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/openfoodfacts-mcp-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"mcpName": "io.github.cyanheads/openfoodfacts-mcp-server",
|
|
5
5
|
"description": "Look up food products by barcode, search by ingredient or nutrition filter, compare products side-by-side, and browse the canonical tag vocabulary via MCP. STDIO or Streamable HTTP.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "https://github.com/cyanheads/openfoodfacts-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.1",
|
|
10
10
|
"remotes": [
|
|
11
11
|
{
|
|
12
12
|
"type": "streamable-http",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
20
20
|
"identifier": "@cyanheads/openfoodfacts-mcp-server",
|
|
21
21
|
"runtimeHint": "bun",
|
|
22
|
-
"version": "0.3.
|
|
22
|
+
"version": "0.3.1",
|
|
23
23
|
"packageArguments": [
|
|
24
24
|
{
|
|
25
25
|
"type": "positional",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"name": "OFF_RATE_LIMIT_PRODUCT",
|
|
43
|
-
"description": "Product read rate limit (requests/min).",
|
|
43
|
+
"description": "Product read rate limit (requests/min). Matches the 15 req/min/IP Open Food Facts documents for product reads.",
|
|
44
44
|
"format": "string",
|
|
45
45
|
"isRequired": false,
|
|
46
|
-
"default": "
|
|
46
|
+
"default": "15"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"name": "OFF_RATE_LIMIT_SEARCH",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
77
77
|
"identifier": "@cyanheads/openfoodfacts-mcp-server",
|
|
78
78
|
"runtimeHint": "bun",
|
|
79
|
-
"version": "0.3.
|
|
79
|
+
"version": "0.3.1",
|
|
80
80
|
"packageArguments": [
|
|
81
81
|
{
|
|
82
82
|
"type": "positional",
|
|
@@ -97,10 +97,10 @@
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"name": "OFF_RATE_LIMIT_PRODUCT",
|
|
100
|
-
"description": "Product read rate limit (requests/min).",
|
|
100
|
+
"description": "Product read rate limit (requests/min). Matches the 15 req/min/IP Open Food Facts documents for product reads.",
|
|
101
101
|
"format": "string",
|
|
102
102
|
"isRequired": false,
|
|
103
|
-
"default": "
|
|
103
|
+
"default": "15"
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
"name": "OFF_RATE_LIMIT_SEARCH",
|