@cyanheads/openfoodfacts-mcp-server 0.1.7 → 0.1.9

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 (32) hide show
  1. package/AGENTS.md +35 -28
  2. package/CLAUDE.md +35 -28
  3. package/Dockerfile +11 -7
  4. package/README.md +12 -8
  5. package/changelog/0.1.x/0.1.8.md +21 -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/browse-taxonomy.tool.js +2 -2
  9. package/dist/mcp-server/tools/definitions/browse-taxonomy.tool.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts +25 -2
  11. package/dist/mcp-server/tools/definitions/compare-products.tool.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/compare-products.tool.js +77 -22
  13. package/dist/mcp-server/tools/definitions/compare-products.tool.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts +33 -15
  15. package/dist/mcp-server/tools/definitions/get-product.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/get-product.tool.js +23 -2
  17. package/dist/mcp-server/tools/definitions/get-product.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/index.d.ts +116 -51
  19. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts +28 -4
  21. package/dist/mcp-server/tools/definitions/search-products.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/search-products.tool.js +88 -17
  23. package/dist/mcp-server/tools/definitions/search-products.tool.js.map +1 -1
  24. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts +46 -9
  25. package/dist/services/openfoodfacts/openfoodfacts-service.d.ts.map +1 -1
  26. package/dist/services/openfoodfacts/openfoodfacts-service.js +274 -108
  27. package/dist/services/openfoodfacts/openfoodfacts-service.js.map +1 -1
  28. package/dist/services/openfoodfacts/types.d.ts +5 -1
  29. package/dist/services/openfoodfacts/types.d.ts.map +1 -1
  30. package/dist/services/taxonomy/taxonomy-service.d.ts.map +1 -1
  31. package/package.json +11 -10
  32. 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.7
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.12`
4
+ **Version:** 0.1.9
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.7
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.10.12`
4
+ **Version:** 0.1.9
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.7-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.9-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
 
@@ -56,11 +56,13 @@ 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
58
  - Structured filters: `categories_tag`, `brands_tag`, `labels_tag`, `nutrition_grade` (a–e), `nova_group` (1–4), `countries_tag`
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`)
59
60
  - All filter values are canonical tag IDs — use `off_browse_taxonomy` to resolve human terms (e.g., "organic" → `en:organic`)
60
61
  - Pagination via `page` (1-based) and `page_size` (1–50, default 20); response includes `total` count for computing total pages
62
+ - 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
61
63
  - Returns summary rows (barcode, name, brand, Nutri-Score, NOVA, categories) — use `off_get_product` for full label data
62
64
  - Result counts reflect contributed products, not total products on the market
63
- - Search rate-limited to ~10 requests/min by the Open Food Facts API
65
+ - Search limited to ~10 requests/min by this server's own client-side budget, kept well inside what Open Food Facts asks of clients
64
66
 
65
67
  ---
66
68
 
@@ -68,10 +70,11 @@ Search Open Food Facts by text and/or structured tag filters.
68
70
 
69
71
  Side-by-side nutrition and scoring comparison for 2–10 barcodes.
70
72
 
71
- - Fetches all products in parallel
73
+ - Accepts 2–10 barcodes, compared in the order provided
72
74
  - Returns a normalized comparison table: energy (kcal/100g), fat, saturated fat, sugars, salt, protein, fiber, Nutri-Score, NOVA group, and Green-Score
73
75
  - Missing nutrition data is preserved as `null` — comparisons are not imputed or estimated
74
76
  - `not_found` list identifies barcodes with no contributor record (partial results are not an error)
77
+ - `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
75
78
 
76
79
  ---
77
80
 
@@ -81,8 +84,8 @@ Browse the canonical Open Food Facts tag vocabulary before building `off_search_
81
84
 
82
85
  - Facets: `categories`, `labels`, `allergens`, `additives`, `countries`, `nova_groups`, `nutrition_grades`
83
86
  - Optional `search` parameter: case-insensitive substring match against tag ID or display name (e.g., `"gluten"` → `en:gluten`, `en:no-gluten`, `en:no-added-gluten`)
84
- - Taxonomy is embedded (not fetched live) because the OFF taxonomy API is unavailable to anonymous bot clients at current traffic levels
85
- - Categories facet has 200K+ entries always include a `search` term when browsing categories
87
+ - Taxonomy is served from a curated in-process vocabulary no live network call, so lookups are instant and offline
88
+ - The categories facet is broadpass a `search` term to narrow it to the relevant tags
86
89
  - `limit` controls results returned (1–100, default 20)
87
90
 
88
91
  ---
@@ -101,8 +104,8 @@ Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp
101
104
  Open Food Facts-specific:
102
105
 
103
106
  - No API key required — the identifying `User-Agent` header (required by OFF terms) is baked into the service layer
104
- - Token-bucket rate limiting per endpoint class: product reads (~100/min), search (~10/min)
105
- - Automatic retry (3 attempts, 500ms base) with HTML error page detection for 503 during high load
107
+ - 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
108
+ - 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
106
109
  - Nutriments normalized from raw hyphenated keys (`energy-kcal_100g`) to underscore form — only the `_100g` and `_serving` variants are returned
107
110
  - Embedded tag taxonomy for `off_browse_taxonomy` — curated 200+ category subset, full allergen/label/additive vocabularies
108
111
 
@@ -111,7 +114,8 @@ Agent-friendly output:
111
114
  - `found` field on every product response — explicit `false` when a barcode has no contributor record, not a thrown error
112
115
  - Missing fields signal incomplete crowd-sourced data, not product attribute absence — surfaced in descriptions and format output
113
116
  - Computed scores (Nutri-Score, NOVA, Green-Score) returned as-is with regional caveat notes — not interpreted or normalized to health claims
114
- - `not_found` list in `off_compare_products` allows partial batch comparisons without request failure
117
+ - `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"
118
+ - 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
115
119
 
