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