mcp-recipes 1.3.1 → 3.0.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/README.md CHANGED
@@ -5,131 +5,46 @@
5
5
  [![license](https://img.shields.io/npm/l/mcp-recipes.svg)](./LICENSE)
6
6
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-recipes)
7
7
  [![Glama](https://glama.ai/mcp/servers/smeet666/mcp-recipes/badges/score.svg)](https://glama.ai/mcp/servers/smeet666/mcp-recipes)
8
+ [![M8ven](https://m8ven.ai/badge/mcp/smeet666-mcp-recipes-1o0x5l?variant=verified)](https://m8ven.ai/mcp/smeet666-mcp-recipes-1o0x5l)
8
9
  [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=recipes&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZWNpcGVzIl19)
9
10
  [![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=recipes&config=%7B%22name%22%3A%22recipes%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-recipes%22%5D%7D)
10
11
 
11
- One question, several kitchens. An MCP server that asks every recipe source it
12
- reads at the same time, merges what they say, and scales quantities in French
13
- and English.
14
-
15
- Today it reads two: **Marmiton**, in French, and the **Wikibooks Cookbook**, in
16
- English.
17
-
18
- No API key. No account. Read-only.
12
+ Recipes live on many sites, and each one writes them its own way: a French
13
+ cooking site publishes in French, with its own measures and its own idea of what
14
+ a serving is, and a wiki cookbook in English, with equipment lists and prose the
15
+ first has no field for. Asking a question of one of them answers about one of
16
+ them.
17
+
18
+ This server reads five at once. Three publish in French:
19
+ [Marmiton](https://www.marmiton.org), where home cooks publish,
20
+ [Ptitchef](https://www.ptitchef.com), which files its recipes under a tree of
21
+ ingredients, and [Supertoinette](https://www.supertoinette.com), which prints a
22
+ resting time of its own. Two publish in English: the
23
+ [Wikibooks Cookbook](https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents),
24
+ written and maintained in the open, and
25
+ [BBC Good Food](https://www.bbcgoodfood.com), which groups an ingredient list
26
+ under headings. You can search them all with one question, read a recipe from any
27
+ of them in one shape, put several versions of the same dish side by side, and
28
+ rescale any ingredient list. It needs no API key and no account.
29
+
30
+ _[Version française](#mcp-recipes-français)_
19
31
 
20
32
  ---
21
33
 
22
- ## What it does
23
-
24
- Marmiton is a French recipe site with reader ratings and millions of visitors.
25
- The Wikibooks Cookbook is an English wiki written by whoever edits it, published
26
- under a Creative Commons licence. They write the same dishes differently, and
27
- that difference is worth having in one place.
28
-
29
- This server:
30
-
31
- - **searches every source at once** and returns one list, each row naming the
32
- source it came from;
33
- - **reads one recipe** in full and rescales it to any number of people;
34
- - **scales an ingredient list** you already hold, offline, in either language or
35
- a list holding both;
36
- - **puts one dish side by side**, as each source writes it, all rescaled to the
37
- same number of servings so the lists stand comparison.
38
-
39
- ### What makes the answers usable
40
-
41
- Multiplying every number by a factor is arithmetically correct and practically
42
- useless: it produces "2.4 eggs" and "0.67 pinch of salt" with the same
43
- confidence as "267 g flour". Every quantity here lands where a cook can act on
44
- it:
45
-
46
- | The line says | Times six | Why |
47
- | ----------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------ |
48
- | `une pincée de sel` | `6 pincées de sel` | A pinch keeps the size a hand gives it; the count carries the proportion |
49
- | `1 dose (cup) de Mountain Dew` | `6 doses (cup) de Mountain Dew` | A French line naming an English measure stays French |
50
- | `2/3 d'un flacon de fleur d'oranger` | `4 flacons de fleur d'oranger` | A share of one thing becomes a count of them |
51
- | `1/4 litre de lait` | `1,5 l de lait` | French writes its decimals with a comma |
52
- | `250 ml milk` | `1.5 l milk` | English writes them with a dot, and both climb the metric ladder |
53
- | `2/3 of a bottle of orange blossom water` | `4 bottles of orange blossom water` | The bottles are what is counted, and the water comes with them |
54
- | `a capful of rum` | `6 capfuls rum` | A measure named after what holds it is read as one, listed or not |
55
-
56
- ### What divides, and what does not
57
-
58
- A measure divides as far as half of what it holds is still a quantity a kitchen
59
- can take out. Almost everything is: what a tin, a sachet or a carton
60
- holds is poured, weighed or spooned, so half a tin of tomatoes is half a tin of
61
- tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of
62
- thyme are cut with a knife. A handful is halved by taking less.
63
-
64
- An egg is the exception, along with a yolk and an egg white on their own: half
65
- of one means beating it and weighing the result, which no recipe asks for. A
66
- clou de girofle, a zeste and a zest go with it, having no half anyone takes.
67
-
68
- A few measures go the other way and divide further. A pot, a bouteille, a
69
- bottle, a jar and a block hold enough for a quarter to be a portion someone
70
- serves. Une tranche and a slice are already cut off something larger, and the
71
- board that produced one takes a corner off it in the same gesture. And a word can state a number of things rather than a measure of them,
72
- so `2 dozen mushrooms` at three quarters comes back as **18 mushrooms**. A blanc
73
- belongs to whichever food the line named: the white of an egg is counted whole,
74
- a blanc de poulet is meat and halves.
75
-
76
- A thing counted on its own is divided by the size of one against what a recipe
77
- puts in, in either language. A crevette and a shrimp, une moule and a mussel,
78
- une noisette and a hazelnut, un grain de poivre and a peppercorn, un anis étoilé
79
- and a star anise is already a portion: a recipe counts twelve of them and a
80
- smaller recipe puts one fewer in the pan, so they land on a whole number. Un
81
- gigot and a leg of lamb, une baguette, un camembert, un ananas and a pineapple,
82
- une pastèque and a watermelon, une pintade and a guinea fowl, un poulet and a
83
- chicken, un poireau and a leek
84
- sit at the other end of that comparison, asked for by the one or the two and shared out with a knife, so they go as far as the
85
- quarter. A cut carved off one of them stops at the half, une cuisse and a thigh
86
- being the portion the knife already produced. Un jus, and a juice, stops at the
87
- half too: half the jus of a citron is
88
- taken by squeezing half the fruit, and a quarter of one has to be poured out and
89
- measured back.
90
-
91
- The word `clove` names two foods, and the line says which. Garlic in the line
92
- makes it the wedge broken off a bulb, the gousse d'ail, which a knife splits in
93
- two; on its own, or written `whole cloves`, it is the dried flower bud, the
94
- clou de girofle, and a count of those lands on a whole number.
95
-
96
- - `3 eggs` taken from six people to twenty-five gives **13 eggs**, and never a
97
- half of one.
98
- - `1 can tomatoes` at four tenths gives **half a tin**, with a note saying the
99
- line no longer holds its share of the recipe.
100
- - `2 tablespoons butter OR 30 g margarine` doubled scales **both** branches, and
101
- the line is never reported as exact arithmetic: how far one stands for the
102
- other is the page's own claim.
103
- - `4 tablespoons` at a tenth comes down to **1.2 teaspoons**. A measurement moves
104
- to a smaller unit _before_ it is rounded, so a small share survives.
105
- - `1 lb 4 oz beef` doubled gives **2.5 lb beef**, and `1 kg 500 de farine` gives
106
- **3 kg de farine**. Two measures off one ladder written side by side are one
107
- quantity, and the smaller one can be left unwritten.
108
- - `12 oz can tomatoes` comes back as published, with a note saying the measure
109
- gives what one tin holds and that the line states no count of them.
110
- - `2 h de repos` comes back as published. A rest, a proof or a bake takes as long
111
- for a large batch as for a small one, so a length of time is not a proportion.
112
- - `quelques feuilles de basilic` comes back as published, as do `a few basil
113
- leaves` and the same line with `plusieurs`. A word saying there are several
114
- states no count, and any figure put behind it would be this server's reading
115
- rather than the page's quantity.
116
-
117
- Every line comes back with a `scaling` field: `scaled` when the arithmetic
118
- landed on the exact product, `rounded` when something had to move for the line
119
- to stay usable, `unscaled` when the line carries nothing to multiply. A rounded
120
- line says what it was rounded from and in which direction.
34
+ ## Install
121
35
 
122
- ---
36
+ **One-click install**
123
37
 
124
- ## Install
38
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=recipes&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZWNpcGVzIl19)
39
+ [![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=recipes&config=%7B%22name%22%3A%22recipes%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-recipes%22%5D%7D)
125
40
 
126
- Node 20 or later.
41
+ **Claude Code**
127
42
 
128
43
  ```bash
129
- npx -y mcp-recipes
44
+ claude mcp add recipes -- npx -y mcp-recipes
130
45
  ```
131
46
 
132
- ### Claude Desktop
47
+ **Claude Desktop, Cursor, and any client using the standard config format**
133
48
 
134
49
  ```json
135
50
  {
@@ -142,445 +57,325 @@ npx -y mcp-recipes
142
57
  }
143
58
  ```
144
59
 
145
- ### From a clone
60
+ Node 24 or later is required, and no environment variable has to be set.
146
61
 
147
- ```bash
148
- npm install
149
- npm run build
150
- node dist/index.js
62
+ ### With Docker
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "recipes": {
68
+ "command": "docker",
69
+ "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:3.0.0"]
70
+ }
71
+ }
72
+ }
151
73
  ```
152
74
 
153
- ---
75
+ `-i` keeps stdin open, which is where the protocol travels, and `-t` is left out
76
+ because a TTY rewrites the stream. The container needs outbound HTTPS to
77
+ `www.marmiton.org`, `api.wikimedia.org`, `www.ptitchef.com`,
78
+ `www.bbcgoodfood.com` and `www.supertoinette.com`, and nothing else:
79
+ no volume, no port, no credential.
154
80
 
155
- ## The four tools
81
+ ### Bundle, without npm
156
82
 
157
- ### `search_recipes`
83
+ Download `mcp-recipes-3.0.0.mcpb` from
84
+ [the latest release](https://github.com/smeet666/mcp-recipes/releases/latest) and
85
+ open it. A client that supports MCP bundles installs it on its own, with no npm
86
+ and no configuration file to edit. The bundle carries its dependencies, so
87
+ nothing is fetched at install time.
158
88
 
159
- Asks every source at the same time, so asking them all costs about what asking
160
- one costs.
161
-
162
- | Argument | Type | Meaning |
163
- | ------------------ | ----------------------------- | -------------------------------------------------------------- |
164
- | `query` | string | A dish or an ingredient, in any language a source publishes in |
165
- | `limit_per_source` | integer, 1 to 25, default 5 | Rows to take from each source |
166
- | `sources` | array of source ids, optional | Left out, every source is asked |
167
- | `fan_out` | boolean, default true | Whether shorter wordings derived from the question go out too |
168
-
169
- The query goes to each source's own search as free text. There is no filtering:
170
- a word naming a diet, a time or a calorie count matches only where that source's
171
- index happens to carry it, and a question long enough to read like a request for
172
- conditions gets a note saying so.
173
-
174
- **A question asked in a sentence is also sent in shorter wordings.** These
175
- indexes answer the words they are handed. One of them requires every word given
176
- to appear on the same page, so "comment faire un ceviche de truite à l'origan"
177
- comes back empty from a corpus holding four ceviche recipes; the other ranks on
178
- the words it was handed, so "comment faire" pulls up whatever shares those words
179
- and the dish falls out of the list. Both reach a reader looking like a statement
180
- about a corpus when they are statements about a wording.
181
-
182
- So a question is turned into an ordered ladder of at most three wordings: the
183
- words as asked, the words naming the dish with the framing words set aside, and
184
- the leading dish word on its own. They go out in that order, one at a time, and
185
- stop as soon as enough rows have come back, so a query that already names a dish
186
- costs one request. What comes back is unioned and deduplicated on the id naming
187
- its source. `per_source` lists every wording, what each returned, and the ones
188
- held back and why. Nothing is translated between the sources' languages: a
189
- guessed translation is a word nobody wrote, and a source that publishes in
190
- another language and holds nothing under the words written is an honest absence.
191
-
192
- **A condition is not a keyword.** "sans beurre", "gluten free" and "pour 6
193
- personnes" say what a recipe must avoid or serve. No page is named for what it
194
- leaves out, and the number of people at a table is not printed in a title, so
195
- searching for those words buries the dish. They stay in the wording sent first,
196
- where a source whose index carries them can still match, and they are set aside
197
- from the derived wordings. Dropping them quietly would be the worse move, since
198
- a recipe found this way can hold exactly what the person was avoiding, so every
199
- condition set aside is named back in the notes with a reminder to check the
200
- ingredient list. A number of servings is pointed at the `servings` argument,
201
- which is what actually rescales a recipe.
202
-
203
- A condition is read by the role its words play rather than by the turn of phrase
204
- they are written in, because a list of phrasings has a hole at the first one
205
- nobody thought of. Three roles put a food out of a dish: a negation, whatever
206
- word carries it, since negations are a closed class of function words and
207
- "without butter", "no butter" and "pas de beurre" are all built on one; a
208
- sentence about the eater rather than the dish, as in "je suis allergique aux
209
- noix", which is named back as an allergy because that is the condition whose
210
- failure is a matter of health; and a diet named in one word, which is a class of
211
- recipes defined by what it leaves out and so states an exclusion with no
212
- negation written anywhere. A question can still phrase one in a way this reading
213
- does not catch, and the answer says so rather than presenting the ones it caught
214
- as all there were.
215
-
216
- Which words a condition covers is read off the sentence. A joining word straight
217
- after the marker says its food follows, as in "allergique aux noix", "allergic
218
- to peanuts" or "free of dairy"; with no such word, the food is the one the
219
- marker was written onto the end of, as in "peanut allergy", "lactose intolerant"
220
- or "gluten free". Both orders are needed, since the two languages these sources
221
- publish in put the food on opposite sides. A food's name ends where nothing
222
- carries it further, so "sans gluten gateau au chocolat" leaves out gluten and
223
- looks for the cake. Where a marker has a food on neither side, the answer says a
224
- condition was stated and that its food was not read: naming the wrong word hides
225
- a dish and leaves the food being avoided in the search at the same time, and an
226
- allergen is not something to guess at.
227
-
228
- Setting `fan_out` to false sends exactly the words typed. The wordings that were
229
- not sent are still named, so a caller can see what turning it back on would buy.
230
-
231
- Rows are interleaved one source at a time. No score orders them against each
232
- other, because the sources share none: one carries reader ratings and another
233
- has no author and no rating by nature. The answer says how the order was built.
234
-
235
- `per_source` reports what each source answered in that source's own terms.
236
- Marmiton counts the rows on the single page it serves. The Cookbook states no
237
- total at all, so a short list from it is not evidence that little exists. The
238
- counts measure different things and are never added together.
89
+ ## What you can ask
239
90
 
240
- ### `get_recipe`
91
+ - "Find me carbonara recipes, from wherever you can."
92
+ - "Compare the French and English versions of that dish."
93
+ - "Read the second one for eight people."
94
+ - "Which of them uses cream?"
95
+ - "Scale this list from my notebook by 1.5."
241
96
 
242
- | Argument | Type | Meaning |
243
- | ---------------- | ----------------------------------------- | ----------------------------------------------------------------- |
244
- | `id` | string | From `search_recipes`, such as `marmiton:44078` |
245
- | `servings` | integer, optional | Rescale to this many |
246
- | `sections` | array, default `["ingredients", "steps"]` | `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment` |
247
- | `max_steps` | integer, default 20 | Steps to return; the answer says how many more there are |
248
- | `max_step_chars` | integer, default 600 | Characters kept per step |
249
-
250
- The identifier names its source, so the right one is read without guessing. A
251
- raw identifier is routed by its shape, and the answer says which reading it
252
- used: a bare number is Marmiton's, a page key carrying the Cookbook namespace is
253
- the Cookbook's, and an address is routed by its host. A string no source would
254
- have minted is refused, because sending it anywhere returns whatever that guess
255
- happened to hit.
256
-
257
- `sections_omitted` names what was left out. A field belonging to one of those
258
- sections is empty because nobody asked for it, and a field a source genuinely
259
- does not publish is `null`.
260
-
261
- A page stating no number of servings comes back as published and says why:
262
- dividing by a yield nobody wrote would answer for a number of people the page
263
- never claimed. A page stating a span, such as "4 à 6 personnes", is scaled from
264
- the lower end, and the answer gives the factor the upper end would have needed.
265
-
266
- Some sources keep recipes and reference pages under one namespace, so a search
267
- row can be a page _about_ a dish rather than a recipe _for_ it. Only reading the
268
- page tells them apart.
269
-
270
- A part of a recipe this answer holds nothing for is never reported as an absence
271
- on the page, because reading nothing is a fact about this server and only the
272
- page can say whether there was anything to read. Where the page shows a sign of
273
- the part all the same, a heading announcing it or another part of the recipe
274
- that was read, the answer states that this server failed to read what the page
275
- carries. Where the page shows neither, it states that the two cannot be told
276
- apart from here. Either way an empty ingredient list is never evidence that an
277
- ingredient is absent from the dish: someone avoiding butter has to open the page
278
- before ruling it out.
97
+ The ordinary path runs from a search to a reading: a row carries an `id` naming
98
+ its source, and `get_recipe` takes it.
279
99
 
280
- ### `scale_ingredients`
100
+ ## The sources
281
101
 
282
- Offline. Nothing is fetched. The list can come from any of the sources, from a
283
- cookbook on a shelf, or from a photograph of a card.
102
+ | Source | Site | Language |
103
+ | --------------- | ----------------------- | -------- |
104
+ | `marmiton` | `www.marmiton.org` | French |
105
+ | `cookbook` | Wikibooks Cookbook | English |
106
+ | `ptitchef` | `www.ptitchef.com` | French |
107
+ | `goodfood` | `www.bbcgoodfood.com` | English |
108
+ | `supertoinette` | `www.supertoinette.com` | French |
284
109
 
285
- | Argument | Type | Meaning |
286
- | ------------------------------- | ------------------------------------ | ------------------------------------------- |
287
- | `ingredients` | string[] | The lines as written, one ingredient each |
288
- | `factor` | number | What to multiply by |
289
- | `from_servings` + `to_servings` | integers | The factor worked out from a pair of counts |
290
- | `language` | `auto` / `fr` / `en`, default `auto` | `auto` reads each line on its own |
110
+ A row's `id` names its source, so an identifier read from one answer goes back to
111
+ the right site. **Counts are never added across sources**, and a source that
112
+ failed is reported as having failed rather than as having found nothing.
291
113
 
292
- Give `factor`, or the pair. Giving both is refused: a caller who means two
293
- different things by them has made a mistake worth hearing about.
114
+ ## Tools
294
115
 
295
- `auto` is what a mixed list needs. A line is read in the language it is written
296
- in, and comes back written the same way, down to the decimal mark and where the
297
- plural falls.
116
+ | Tool | What it does |
117
+ | ------------------- | ---------------------------------------------------------- |
118
+ | `search_recipes` | Searches every source with one question. |
119
+ | `get_recipe` | Reads one recipe from any source, in one shape. |
120
+ | `compare_recipes` | Puts several versions of the same dish side by side. |
121
+ | `scale_ingredients` | Rescales any ingredient list, with no request to any site. |
298
122
 
299
- ### `compare_recipes`
123
+ ### `search_recipes`
300
124
 
301
- | Argument | Type | Meaning |
302
- | ---------------- | -------------------------------- | ----------------------------------------------- |
303
- | `dish` | string | The dish, in any language a source publishes in |
304
- | `servings` | integer, optional | Rescale every version to this many |
305
- | `sections` | array, default `["ingredients"]` | As above |
306
- | `max_steps` | integer, default 10 | Steps to return per version |
307
- | `max_step_chars` | integer, default 600 | Characters kept per step |
125
+ Searches every source with one question.
308
126
 
309
- This is the tool that exists because there is more than one source. It reads
310
- each source's closest match, rescales them all to the same yield, and states
311
- what differs: the quantities, the yields, the times, and which fields each
312
- source publishes at all. Where the versions agree, it says nothing.
127
+ | Argument | Type | Required | What it does |
128
+ | ------------------ | ----------------------------- | -------- | ---------------------------------------------------------------- |
129
+ | `query` | string, 1 to 200 characters | yes | The dish or the ingredient to look for. |
130
+ | `limit_per_source` | integer, 1 to 25, default `5` | no | Rows to keep from each source. |
131
+ | `sources` | array of source ids | no | Ask these sources alone. |
132
+ | `fan_out` | boolean, default `true` | no | Ask every source rather than stopping at the first that answers. |
313
133
 
314
- The versions are shown side by side, each in the units its own source published.
315
- A gram in one and a cup in another are two ways of writing a recipe.
134
+ **In return:** `results`, rows carrying `id`, which `get_recipe` takes;
135
+ `source` and `source_name` saying which site published the row; `title`; `url`;
136
+ `image_url`; and an `excerpt` where the source offers one. `per_source` gives one
137
+ report per site with its `status`, reading `answered` or `failed`, the `count` it
138
+ contributed, and its `reported_total` alongside `reported_total_means`, which
139
+ says what that number counts on that site. `order` says in words how the list was
140
+ built.
316
141
 
317
- When one source is missing, the answer says which of three things happened: its
318
- search failed, it offered a row that could not be read, or it offered nothing
319
- close enough to compare.
142
+ ### `get_recipe`
320
143
 
321
- ---
144
+ Reads one recipe from any source, in one shape.
145
+
146
+ | Argument | Type | Required | What it does |
147
+ | ---------------- | -------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
148
+ | `id` | string, 1 to 500 characters | yes | The identifier a row carries, such as `marmiton:44078`. Two sources address a recipe by a bare number, so spell an id with its source. |
149
+ | `servings` | integer, 1 to 500 | no | Rescale the ingredients to this many servings. |
150
+ | `sections` | array of `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment`, default `["ingredients", "steps"]` | no | Which parts to return. |
151
+ | `max_steps` | integer, 1 to 100, default `20` | no | Steps to serve. |
152
+ | `max_step_chars` | integer, 80 to 4000, default `600` | no | Characters kept per step. |
153
+
154
+ **In return:** the recipe in the shape every source is rendered into, whichever
155
+ published it: its title, its address, its ingredients with each line's `scaling`,
156
+ its steps, and the sections asked for. A field one source publishes and another
157
+ has no notion of comes back absent rather than invented. `rest_minutes` carries a
158
+ resting time from a source that prints one apart, and is in no other time here.
159
+ `steps_as_one_block` says when a source published its method as one block of
160
+ prose rather than as steps. `withheld` names a part a source keeps for its
161
+ subscribers, which is a part the page has rather than a part that could not be
162
+ read. Raise `max_step_chars` when a step was cut mid-sentence.
322
163
 
323
- ## What it refuses to claim
324
-
325
- Each of these is a rule the code is held to, with a test naming it.
326
-
327
- - **A source that failed is named as a source that failed.** An answer holding
328
- rows from some sources says nothing about what the others hold. "Marmiton was
329
- unreachable" and "Marmiton holds no such recipe" are different statements
330
- about the world, and a caller that cannot tell them apart will make the second
331
- one.
332
- - **No total is invented.** Each source's own number is reported with what it
333
- counts, in that source's terms, and the counts are never summed.
334
- - **No ranking across sources.** They share no scale, so rows interleave and the
335
- answer says how the order was built.
336
- - **No conversion between measuring systems.** A line keeps the units its source
337
- published, and the source is named beside it.
338
- - **A yield is repeated in the source's own words.** "4 à 6 personnes" and
339
- "4 personnes" are different claims.
340
- - **A field a source does not publish is `null`**, and a field nobody asked for
341
- is listed in `sections_omitted`.
342
- - **Text from a source cannot imitate this server.** Anything a source or a
343
- caller wrote is put on a single line before it is rendered, with markdown
344
- image syntax defused and any opening that looks like one of the server's own
345
- markers indented. The structured payload keeps the text exactly as published.
164
+ ### `compare_recipes`
346
165
 
347
- ---
166
+ Puts several versions of the same dish side by side.
348
167
 
349
- ## Settings
168
+ | Argument | Type | Required | What it does |
169
+ | ---------------- | ----------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------- |
170
+ | `dish` | string, 1 to 200 characters | yes | The dish to compare. |
171
+ | `servings` | integer, 1 to 500 | no | Rescale every version to this many servings. |
172
+ | `sections` | array of `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment`, default `["ingredients"]` | no | Which parts to return per version. |
173
+ | `max_steps` | integer, 1 to 100, default `10` | no | Steps to serve per version. |
174
+ | `max_step_chars` | integer, 80 to 4000, default `600` | no | Characters kept per step. |
175
+ | `sources` | array of source ids | no | Compare these sources alone. |
350
176
 
351
- All optional. A value that cannot be read is reported on stderr and the default
352
- stands, because a server that refuses to start over a typo is very hard to
353
- diagnose from inside a host application.
177
+ **In return:** `versions`, one recipe per source that answered, all rescaled to
178
+ the same number of servings so their quantities can be read against each other,
179
+ and `differences`, what separates them. `per_source` reports each site as a
180
+ search does.
354
181
 
355
- | Variable | Default | Meaning |
356
- | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
357
- | `RECIPES_USER_AGENT` | — | Identify your own client. The project's identifier is appended, so a source can always reach a human |
358
- | `RECIPES_MIN_INTERVAL_MS` | `1000` | Minimum gap between two requests to one source. Floor 500 |
359
- | `RECIPES_TIMEOUT_MS` | `20000` | Deadline for one request |
360
- | `RECIPES_MAX_RETRIES` | `3` | Retries on rate limiting and transient failures |
361
- | `RECIPES_CACHE_TTL_MS` | `900000` | In-memory cache lifetime. `0` turns it off |
362
- | `RECIPES_CACHE_MAX_ENTRIES` | `200` | In-memory cache size |
363
- | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug`. Logs go to stderr |
182
+ ### `scale_ingredients`
364
183
 
365
- No source publishes a ceiling for a client like this one, so the pacing is
366
- self-imposed politeness. Configuration can widen it, and the floor holds
367
- whichever way a setting arrives.
184
+ Rescales any ingredient list, with no request to either site.
368
185
 
369
- ---
186
+ | Argument | Type | Required | What it does |
187
+ | --------------- | ------------------------------------ | ---------- | ------------------------------------------ |
188
+ | `ingredients` | array of 1 to 200 lines | yes | The lines to rescale. |
189
+ | `factor` | number, up to 1000 | one of two | The multiplier to apply. |
190
+ | `from_servings` | integer, 1 to 500 | one of two | How many servings the list is written for. |
191
+ | `to_servings` | integer, 1 to 500 | one of two | How many servings are wanted. |
192
+ | `language` | `auto`, `fr` or `en`, default `auto` | no | How each line is read. |
370
193
 
371
- ## As a library
194
+ Pass `factor`, or the `from_servings` and `to_servings` pair. `auto` reads each
195
+ line on its own, which is what a list holding both languages needs; naming a
196
+ language reads every line that way.
372
197
 
373
- Two entry points are published beside the server, with no protocol attached.
198
+ **In return:** the rescaled lines in the shape `get_recipe` returns, each with
199
+ its `scaling`.
374
200
 
375
- ```ts
376
- import { RecipesClient } from "mcp-recipes/client";
201
+ ## Rescaling the quantities
377
202
 
378
- const client = new RecipesClient();
379
- const merged = await client.searchRecipes("carbonara", 3);
203
+ A quantity is stated in the unit that suits it, so a line can come back in a
204
+ different unit from the one the recipe used: 200 g multiplied by twenty reads
205
+ `4 kg`.
380
206
 
381
- for (const report of merged.reports) {
382
- console.log(report.name, report.status, report.error?.code ?? "");
383
- }
384
- ```
207
+ How finely an ingredient can be divided depends on what it is. A baguette can be
208
+ cut in two, in three or in four; an egg cannot be shared out. A quantity landing
209
+ between the two is rounded, and the rescaled recipe then departs a little from
210
+ the proportions of the original. The line carries `rounded`, and its note says
211
+ what was done.
385
212
 
386
- ```ts
387
- import { scaleIngredients } from "mcp-recipes/scale";
213
+ The sources write their quantities in their own languages, and a line is read
214
+ in the language it was written in. The figures are this server's arithmetic, so
215
+ say they were recomputed when you show them.
388
216
 
389
- scaleIngredients(["200 g de farine", "3 eggs"], { factor: 2 });
390
- // [{ text: "400 g de farine", language: "fr", scaling: "scaled", … },
391
- // { text: "6 eggs", language: "en", scaling: "scaled", … }]
392
- ```
217
+ ## What an answer states about the sources
393
218
 
394
- `RecipesClient` also takes stand-in readers, so a program embedding it can put
395
- its own cache in front of a source, or drive it from fixed answers in a test:
219
+ Every answer accounts for each source separately. A site that failed, one nobody
220
+ asked, and one that answered with nothing are three different things, and they
221
+ are reported as three. A total stays beside the source that published it, with
222
+ what that source counts when it says it: one site counts a whole category and the
223
+ other counts the rows it served.
396
224
 
397
- ```ts
398
- new RecipesClient({ readers: { marmiton: myReader } });
399
- ```
225
+ ## Configuration
400
226
 
401
- A program bringing corpora of its own replaces the registry outright with
402
- `sources`, and every tool works over however many it holds.
227
+ Every variable is optional. Set them in the `env` block of your client config.
403
228
 
404
- ---
229
+ | Variable | Default | What it does |
230
+ | --------------------------- | -------------------- | ------------------------------------------------------------------------------------ |
231
+ | `RECIPES_USER_AGENT` | the project identity | Names your application to both sites, with an address where a person can be reached. |
232
+ | `RECIPES_MIN_INTERVAL_MS` | `1000` | Gap between two requests to one site, from 500 to 60000. |
233
+ | `RECIPES_TIMEOUT_MS` | `20000` | Deadline for one request, from 1000 to 120000. |
234
+ | `RECIPES_MAX_RETRIES` | `3` | Attempts after a transient failure, from 0 to 8. |
235
+ | `RECIPES_CACHE_TTL_MS` | `900000` | How long an answer stays in memory, from 0 to 86400000. |
236
+ | `RECIPES_CACHE_MAX_ENTRIES` | `200` | Answers held in memory at once, from 1 to 5000. |
237
+ | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info` or `debug`, written to stderr. |
405
238
 
406
- ## When something goes wrong
239
+ A value outside its range falls back to the default, and the reason is written to
240
+ stderr.
407
241
 
408
- Six error codes, and no others.
242
+ ## Errors
409
243
 
410
- | Code | Means |
411
- | --------------- | ---------------------------------------------------- |
412
- | `not_found` | A source answered, and holds no such recipe |
413
- | `invalid_input` | The arguments could not produce a request |
414
- | `rate_limited` | A source asked this client to slow down |
415
- | `parse_failure` | An answer arrived in a shape this client cannot read |
416
- | `network_error` | The request did not complete |
417
- | `timeout` | The request exceeded its deadline |
244
+ Every failure carries one of six codes, a message, and where it helps a hint
245
+ naming the next move.
418
246
 
419
- `rate_limited` means the recipe may well exist. Wait and ask again.
247
+ | Code | What happened | What to do |
248
+ | --------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------- |
249
+ | `not_found` | A source answered, and holds no such recipe. | Check the identifier with `search_recipes`. |
250
+ | `invalid_input` | The arguments were refused before any request went out. | Read the message, which names the argument. |
251
+ | `rate_limited` | A source asked this client to slow down. | Wait, then call again with the same arguments. The recipe is still there. |
252
+ | `parse_failure` | A page loaded and the expected content was absent. | Report it at [the issue tracker](https://github.com/smeet666/mcp-recipes/issues). |
253
+ | `network_error` | The request did not complete. | Try again shortly. |
254
+ | `timeout` | The request passed its deadline. | Raise `RECIPES_TIMEOUT_MS`, or ask for fewer rows. |
420
255
 
421
- A `parse_failure` usually means a source changed how it answers. That is worth
422
- reporting: <https://github.com/smeet666/mcp-recipes/issues>
256
+ A source that failed is reported per source rather than failing the whole answer,
257
+ so one silent site never hides the other.
423
258
 
424
- If a search comes back short, read `per_source` before concluding anything.
259
+ ## As a library
425
260
 
426
- ---
261
+ The layer reading the two sites is published on its own, with its pacing, its
262
+ cache and its errors, and with no protocol attached.
427
263
 
428
- ## Development
264
+ ```ts
265
+ import { RecipesClient } from "mcp-recipes/client";
429
266
 
430
- ```bash
431
- npm install
432
- npm run typecheck
433
- npm test # unit tests, no network
434
- npm run build
267
+ const client = new RecipesClient();
268
+ const read = await client.search({ query: "carbonara", limitPerSource: 3 });
269
+ console.log(read.data.results.length);
435
270
  ```
436
271
 
437
- The unit suite runs against stand-in sources, so it is deterministic: time is
438
- pinned to a fixed epoch, and every assertion is exact. A live suite sits behind
439
- an environment variable and makes one request per route:
272
+ Each read answers `{ data, cached }`, and throws an error carrying one of the six
273
+ codes. Each site keeps its own pace, and the floor holds here as well.
440
274
 
441
- ```bash
442
- RECIPES_LIVE=1 npm run test:live
443
- ```
275
+ ## Pacing and attribution
444
276
 
445
- A nightly job runs that suite against the real sites and opens an issue when it
446
- fails, because the unit tests cannot notice that a source changed.
277
+ Each site is paced on its own, one request at a time with at least a second
278
+ between two, and the floor of half a second holds however the server is
279
+ configured. Asking every site at once therefore costs each of them one request,
280
+ never two. The `User-Agent` always ends with the project identity and an address
281
+ where a person can be reached.
447
282
 
448
- **Adding a source.** Write an adapter: what it is called, how it recognises one
449
- of its own identifiers, how to search it, and how to read one recipe off it.
450
- Register it in `src/sources/registry.ts`. No tool, no merge and no error path
451
- has a branch per source, so nothing else changes.
283
+ Every row carries the address of the recipe's own page and the name of the site
284
+ that published it. The Cookbook pages are published under
285
+ [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), which asks that
286
+ what is built on them be shared under the same licence. Marmiton, Ptitchef, BBC
287
+ Good Food and Supertoinette state no terms on a recipe page, and their recipes
288
+ belong to those sites and to the cooks who wrote them. Silence is not a grant, so
289
+ credit the site and link the page you took a recipe from.
452
290
 
453
- **Dependencies.** The reading of each site is a published library this server
454
- depends on rather than code it carries: `mcp-marmiton` for Marmiton and
455
- `mcp-wikibooks-cookbook` for the Cookbook. Each keeps its own pacing, its own
456
- cache and its own error taxonomy. Everything above the seam, including the whole
457
- of the bilingual scaling, lives here.
291
+ A recipe BBC Good Food keeps for its subscribers comes back without its
292
+ ingredients and its method, named as a recipe held back, with the address of its
293
+ page. This server does not reconstruct what that site chose to sell.
458
294
 
459
- ---
295
+ Two figures the sites publish are not repeated here. A difficulty is a word each
296
+ site writes its own way, on no scale any of them publishes, so it sits on no axis
297
+ along which two versions could be put. A cost is a price in euros on one site and
298
+ a rank inside its own list on another, and one field holding both would invite
299
+ them to be compared.
460
300
 
461
- ## Licence and credit
301
+ This MCP server is an unofficial project, with no affiliation to any of the
302
+ sites it reads.
462
303
 
463
- This server is MIT. What it returns is not.
304
+ ## Privacy
464
305
 
465
- Cookbook pages are published under **Creative Commons Attribution-Share Alike
466
- 4.0**, which requires attribution: every recipe carries its licence and its
467
- link, and both have to travel with anything you repeat.
306
+ This server collects nothing about you and sends nothing to its author. It runs
307
+ on your machine, contacts `www.marmiton.org`, `api.wikimedia.org`,
308
+ `www.ptitchef.com`, `www.bbcgoodfood.com` and `www.supertoinette.com` and nothing
309
+ else, holds its answers in memory while it runs,
310
+ and writes nothing to disk. [PRIVACY.md](PRIVACY.md) states what a request
311
+ carries and which settings change any of it.
468
312
 
469
- Marmiton states no terms of use on a recipe page. Silence is not permission.
470
- Credit the source and link the page.
313
+ ## Development
471
314
 
472
- Every result carries a `url`. Use it.
315
+ ```bash
316
+ npm install
317
+ npm run build:fixtures
318
+ npm test
319
+ npm run check
320
+ ```
473
321
 
474
- ---
322
+ Tests run against generated fixtures and make no network request. The live suite,
323
+ `npm run test:live`, makes one request per route and runs nightly against the
324
+ sites themselves.
475
325
 
476
- ---
326
+ ## Contributing
477
327
 
478
- # mcp-recipes (français)
328
+ Bugs, questions and ideas belong in
329
+ [the issue tracker](https://github.com/smeet666/mcp-recipes/issues). Pull
330
+ requests are welcome; opening an issue first helps agree on the shape of the
331
+ change. See [CONTRIBUTING.md](CONTRIBUTING.md).
479
332
 
480
- Une question, plusieurs cuisines. Un serveur MCP qui interroge en même temps
481
- toutes les sources de recettes qu'il lit, fusionne leurs réponses, et met les
482
- quantités à l'échelle en français comme en anglais.
333
+ ## License
483
334
 
484
- Il en lit deux aujourd'hui : **Marmiton**, en français, et le **Wikibooks
485
- Cookbook**, en anglais.
486
-
487
- Aucune clé d'API. Aucun compte. Lecture seule.
335
+ MIT, see [LICENSE](LICENSE). The recipes belong to the sites that published them
336
+ and to their authors.
488
337
 
489
338
  ---
490
339
 
491
- ## Ce qu'il fait
492
-
493
- Marmiton est un site de recettes français, avec des notes de lecteurs et
494
- plusieurs millions de visiteurs. Le Wikibooks Cookbook est un wiki anglophone
495
- écrit par qui veut l'éditer, publié sous licence Creative Commons. Les deux
496
- écrivent les mêmes plats différemment, et cette différence vaut d'être réunie au
497
- même endroit.
498
-
499
- Ce serveur :
500
-
501
- - **cherche dans toutes les sources à la fois** et renvoie une seule liste,
502
- chaque ligne nommant la source d'où elle vient ;
503
- - **lit une recette** en entier et la recalcule pour le nombre de personnes
504
- voulu ;
505
- - **met à l'échelle une liste d'ingrédients** que vous avez déjà, hors ligne,
506
- dans l'une ou l'autre langue, ou dans une liste qui mélange les deux ;
507
- - **place un même plat côte à côte**, tel que chaque source l'écrit, toutes
508
- ramenées au même nombre de parts pour que les listes se comparent.
509
-
510
- ### Ce qui rend les réponses utilisables
511
-
512
- Multiplier chaque nombre par un facteur est juste arithmétiquement et inutile en
513
- cuisine : cela donne « 2,4 oeufs » et « 0,67 pincée de sel » avec le même aplomb
514
- que « 267 g de farine ». Ici, chaque quantité tombe là où un cuisinier peut la
515
- suivre :
516
-
517
- | La ligne dit | Fois six | Pourquoi |
518
- | ------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------ |
519
- | `une pincée de sel` | `6 pincées de sel` | Une pincée garde la taille que la main lui donne ; c'est le compte qui porte la proportion |
520
- | `1 dose (cup) de Mountain Dew` | `6 doses (cup) de Mountain Dew` | Une ligne française qui cite une mesure anglaise reste française |
521
- | `2/3 d'un flacon de fleur d'oranger` | `4 flacons de fleur d'oranger` | Une part d'une chose devient un compte de ces choses |
522
- | `1/4 litre de lait` | `1,5 l de lait` | Le français écrit ses décimales avec une virgule |
523
- | `250 ml milk` | `1.5 l milk` | L'anglais avec un point, et les deux montent l'échelle métrique |
524
- | `a capful of rum` | `6 capfuls rum` | Une mesure nommée d'après ce qui la contient est lue comme telle, répertoriée ou non |
525
-
526
- ### Ce qui se divise, et ce qui ne se divise pas
527
-
528
- Une mesure se divise tant que la moitié de son contenu reste une quantité qu'une
529
- cuisine sait prélever. C'est presque toujours le cas : ce que contient une
530
- boîte, un bocal, un sachet ou une brique se verse, se pèse ou se
531
- cuillère, donc une demi-boîte de tomates est une demi-boîte de tomates et le
532
- reste se garde au frais. Une feuille de gélatine et une branche de thym se
533
- coupent au couteau. Une poignée se divise en prenant moins.
534
-
535
- L'œuf fait exception, avec le jaune et le blanc d'œuf pris à part : en prélever
536
- la moitié demanderait de le battre et de le peser, ce qu'aucune recette ne
537
- demande. Le clou de girofle et le zeste le rejoignent, faute de moitié que
538
- quiconque prélève.
539
-
540
- Quelques cas vont dans l'autre sens et se divisent plus loin, parce qu'ils
541
- contiennent assez pour qu'un quart soit une portion : un pot, une bouteille, un
542
- bottle, un block. Et un mot peut énoncer un nombre de choses plutôt qu'une
543
- mesure, si bien que « 2 dozen mushrooms » réduits d'un quart reviennent en
544
- **18 mushrooms**. Un blanc appartient à l'aliment que la ligne nomme : le blanc
545
- d'œuf se compte entier, le blanc de poulet est une viande et se coupe en deux.
546
-
547
- - `3 eggs` de six personnes à vingt-cinq donne **13 eggs**, jamais une demie.
548
- - `1 can tomatoes` à quatre dixièmes donne **une demi-boîte**, avec la mention
549
- que la ligne ne porte plus sa part de la recette.
550
- - `2 tablespoons butter OR 30 g margarine` doublé met les **deux** branches à
551
- l'échelle, et la ligne n'est jamais annoncée comme exacte : à quel point l'une
552
- vaut l'autre, c'est la page qui l'affirme.
553
- - `4 tablespoons` au dixième descend à **1.2 teaspoons**. Une mesure passe à une
554
- unité plus petite _avant_ d'être arrondie, pour qu'une petite part survive.
555
- - `1 lb 4 oz beef` doublé donne **2.5 lb beef**, et `1 kg 500 de farine` donne
556
- **3 kg de farine**. Deux mesures d'une même échelle écrites côte à côte sont
557
- une seule quantité, et la plus petite peut rester sous-entendue.
558
- - `12 oz can tomatoes` revient telle que publiée, avec la mention que la mesure
559
- donne ce que tient une boîte et que la ligne n'énonce aucun nombre de boîtes.
560
- - `2 h de repos` revient telle que publiée. Un repos, une pousse ou une cuisson
561
- durent autant pour une grande fournée que pour une petite : une durée n'est pas
562
- une proportion.
563
- - `quelques feuilles de basilic` revient telle que publiée, comme `a few basil
564
- leaves` et comme la même ligne avec `plusieurs`. Un mot qui dit qu'il y en a
565
- plusieurs n'énonce aucun compte, et tout chiffre placé derrière lui serait une
566
- lecture de ce serveur plutôt qu'une quantité de la page.
567
-
568
- Chaque ligne revient avec un champ `scaling` : `scaled` quand le calcul est tombé
569
- juste, `rounded` quand quelque chose a dû bouger pour que la ligne reste
570
- utilisable, `unscaled` quand la ligne ne porte rien à multiplier. Une ligne
571
- arrondie dit depuis quelle valeur et dans quel sens.
340
+ <a name="mcp-recipes-français"></a>
572
341
 
573
- ---
342
+ # mcp-recipes (français)
343
+
344
+ _[English version](#mcp-recipes)_
345
+
346
+ Les recettes vivent sur beaucoup de sites, et chacun les écrit à sa façon : un
347
+ site de cuisine français publie en français, avec ses mesures et son idée de ce
348
+ qu'est une part, et un wiki de cuisine en anglais, avec des listes de matériel et
349
+ une prose pour lesquelles le premier n'a aucun champ. Poser une question à l'un
350
+ d'eux répond au sujet de l'un d'eux.
351
+
352
+ Ce serveur en lit cinq à la fois. Trois publient en français :
353
+ [Marmiton](https://www.marmiton.org), où des cuisiniers publient,
354
+ [Ptitchef](https://www.ptitchef.com), qui range ses recettes sous un arbre
355
+ d'ingrédients, et [Supertoinette](https://www.supertoinette.com), qui imprime un
356
+ temps de repos à part. Deux publient en anglais : le
357
+ [Cookbook des Wikibooks](https://en.wikibooks.org/wiki/Cookbook:Table_of_Contents),
358
+ écrit et entretenu à découvert, et
359
+ [BBC Good Food](https://www.bbcgoodfood.com), qui groupe une liste d'ingrédients
360
+ sous des intertitres. On peut chercher dans les cinq avec une seule question,
361
+ lire une recette de n'importe lequel sous une seule forme, mettre plusieurs
362
+ versions d'un même plat côte à côte, et adapter n'importe quelle liste
363
+ d'ingrédients. Aucune clé d'API, aucun compte.
574
364
 
575
365
  ## Installation
576
366
 
577
- Node 20 ou plus récent.
367
+ **Installation en un clic**
368
+
369
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=recipes&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZWNpcGVzIl19)
370
+ [![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=recipes&config=%7B%22name%22%3A%22recipes%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-recipes%22%5D%7D)
371
+
372
+ **Claude Code**
578
373
 
579
374
  ```bash
580
- npx -y mcp-recipes
375
+ claude mcp add recipes -- npx -y mcp-recipes
581
376
  ```
582
377
 
583
- ### Claude Desktop
378
+ **Claude Desktop, Cursor, et tout client au format de configuration standard**
584
379
 
585
380
  ```json
586
381
  {
@@ -593,260 +388,284 @@ npx -y mcp-recipes
593
388
  }
594
389
  ```
595
390
 
596
- ---
391
+ Node 24 ou plus récent est nécessaire, et aucune variable d'environnement n'est à
392
+ renseigner.
393
+
394
+ ### Avec Docker
395
+
396
+ ```json
397
+ {
398
+ "mcpServers": {
399
+ "recipes": {
400
+ "command": "docker",
401
+ "args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:3.0.0"]
402
+ }
403
+ }
404
+ }
405
+ ```
406
+
407
+ `-i` garde l'entrée standard ouverte, qui est le canal du protocole, et `-t` est
408
+ omis parce qu'un TTY réécrit le flux. Le conteneur a besoin d'un accès HTTPS
409
+ sortant vers `www.marmiton.org`, `api.wikimedia.org`, `www.ptitchef.com`,
410
+ `www.bbcgoodfood.com` et `www.supertoinette.com`, et de
411
+ rien d'autre : aucun volume, aucun port, aucun identifiant.
412
+
413
+ ### Bundle, sans npm
414
+
415
+ Téléchargez `mcp-recipes-3.0.0.mcpb` depuis
416
+ [la dernière publication](https://github.com/smeet666/mcp-recipes/releases/latest)
417
+ et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
418
+ sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
419
+ rien n'est téléchargé à l'installation.
420
+
421
+ ## Ce qu'on peut demander
422
+
423
+ - « Trouve-moi des recettes de carbonara, d'où que tu puisses. »
424
+ - « Compare les versions française et anglaise de ce plat. »
425
+ - « Lis-moi la seconde pour huit personnes. »
426
+ - « Laquelle utilise de la crème ? »
427
+ - « Multiplie par 1,5 cette liste de mon carnet. »
428
+
429
+ Le chemin ordinaire va d'une recherche à une lecture : une ligne porte un `id`
430
+ qui nomme sa source, et `get_recipe` le reprend.
431
+
432
+ ## Les sources
433
+
434
+ | Source | Site | Langue |
435
+ | --------------- | ----------------------- | -------- |
436
+ | `marmiton` | `www.marmiton.org` | français |
437
+ | `cookbook` | Cookbook Wikibooks | anglais |
438
+ | `ptitchef` | `www.ptitchef.com` | français |
439
+ | `goodfood` | `www.bbcgoodfood.com` | anglais |
440
+ | `supertoinette` | `www.supertoinette.com` | français |
441
+
442
+ L'`id` d'une ligne nomme sa source, donc un identifiant lu dans une réponse
443
+ retourne vers le bon site. **Les comptes ne sont jamais additionnés entre
444
+ sources**, et une source qui a échoué est rapportée comme ayant échoué plutôt que
445
+ comme n'ayant rien trouvé.
446
+
447
+ ## Les outils
597
448
 
598
- ## Les quatre outils
449
+ | Outil | Ce qu'il fait |
450
+ | ------------------- | ---------------------------------------------------------- |
451
+ | `search_recipes` | Cherche dans toutes les sources avec une seule question. |
452
+ | `get_recipe` | Lit une recette de l'une ou l'autre, sous une seule forme. |
453
+ | `compare_recipes` | Met plusieurs versions d'un même plat côte à côte. |
454
+ | `scale_ingredients` | Adapte n'importe quelle liste d'ingrédients, sans requête. |
599
455
 
600
456
  ### `search_recipes`
601
457
 
602
- Interroge toutes les sources en parallèle : les demander toutes coûte à peu près
603
- ce que coûte en demander une.
604
-
605
- | Argument | Type | Sens |
606
- | ------------------ | ------------------------------------------- | ------------------------------------------------------------------- |
607
- | `query` | string | Un plat ou un ingrédient, dans une des langues des sources |
608
- | `limit_per_source` | entier, 1 à 25, défaut 5 | Lignes prises à chaque source |
609
- | `sources` | tableau d'identifiants de source, optionnel | Absent, toutes sont interrogées |
610
- | `fan_out` | booléen, défaut true | Envoyer aussi des formulations plus courtes dérivées de la question |
611
-
612
- La requête part vers la recherche de chaque source en texte libre. Il n'y a
613
- aucun filtrage : un mot désignant un régime, un temps ou un nombre de calories
614
- ne compte que si l'index de la source le porte, et une question assez longue
615
- pour ressembler à une demande de conditions reçoit une note qui le dit.
616
-
617
- **Une question posée en phrase part aussi sous des formulations plus courtes.**
618
- Ces index répondent aux mots qu'on leur donne. L'un exige que chaque mot fourni
619
- figure sur la même page : « comment faire un ceviche de truite à l'origan » en
620
- revient vide alors que le corpus tient quatre recettes de ceviche. L'autre
621
- classe sur les mots reçus : « comment faire » remonte tout ce qui les partage et
622
- le plat sort de la liste. Dans les deux cas, le lecteur reçoit ce qui ressemble
623
- à un constat sur un corpus alors que c'est un constat sur une formulation.
624
-
625
- Une question devient donc une échelle ordonnée d'au plus trois formulations :
626
- les mots tels quels, les mots qui nomment le plat une fois écartés ceux qui
627
- encadrent la question, puis le premier mot du plat seul. Elles partent dans cet
628
- ordre, une à la fois, et s'arrêtent dès qu'il y a assez de lignes : une requête
629
- qui nomme déjà un plat coûte une requête. Ce qui revient est réuni et dédupliqué
630
- sur l'identifiant qui nomme sa source. `per_source` liste chaque formulation, ce
631
- qu'elle a rendu, et celles qui ont été retenues avec la raison. Rien n'est
632
- traduit d'une langue de source vers l'autre : une traduction devinée est un mot
633
- que personne n'a écrit, et une source qui publie dans une autre langue et ne
634
- tient rien sous les mots écrits est une absence honnête.
635
-
636
- **Une contrainte n'est pas un mot-clé.** « sans beurre », « gluten free » et
637
- « pour 6 personnes » disent ce que la recette doit éviter ou servir. Aucune page
638
- n'est nommée d'après ce qu'elle exclut, et le nombre de convives ne figure pas
639
- dans un titre : chercher ces mots enterre le plat. Ils restent dans la
640
- formulation envoyée en premier, où une source dont l'index les porte peut encore
641
- les faire correspondre, et ils sont écartés des formulations dérivées. Les
642
- laisser tomber en silence serait pire, puisque la recette trouvée peut contenir
643
- exactement ce que la personne évitait : chaque contrainte écartée est donc
644
- rappelée dans les notes, avec l'invitation à vérifier la liste d'ingrédients. Un
645
- nombre de parts est renvoyé vers l'argument `servings`, qui est ce qui remet
646
- réellement une recette à l'échelle.
647
-
648
- Une contrainte se lit au rôle que jouent ses mots, pas à la tournure dans
649
- laquelle ils sont écrits : une liste de tournures se troue au premier cas non
650
- prévu. Trois rôles sortent un aliment du plat. Une négation, quel que soit le
651
- mot qui la porte, puisque les négations forment une classe fermée de mots
652
- grammaticaux et que « sans beurre », « no butter » et « pas de beurre » reposent
653
- toutes sur l'une d'elles. Une phrase sur la personne plutôt que sur le plat,
654
- comme « je suis allergique aux noix », rappelée comme une allergie parce que
655
- c'est la contrainte dont l'échec relève de la santé. Et un régime nommé en un
656
- mot, qui est une classe de recettes définie par ce qu'elle exclut et énonce donc
657
- une exclusion sans qu'aucune négation soit écrite. Une question peut encore
658
- formuler une contrainte que cette lecture ne saisit pas, et la réponse le dit
659
- plutôt que de présenter les contraintes lues comme les seules présentes.
660
-
661
- La portée d'une contrainte se lit dans la phrase. Un mot de liaison juste après
662
- le marqueur dit que l'aliment le suit : « allergique aux noix », « allergic to
663
- peanuts », « free of dairy ». Sans ce mot, l'aliment est celui sur lequel le
664
- marqueur a été accolé : « peanut allergy », « lactose intolerant », « gluten
665
- free ». Les deux ordres sont nécessaires, puisque les deux langues que publient
666
- ces sources placent l'aliment de part et d'autre. Le nom d'un aliment s'arrête
667
- là où plus rien ne le prolonge, si bien que « sans gluten gateau au chocolat »
668
- écarte le gluten et cherche le gâteau. Quand un marqueur n'a d'aliment d'aucun
669
- côté, la réponse dit qu'une contrainte a été énoncée et que son aliment n'a pas
670
- été lu : nommer le mauvais mot cache un plat et laisse en même temps dans la
671
- recherche l'aliment que l'on évite, et un allergène ne se devine pas.
672
-
673
- Mettre `fan_out` à false envoie exactement les mots saisis. Les formulations non
674
- envoyées sont quand même nommées, pour qu'un appelant voie ce que le réactiver
675
- lui apporterait.
676
-
677
- Les lignes alternent d'une source à l'autre. Aucune note ne les classe les unes
678
- contre les autres, faute d'échelle commune : l'une porte des avis de lecteurs,
679
- l'autre n'a par nature ni auteur ni note. La réponse dit comment l'ordre a été
680
- construit.
681
-
682
- `per_source` rapporte ce qu'a répondu chaque source, dans les termes de cette
683
- source. Marmiton compte les lignes de l'unique page qu'il sert. Le Cookbook
684
- n'annonce aucun total, donc une liste courte venant de lui ne prouve pas qu'il y
685
- ait peu de choses. Ces nombres comptent des choses différentes et ne sont jamais
686
- additionnés.
458
+ Cherche dans toutes les sources avec une seule question.
459
+
460
+ | Argument | Type | Requis | Ce qu'il fait |
461
+ | ------------------ | -------------------------------- | ------ | ----------------------------------------------------------------------- |
462
+ | `query` | chaîne, 1 à 200 caractères | oui | Le plat ou l'ingrédient cherché. |
463
+ | `limit_per_source` | entier, 1 à 25, défaut `5` | non | Lignes à garder de chaque source. |
464
+ | `sources` | tableau d'identifiants de source | non | N'interroger que ces sources. |
465
+ | `fan_out` | booléen, défaut `true` | non | Interroger chaque source plutôt que s'arrêter à la première qui répond. |
466
+
467
+ **En retour :** `results`, des lignes portant `id`, que `get_recipe` reprend ;
468
+ `source` et `source_name` qui disent quel site a publié la ligne ; `title` ;
469
+ `url` ; `image_url` ; et un `excerpt` là où la source en propose un.
470
+ `per_source` donne un rapport par site avec son `status`, valant `answered` ou
471
+ `failed`, le `count` qu'il a fourni, et son `reported_total` accompagné de
472
+ `reported_total_means`, qui dit ce que ce nombre compte sur ce site. `order` dit
473
+ en mots comment la liste a été bâtie.
687
474
 
688
475
  ### `get_recipe`
689
476
 
690
- | Argument | Type | Sens |
691
- | ---------------- | ------------------------------------------ | ----------------------------------------------------------------- |
692
- | `id` | string | Issu de `search_recipes`, par exemple `marmiton:44078` |
693
- | `servings` | entier, optionnel | Recalculer pour ce nombre de personnes |
694
- | `sections` | tableau, défaut `["ingredients", "steps"]` | `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment` |
695
- | `max_steps` | entier, défaut 20 | Étapes renvoyées ; la réponse dit combien il en reste |
696
- | `max_step_chars` | entier, défaut 600 | Caractères gardés par étape |
697
-
698
- L'identifiant nomme sa source, donc la bonne est lue sans deviner. Un
699
- identifiant brut est routé sur sa forme et la réponse dit quelle lecture elle a
700
- faite : un nombre nu est celui de Marmiton, une clé de page portant l'espace de
701
- noms du Cookbook est celle du Cookbook, une adresse est routée par son hôte. Une
702
- chaîne qu'aucune source n'aurait produite est refusée, car l'envoyer quelque
703
- part renverrait ce que ce pari aurait touché.
704
-
705
- `sections_omitted` nomme ce qui a été laissé de côté. Un champ appartenant à
706
- l'une de ces sections est vide parce que personne ne l'a demandé, et un champ
707
- qu'une source ne publie pas vaut `null`.
708
-
709
- Une page qui n'annonce aucun nombre de parts revient telle que publiée, en
710
- disant pourquoi. Une page qui annonce une fourchette, « 4 à 6 personnes », est
711
- recalculée depuis la borne basse, et la réponse donne le facteur qu'aurait
712
- demandé la borne haute.
713
-
714
- Une partie de recette dont cette réponse ne tient rien n'est jamais rapportée
715
- comme une absence sur la page : n'avoir rien lu est un fait sur ce serveur, et
716
- seule la page peut dire s'il y avait quelque chose à lire. Quand la page en
717
- montre malgré tout un signe, un titre de section qui l'annonce ou une autre
718
- partie de la recette qui a été lue, la réponse dit que ce serveur n'a pas su
719
- lire ce que la page porte. Quand la page n'en montre aucun, elle dit que les
720
- deux cas sont indiscernables d'ici. Dans les deux cas, une liste d'ingrédients
721
- vide ne prouve jamais qu'un ingrédient est absent du plat : qui évite le beurre
722
- doit ouvrir la page avant de l'écarter.
477
+ Lit une recette de n'importe quelle source, sous une seule forme.
478
+
479
+ | Argument | Type | Requis | Ce qu'il fait |
480
+ | ---------------- | --------------------------------------------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------- |
481
+ | `id` | chaîne, 1 à 500 caractères | oui | L'identifiant d'une ligne, tel que `marmiton:44078`. Deux sources adressent une recette par un nombre nu : écrivez l'id avec sa source. |
482
+ | `servings` | entier, 1 à 500 | non | Adapte les ingrédients à ce nombre de parts. |
483
+ | `sections` | tableau de `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment`, défaut `["ingredients", "steps"]` | non | Les parties à rendre. |
484
+ | `max_steps` | entier, 1 à 100, défaut `20` | non | Étapes à servir. |
485
+ | `max_step_chars` | entier, 80 à 4000, défaut `600` | non | Caractères gardés par étape. |
486
+
487
+ **En retour :** la recette dans la forme où toutes les sources sont rendues, quelle
488
+ que soit celle qui l'a publiée : son titre, son adresse, ses ingrédients avec le
489
+ `scaling` de chaque ligne, ses étapes, et les parties demandées. Un champ qu'une
490
+ source publie et dont une autre n'a pas la notion revient absent plutôt
491
+ qu'inventé. `rest_minutes` porte le temps de repos d'une source qui l'imprime à
492
+ part, et il n'entre dans aucun autre temps rendu ici. `steps_as_one_block` dit
493
+ quand une source a publié sa méthode d'un seul bloc de prose plutôt qu'en étapes.
494
+ `withheld` nomme la partie qu'une source réserve à ses abonnés, qui est une
495
+ partie que la page porte et non une partie illisible. Augmentez `max_step_chars`
496
+ quand une étape a été coupée au milieu d'une phrase.
497
+
498
+ ### `compare_recipes`
499
+
500
+ Met plusieurs versions d'un même plat côte à côte.
501
+
502
+ | Argument | Type | Requis | Ce qu'il fait |
503
+ | ---------------- | ------------------------------------------------------------------------------------------------------ | ------ | ------------------------------------------- |
504
+ | `dish` | chaîne, 1 à 200 caractères | oui | Le plat à comparer. |
505
+ | `servings` | entier, 1 à 500 | non | Adapte chaque version à ce nombre de parts. |
506
+ | `sections` | tableau de `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment`, défaut `["ingredients"]` | non | Les parties à rendre par version. |
507
+ | `max_steps` | entier, 1 à 100, défaut `10` | non | Étapes à servir par version. |
508
+ | `max_step_chars` | entier, 80 à 4000, défaut `600` | non | Caractères gardés par étape. |
509
+ | `sources` | tableau d'ids de source | non | Ne compare que ces sources. |
510
+
511
+ **En retour :** `versions`, une recette par source ayant répondu, toutes adaptées
512
+ au même nombre de parts pour que leurs quantités se lisent l'une contre l'autre,
513
+ et `differences`, ce qui les sépare. `per_source` rapporte chaque site comme le
514
+ fait une recherche.
723
515
 
724
516
  ### `scale_ingredients`
725
517
 
726
- Hors ligne. Rien n'est téléchargé. La liste peut venir de n'importe quelle
727
- source, d'un livre de cuisine sur une étagère, ou d'une photo de fiche.
518
+ Adapte n'importe quelle liste d'ingrédients, sans requête à l'un ou l'autre site.
728
519
 
729
- | Argument | Type | Sens |
730
- | ------------------------------- | ----------------------------------- | -------------------------------------------------- |
731
- | `ingredients` | string[] | Les lignes telles quelles, un ingrédient par ligne |
732
- | `factor` | nombre | Par quoi multiplier |
733
- | `from_servings` + `to_servings` | entiers | Le facteur déduit d'un couple de nombres de parts |
734
- | `language` | `auto` / `fr` / `en`, défaut `auto` | `auto` lit chaque ligne pour elle-même |
520
+ | Argument | Type | Requis | Ce qu'il fait |
521
+ | --------------- | ----------------------------------- | ------------- | ----------------------------------------- |
522
+ | `ingredients` | tableau de 1 à 200 lignes | oui | Les lignes à adapter. |
523
+ | `factor` | nombre, jusqu'à 1000 | l'un des deux | Le multiplicateur à appliquer. |
524
+ | `from_servings` | entier, 1 à 500 | l'un des deux | Le nombre de parts de la liste d'origine. |
525
+ | `to_servings` | entier, 1 à 500 | l'un des deux | Le nombre de parts voulu. |
526
+ | `language` | `auto`, `fr` ou `en`, défaut `auto` | non | Comment chaque ligne est lue. |
735
527
 
736
- Donnez `factor`, ou le couple. Donner les deux est refusé : un appelant qui leur
737
- fait dire deux choses différentes a commis une erreur qui mérite d'être
738
- signalée.
528
+ Passez `factor`, ou le couple `from_servings` et `to_servings`. `auto` lit chaque
529
+ ligne pour elle-même, ce dont a besoin une liste portant les deux langues ;
530
+ nommer une langue lit toutes les lignes ainsi.
739
531
 
740
- `auto` est ce dont une liste mixte a besoin. Une ligne est lue dans la langue où
741
- elle est écrite, et revient écrite pareil, jusqu'à la virgule décimale et à
742
- l'endroit où tombe le pluriel.
532
+ **En retour :** les lignes adaptées dans la forme que rend `get_recipe`, chacune
533
+ avec son `scaling`.
743
534
 
744
- ### `compare_recipes`
535
+ ## L'adaptation des quantités
745
536
 
746
- | Argument | Type | Sens |
747
- | ---------------- | --------------------------------- | ------------------------------------------- |
748
- | `dish` | string | Le plat, dans une des langues des sources |
749
- | `servings` | entier, optionnel | Ramener chaque version à ce nombre de parts |
750
- | `sections` | tableau, défaut `["ingredients"]` | Comme ci-dessus |
751
- | `max_steps` | entier, défaut 10 | Étapes renvoyées par version |
752
- | `max_step_chars` | entier, défaut 600 | Caractères gardés par étape |
537
+ Une quantité est exprimée dans l'unité qui lui convient. Après adaptation, une
538
+ ligne peut donc apparaître dans une autre unité que celle de la recette : 200 g
539
+ multipliés par vingt donnent `4 kg`.
753
540
 
754
- C'est l'outil qui existe parce qu'il y a plus d'une source. Il lit la version la
755
- plus proche chez chacune, les ramène au même rendement, et énonce ce qui
756
- diffère : les quantités, les rendements, les temps, et les champs que chaque
757
- source publie. Là où les versions s'accordent, il se tait.
541
+ La finesse à laquelle un ingrédient se coupe dépend de sa nature. Une baguette se
542
+ coupe en deux, en trois ou en quatre ; un oeuf ne se partage pas. Une quantité
543
+ qui tombe entre les deux est donc arrondie, et la recette adaptée s'écarte alors
544
+ un peu des proportions de l'originale. La ligne porte `rounded`, et sa note dit
545
+ ce qui a été fait.
758
546
 
759
- Les versions sont montrées côte à côte, chacune dans les unités que sa source a
760
- publiées. Un gramme dans l'une et une tasse dans l'autre sont deux façons
761
- d'écrire une recette.
547
+ Les sources écrivent leurs quantités dans leur propre langue, et une ligne
548
+ est lue dans la langue où elle a été écrite. Les chiffres sont l'arithmétique de
549
+ ce serveur, donc dites qu'ils ont été recalculés quand vous les montrez.
762
550
 
763
- Quand une source manque, la réponse dit laquelle des trois choses s'est
764
- produite : sa recherche a échoué, elle a proposé une ligne illisible, ou elle
765
- n'a rien proposé d'assez proche pour comparer.
551
+ ## Ce qu'une réponse dit des sources
766
552
 
767
- ---
553
+ Chaque réponse rend compte de chaque source séparément. Un site qui a échoué, un
554
+ que personne n'a interrogé et un qui a répondu vide sont trois choses
555
+ différentes, et elles sont rapportées comme trois. Un total reste à côté de la
556
+ source qui l'a publié, avec ce que cette source compte en le disant : l'un compte
557
+ une catégorie entière et l'autre compte les lignes qu'il a servies.
768
558
 
769
- ## Ce qu'il refuse d'affirmer
770
-
771
- Chacune de ces règles est tenue par un test qui la nomme.
772
-
773
- - **Une source en échec est nommée comme une source en échec.** Une réponse qui
774
- ne porte que les lignes de certaines sources ne dit rien de ce que contiennent
775
- les autres.
776
- - **Aucun total n'est inventé.** Le nombre de chaque source est rapporté avec ce
777
- qu'il compte, dans les termes de cette source, et ces nombres ne sont jamais
778
- additionnés.
779
- - **Aucun classement entre les sources.** Elles n'ont pas d'échelle commune,
780
- donc les lignes alternent et la réponse dit comment l'ordre a été construit.
781
- - **Aucune conversion entre systèmes de mesure.** Une ligne garde les unités que
782
- sa source a publiées, et la source est nommée à côté.
783
- - **Un rendement est repris dans les mots de la source.** « 4 à 6 personnes » et
784
- « 4 personnes » sont deux affirmations différentes.
785
- - **Un champ qu'une source ne publie pas vaut `null`**, et un champ que personne
786
- n'a demandé figure dans `sections_omitted`.
787
- - **Le texte d'une source ne peut pas imiter ce serveur.** Tout ce qu'une source
788
- ou un appelant a écrit tient sur une seule ligne avant affichage, la syntaxe
789
- d'image markdown est désamorcée, et une ouverture ressemblant à un marqueur du
790
- serveur est indentée. La charge structurée conserve le texte tel que publié.
559
+ ## Configuration
791
560
 
792
- ---
561
+ Chaque variable est facultative. Elles se posent dans le bloc `env` de la
562
+ configuration du client.
793
563
 
794
- ## Réglages
564
+ | Variable | Défaut | Ce qu'elle fait |
565
+ | --------------------------- | -------------------- | ---------------------------------------------------------------------------------------- |
566
+ | `RECIPES_USER_AGENT` | l'identité du projet | Nomme votre application auprès des deux sites, avec une adresse où joindre une personne. |
567
+ | `RECIPES_MIN_INTERVAL_MS` | `1000` | Écart entre deux requêtes vers un même site, de 500 à 60000. |
568
+ | `RECIPES_TIMEOUT_MS` | `20000` | Délai d'une requête, de 1000 à 120000. |
569
+ | `RECIPES_MAX_RETRIES` | `3` | Tentatives après un échec passager, de 0 à 8. |
570
+ | `RECIPES_CACHE_TTL_MS` | `900000` | Durée pendant laquelle une réponse reste en mémoire, de 0 à 86400000. |
571
+ | `RECIPES_CACHE_MAX_ENTRIES` | `200` | Réponses gardées en mémoire à la fois, de 1 à 5000. |
572
+ | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info` ou `debug`, écrit sur la sortie d'erreur. |
795
573
 
796
- Tous optionnels. Une valeur illisible est signalée sur stderr et la valeur par
797
- défaut s'applique : un serveur qui refuse de démarrer à cause d'une faute de
798
- frappe est très difficile à diagnostiquer depuis l'application hôte.
574
+ Une valeur hors de sa plage retombe sur le défaut, et la raison est écrite sur la
575
+ sortie d'erreur.
799
576
 
800
- | Variable | Défaut | Sens |
801
- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
802
- | `RECIPES_USER_AGENT` | — | Identifiez votre client. L'identité du projet est ajoutée, pour qu'une source puisse joindre un humain |
803
- | `RECIPES_MIN_INTERVAL_MS` | `1000` | Écart minimal entre deux requêtes vers une même source. Plancher 500 |
804
- | `RECIPES_TIMEOUT_MS` | `20000` | Délai maximal d'une requête |
805
- | `RECIPES_MAX_RETRIES` | `3` | Réessais en cas de limitation ou d'échec passager |
806
- | `RECIPES_CACHE_TTL_MS` | `900000` | Durée de vie du cache mémoire. `0` le désactive |
807
- | `RECIPES_CACHE_MAX_ENTRIES` | `200` | Taille du cache mémoire |
808
- | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug`. Sur stderr |
577
+ ## Erreurs
809
578
 
810
- Aucune source ne publie de plafond pour un client comme celui-ci : le rythme est
811
- une politesse que le serveur s'impose. La configuration peut l'élargir, et le
812
- plancher tient par quelque chemin que le réglage arrive.
579
+ Chaque échec porte un des six codes, un message, et quand cela aide une
580
+ indication du geste suivant.
813
581
 
814
- ---
582
+ | Code | Ce qui s'est passé | Que faire |
583
+ | --------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------- |
584
+ | `not_found` | Une source a répondu, et n'a pas cette recette. | Vérifiez l'identifiant avec `search_recipes`. |
585
+ | `invalid_input` | Les arguments ont été refusés avant toute requête. | Lisez le message, qui nomme l'argument. |
586
+ | `rate_limited` | Une source demande à ce client de ralentir. | Attendez, puis rappelez avec les mêmes arguments. La recette est toujours là. |
587
+ | `parse_failure` | Une page a chargé et le contenu attendu est absent. | Signalez-le sur [le suivi d'incidents](https://github.com/smeet666/mcp-recipes/issues). |
588
+ | `network_error` | La requête n'a pas abouti. | Réessayez sous peu. |
589
+ | `timeout` | La requête a dépassé son délai. | Augmentez `RECIPES_TIMEOUT_MS`, ou demandez moins de lignes. |
590
+
591
+ Une source qui échoue est rapportée source par source plutôt que de faire échouer
592
+ toute la réponse, donc un site silencieux n'en cache jamais un autre.
593
+
594
+ ## Comme bibliothèque
595
+
596
+ La couche qui lit les deux sites est publiée seule, avec son rythme, son cache et
597
+ ses erreurs, sans protocole attaché.
815
598
 
816
- ## En cas de problème
599
+ ```ts
600
+ import { RecipesClient } from "mcp-recipes/client";
817
601
 
818
- Six codes d'erreur, et pas d'autres.
602
+ const client = new RecipesClient();
603
+ const read = await client.search({ query: "carbonara", limitPerSource: 3 });
604
+ console.log(read.data.results.length);
605
+ ```
819
606
 
820
- | Code | Sens |
821
- | --------------- | --------------------------------------------------------------- |
822
- | `not_found` | Une source a répondu, et n'a pas cette recette |
823
- | `invalid_input` | Les arguments ne permettaient pas de former une requête |
824
- | `rate_limited` | Une source a demandé à ce client de ralentir |
825
- | `parse_failure` | Une réponse est arrivée dans une forme illisible pour ce client |
826
- | `network_error` | La requête n'a pas abouti |
827
- | `timeout` | La requête a dépassé son délai |
607
+ Chaque lecture répond `{ data, cached }`, et lève une erreur portant un des six
608
+ codes. Chaque site garde son propre rythme, et le plancher tient également ici.
828
609
 
829
- `rate_limited` laisse entière la possibilité que la recette existe. Attendez et
830
- redemandez.
610
+ ## Rythme et attribution
831
611
 
832
- Un `parse_failure` signifie en général qu'une source a changé sa façon de
833
- répondre. Cela vaut un signalement :
834
- <https://github.com/smeet666/mcp-recipes/issues>
612
+ Chaque site est cadencé pour lui-même, une requête à la fois avec au moins une
613
+ seconde entre deux, et le plancher d'une demi-seconde tient quelle que soit la
614
+ configuration. Les interroger toutes à la fois coûte donc à chacune une requête,
615
+ jamais deux. Le `User-Agent` se termine toujours par l'identité du projet et une
616
+ adresse où joindre une personne.
835
617
 
836
- Si une recherche revient courte, lisez `per_source` avant d'en conclure quoi que
837
- ce soit.
618
+ Chaque ligne porte l'adresse de la page de la recette et le nom du site qui l'a
619
+ publiée. Les pages du Cookbook sont publiées sous
620
+ [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.fr), qui
621
+ demande que ce qu'on bâtit dessus soit partagé sous la même licence. Marmiton,
622
+ Ptitchef, BBC Good Food et Supertoinette n'énoncent aucune condition sur une page
623
+ de recette, et leurs recettes appartiennent à ces sites et aux cuisiniers qui les
624
+ ont écrites. Le silence n'est pas une autorisation : créditez le site et liez la
625
+ page d'où vient la recette.
838
626
 
839
- ---
627
+ Une recette que BBC Good Food réserve à ses abonnés revient sans ses ingrédients
628
+ ni sa méthode, nommée comme une recette retenue, avec l'adresse de sa page. Ce
629
+ serveur ne reconstitue pas ce que ce site a choisi de vendre.
630
+
631
+ Deux chiffres que les sites publient ne sont pas repris ici. Une difficulté est
632
+ un mot que chaque site écrit à sa façon, sur aucune échelle publiée : elle ne
633
+ siège sur aucun axe le long duquel deux versions se compareraient. Un coût est un
634
+ prix en euros sur un site et un rang dans sa propre liste sur un autre, et un
635
+ seul champ portant les deux inviterait à les comparer.
636
+
637
+ Ce MCP est un projet non officiel, sans affiliation à aucun des sites qu'il
638
+ lit.
639
+
640
+ ## Confidentialité
641
+
642
+ Ce serveur ne collecte rien sur vous et n'envoie rien à son auteur. Il tourne sur
643
+ votre machine, ne joint que `www.marmiton.org`, `api.wikimedia.org`,
644
+ `www.ptitchef.com`, `www.bbcgoodfood.com` et `www.supertoinette.com`, garde ses réponses en mémoire le temps qu'il tourne, et
645
+ n'écrit rien sur le disque. [PRIVACY.md](PRIVACY.md) dit ce qu'une requête
646
+ emporte et quels réglages changent cela.
647
+
648
+ ## Développement
649
+
650
+ ```bash
651
+ npm install
652
+ npm run build:fixtures
653
+ npm test
654
+ npm run check
655
+ ```
840
656
 
841
- ## Licence et crédit
657
+ Les tests s'exécutent sur des fixtures engendrées et n'émettent aucune requête.
658
+ La suite en direct, `npm run test:live`, émet une requête par route et tourne
659
+ chaque nuit contre les sites eux-mêmes.
842
660
 
843
- Ce serveur est sous licence MIT. Ce qu'il renvoie ne l'est pas.
661
+ ## Contribuer
844
662
 
845
- Les pages du Cookbook sont publiées sous **Creative Commons Attribution-Share
846
- Alike 4.0**, qui exige l'attribution : chaque recette porte sa licence et son
847
- lien, et les deux doivent voyager avec ce que vous reprenez.
663
+ Les anomalies, les questions et les idées ont leur place dans
664
+ [le suivi d'incidents](https://github.com/smeet666/mcp-recipes/issues). Les
665
+ propositions de modification sont bienvenues ; ouvrir un ticket d'abord aide à
666
+ s'accorder sur la forme du changement. Voir [CONTRIBUTING.md](CONTRIBUTING.md).
848
667
 
849
- Marmiton n'énonce aucune condition d'utilisation sur une page de recette. Le
850
- silence n'est pas une autorisation. Créditez la source et liez la page.
668
+ ## Licence
851
669
 
852
- Chaque résultat porte une `url`. Servez-vous-en.
670
+ MIT, voir [LICENSE](LICENSE). Les recettes appartiennent aux sites qui les ont
671
+ publiées et à leurs auteurs.