@cyanheads/openfoodfacts-mcp-server 0.1.8 → 0.1.10

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 (30) hide show
  1. package/AGENTS.md +35 -28
  2. package/CLAUDE.md +35 -28
  3. package/Dockerfile +11 -7
  4. package/README.md +14 -9
  5. package/changelog/0.1.x/0.1.10.md +22 -0
  6. package/changelog/0.1.x/0.1.9.md +34 -0
  7. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.d.ts +3 -3
  8. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts +25 -2
  9. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/compare-products.tool.js +76 -21
  11. package/dist/mcp-server/tools/definitions/compare-products.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts +33 -15
  13. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/get-product.tool.js +23 -2
  15. package/dist/mcp-server/tools/definitions/get-product.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/index.d.ts +126 -52
  17. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts +38 -5
  19. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/search-products.tool.js +147 -19
  21. package/dist/mcp-server/tools/definitions/search-products.tool.js.map +1 -1
  22. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts +32 -10
  23. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts.map +1 -1
  24. package/dist/services/openfoodfacts/openfoodfacts-service.js +235 -99
  25. package/dist/services/openfoodfacts/openfoodfacts-service.js.map +1 -1
  26. package/dist/services/openfoodfacts/types.d.ts +30 -0
  27. package/dist/services/openfoodfacts/types.d.ts.map +1 -1
  28. package/dist/services/taxonomy/taxonomy-service.d.ts.map +1 -1
  29. package/package.json +11 -10
  30. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** openfoodfacts-mcp-server
4
- **Version:** 0.1.8
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.12`
4
+ **Version:** 0.1.10
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
8
8
  **Zod:** ^4.4.3
@@ -17,7 +17,7 @@
17
17
  - **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
18
18
  - **Use `ctx.log`** for request-scoped logging. No `console` calls.
19
19
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
20
- - **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
20
+ - **Check `ctx.elicit`** for presence before calling.
21
21
  - **Secrets in env vars only** — never hardcoded.
22
22
  - **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
23
23
 
@@ -96,6 +96,8 @@ export function getServerConfig() {
96
96
 
97
97
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
98
98
 
99
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
100
+
99
101
  ### Server instructions
100
102
 
101
103
  `createApp({ instructions })` — optional server-level orientation, sent to clients on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
@@ -109,13 +111,14 @@ Handlers receive a unified `ctx` object. Key properties:
109
111
  | Property | Description |
110
112
  |:---------|:------------|
111
113
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
112
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
113
- | `ctx.elicit` | Ask user for structured input. **Check for presence first:** `if (ctx.elicit) { ... }` |
114
- | `ctx.sample` | Request LLM completion from the client. **Check for presence first:** `if (ctx.sample) { ... }` |
114
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
115
+ | `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
116
+ | `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
117
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
115
118
  | `ctx.signal` | `AbortSignal` for cancellation. |
116
119
  | `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
117
120
  | `ctx.requestId` | Unique request ID. |
118
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
121
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
119
122
 
120
123
  ---
121
124
 
@@ -123,7 +126,7 @@ Handlers receive a unified `ctx` object. Key properties:
123
126
 
124
127
  Handlers throw — the framework catches, classifies, and formats.
125
128
 
