mcp-bbc-goodfood 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions
5
+ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.1.0
8
+
9
+ ### Added
10
+
11
+ - `list_filters`, which publishes the axes a recipe search can be narrowed
12
+ along, with the values each one takes and how many recipes carry them. Pass a
13
+ query to measure the counts inside one search, or leave it out for the site's
14
+ whole listing.
15
+ - `search_recipes`, which searches the site and returns a listing. It narrows by
16
+ diet, cuisine, meal type, difficulty, a bound on time, calories, servings or
17
+ rating, and `exclude_premium` leaves out what sits behind the site's
18
+ subscription. Every row carries the path of its page.
19
+ - `get_recipe`, which reads one recipe from the path a search row carries: its
20
+ ingredients, its steps, its times, its rating and its nutrition. A recipe
21
+ behind the site's subscription comes back with everything except its
22
+ ingredients and steps, and says so.
23
+ - `scale_ingredients`, which puts a list of ingredient lines to a different
24
+ number of people without reading anything on the site. Every line says under
25
+ `scaling` whether the arithmetic landed exactly or whether the figure moved to
26
+ stay usable in a kitchen.
27
+ A quantity is said in the unit that states it exactly: 2 g divided by ten come
28
+ back as 200 mg, and 200 g multiplied by twenty come back as 4 kg. A figure
29
+ rises only when the larger unit states it exactly, so 7492.5 g stays in grams.
30
+ - `servings` on `get_recipe`, which puts the recipe to that many people. A
31
+ recipe whose page states no number of servings is left as the site publishes
32
+ it, and a note says why.
33
+ - `total_is_ceiling` on the answers that carry a total, which says the figure
34
+ landed on the largest number of rows one search will serve and therefore
35
+ states a floor rather than a count.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damien Bouissou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,529 @@
1
+ # mcp-bbc-goodfood
2
+
3
+ [![npm](https://img.shields.io/npm/v/mcp-bbc-goodfood.svg)](https://www.npmjs.com/package/mcp-bbc-goodfood)
4
+ [![CI](https://github.com/smeet666/mcp-bbc-goodfood/actions/workflows/ci.yml/badge.svg)](https://github.com/smeet666/mcp-bbc-goodfood/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/mcp-bbc-goodfood.svg)](./LICENSE)
6
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-bbc-goodfood)
7
+ [![Glama](https://glama.ai/mcp/servers/smeet666/mcp-bbc-goodfood/badges/score.svg)](https://glama.ai/mcp/servers/smeet666/mcp-bbc-goodfood)
8
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=bbc-goodfood&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1iYmMtZ29vZGZvb2QiXX0%3D)
9
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=bbc-goodfood&config=%7B%22name%22%3A%22bbc-goodfood%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-bbc-goodfood%22%5D%7D)
10
+
11
+ An MCP server that reads recipes on [BBC Good Food](https://www.bbcgoodfood.com).
12
+ Read-only, no API key, no account.
13
+
14
+ _[Version française](#mcp-bbc-goodfood-français)_
15
+
16
+ ---
17
+
18
+ ## Why it exists
19
+
20
+ BBC Good Food accepts any value on a search facet, and answers one it does not
21
+ know with a total of zero. Ask for `glutenfree` instead of `gluten-free` and the
22
+ site says nothing matches, with the same confidence it says 48 recipes match the
23
+ correct spelling. A model that guesses a spelling gets a confident absence
24
+ instead of a refusal.
25
+
26
+ This server publishes the vocabulary, so the question can be asked properly.
27
+
28
+ ## The tools
29
+
30
+ ### `list_filters`
31
+
32
+ Lists the axes a recipe search can be narrowed along, with the values each one
33
+ takes and how many recipes carry them.
34
+
35
+ | Argument | Type | Meaning |
36
+ | -------- | ---------------- | --------------------------------------------------------------------------------- |
37
+ | `query` | string, optional | Measure the counts inside this search. Leave it out for the site's whole listing. |
38
+
39
+ The site counts its facets over the rows a search returns, so the two answer
40
+ different questions. Within `chicken`, `diet` reports `gluten-free 48`. Across
41
+ the whole listing it reports `gluten-free 2810`. Neither is comparable to the
42
+ other, and the answer says which scope it measured.
43
+
44
+ ```json
45
+ {
46
+ "query": "chicken",
47
+ "filters": [
48
+ {
49
+ "name": "diet",
50
+ "label": "Diets",
51
+ "options": [{ "value": "gluten-free", "label": "Gluten-free", "count": 48 }],
52
+ "option_count": 10
53
+ }
54
+ ],
55
+ "filter_count": 9,
56
+ "total_available": 363,
57
+ "total_is_ceiling": false,
58
+ "source": "BBC Good Food",
59
+ "notes": ["…"]
60
+ }
61
+ ```
62
+
63
+ ### `search_recipes`
64
+
65
+ Searches recipes and returns a listing. Every row carries the path of its page,
66
+ which is what `get_recipe` reads.
67
+
68
+ | Argument | Type | Meaning |
69
+ | ----------------------------------------------------------------- | ----------------- | --------------------------------------------------- |
70
+ | `query` | string | A dish, an ingredient, a technique. |
71
+ | `limit`, `page` | integer, optional | Rows per page and which page. Defaults to 30 and 1. |
72
+ | `sort` | string, optional | How the site orders the listing. |
73
+ | `diet`, `cuisine`, `meal_type`, `difficulty` | string, optional | A value `list_filters` publishes. |
74
+ | `max_total_minutes`, `max_calories`, `min_servings`, `min_rating` | number, optional | A bound on the recipe. |
75
+ | `exclude_premium` | boolean, optional | Leave out what sits behind the site's subscription. |
76
+
77
+ The site accepts any value on a facet and answers one it does not know with a
78
+ total of zero, so a guessed spelling comes back as a confident absence rather
79
+ than as a refusal. Call `list_filters` first.
80
+
81
+ ### `get_recipe`
82
+
83
+ Reads one recipe: its ingredients, its steps, its times, its rating and its
84
+ nutrition.
85
+
86
+ | Argument | Type | Meaning |
87
+ | ---------- | ----------------- | ---------------------------------------------------------- |
88
+ | `id` | string | The page's own path, as a `search_recipes` row carries it. |
89
+ | `servings` | integer, optional | Put the ingredients to this many people. |
90
+
91
+ A recipe behind the site's subscription comes back with everything except its
92
+ ingredients and its steps, and says so. The page carries them, which is exactly
93
+ why the rule exists: reading past a wall the site put in front of its own
94
+ readers would make this server the way around it.
95
+
96
+ ### `scale_ingredients`
97
+
98
+ Puts a list of ingredient lines to a different number of people, without reading
99
+ anything on the site.
100
+
101
+ | Argument | Type | Meaning |
102
+ | ------------------------------ | ----------------- | ------------------------------------------------ |
103
+ | `ingredients` | string[] | The lines to scale, as a recipe writes them. |
104
+ | `factor` | number, optional | What to multiply every quantity by. |
105
+ | `from_servings`, `to_servings` | integer, optional | The two ends of the change, instead of a factor. |
106
+
107
+ ```json
108
+ {
109
+ "factor": 0.5,
110
+ "ingredients": [
111
+ {
112
+ "text": "100 g plain flour",
113
+ "original": "200g plain flour",
114
+ "scaling": "scaled",
115
+ "amount": 100,
116
+ "amount_max": null,
117
+ "unit": "g"
118
+ },
119
+ {
120
+ "text": "2 eggs",
121
+ "original": "3 eggs",
122
+ "scaling": "rounded",
123
+ "amount": 2,
124
+ "amount_max": null,
125
+ "unit": null
126
+ },
127
+ {
128
+ "text": "salt and pepper",
129
+ "original": "salt and pepper",
130
+ "scaling": "unscaled",
131
+ "amount": null,
132
+ "amount_max": null,
133
+ "unit": null
134
+ }
135
+ ],
136
+ "scaled_count": 1,
137
+ "rounded_count": 1,
138
+ "unscaled_count": 1,
139
+ "source": "BBC Good Food",
140
+ "notes": ["…"]
141
+ }
142
+ ```
143
+
144
+ ## What the answers refuse to overstate
145
+
146
+ **The published values are a shortlist.** The site shows the ten most frequent
147
+ values on an axis and accepts others it never lists: `cuisine=mexican` narrows a
148
+ search to 306 recipes without appearing among the ten. The answer says the list
149
+ is an excerpt rather than calling it the set of accepted values, because a
150
+ server that called it that would refuse values that work.
151
+
152
+ **A total can be a floor.** The site serves at most 10 000 rows for one search
153
+ and stops there. A total landing exactly on that figure was cut, so it states a
154
+ floor rather than a count, and `total_is_ceiling` says so.
155
+
156
+ **A count the site published nothing for is `null`, never `0`.** On a scale that
157
+ starts at zero the two would be indistinguishable. A rating runs from one star to
158
+ five, so a recipe nobody has rated comes back as `null` rather than as a recipe
159
+ rated zero.
160
+
161
+ **A recalculated quantity says that it was recalculated.** The figures a scaled
162
+ list carries are this server's arithmetic and not the site's, and every line says
163
+ under `scaling` whether the arithmetic landed exactly or whether the figure moved
164
+ to stay usable in a kitchen. Three eggs halved come back as two, because half an
165
+ egg is not an amount a kitchen measures out; a cook told `2` deserves to know
166
+ which of the two happened.
167
+
168
+ **A quantity is said in the unit that states it exactly.** Two grams divided by
169
+ ten come back as 200 mg, and two hundred grams multiplied by twenty come back as
170
+ 4 kg. A figure only moves up the ladder when the larger unit states it exactly:
171
+ 1875 g reads 1.875 kg, and 7492.5 g stays in grams because 7.4925 kg would need a
172
+ fourth decimal. A cook who cannot weigh the figure back is worse served by the
173
+ shorter one.
174
+
175
+ **A recipe whose page states no servings cannot be put to a number of people.**
176
+ The `servings` argument is then left without effect and a note says so, because
177
+ the multiplication would have to start from a figure the site never wrote.
178
+
179
+ ## Install
180
+
181
+ ```bash
182
+ npx mcp-bbc-goodfood
183
+ ```
184
+
185
+ ### Claude Code
186
+
187
+ ```bash
188
+ claude mcp add bbc-goodfood -- npx -y mcp-bbc-goodfood
189
+ ```
190
+
191
+ ### Any MCP client
192
+
193
+ ```json
194
+ {
195
+ "mcpServers": {
196
+ "bbc-goodfood": {
197
+ "command": "npx",
198
+ "args": ["-y", "mcp-bbc-goodfood"]
199
+ }
200
+ }
201
+ }
202
+ ```
203
+
204
+ ### Container
205
+
206
+ ```bash
207
+ docker build -t mcp-bbc-goodfood .
208
+ docker run -i --rm mcp-bbc-goodfood
209
+ ```
210
+
211
+ The container needs to reach `www.bbcgoodfood.com` and nothing else. It takes no
212
+ credentials, because there are none to take.
213
+
214
+ ## Settings
215
+
216
+ Every setting is an environment variable, and none is required. A value outside
217
+ its range is refused with a line on stderr and the default stands: a setting that
218
+ cannot take effect says so rather than being quietly clamped.
219
+
220
+ | Variable | Default | Range |
221
+ | ----------------------- | ------- | ---------------------------------------------------------------------------------- |
222
+ | `BGF_USER_AGENT` | — | Your own identifier. This project's stays appended, so the site can reach a human. |
223
+ | `BGF_MIN_INTERVAL_MS` | 1500 | 1000 to 60000 |
224
+ | `BGF_TIMEOUT_MS` | 20000 | 1000 to 120000 |
225
+ | `BGF_MAX_RETRIES` | 3 | 0 to 8 |
226
+ | `BGF_CACHE_TTL_MS` | 900000 | 0 to 86400000, 0 turns storage off |
227
+ | `BGF_CACHE_MAX_ENTRIES` | 200 | 1 to 5000 |
228
+ | `BGF_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug` |
229
+
230
+ The pacing floor cannot be lowered from outside. The site is free to read and
231
+ publishes no crawl delay, which is a reason to be careful rather than a licence
232
+ to be fast.
233
+
234
+ ## As a library
235
+
236
+ The reading layer is published on its own, with its pacing, its storage and its
237
+ error vocabulary and no protocol attached:
238
+
239
+ ```ts
240
+ import { GoodFoodClient } from "mcp-bbc-goodfood/client";
241
+ ```
242
+
243
+ ## Errors
244
+
245
+ Six codes and no more. A caller branches on the code that opens the message.
246
+
247
+ | Code | What it means |
248
+ | --------------- | --------------------------------------------------------------------------------------- |
249
+ | `not_found` | The site holds nothing at that address |
250
+ | `invalid_input` | The arguments could not produce a request |
251
+ | `rate_limited` | The site asked this client to slow down. It says nothing about whether anything matched |
252
+ | `parse_failure` | An answer arrived in a shape this server cannot read |
253
+ | `network_error` | The request could not be completed |
254
+ | `timeout` | No answer arrived within the deadline |
255
+
256
+ ## Attribution
257
+
258
+ Recipes, titles and counts belong to BBC Good Food. Every answer carries the
259
+ source, and a listing shown to a reader should credit the site and link the page.
260
+
261
+ ## Contributing
262
+
263
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Tests come first, coverage has a floor of
264
+ 100%, and the rule everything follows is that the server never says anything the
265
+ data does not carry.
266
+
267
+ Licensed under [MIT](LICENSE).
268
+
269
+ ---
270
+
271
+ # mcp-bbc-goodfood (français)
272
+
273
+ Un serveur MCP qui lit les recettes de [BBC Good Food](https://www.bbcgoodfood.com).
274
+ En lecture seule, sans clé d'API et sans compte.
275
+
276
+ ## Pourquoi il existe
277
+
278
+ BBC Good Food accepte n'importe quelle valeur sur une facette de recherche, et
279
+ répond à une valeur qu'il ne connaît pas par un total de zéro. Demandez
280
+ `glutenfree` au lieu de `gluten-free` et le site répond que rien ne correspond,
281
+ avec l'assurance qu'il met à dire que 48 recettes correspondent à la bonne
282
+ orthographe. Un modèle qui devine une orthographe reçoit une absence confiante
283
+ plutôt qu'un refus.
284
+
285
+ Ce serveur publie le vocabulaire, pour que la question soit posable.
286
+
287
+ ## Les outils
288
+
289
+ ### `list_filters`
290
+
291
+ Publie les axes le long desquels une recherche de recettes se resserre, avec les
292
+ valeurs que chacun prend et le nombre de recettes qui les portent.
293
+
294
+ | Argument | Type | Sens |
295
+ | -------- | ------------------- | ---------------------------------------------------------------------------------------- |
296
+ | `query` | chaîne, facultative | Mesure les décomptes à l'intérieur de cette recherche. Sans elle, sur tout le catalogue. |
297
+
298
+ Le site compte ses facettes sur les lignes que la recherche rend, si bien que les
299
+ deux répondent à deux questions différentes. Dans `chicken`, `diet` annonce
300
+ `gluten-free 48`. Sur tout le catalogue, il annonce `gluten-free 2810`. Aucun des
301
+ deux ne se compare à l'autre, et la réponse dit dans quelle étendue elle a mesuré.
302
+
303
+ ```json
304
+ {
305
+ "query": "chicken",
306
+ "filters": [
307
+ {
308
+ "name": "diet",
309
+ "label": "Diets",
310
+ "options": [{ "value": "gluten-free", "label": "Gluten-free", "count": 48 }],
311
+ "option_count": 10
312
+ }
313
+ ],
314
+ "filter_count": 9,
315
+ "total_available": 363,
316
+ "total_is_ceiling": false,
317
+ "source": "BBC Good Food",
318
+ "notes": ["…"]
319
+ }
320
+ ```
321
+
322
+ ### `search_recipes`
323
+
324
+ Cherche des recettes et rend une liste. Chaque ligne porte le chemin de sa page,
325
+ qui est ce que `get_recipe` lit.
326
+
327
+ | Argument | Type | Sens |
328
+ | ----------------------------------------------------------------- | ------------------- | -------------------------------------------------------- |
329
+ | `query` | chaîne | Un plat, un ingrédient, une technique. |
330
+ | `limit`, `page` | entier, facultatif | Lignes par page et quelle page. Par défaut 30 et 1. |
331
+ | `sort` | chaîne, facultative | L'ordre dans lequel le site range la liste. |
332
+ | `diet`, `cuisine`, `meal_type`, `difficulty` | chaîne, facultative | Une valeur que `list_filters` publie. |
333
+ | `max_total_minutes`, `max_calories`, `min_servings`, `min_rating` | nombre, facultatif | Une borne sur la recette. |
334
+ | `exclude_premium` | booléen, facultatif | Laisse de côté ce qui est derrière l'abonnement du site. |
335
+
336
+ Le site accepte n'importe quelle valeur sur une facette et répond à une valeur
337
+ qu'il ne connaît pas par un total de zéro : une orthographe devinée revient comme
338
+ une absence assurée et non comme un refus. Appelle `list_filters` d'abord.
339
+
340
+ ### `get_recipe`
341
+
342
+ Lit une recette : ses ingrédients, ses étapes, ses temps, sa note et ses valeurs
343
+ nutritionnelles.
344
+
345
+ | Argument | Type | Sens |
346
+ | ---------- | ------------------ | -------------------------------------------------------------------- |
347
+ | `id` | chaîne | Le chemin de la page, tel qu'une ligne de `search_recipes` le porte. |
348
+ | `servings` | entier, facultatif | Remet les ingrédients à ce nombre de parts. |
349
+
350
+ Une recette derrière l'abonnement du site revient avec tout sauf ses ingrédients
351
+ et ses étapes, et le dit. La page les porte, et c'est exactement pourquoi la
352
+ règle existe : lire au-delà d'un mur que le site a dressé devant ses propres
353
+ lecteurs ferait de ce serveur le moyen de le contourner.
354
+
355
+ ### `scale_ingredients`
356
+
357
+ Remet une liste de lignes d'ingrédients à un autre nombre de personnes, sans rien
358
+ lire sur le site.
359
+
360
+ | Argument | Type | Sens |
361
+ | ------------------------------ | ------------------ | ------------------------------------------------------------------- |
362
+ | `ingredients` | chaîne[] | Les lignes à remettre à l'échelle, telles qu'une recette les écrit. |
363
+ | `factor` | nombre, facultatif | Ce par quoi multiplier chaque quantité. |
364
+ | `from_servings`, `to_servings` | entier, facultatif | Les deux bouts du changement, à la place d'un facteur. |
365
+
366
+ ```json
367
+ {
368
+ "factor": 0.5,
369
+ "ingredients": [
370
+ {
371
+ "text": "100 g plain flour",
372
+ "original": "200g plain flour",
373
+ "scaling": "scaled",
374
+ "amount": 100,
375
+ "amount_max": null,
376
+ "unit": "g"
377
+ },
378
+ {
379
+ "text": "2 eggs",
380
+ "original": "3 eggs",
381
+ "scaling": "rounded",
382
+ "amount": 2,
383
+ "amount_max": null,
384
+ "unit": null
385
+ },
386
+ {
387
+ "text": "salt and pepper",
388
+ "original": "salt and pepper",
389
+ "scaling": "unscaled",
390
+ "amount": null,
391
+ "amount_max": null,
392
+ "unit": null
393
+ }
394
+ ],
395
+ "scaled_count": 1,
396
+ "rounded_count": 1,
397
+ "unscaled_count": 1,
398
+ "source": "BBC Good Food",
399
+ "notes": ["…"]
400
+ }
401
+ ```
402
+
403
+ ## Ce que les réponses refusent d'affirmer
404
+
405
+ **Les valeurs publiées sont un extrait.** Le site montre les dix valeurs les plus
406
+ fréquentes d'un axe et en accepte d'autres qu'il ne liste jamais : `cuisine=mexican`
407
+ resserre une recherche à 306 recettes sans figurer parmi les dix. La réponse dit
408
+ que la liste est un extrait plutôt que de l'appeler l'ensemble des valeurs
409
+ acceptées, parce qu'un serveur qui l'appellerait ainsi refuserait des valeurs qui
410
+ fonctionnent.
411
+
412
+ **Un total peut être un plancher.** Le site sert au plus 10 000 lignes pour une
413
+ recherche et s'arrête là. Un total qui tombe exactement sur ce chiffre a été
414
+ coupé : il énonce un plancher et non un compte, et `total_is_ceiling` le dit.
415
+
416
+ **Un décompte que le site n'a pas publié vaut `null`, jamais `0`.** Sur une
417
+ échelle qui commence à zéro, les deux seraient indiscernables. Une note va d'une
418
+ étoile à cinq, donc une recette que personne n'a notée revient à `null` plutôt
419
+ qu'en recette notée zéro.
420
+
421
+ **Une quantité recalculée dit qu'elle a été recalculée.** Les chiffres d'une
422
+ liste remise à l'échelle sont l'arithmétique de ce serveur et non celle du site,
423
+ et chaque ligne dit sous `scaling` si le calcul est tombé juste ou si le chiffre
424
+ a bougé pour rester utilisable en cuisine. Trois œufs divisés par deux reviennent
425
+ à deux, parce qu'un demi-œuf n'est pas une quantité qu'une cuisine mesure ; un
426
+ cuisinier à qui l'on annonce `2` mérite de savoir lequel des deux s'est produit.
427
+
428
+ **Une quantité s'énonce dans l'unité qui la dit exactement.** Deux grammes
429
+ divisés par dix reviennent en 200 mg, et deux cents grammes multipliés par vingt
430
+ reviennent en 4 kg. Un chiffre ne monte d'un échelon que si l'unité du dessus
431
+ l'énonce exactement : 1875 g se lit 1,875 kg, et 7492,5 g reste en grammes parce
432
+ que 7,4925 kg demanderait une quatrième décimale. Un cuisinier qui ne peut pas
433
+ repeser le chiffre est plus mal servi par le plus court.
434
+
435
+ **Une recette dont la page n'énonce aucun nombre de parts ne peut pas être remise
436
+ à un nombre de personnes.** L'argument `servings` reste alors sans effet et une
437
+ note le dit, parce que la multiplication devrait partir d'un chiffre que le site
438
+ n'a jamais écrit.
439
+
440
+ ## Installation
441
+
442
+ ```bash
443
+ npx mcp-bbc-goodfood
444
+ ```
445
+
446
+ ### Claude Code
447
+
448
+ ```bash
449
+ claude mcp add bbc-goodfood -- npx -y mcp-bbc-goodfood
450
+ ```
451
+
452
+ ### N'importe quel client MCP
453
+
454
+ ```json
455
+ {
456
+ "mcpServers": {
457
+ "bbc-goodfood": {
458
+ "command": "npx",
459
+ "args": ["-y", "mcp-bbc-goodfood"]
460
+ }
461
+ }
462
+ }
463
+ ```
464
+
465
+ ### Conteneur
466
+
467
+ ```bash
468
+ docker build -t mcp-bbc-goodfood .
469
+ docker run -i --rm mcp-bbc-goodfood
470
+ ```
471
+
472
+ Le conteneur doit joindre `www.bbcgoodfood.com` et rien d'autre. Il ne prend
473
+ aucun identifiant, puisqu'il n'y en a aucun à prendre.
474
+
475
+ ## Réglages
476
+
477
+ Chaque réglage est une variable d'environnement, et aucune n'est obligatoire. Une
478
+ valeur hors bornes est refusée par une ligne sur stderr et le défaut tient : un
479
+ réglage qui ne peut pas prendre effet le dit, plutôt que d'être borné en silence.
480
+
481
+ | Variable | Défaut | Bornes |
482
+ | ----------------------- | ------- | ---------------------------------------------------------------------------------------------- |
483
+ | `BGF_USER_AGENT` | — | Votre identifiant. Celui du projet reste ajouté, pour que le site puisse joindre une personne. |
484
+ | `BGF_MIN_INTERVAL_MS` | 1500 | 1000 à 60000 |
485
+ | `BGF_TIMEOUT_MS` | 20000 | 1000 à 120000 |
486
+ | `BGF_MAX_RETRIES` | 3 | 0 à 8 |
487
+ | `BGF_CACHE_TTL_MS` | 900000 | 0 à 86400000, 0 éteint le stockage |
488
+ | `BGF_CACHE_MAX_ENTRIES` | 200 | 1 à 5000 |
489
+ | `BGF_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug` |
490
+
491
+ Le plancher du rythme ne peut pas être abaissé de l'extérieur. Le site est gratuit
492
+ à lire et ne publie aucun délai d'exploration, ce qui est une raison d'être
493
+ prudent plutôt qu'une licence d'aller vite.
494
+
495
+ ## Comme bibliothèque
496
+
497
+ La couche de lecture est publiée seule, avec son rythme, son stockage et sa
498
+ taxonomie d'erreurs, sans protocole attaché :
499
+
500
+ ```ts
501
+ import { GoodFoodClient } from "mcp-bbc-goodfood/client";
502
+ ```
503
+
504
+ ## Erreurs
505
+
506
+ Six codes et pas un de plus. Un appelant branche sur le code qui ouvre le message.
507
+
508
+ | Code | Ce qu'il veut dire |
509
+ | --------------- | ---------------------------------------------------------------------- |
510
+ | `not_found` | Le site n'a rien à cette adresse |
511
+ | `invalid_input` | Les arguments ne pouvaient pas produire de requête |
512
+ | `rate_limited` | Le site a demandé de ralentir. Cela ne dit rien sur ce qui correspond |
513
+ | `parse_failure` | Une réponse est arrivée dans une forme que ce serveur ne sait pas lire |
514
+ | `network_error` | La requête n'a pas pu aboutir |
515
+ | `timeout` | Aucune réponse n'est arrivée dans le délai |
516
+
517
+ ## Attribution
518
+
519
+ Les recettes, les titres et les comptes appartiennent à BBC Good Food. Chaque
520
+ réponse porte la source, et un listing montré à un lecteur doit créditer le site
521
+ et lier la page.
522
+
523
+ ## Contribuer
524
+
525
+ Voir [CONTRIBUTING.md](CONTRIBUTING.md). Les tests d'abord, un plancher de
526
+ couverture à 100 %, et la règle qui gouverne tout : le serveur ne dit jamais
527
+ quelque chose que la donnée ne porte pas.
528
+
529
+ Sous licence [MIT](LICENSE).