116
120
  ## Getting started
117
121
 
@@ -0,0 +1,21 @@
1
+ ---
2
+ summary: "Combine free-text queries with tag filters in off_search_products via a single search-a-licious Lucene query, escape free text against injection, rewrite tool descriptions to drop implementation details, and derive USER_AGENT from package.json."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.8 — 2026-07-05
8
+
9
+ ## Added
10
+
11
+ - **`off_search_products`** — a text `query` now combines with tag filters instead of the two being mutually exclusive; `buildTextSearchQuery` folds recognized filters (`categories_tag`, `brands_tag`, `labels_tag`, `nutrition_grade`, `nova_group`, `countries_tag`) into one search-a-licious Lucene `q` alongside the free text. Score/NOVA filters use different field names on this path (`nutriscore_grade`, `nova_group`, bare values, no `_tags` suffix) than the tag-only `/api/v2/search` route (`nutrition_grades_tags`, `nova_groups_tags`). ([#6](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/6))
12
+
13
+ ## Changed
14
+
15
+ - **Tool descriptions** — `off_search_products`, `off_compare_products`, and `off_browse_taxonomy` descriptions rewritten to drop backend implementation details (mutually-exclusive routing language, "fetched in parallel," the taxonomy's anonymous-bot-client access rationale, directive `limit` phrasing) in favor of user-facing contract text; mirrored in `README.md` and `docs/design.md`. ([#7](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/7))
16
+ - **`docs/design.md`** — stale field-name reference corrected: `nutrition_grades`/`nova_groups` → `nutrition_grades_tags`/`nova_groups_tags` (the bare keys are silently ignored by `/api/v2/search`; documentation-only, matches existing shipped behavior).
17
+
18
+ ## Fixed
19
+
20
+ - **Lucene query injection** — free-text terms passed to `off_search_products` are now escaped (`escapeLuceneQueryText`) before being folded into the Lucene `q`; previously an unescaped reserved character (e.g. `query: "brands: nutella"`) was parsed as a `field:value` filter clause instead of literal search text.
21
+ - **`USER_AGENT` version drift** — read from `package.json` at load instead of a hardcoded string, so the header sent to Open Food Facts can no longer fall behind the shipped release. ([#8](https://github.com/cyanheads/openfoodfacts-mcp-server/issues/8))
@@ -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
  }>;
@@ -6,7 +6,7 @@ import { tool, z } from '@cyanheads/mcp-ts-core';
6
6
  import { getTaxonomyService } from '../../../services/taxonomy/taxonomy-service.js';
7
7
  export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
8
8
  title: 'Browse Food Facts Taxonomy',
9
- description: 'Browse and search the canonical tag vocabulary for Open Food Facts filter facets. Returns tag IDs and display names for use as filter values in off_search_products. Covers categories, labels/certifications, allergens, additives, countries, NOVA groups, and Nutri-Score grades. The taxonomy is embedded — not fetched live — because the OFF taxonomy API is unavailable to anonymous bot clients. Tag IDs use the "en:" prefix convention (e.g. "en:organic", "en:gluten-free", "en:milk"). Always use these tag IDs as filter values, not plain English terms.',
9
+ description: 'Browse and search the canonical tag vocabulary for Open Food Facts filter facets. Returns tag IDs and display names for use as filter values in off_search_products. Covers categories, labels/certifications, allergens, additives, countries, NOVA groups, and Nutri-Score grades. Tag IDs use the "en:" prefix convention (e.g. "en:organic", "en:gluten-free", "en:milk"). Use these tag IDs as filter values, not plain English terms.',
10
10
  annotations: {
11
11
  readOnlyHint: true,
12
12
  idempotentHint: true,
@@ -34,7 +34,7 @@ export const offBrowseTaxonomyTool = tool('off_browse_taxonomy', {
34
34
  .min(1)
35
35
  .max(100)
36
36
  .default(20)
37
- .describe('Maximum entries to return (1–100, default 20). Categories has many entries always provide a search term when browsing categories.'),
37
+ .describe('Maximum entries to return (1–100, default 20). The categories facet is broad; a search term narrows it to the relevant tags.'),
38
38
  }),
39
39
  output: z.object({
40
40
  facet: z.string().describe('The facet name that was queried (echoes the input).'),
@@ -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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"}