126
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move ( 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
129
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
127
130
 
128
131
  ```ts
129
132
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -135,7 +138,7 @@ errors: [
135
138
  ],
136
139
  async handler(input, ctx) {
137
140
  const item = await db.find(input.id);
138
- if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
141
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));
139
142
  return item;
140
143
  }
141
144
  ```
@@ -200,7 +203,7 @@ src/
200
203
 
201
204
  ## Skills
202
205
 
203
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
206
+ Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry.
204
207
 
205
208
  **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
206
209
 
@@ -220,7 +223,6 @@ Available skills:
220
223
  | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
221
224
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
222
225
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
223
- | `devcheck` | Lint, format, typecheck, audit |
224
226
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
225
227
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
226
228
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
@@ -228,12 +230,14 @@ Available skills:
228
230
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
229
231
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
230
232
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
233
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
231
234
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
232
235
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
233
236
  | `api-config` | AppConfig, parseConfig, env vars |
234
237
  | `api-context` | Context interface, logger, state, progress |
235
238
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
236
239
  | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
240
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
237
241
  | `api-services` | LLM, Speech, Graph services |
238
242
  | `api-testing` | createMockContext, test patterns |
239
243
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
@@ -248,30 +252,33 @@ When you complete a skill's checklist, check the boxes and add a completion time
248
252
 
249
253
  ## Commands
250
254
 
251
- **Runtime:** Scripts use `tsx`both `npm run <cmd>` and `bun run <cmd>` work. `bun` is slightly faster for script invocation but not required.
255
+ **Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
252
256
 
253
257
  | Command | Purpose |
254
258
  |:--------|:--------|
255
- | `npm run build` | Compile TypeScript |
256
- | `npm run rebuild` | Clean + build |
257
- | `npm run clean` | Remove build artifacts |
258
- | `npm run devcheck` | Lint + format + typecheck + security + changelog sync |
259
+ | `bun run build` | Compile TypeScript |
260
+ | `bun run rebuild` | Clean + build |
261
+ | `bun run clean` | Remove build artifacts |
262
+ | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
259
263
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
260
- | `npm run tree` | Generate directory structure doc |
261
- | `npm run format` | Auto-fix formatting (safe fixes only) |
262
- | `npm run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
263
- | `npm test` | Run tests |
264
- | `npm run start:stdio` | Production mode (stdio) |
265
- | `npm run start:http` | Production mode (HTTP) |
266
- | `npm run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
267
- | `npm run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
268
- | `npm run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
264
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
265
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
266
+ | `bun run list-skills` | Print the skill registry |
267
+ | `bun run tree` | Generate directory structure doc |
268
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
269
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
270
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
271
+ | `bun run start:stdio` | Production mode (stdio) |
272
+ | `bun run start:http` | Production mode (HTTP) |
273
+ | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
274
+ | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
275
+ | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
269
276
 
270
277
  ---
271
278
 
272
279
  ## Bundling
273
280
 
274
- `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
281
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
275
282
 
276
283
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
277
284
 
@@ -289,14 +296,14 @@ Each per-version file opens with YAML frontmatter:
289
296
  ---
290
297
  summary: "One-line headline, ≤350 chars" # required — powers the rollup index
291
298
  breaking: false # optional — true flags breaking changes
292
- security: false # optional — true flags security fixes
299
+ security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
293
300
  ---
294
301
 
295
302
  # 0.1.0 — YYYY-MM-DD
296
303
  ...
297
304
  ```
298
305
 
299
- `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
306
+ `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
300
307
 
301
308
  `agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
302
309
 
package/CLAUDE.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** openfoodfacts-mcp-server
4
- **Version:** 0.1.8
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.12`
4
+ **Version:** 0.1.10
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
8
8
  **Zod:** ^4.4.3
@@ -17,7 +17,7 @@
17
17
  - **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
18
18
  - **Use `ctx.log`** for request-scoped logging. No `console` calls.
19
19
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
20
- - **Check `ctx.elicit` / `ctx.sample`** for presence before calling.
20
+ - **Check `ctx.elicit`** for presence before calling.
21
21
  - **Secrets in env vars only** — never hardcoded.
22
22
  - **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
23
23
 
@@ -96,6 +96,8 @@ export function getServerConfig() {
96
96
 
97
97
  `parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
98
98
 
99
+ For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
100
+
99
101
  ### Server instructions
100
102
 
101
103
  `createApp({ instructions })` — optional server-level orientation, sent to clients on every `initialize` as session-level context. Use it for deployment guidance (connection aliases, regional notes, scope hints) instead of repeating the same context across tool descriptions. Client adoption is uneven, but there's no downside when set.
@@ -109,13 +111,14 @@ Handlers receive a unified `ctx` object. Key properties:
109
111
  | Property | Description |
110
112
  |:---------|:------------|
111
113
  | `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
112
- | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
113
- | `ctx.elicit` | Ask user for structured input. **Check for presence first:** `if (ctx.elicit) { ... }` |
114
- | `ctx.sample` | Request LLM completion from the client. **Check for presence first:** `if (ctx.sample) { ... }` |
114
+ | `ctx.state` | Tenant-scoped KV — `.get(key)`, `.set(key, value, { ttl? })`, `.delete(key)`, `.getMany(keys)`, `.list(prefix, { cursor, limit })`. Accepts any serializable value. |
115
+ | `ctx.elicit` | Ask user for structured input — form call `(message, schema)` or `.url(message, url)` for an external link. **Check for presence first:** `if (ctx.elicit) { ... }` |
116
+ | `ctx.enrich` | Success-path agent context (empty-result notices, query echo, pagination totals) — `ctx.enrich(...)` or `.notice()` / `.total()` / `.echo()` / `.truncated()`. Reaches `structuredContent` and `content[]`; lands only when the definition declares an `enrichment` block (no-op otherwise). |
117
+ | `ctx.content` | Non-text content blocks — `.image(data, mimeType)`, `.audio(data, mimeType)`, or `ctx.content(block)` for a raw block. Prepended to `content[]` after `format()`; never enters `structuredContent`. |
115
118
  | `ctx.signal` | `AbortSignal` for cancellation. |
116
119
  | `ctx.progress` | Task progress (present when `task: true`) — `.setTotal(n)`, `.increment()`, `.update(message)`. |
117
120
  | `ctx.requestId` | Unique request ID. |
118
- | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio. |
121
+ | `ctx.tenantId` | Tenant ID from JWT; `'default'` for stdio or HTTP with auth off. |
119
122
 
120
123
  ---
121
124
 
@@ -123,7 +126,7 @@ Handlers receive a unified `ctx` object. Key properties:
123
126
 
124
127
  Handlers throw — the framework catches, classifies, and formats.
125
128
 
126
- **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required descriptive metadata for the agent's next move ( 5 words, lint-validated); for the wire `data.recovery.hint` (mirrored into `content[]` text), pass explicitly at the throw site when dynamic context matters: `ctx.fail('reason', msg, { recovery: { hint: '...' } })`. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
129
+ **Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` / `resource()` to receive `ctx.fail(reason, …)` typed against the reason union. TypeScript catches typos at compile time, `data.reason` is auto-populated for observability, linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. Pass `ctx.recoveryFor('reason')` as the throw's data to put it on the wire (`data.recovery.hint`, mirrored into `content[]` text); override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring.
127
130
 
128
131
  ```ts
129
132
  import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
@@ -135,7 +138,7 @@ errors: [
135
138
  ],
136
139
  async handler(input, ctx) {
137
140
  const item = await db.find(input.id);
138
- if (!item) throw ctx.fail('no_match', `No item ${input.id}`);
141
+ if (!item) throw ctx.fail('no_match', `No item ${input.id}`, ctx.recoveryFor('no_match'));
139
142
  return item;
140
143
  }
141
144
  ```
@@ -200,7 +203,7 @@ src/
200
203
 
201
204
  ## Skills
202
205
 
203
- Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool.
206
+ Skills are modular instructions in `skills/` at the project root. Read them directly when a task matches — e.g., `skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry.
204
207
 
205
208
  **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
206
209
 
@@ -220,7 +223,6 @@ Available skills:
220
223
  | `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
221
224
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
222
225
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
223
- | `devcheck` | Lint, format, typecheck, audit |
224
226
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
225
227
  | `git-wrapup` | Land working-tree changes as a versioned commit + annotated tag — version bump, changelog, verify, tag. Local only. |
226
228
  | `release-and-publish` | Push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
@@ -228,12 +230,14 @@ Available skills:
228
230
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
229
231
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
230
232
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
233
+ | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
231
234
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
232
235
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
233
236
  | `api-config` | AppConfig, parseConfig, env vars |
234
237
  | `api-context` | Context interface, logger, state, progress |
235
238
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
236
239
  | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
240
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
237
241
  | `api-services` | LLM, Speech, Graph services |
238
242
  | `api-testing` | createMockContext, test patterns |
239
243
  | `api-utils` | Formatting, parsing, security, pagination, scheduling, telemetry helpers |
@@ -248,30 +252,33 @@ When you complete a skill's checklist, check the boxes and add a completion time
248
252
 
249
253
  ## Commands
250
254
 
251
- **Runtime:** Scripts use `tsx`both `npm run <cmd>` and `bun run <cmd>` work. `bun` is slightly faster for script invocation but not required.
255
+ **Runtime:** Scripts use Bun's native TypeScript execution — `bun run <cmd>` is the standard invocation. `npm run <cmd>` also works (npm delegates to bun).
252
256
 
253
257
  | Command | Purpose |
254
258
  |:--------|:--------|
255
- | `npm run build` | Compile TypeScript |
256
- | `npm run rebuild` | Clean + build |
257
- | `npm run clean` | Remove build artifacts |
258
- | `npm run devcheck` | Lint + format + typecheck + security + changelog sync |
259
+ | `bun run build` | Compile TypeScript |
260
+ | `bun run rebuild` | Clean + build |
261
+ | `bun run clean` | Remove build artifacts |
262
+ | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
259
263
  | `bun run audit:refresh` | Delete `bun.lock`, reinstall, and re-run `bun audit`. Use when `devcheck` flags a transitive advisory — Bun's `update` is sticky on transitive resolutions, so the advisory may be a stale-lockfile false positive. If it survives the refresh, it's real. |
260
- | `npm run tree` | Generate directory structure doc |
261
- | `npm run format` | Auto-fix formatting (safe fixes only) |
262
- | `npm run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
263
- | `npm test` | Run tests |
264
- | `npm run start:stdio` | Production mode (stdio) |
265
- | `npm run start:http` | Production mode (HTTP) |
266
- | `npm run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
267
- | `npm run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
268
- | `npm run bundle` | Build and pack as `.mcpb` for one-click Claude Desktop install |
264
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
265
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
266
+ | `bun run list-skills` | Print the skill registry |
267
+ | `bun run tree` | Generate directory structure doc |
268
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
269
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
270
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
271
+ | `bun run start:stdio` | Production mode (stdio) |
272
+ | `bun run start:http` | Production mode (HTTP) |
273
+ | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/*.md` |
274
+ | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
275
+ | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
269
276
 
270
277
  ---
271
278
 
272
279
  ## Bundling
273
280
 
274
- `npm run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
281
+ `bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips two classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`skills/`, `.claude/`, `.agents/`, `SKILL.md`) and platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
275
282
 
276
283
  **Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match.
277
284
 
@@ -289,14 +296,14 @@ Each per-version file opens with YAML frontmatter:
289
296
  ---
290
297
  summary: "One-line headline, ≤350 chars" # required — powers the rollup index
291
298
  breaking: false # optional — true flags breaking changes
292
- security: false # optional — true flags security fixes
299
+ security: false # optional — true ONLY for a source-code security fix, never a dependency CVE bump
293
300
  ---
294
301
 
295
302
  # 0.1.0 — YYYY-MM-DD
296
303
  ...
297
304
  ```
298
305
 
299
- `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section. When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
306
+ `breaking: true` renders a `· ⚠️ Breaking` badge — use it when consumers must update code on upgrade (signature changes, removed APIs, config renames). `security: true` renders a `· 🛡️ Security` badge and pairs with a `## Security` body section — set it only for a security fix in this server's *own source code*, never for a routine dependency or transitive CVE bump (record those under `## Dependencies`). When both are set, badges render `· ⚠️ Breaking · 🛡️ Security`.
300
307
 
301
308
  `agent-notes` is an optional free-form field for maintenance agents processing the release downstream. Content here won't appear in the rendered CHANGELOG — it's consumed by agents running the `maintenance` skill. Use it for adoption instructions that don't fit the human-facing sections: new files to create, fields to populate, one-time migration steps. Omit entirely when there's nothing to say.
302
309
 
package/Dockerfile CHANGED
@@ -4,15 +4,17 @@
4
4
  # This stage installs all dependencies (including dev), builds the TypeScript
5
5
  # source code into JavaScript, and prepares the production assets.
6
6
  # ==============================================================================
7
- FROM oven/bun:1.3 AS build
7
+ FROM oven/bun:1.3.14 AS build
8
8
 
9
9
  WORKDIR /usr/src/app
10
10
 
11
11
  # Copy dependency manifests for optimized layer caching
12
12
  COPY package.json bun.lock ./
13
13
 
14
- # Install all dependencies (including dev dependencies for building)
15
- RUN bun install --frozen-lockfile
14
+ # Install all dependencies (including dev dependencies for building).
15
+ # The BuildKit cache mount persists Bun's global package cache across builds.
16
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
17
+ bun install --frozen-lockfile --ignore-scripts
16
18
 
17
19
  # Copy the rest of the source code
18
20
  COPY . .
@@ -28,7 +30,7 @@ RUN bun run build
28
30
  # application. It uses a slim base image and only includes production
29
31
  # dependencies and build artifacts.
30
32
  # ==============================================================================
31
- FROM oven/bun:1.3-slim AS production
33
+ FROM oven/bun:1.3.14-slim AS production
32
34
 
33
35
  WORKDIR /usr/src/app
34
36
 
@@ -51,14 +53,16 @@ COPY package.json bun.lock ./
51
53
 
52
54
  # Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
53
55
  # that are not needed in the final production image.
54
- RUN bun install --production --frozen-lockfile --ignore-scripts
56
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
57
+ bun install --production --frozen-lockfile --ignore-scripts
55
58
 
56
59
  # Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
57
60
  # These are not bundled by default to keep the base image lean. Enable at build time
58
61
  # with: docker build --build-arg OTEL_ENABLED=true
59
62
  ARG OTEL_ENABLED=true
60
- RUN if [ "$OTEL_ENABLED" = "true" ]; then \
61
- bun add @hono/otel \
63
+ RUN --mount=type=cache,target=/root/.bun/install/cache \
64
+ if [ "$OTEL_ENABLED" = "true" ]; then \
65
+ bun add --omit=dev --ignore-scripts @hono/otel \
62
66
  @opentelemetry/instrumentation-http \
63
67
  @opentelemetry/exporter-metrics-otlp-http \
64
68
  @opentelemetry/exporter-trace-otlp-http \
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.1.8-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-^1.29.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-^6.0.3-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.1.10-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-^1.29.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.3.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -34,7 +34,7 @@ Four tools for working with [Open Food Facts](https://world.openfoodfacts.org/)
34
34
  | Tool | Description |
35
35
  |:-----|:------------|
36
36
  | `off_get_product` | Fetch a packaged food product by barcode. Returns name, brand, quantity, ingredients, allergens, additives, Nutri-Score, NOVA group, Green-Score, nutrition per 100g/serving, categories, labels, and data completeness. |
37
- | `off_search_products` | Search by text query and/or structured tag filters (category, brand, label, Nutri-Score grade, NOVA group, country). Returns summary rows with barcodes for follow-up lookups. |
37
+ | `off_search_products` | Search by text query and/or structured tag filters (category, brand, label, allergen, additive, Nutri-Score grade, NOVA group, country). Returns summary rows with barcodes for follow-up lookups. |
38
38
  | `off_compare_products` | Side-by-side nutrition and scoring comparison for 2–10 products by barcode. Returns a normalized table of energy, macros, salt, Nutri-Score, NOVA, and Green-Score. |
39
39
  | `off_browse_taxonomy` | Browse and search the canonical tag vocabulary (categories, labels, allergens, additives, countries, NOVA groups, Nutri-Score grades) for use as filter values in `off_search_products`. |
40
40
 
@@ -55,13 +55,16 @@ Fetch a packaged food product by barcode (EAN-13 or UPC).
55
55
  Search Open Food Facts by text and/or structured tag filters.
56
56
 
57
57
  - Full-text search across product names, brands, and ingredients
58
- - Structured filters: `categories_tag`, `brands_tag`, `labels_tag`, `nutrition_grade` (a–e), `nova_group` (1–4), `countries_tag`
58
+ - Structured filters: `categories_tag`, `brands_tag`, `labels_tag`, `allergens_tag`, `additives_tag`, `nutrition_grade` (a–e), `nova_group` (1–4), `countries_tag`
59
59
  - Text query and tag filters combine — a query with filters returns products that match the text *and* satisfy every filter (e.g., `query: "dark chocolate"` + `labels_tag: en:organic` + `countries_tag: en:france`)
60
- - All filter values are canonical tag IDs — use `off_browse_taxonomy` to resolve human terms (e.g., "organic" → `en:organic`)
61
- - Pagination via `page` (1-based) and `page_size` (1–50, default 20); response includes `total` count for computing total pages
60
+ - All filter values are canonical tag IDs — use `off_browse_taxonomy` to resolve human terms (e.g., "organic" → `en:organic`). `brands_tag` takes a brand slug and matches it exactly; open-ended brand wording belongs in `query`
61
+ - `additives_tag` filters only on searches with no text query — the text backend does not index additives, so pairing the two is rejected up front rather than returning an empty result set that looks like "no such product"
62
+ - Pagination via `page` (1-based) and `page_size` (1–50, default 20)
63
+ - `total` is exact on tag-only searches. Text searches stop counting at 10,000 matches, and when that ceiling is hit the response says so with `total_is_lower_bound: true` and renders the count as `10000+` — add filters for an exact figure
64
+ - Searches carrying a text query serve only the first 10,000 results — a deeper `page * page_size` is rejected up front with the highest reachable page, not sent and retried. Tag-only searches publish no window, but deep pages are refused unpredictably, so narrowing the filters beats paging far in
62
65
  - Returns summary rows (barcode, name, brand, Nutri-Score, NOVA, categories) — use `off_get_product` for full label data
63
66
  - Result counts reflect contributed products, not total products on the market
64
- - Search rate-limited to ~10 requests/min by the Open Food Facts API
67
+ - Search limited to ~10 requests/min by this server's own client-side budget, kept well inside what Open Food Facts asks of clients
65
68
 
66
69
  ---
67
70
 
@@ -73,6 +76,7 @@ Side-by-side nutrition and scoring comparison for 2–10 barcodes.
73
76
  - Returns a normalized comparison table: energy (kcal/100g), fat, saturated fat, sugars, salt, protein, fiber, Nutri-Score, NOVA group, and Green-Score
74
77
  - Missing nutrition data is preserved as `null` — comparisons are not imputed or estimated
75
78
  - `not_found` list identifies barcodes with no contributor record (partial results are not an error)
79
+ - `failed` list identifies barcodes whose fetch failed, with the per-barcode reason — kept separate from `not_found`, which claims the opposite. A failed barcode never blocks the rows that resolved
76
80
 
77
81
  ---
78
82
 
@@ -102,8 +106,8 @@ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp
102
106
  Open Food Facts-specific:
103
107
 
104
108
  - No API key required — the identifying `User-Agent` header (required by OFF terms) is baked into the service layer
105
- - Token-bucket rate limiting per endpoint class: product reads (~100/min), search (~10/min)
106
- - Automatic retry (3 attempts, 500ms base) with HTML error page detection for 503 during high load
109
+ - Token-bucket rate limiting per endpoint class: product reads (~100/min), search (~10/min). A local refusal says so — it never reports itself as an Open Food Facts rate limit
110
+ - 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
107
111
  - Nutriments normalized from raw hyphenated keys (`energy-kcal_100g`) to underscore form — only the `_100g` and `_serving` variants are returned
108
112
  - Embedded tag taxonomy for `off_browse_taxonomy` — curated 200+ category subset, full allergen/label/additive vocabularies
109
113
 
@@ -112,7 +116,8 @@ Agent-friendly output:
112
116
  - `found` field on every product response — explicit `false` when a barcode has no contributor record, not a thrown error
113
117
  - Missing fields signal incomplete crowd-sourced data, not product attribute absence — surfaced in descriptions and format output
114
118
  - Computed scores (Nutri-Score, NOVA, Green-Score) returned as-is with regional caveat notes — not interpreted or normalized to health claims
115
- - `not_found` list in `off_compare_products` allows partial batch comparisons without request failure
119
+ - `not_found` list in `off_compare_products` allows partial batch comparisons without request failure — and a barcode whose fetch failed lands in `failed` instead, so a transport error is never reported as "no contributor record"
120
+ - Every failure carries a declared `reason` and a recovery hint on both client surfaces — timeouts, upstream outages, upstream rejections, and rate limits each resolve to their own error code and their own next step
116
121
 
117
122
  ## Getting started
118
123
 
@@ -0,0 +1,22 @@
1
+ ---
2
+ summary: "Add allergens_tag/additives_tag search filters, correct the brands_tag exact-match description, and stop reporting the text backend's clipped 10,000-result count as an exact total."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.10 — 2026-07-26
8
+
9
+ ## Added
10
+
11
+ - **`off_search_products`** — `allergens_tag` and `additives_tag` structured filters. `allergens_tag` filters on both the tag and text routing paths; `additives_tag` filters only on tag-only searches, because the text-search backend does not index that facet — pairing it with `query` raises a new `additives_filter_needs_tag_search` validation error instead of silently returning zero results. ([#10](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/10))
12
+ - **`off_search_products`** — a required `total_is_lower_bound` output field. The text-search backend clips its reported hit count at 10,000 and exposes an `is_count_exact` flag when it does; the tool now reads that flag instead of comparing `total` against a local constant. `format()` renders `10000+ total products` when the flag is set. ([#18](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/18))
13
+
14
+ ## Changed
15
+
16
+ - **`off_search_products`** — truncation guidance for a clipped text search now states the reachable page bound and that the backend stopped counting, instead of computing a page total from the capped count.
17
+ - **`SearchResult`** (`src/services/openfoodfacts/types.ts`) — new normalized envelope shared by both search backends, replacing the ad hoc inline return types on `searchProducts`/`searchProductsByText`/`searchProductsByTags`.
18
+
19
+ ## Fixed
20
+
21
+ - **`off_search_products`** — `brands_tag`'s description no longer claims fuzzy/partial matching; both routing paths match the normalized brand slug exactly. ([#13](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/13))
22
+ - **`off_search_products`** — a text query with more than 10,000 matches no longer reports `total: 10000` as an exact database count.
@@ -0,0 +1,34 @@
1
+ ---
2
+ summary: "Declare typed reasons (upstream_error, upstream_timeout, upstream_rejected, rate_limited) across the service layer, split off_compare_products failures from not_found, pre-flight the text-search 10k-result window, and adopt mcp-ts-core ^0.11.0."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.9 — 2026-07-26
8
+
9
+ ## Added
10
+
11
+ - **`off_search_products`** — a `page_out_of_range` validation error rejects `page * page_size` beyond the text backend's 10,000-result window before the request is sent, naming the highest reachable page instead of surfacing a retried upstream failure. ([#19](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/19))
12
+ - **`off_compare_products`** — a `failed` output array reports barcodes whose fetch failed, distinct from `not_found` (confirmed no contributor record). A mixed batch keeps the rows that resolved instead of aborting or misreporting the failure as "not found." ([#11](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/11))
13
+ - **`upstream_timeout`, `upstream_rejected`, `rate_limited`** — declared on `off_get_product`, `off_search_products`, and `off_compare_products`, alongside the existing `upstream_error`. Every service-layer failure now carries `data.reason` and `data.recovery.hint` on both client surfaces. ([#12](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/12))
14
+
15
+ ## Changed
16
+
17
+ - **`openfoodfacts-service.ts`** — every fetch site now goes through `fetchWithTimeout`/`ctx.recoveryFor`, replacing the hand-rolled status ladder. A 4xx is classified `upstream_rejected` and is not retried (previously retried 4x as `ServiceUnavailable`); the upstream's own `detail` body is surfaced in the message. 5xx, timeouts, and 429 remain retryable.
18
+ - **`RateLimiter.check()`** — takes `ctx` and raises `rate_limited` naming this server's own budget and the seconds until a slot frees, rather than attributing the refusal to Open Food Facts.
19
+ - **`off_search_products` truncation guidance** — counts pages the backend will actually serve (capped at the 10k-result window on the text path) instead of the total implied by the match count.
20
+
21
+ ## Fixed
22
+
23
+ - **`off_compare_products`** — a rejected settlement is no longer reclassified as `not_found` when its error message lacks the literal string "Open Food Facts." Classification is by settlement state, never message text. ([#11](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/11))
24
+
25
+ ## Dependencies
26
+
27
+ - `@cyanheads/mcp-ts-core` `^0.10.12` → `^0.11.0`
28
+ - `typescript` `^6.0.3` → `^7.0.2`
29
+ - `@biomejs/biome` `^2.5.0` → `^2.5.5`
30
+ - `@types/node` `^26.0.0` → `^26.1.1`
31
+ - `ignore` `^7.0.5` → `^7.0.6`
32
+ - `tsc-alias` `^1.8.17` → `^1.9.1`
33
+ - `vitest` `^4.1.9` → `^4.1.10`
34
+ - `@socketsecurity/bun-security-scanner` `^1.1.2` added (Bun install-time malware/typosquat/CVE scanning, configured in `bunfig.toml`)
@@ -5,11 +5,11 @@
5
5
  import { z } from '@cyanheads/mcp-ts-core';
6
6
  export declare const offBrowseTaxonomyTool: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
7
7
  facet: z.ZodEnum<{
8
- categories: "categories";
9
- labels: "labels";
10
- allergens: "allergens";
11
8
  additives: "additives";
9
+ allergens: "allergens";
10
+ categories: "categories";
12
11
  countries: "countries";
12
+ labels: "labels";
13
13
  nova_groups: "nova_groups";
14
14
  nutrition_grades: "nutrition_grades";
15
15
  }>;
@@ -26,11 +26,34 @@ export declare const offCompareProductsTool: import("@cyanheads/mcp-ts-core").To
26
26
  }, z.core.$strip>>;
27
27
  succeeded: z.ZodNumber;
28
28
  not_found: z.ZodArray<z.ZodString>;
29
+ failed: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ barcode: z.ZodString;
31
+ reason: z.ZodString;
32
+ error: z.ZodString;
33
+ }, z.core.$strip>>>;
29
34
  }, z.core.$strip>, readonly [{
30
35
  readonly reason: "upstream_error";
31
36
  readonly code: JsonRpcErrorCode.ServiceUnavailable;
32
- readonly when: "Open Food Facts API returns 5xx or is unreachable for one or more barcodes";
37
+ readonly when: "Open Food Facts returns 5xx, serves an HTML error page, or is unreachable surfaced per barcode in failed[]";
38
+ readonly retryable: true;
39
+ readonly recovery: "Retry the barcodes listed in failed after a brief pause. Rows that already resolved are kept, so only the failures need repeating.";
40
+ }, {
41
+ readonly reason: "upstream_timeout";
42
+ readonly code: JsonRpcErrorCode.Timeout;
43
+ readonly when: "Open Food Facts did not answer within the request deadline — surfaced per barcode in failed[]";
44
+ readonly retryable: true;
45
+ readonly recovery: "Retry the barcodes listed in failed, or fetch them one at a time with off_get_product to reduce the load per request.";
46
+ }, {
47
+ readonly reason: "upstream_rejected";
48
+ readonly code: JsonRpcErrorCode.InvalidParams;
49
+ readonly when: "Open Food Facts answers 4xx for a barcode — surfaced per barcode in failed[]";
50
+ readonly retryable: false;
51
+ readonly recovery: "Do not retry unchanged. Check the digits of the barcodes listed in failed, then look them up individually with off_get_product.";
52
+ }, {
53
+ readonly reason: "rate_limited";
54
+ readonly code: JsonRpcErrorCode.RateLimited;
55
+ readonly when: "This server's own per-minute request budget is spent, or Open Food Facts answers 429 — surfaced per barcode in failed[]";
33
56
  readonly retryable: true;
34
- readonly recovery: "Retry after a brief pause. Partial failures surface individual barcodes in not_found verify those barcodes with off_get_product.";
57
+ readonly recovery: "Wait the seconds given in the failed entry, then retry only those barcodes. Compare fewer barcodes per call to stay inside the budget.";
35
58
  }], undefined>;
36
59
  //# 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,EAAsB,MAAM,+BAA+B,CAAC;AAkErF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA0NjC,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;AA4F3E,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsQjC,CAAC"}