mcp-recipes 1.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 ADDED
@@ -0,0 +1,684 @@
1
+ # mcp-recipes
2
+
3
+ One question, several kitchens. An MCP server that asks every recipe source it
4
+ reads at the same time, merges what they say, and scales quantities in French
5
+ and English.
6
+
7
+ Today it reads two: **Marmiton**, in French, and the **Wikibooks Cookbook**, in
8
+ English.
9
+
10
+ No API key. No account. Read-only.
11
+
12
+ ---
13
+
14
+ ## What it does
15
+
16
+ Marmiton is a French recipe site with reader ratings and millions of visitors.
17
+ The Wikibooks Cookbook is an English wiki written by whoever edits it, published
18
+ under a Creative Commons licence. They write the same dishes differently, and
19
+ that difference is worth having in one place.
20
+
21
+ This server:
22
+
23
+ - **searches every source at once** and returns one list, each row naming the
24
+ source it came from;
25
+ - **reads one recipe** in full and rescales it to any number of people;
26
+ - **scales an ingredient list** you already hold, offline, in either language or
27
+ a list holding both;
28
+ - **puts one dish side by side**, as each source writes it, all rescaled to the
29
+ same number of servings so the lists stand comparison.
30
+
31
+ ### What makes the answers usable
32
+
33
+ Multiplying every number by a factor is arithmetically correct and practically
34
+ useless: it produces "2.4 eggs" and "0.67 pinch of salt" with the same
35
+ confidence as "267 g flour". Every quantity here lands where a cook can act on
36
+ it:
37
+
38
+ | The line says | Times six | Why |
39
+ | ----------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------ |
40
+ | `une pincée de sel` | `6 pincées de sel` | A pinch keeps the size a hand gives it; the count carries the proportion |
41
+ | `1 dose (cup) de Mountain Dew` | `6 doses (cup) de Mountain Dew` | A French line naming an English measure stays French |
42
+ | `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 |
43
+ | `1/4 litre de lait` | `1,5 l de lait` | French writes its decimals with a comma |
44
+ | `250 ml milk` | `1.5 l milk` | English writes them with a dot, and both climb the metric ladder |
45
+ | `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 |
46
+ | `a capful of rum` | `6 capfuls rum` | A measure named after what holds it is read as one, listed or not |
47
+
48
+ ### What divides, and what does not
49
+
50
+ A measure divides as far as half of what it holds is still a quantity a kitchen
51
+ can take out. Almost everything is: what a tin, a sachet or a carton
52
+ holds is poured, weighed or spooned, so half a tin of tomatoes is half a tin of
53
+ tomatoes and the rest keeps in the fridge. A leaf of gelatine and a sprig of
54
+ thyme are cut with a knife. A handful is halved by taking less.
55
+
56
+ An egg is the exception, along with a yolk and an egg white on their own: half
57
+ of one means beating it and weighing the result, which no recipe asks for. A
58
+ clou de girofle, a zeste and a zest go with it, having no half anyone takes.
59
+
60
+ A few measures go the other way and divide further. A pot, a bouteille, a
61
+ bottle, a jar and a block hold enough for a quarter to be a portion someone
62
+ serves. Une gousse, une tranche and a slice are already cut off something
63
+ larger, and the board that produced one takes a corner off it in the same
64
+ gesture. And a word can state a number of things rather than a measure of them,
65
+ so `2 dozen mushrooms` at three quarters comes back as **18 mushrooms**. A blanc
66
+ belongs to whichever food the line named: the white of an egg is counted whole,
67
+ a blanc de poulet is meat and halves.
68
+
69
+ A thing counted on its own is divided by the size of one against what a recipe
70
+ puts in, in either language. A crevette and a shrimp, une moule and a mussel,
71
+ une noisette and a hazelnut, un grain de poivre and a peppercorn, un anis étoilé
72
+ and a star anise is already a portion: a recipe counts twelve of them and a
73
+ smaller recipe puts one fewer in the pan, so they land on a whole number. Un
74
+ gigot and a leg of lamb, une baguette, un camembert, un ananas and a pineapple,
75
+ une pastèque and a watermelon, une pintade and a guinea fowl, un poulet and a
76
+ chicken, un poireau and a leek
77
+ 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
78
+ quarter. A cut carved off one of them stops at the half, une cuisse and a thigh
79
+ being the portion the knife already produced. Un jus, and a juice, stops at the
80
+ half too: half the jus of a citron is
81
+ taken by squeezing half the fruit, and a quarter of one has to be poured out and
82
+ measured back.
83
+
84
+ The word `clove` names two foods, and the line says which. Garlic in the line
85
+ makes it the wedge broken off a bulb, the gousse d'ail, which a knife takes to a
86
+ quarter; on its own, or written `whole cloves`, it is the dried flower bud, the
87
+ clou de girofle, and a count of those lands on a whole number.
88
+
89
+ - `3 eggs` taken from six people to twenty-five gives **13 eggs**, and never a
90
+ half of one.
91
+ - `1 can tomatoes` at four tenths gives **half a tin**, with a note saying the
92
+ line no longer holds its share of the recipe.
93
+ - `2 tablespoons butter OR 30 g margarine` doubled scales **both** branches, and
94
+ the line is never reported as exact arithmetic: how far one stands for the
95
+ other is the page's own claim.
96
+ - `4 tablespoons` at a tenth comes down to **1.2 teaspoons**. A measurement moves
97
+ to a smaller unit _before_ it is rounded, so a small share survives.
98
+
99
+ Every line comes back with a `scaling` field: `scaled` when the arithmetic
100
+ landed on the exact product, `rounded` when something had to move for the line
101
+ to stay usable, `unscaled` when the line carries nothing to multiply. A rounded
102
+ line says what it was rounded from and in which direction.
103
+
104
+ ---
105
+
106
+ ## Install
107
+
108
+ Node 20 or later.
109
+
110
+ ```bash
111
+ npx -y mcp-recipes
112
+ ```
113
+
114
+ ### Claude Desktop
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "recipes": {
120
+ "command": "npx",
121
+ "args": ["-y", "mcp-recipes"]
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ ### From a clone
128
+
129
+ ```bash
130
+ npm install
131
+ npm run build
132
+ node dist/index.js
133
+ ```
134
+
135
+ ---
136
+
137
+ ## The four tools
138
+
139
+ ### `search_recipes`
140
+
141
+ Asks every source at the same time, so asking them all costs about what asking
142
+ one costs.
143
+
144
+ | Argument | Type | Meaning |
145
+ | ------------------ | ----------------------------- | -------------------------------------------------------------- |
146
+ | `query` | string | A dish or an ingredient, in any language a source publishes in |
147
+ | `limit_per_source` | integer, 1 to 25, default 5 | Rows to take from each source |
148
+ | `sources` | array of source ids, optional | Left out, every source is asked |
149
+
150
+ The query goes to each source's own search as free text. There is no filtering:
151
+ a word naming a diet, a time or a calorie count matches only where that source's
152
+ index happens to carry it, and a question long enough to read like a request for
153
+ conditions gets a note saying so.
154
+
155
+ Rows are interleaved one source at a time. No score orders them against each
156
+ other, because the sources share none: one carries reader ratings and another
157
+ has no author and no rating by nature. The answer says how the order was built.
158
+
159
+ `per_source` reports what each source answered in that source's own terms.
160
+ Marmiton counts the rows on the single page it serves. The Cookbook states no
161
+ total at all, so a short list from it is not evidence that little exists. The
162
+ counts measure different things and are never added together.
163
+
164
+ ### `get_recipe`
165
+
166
+ | Argument | Type | Meaning |
167
+ | ---------------- | ----------------------------------------- | ----------------------------------------------------------------- |
168
+ | `id` | string | From `search_recipes`, such as `marmiton:44078` |
169
+ | `servings` | integer, optional | Rescale to this many |
170
+ | `sections` | array, default `["ingredients", "steps"]` | `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment` |
171
+ | `max_steps` | integer, default 20 | Steps to return; the answer says how many more there are |
172
+ | `max_step_chars` | integer, default 600 | Characters kept per step |
173
+
174
+ The identifier names its source, so the right one is read without guessing. A
175
+ raw identifier is routed by its shape, and the answer says which reading it
176
+ used: a bare number is Marmiton's, a page key carrying the Cookbook namespace is
177
+ the Cookbook's, and an address is routed by its host. A string no source would
178
+ have minted is refused, because sending it anywhere returns whatever that guess
179
+ happened to hit.
180
+
181
+ `sections_omitted` names what was left out. A field belonging to one of those
182
+ sections is empty because nobody asked for it, and a field a source genuinely
183
+ does not publish is `null`.
184
+
185
+ A page stating no number of servings comes back as published and says why:
186
+ dividing by a yield nobody wrote would answer for a number of people the page
187
+ never claimed. A page stating a span, such as "4 à 6 personnes", is scaled from
188
+ the lower end, and the answer gives the factor the upper end would have needed.
189
+
190
+ Some sources keep recipes and reference pages under one namespace, so a search
191
+ row can be a page _about_ a dish rather than a recipe _for_ it. Only reading the
192
+ page tells them apart, and this tool says when a page carries no ingredient
193
+ list.
194
+
195
+ ### `scale_ingredients`
196
+
197
+ Offline. Nothing is fetched. The list can come from any of the sources, from a
198
+ cookbook on a shelf, or from a photograph of a card.
199
+
200
+ | Argument | Type | Meaning |
201
+ | ------------------------------- | ------------------------------------ | ------------------------------------------- |
202
+ | `ingredients` | string[] | The lines as written, one ingredient each |
203
+ | `factor` | number | What to multiply by |
204
+ | `from_servings` + `to_servings` | integers | The factor worked out from a pair of counts |
205
+ | `language` | `auto` / `fr` / `en`, default `auto` | `auto` reads each line on its own |
206
+
207
+ Give `factor`, or the pair. Giving both is refused: a caller who means two
208
+ different things by them has made a mistake worth hearing about.
209
+
210
+ `auto` is what a mixed list needs. A line is read in the language it is written
211
+ in, and comes back written the same way, down to the decimal mark and where the
212
+ plural falls.
213
+
214
+ ### `compare_recipes`
215
+
216
+ | Argument | Type | Meaning |
217
+ | ---------------- | -------------------------------- | ----------------------------------------------- |
218
+ | `dish` | string | The dish, in any language a source publishes in |
219
+ | `servings` | integer, optional | Rescale every version to this many |
220
+ | `sections` | array, default `["ingredients"]` | As above |
221
+ | `max_steps` | integer, default 10 | Steps to return per version |
222
+ | `max_step_chars` | integer, default 600 | Characters kept per step |
223
+
224
+ This is the tool that exists because there is more than one source. It reads
225
+ each source's closest match, rescales them all to the same yield, and states
226
+ what differs: the quantities, the yields, the times, and which fields each
227
+ source publishes at all. Where the versions agree, it says nothing.
228
+
229
+ The versions are shown side by side, each in the units its own source published.
230
+ A gram in one and a cup in another are two ways of writing a recipe.
231
+
232
+ When one source is missing, the answer says which of three things happened: its
233
+ search failed, it offered a row that could not be read, or it offered nothing
234
+ close enough to compare.
235
+
236
+ ---
237
+
238
+ ## What it refuses to claim
239
+
240
+ Each of these is a rule the code is held to, with a test naming it.
241
+
242
+ - **A source that failed is named as a source that failed.** An answer holding
243
+ rows from some sources says nothing about what the others hold. "Marmiton was
244
+ unreachable" and "Marmiton holds no such recipe" are different statements
245
+ about the world, and a caller that cannot tell them apart will make the second
246
+ one.
247
+ - **No total is invented.** Each source's own number is reported with what it
248
+ counts, in that source's terms, and the counts are never summed.
249
+ - **No ranking across sources.** They share no scale, so rows interleave and the
250
+ answer says how the order was built.
251
+ - **No conversion between measuring systems.** A line keeps the units its source
252
+ published, and the source is named beside it.
253
+ - **A yield is repeated in the source's own words.** "4 à 6 personnes" and
254
+ "4 personnes" are different claims.
255
+ - **A field a source does not publish is `null`**, and a field nobody asked for
256
+ is listed in `sections_omitted`.
257
+ - **Text from a source cannot imitate this server.** Anything a source or a
258
+ caller wrote is put on a single line before it is rendered, with markdown
259
+ image syntax defused and any opening that looks like one of the server's own
260
+ markers indented. The structured payload keeps the text exactly as published.
261
+
262
+ ---
263
+
264
+ ## Settings
265
+
266
+ All optional. A value that cannot be read is reported on stderr and the default
267
+ stands, because a server that refuses to start over a typo is very hard to
268
+ diagnose from inside a host application.
269
+
270
+ | Variable | Default | Meaning |
271
+ | --------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
272
+ | `RECIPES_USER_AGENT` | — | Identify your own client. The project's identifier is appended, so a source can always reach a human |
273
+ | `RECIPES_MIN_INTERVAL_MS` | `1000` | Minimum gap between two requests to one source. Floor 500 |
274
+ | `RECIPES_TIMEOUT_MS` | `20000` | Deadline for one request |
275
+ | `RECIPES_MAX_RETRIES` | `3` | Retries on rate limiting and transient failures |
276
+ | `RECIPES_CACHE_TTL_MS` | `900000` | In-memory cache lifetime. `0` turns it off |
277
+ | `RECIPES_CACHE_MAX_ENTRIES` | `200` | In-memory cache size |
278
+ | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug`. Logs go to stderr |
279
+
280
+ No source publishes a ceiling for a client like this one, so the pacing is
281
+ self-imposed politeness. Configuration can widen it, and the floor holds
282
+ whichever way a setting arrives.
283
+
284
+ ---
285
+
286
+ ## As a library
287
+
288
+ Two entry points are published beside the server, with no protocol attached.
289
+
290
+ ```ts
291
+ import { RecipesClient } from "mcp-recipes/client";
292
+
293
+ const client = new RecipesClient();
294
+ const merged = await client.searchRecipes("carbonara", 3);
295
+
296
+ for (const report of merged.reports) {
297
+ console.log(report.name, report.status, report.error?.code ?? "");
298
+ }
299
+ ```
300
+
301
+ ```ts
302
+ import { scaleIngredients } from "mcp-recipes/scale";
303
+
304
+ scaleIngredients(["200 g de farine", "3 eggs"], { factor: 2 });
305
+ // [{ text: "400 g de farine", language: "fr", scaling: "scaled", … },
306
+ // { text: "6 eggs", language: "en", scaling: "scaled", … }]
307
+ ```
308
+
309
+ `RecipesClient` also takes stand-in readers, so a program embedding it can put
310
+ its own cache in front of a source, or drive it from fixed answers in a test:
311
+
312
+ ```ts
313
+ new RecipesClient({ readers: { marmiton: myReader } });
314
+ ```
315
+
316
+ A program bringing corpora of its own replaces the registry outright with
317
+ `sources`, and every tool works over however many it holds.
318
+
319
+ ---
320
+
321
+ ## When something goes wrong
322
+
323
+ Six error codes, and no others.
324
+
325
+ | Code | Means |
326
+ | --------------- | ---------------------------------------------------- |
327
+ | `not_found` | A source answered, and holds no such recipe |
328
+ | `invalid_input` | The arguments could not produce a request |
329
+ | `rate_limited` | A source asked this client to slow down |
330
+ | `parse_failure` | An answer arrived in a shape this client cannot read |
331
+ | `network_error` | The request did not complete |
332
+ | `timeout` | The request exceeded its deadline |
333
+
334
+ `rate_limited` means the recipe may well exist. Wait and ask again.
335
+
336
+ A `parse_failure` usually means a source changed how it answers. That is worth
337
+ reporting: <https://github.com/smeet666/mcp-recipes/issues>
338
+
339
+ If a search comes back short, read `per_source` before concluding anything.
340
+
341
+ ---
342
+
343
+ ## Development
344
+
345
+ ```bash
346
+ npm install
347
+ npm run typecheck
348
+ npm test # unit tests, no network
349
+ npm run build
350
+ ```
351
+
352
+ The unit suite runs against stand-in sources, so it is deterministic: time is
353
+ pinned to a fixed epoch, and every assertion is exact. A live suite sits behind
354
+ an environment variable and makes one request per route:
355
+
356
+ ```bash
357
+ RECIPES_LIVE=1 npm run test:live
358
+ ```
359
+
360
+ A nightly job runs that suite against the real sites and opens an issue when it
361
+ fails, because the unit tests cannot notice that a source changed.
362
+
363
+ **Adding a source.** Write an adapter: what it is called, how it recognises one
364
+ of its own identifiers, how to search it, and how to read one recipe off it.
365
+ Register it in `src/sources/registry.ts`. No tool, no merge and no error path
366
+ has a branch per source, so nothing else changes.
367
+
368
+ **Dependencies.** The reading of each site is a published library this server
369
+ depends on rather than code it carries: `mcp-marmiton` for Marmiton and
370
+ `mcp-wikibooks-cookbook` for the Cookbook. Each keeps its own pacing, its own
371
+ cache and its own error taxonomy. Everything above the seam, including the whole
372
+ of the bilingual scaling, lives here.
373
+
374
+ ---
375
+
376
+ ## Licence and credit
377
+
378
+ This server is MIT. What it returns is not.
379
+
380
+ Cookbook pages are published under **Creative Commons Attribution-Share Alike
381
+ 4.0**, which requires attribution: every recipe carries its licence and its
382
+ link, and both have to travel with anything you repeat.
383
+
384
+ Marmiton states no terms of use on a recipe page. Silence is not permission.
385
+ Credit the source and link the page.
386
+
387
+ Every result carries a `url`. Use it.
388
+
389
+ ---
390
+
391
+ ---
392
+
393
+ # mcp-recipes (français)
394
+
395
+ Une question, plusieurs cuisines. Un serveur MCP qui interroge en même temps
396
+ toutes les sources de recettes qu'il lit, fusionne leurs réponses, et met les
397
+ quantités à l'échelle en français comme en anglais.
398
+
399
+ Il en lit deux aujourd'hui : **Marmiton**, en français, et le **Wikibooks
400
+ Cookbook**, en anglais.
401
+
402
+ Aucune clé d'API. Aucun compte. Lecture seule.
403
+
404
+ ---
405
+
406
+ ## Ce qu'il fait
407
+
408
+ Marmiton est un site de recettes français, avec des notes de lecteurs et
409
+ plusieurs millions de visiteurs. Le Wikibooks Cookbook est un wiki anglophone
410
+ écrit par qui veut l'éditer, publié sous licence Creative Commons. Les deux
411
+ écrivent les mêmes plats différemment, et cette différence vaut d'être réunie au
412
+ même endroit.
413
+
414
+ Ce serveur :
415
+
416
+ - **cherche dans toutes les sources à la fois** et renvoie une seule liste,
417
+ chaque ligne nommant la source d'où elle vient ;
418
+ - **lit une recette** en entier et la recalcule pour le nombre de personnes
419
+ voulu ;
420
+ - **met à l'échelle une liste d'ingrédients** que vous avez déjà, hors ligne,
421
+ dans l'une ou l'autre langue, ou dans une liste qui mélange les deux ;
422
+ - **place un même plat côte à côte**, tel que chaque source l'écrit, toutes
423
+ ramenées au même nombre de parts pour que les listes se comparent.
424
+
425
+ ### Ce qui rend les réponses utilisables
426
+
427
+ Multiplier chaque nombre par un facteur est juste arithmétiquement et inutile en
428
+ cuisine : cela donne « 2,4 oeufs » et « 0,67 pincée de sel » avec le même aplomb
429
+ que « 267 g de farine ». Ici, chaque quantité tombe là où un cuisinier peut la
430
+ suivre :
431
+
432
+ | La ligne dit | Fois six | Pourquoi |
433
+ | ------------------------------------ | ------------------------------- | ------------------------------------------------------------------------------------------ |
434
+ | `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 |
435
+ | `1 dose (cup) de Mountain Dew` | `6 doses (cup) de Mountain Dew` | Une ligne française qui cite une mesure anglaise reste française |
436
+ | `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 |
437
+ | `1/4 litre de lait` | `1,5 l de lait` | Le français écrit ses décimales avec une virgule |
438
+ | `250 ml milk` | `1.5 l milk` | L'anglais avec un point, et les deux montent l'échelle métrique |
439
+ | `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 |
440
+
441
+ ### Ce qui se divise, et ce qui ne se divise pas
442
+
443
+ Une mesure se divise tant que la moitié de son contenu reste une quantité qu'une
444
+ cuisine sait prélever. C'est presque toujours le cas : ce que contient une
445
+ boîte, un bocal, un sachet ou une brique se verse, se pèse ou se
446
+ cuillère, donc une demi-boîte de tomates est une demi-boîte de tomates et le
447
+ reste se garde au frais. Une feuille de gélatine et une branche de thym se
448
+ coupent au couteau. Une poignée se divise en prenant moins.
449
+
450
+ L'œuf fait exception, avec le jaune et le blanc d'œuf pris à part : en prélever
451
+ la moitié demanderait de le battre et de le peser, ce qu'aucune recette ne
452
+ demande. Le clou de girofle et le zeste le rejoignent, faute de moitié que
453
+ quiconque prélève.
454
+
455
+ Quelques cas vont dans l'autre sens et se divisent plus loin, parce qu'ils
456
+ contiennent assez pour qu'un quart soit une portion : un pot, une bouteille, un
457
+ bottle, un block. Et un mot peut énoncer un nombre de choses plutôt qu'une
458
+ mesure, si bien que « 2 dozen mushrooms » réduits d'un quart reviennent en
459
+ **18 mushrooms**. Un blanc appartient à l'aliment que la ligne nomme : le blanc
460
+ d'œuf se compte entier, le blanc de poulet est une viande et se coupe en deux.
461
+
462
+ - `3 eggs` de six personnes à vingt-cinq donne **13 eggs**, jamais une demie.
463
+ - `1 can tomatoes` à quatre dixièmes donne **une demi-boîte**, avec la mention
464
+ que la ligne ne porte plus sa part de la recette.
465
+ - `2 tablespoons butter OR 30 g margarine` doublé met les **deux** branches à
466
+ l'échelle, et la ligne n'est jamais annoncée comme exacte : à quel point l'une
467
+ vaut l'autre, c'est la page qui l'affirme.
468
+ - `4 tablespoons` au dixième descend à **1.2 teaspoons**. Une mesure passe à une
469
+ unité plus petite _avant_ d'être arrondie, pour qu'une petite part survive.
470
+
471
+ Chaque ligne revient avec un champ `scaling` : `scaled` quand le calcul est tombé
472
+ juste, `rounded` quand quelque chose a dû bouger pour que la ligne reste
473
+ utilisable, `unscaled` quand la ligne ne porte rien à multiplier. Une ligne
474
+ arrondie dit depuis quelle valeur et dans quel sens.
475
+
476
+ ---
477
+
478
+ ## Installation
479
+
480
+ Node 20 ou plus récent.
481
+
482
+ ```bash
483
+ npx -y mcp-recipes
484
+ ```
485
+
486
+ ### Claude Desktop
487
+
488
+ ```json
489
+ {
490
+ "mcpServers": {
491
+ "recipes": {
492
+ "command": "npx",
493
+ "args": ["-y", "mcp-recipes"]
494
+ }
495
+ }
496
+ }
497
+ ```
498
+
499
+ ---
500
+
501
+ ## Les quatre outils
502
+
503
+ ### `search_recipes`
504
+
505
+ Interroge toutes les sources en parallèle : les demander toutes coûte à peu près
506
+ ce que coûte en demander une.
507
+
508
+ | Argument | Type | Sens |
509
+ | ------------------ | ------------------------------------------- | ---------------------------------------------------------- |
510
+ | `query` | string | Un plat ou un ingrédient, dans une des langues des sources |
511
+ | `limit_per_source` | entier, 1 à 25, défaut 5 | Lignes prises à chaque source |
512
+ | `sources` | tableau d'identifiants de source, optionnel | Absent, toutes sont interrogées |
513
+
514
+ La requête part vers la recherche de chaque source en texte libre. Il n'y a
515
+ aucun filtrage : un mot désignant un régime, un temps ou un nombre de calories
516
+ ne compte que si l'index de la source le porte, et une question assez longue
517
+ pour ressembler à une demande de conditions reçoit une note qui le dit.
518
+
519
+ Les lignes alternent d'une source à l'autre. Aucune note ne les classe les unes
520
+ contre les autres, faute d'échelle commune : l'une porte des avis de lecteurs,
521
+ l'autre n'a par nature ni auteur ni note. La réponse dit comment l'ordre a été
522
+ construit.
523
+
524
+ `per_source` rapporte ce qu'a répondu chaque source, dans les termes de cette
525
+ source. Marmiton compte les lignes de l'unique page qu'il sert. Le Cookbook
526
+ n'annonce aucun total, donc une liste courte venant de lui ne prouve pas qu'il y
527
+ ait peu de choses. Ces nombres comptent des choses différentes et ne sont jamais
528
+ additionnés.
529
+
530
+ ### `get_recipe`
531
+
532
+ | Argument | Type | Sens |
533
+ | ---------------- | ------------------------------------------ | ----------------------------------------------------------------- |
534
+ | `id` | string | Issu de `search_recipes`, par exemple `marmiton:44078` |
535
+ | `servings` | entier, optionnel | Recalculer pour ce nombre de personnes |
536
+ | `sections` | tableau, défaut `["ingredients", "steps"]` | `ingredients`, `steps`, `times`, `nutrition`, `tips`, `equipment` |
537
+ | `max_steps` | entier, défaut 20 | Étapes renvoyées ; la réponse dit combien il en reste |
538
+ | `max_step_chars` | entier, défaut 600 | Caractères gardés par étape |
539
+
540
+ L'identifiant nomme sa source, donc la bonne est lue sans deviner. Un
541
+ identifiant brut est routé sur sa forme et la réponse dit quelle lecture elle a
542
+ faite : un nombre nu est celui de Marmiton, une clé de page portant l'espace de
543
+ noms du Cookbook est celle du Cookbook, une adresse est routée par son hôte. Une
544
+ chaîne qu'aucune source n'aurait produite est refusée, car l'envoyer quelque
545
+ part renverrait ce que ce pari aurait touché.
546
+
547
+ `sections_omitted` nomme ce qui a été laissé de côté. Un champ appartenant à
548
+ l'une de ces sections est vide parce que personne ne l'a demandé, et un champ
549
+ qu'une source ne publie pas vaut `null`.
550
+
551
+ Une page qui n'annonce aucun nombre de parts revient telle que publiée, en
552
+ disant pourquoi. Une page qui annonce une fourchette, « 4 à 6 personnes », est
553
+ recalculée depuis la borne basse, et la réponse donne le facteur qu'aurait
554
+ demandé la borne haute.
555
+
556
+ ### `scale_ingredients`
557
+
558
+ Hors ligne. Rien n'est téléchargé. La liste peut venir de n'importe quelle
559
+ source, d'un livre de cuisine sur une étagère, ou d'une photo de fiche.
560
+
561
+ | Argument | Type | Sens |
562
+ | ------------------------------- | ----------------------------------- | -------------------------------------------------- |
563
+ | `ingredients` | string[] | Les lignes telles quelles, un ingrédient par ligne |
564
+ | `factor` | nombre | Par quoi multiplier |
565
+ | `from_servings` + `to_servings` | entiers | Le facteur déduit d'un couple de nombres de parts |
566
+ | `language` | `auto` / `fr` / `en`, défaut `auto` | `auto` lit chaque ligne pour elle-même |
567
+
568
+ Donnez `factor`, ou le couple. Donner les deux est refusé : un appelant qui leur
569
+ fait dire deux choses différentes a commis une erreur qui mérite d'être
570
+ signalée.
571
+
572
+ `auto` est ce dont une liste mixte a besoin. Une ligne est lue dans la langue où
573
+ elle est écrite, et revient écrite pareil, jusqu'à la virgule décimale et à
574
+ l'endroit où tombe le pluriel.
575
+
576
+ ### `compare_recipes`
577
+
578
+ | Argument | Type | Sens |
579
+ | ---------------- | --------------------------------- | ------------------------------------------- |
580
+ | `dish` | string | Le plat, dans une des langues des sources |
581
+ | `servings` | entier, optionnel | Ramener chaque version à ce nombre de parts |
582
+ | `sections` | tableau, défaut `["ingredients"]` | Comme ci-dessus |
583
+ | `max_steps` | entier, défaut 10 | Étapes renvoyées par version |
584
+ | `max_step_chars` | entier, défaut 600 | Caractères gardés par étape |
585
+
586
+ C'est l'outil qui existe parce qu'il y a plus d'une source. Il lit la version la
587
+ plus proche chez chacune, les ramène au même rendement, et énonce ce qui
588
+ diffère : les quantités, les rendements, les temps, et les champs que chaque
589
+ source publie. Là où les versions s'accordent, il se tait.
590
+
591
+ Les versions sont montrées côte à côte, chacune dans les unités que sa source a
592
+ publiées. Un gramme dans l'une et une tasse dans l'autre sont deux façons
593
+ d'écrire une recette.
594
+
595
+ Quand une source manque, la réponse dit laquelle des trois choses s'est
596
+ produite : sa recherche a échoué, elle a proposé une ligne illisible, ou elle
597
+ n'a rien proposé d'assez proche pour comparer.
598
+
599
+ ---
600
+
601
+ ## Ce qu'il refuse d'affirmer
602
+
603
+ Chacune de ces règles est tenue par un test qui la nomme.
604
+
605
+ - **Une source en échec est nommée comme une source en échec.** Une réponse qui
606
+ ne porte que les lignes de certaines sources ne dit rien de ce que contiennent
607
+ les autres.
608
+ - **Aucun total n'est inventé.** Le nombre de chaque source est rapporté avec ce
609
+ qu'il compte, dans les termes de cette source, et ces nombres ne sont jamais
610
+ additionnés.
611
+ - **Aucun classement entre les sources.** Elles n'ont pas d'échelle commune,
612
+ donc les lignes alternent et la réponse dit comment l'ordre a été construit.
613
+ - **Aucune conversion entre systèmes de mesure.** Une ligne garde les unités que
614
+ sa source a publiées, et la source est nommée à côté.
615
+ - **Un rendement est repris dans les mots de la source.** « 4 à 6 personnes » et
616
+ « 4 personnes » sont deux affirmations différentes.
617
+ - **Un champ qu'une source ne publie pas vaut `null`**, et un champ que personne
618
+ n'a demandé figure dans `sections_omitted`.
619
+ - **Le texte d'une source ne peut pas imiter ce serveur.** Tout ce qu'une source
620
+ ou un appelant a écrit tient sur une seule ligne avant affichage, la syntaxe
621
+ d'image markdown est désamorcée, et une ouverture ressemblant à un marqueur du
622
+ serveur est indentée. La charge structurée conserve le texte tel que publié.
623
+
624
+ ---
625
+
626
+ ## Réglages
627
+
628
+ Tous optionnels. Une valeur illisible est signalée sur stderr et la valeur par
629
+ défaut s'applique : un serveur qui refuse de démarrer à cause d'une faute de
630
+ frappe est très difficile à diagnostiquer depuis l'application hôte.
631
+
632
+ | Variable | Défaut | Sens |
633
+ | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
634
+ | `RECIPES_USER_AGENT` | — | Identifiez votre client. L'identité du projet est ajoutée, pour qu'une source puisse joindre un humain |
635
+ | `RECIPES_MIN_INTERVAL_MS` | `1000` | Écart minimal entre deux requêtes vers une même source. Plancher 500 |
636
+ | `RECIPES_TIMEOUT_MS` | `20000` | Délai maximal d'une requête |
637
+ | `RECIPES_MAX_RETRIES` | `3` | Réessais en cas de limitation ou d'échec passager |
638
+ | `RECIPES_CACHE_TTL_MS` | `900000` | Durée de vie du cache mémoire. `0` le désactive |
639
+ | `RECIPES_CACHE_MAX_ENTRIES` | `200` | Taille du cache mémoire |
640
+ | `RECIPES_LOG_LEVEL` | `error` | `silent`, `error`, `info`, `debug`. Sur stderr |
641
+
642
+ Aucune source ne publie de plafond pour un client comme celui-ci : le rythme est
643
+ une politesse que le serveur s'impose. La configuration peut l'élargir, et le
644
+ plancher tient par quelque chemin que le réglage arrive.
645
+
646
+ ---
647
+
648
+ ## En cas de problème
649
+
650
+ Six codes d'erreur, et pas d'autres.
651
+
652
+ | Code | Sens |
653
+ | --------------- | --------------------------------------------------------------- |
654
+ | `not_found` | Une source a répondu, et n'a pas cette recette |
655
+ | `invalid_input` | Les arguments ne permettaient pas de former une requête |
656
+ | `rate_limited` | Une source a demandé à ce client de ralentir |
657
+ | `parse_failure` | Une réponse est arrivée dans une forme illisible pour ce client |
658
+ | `network_error` | La requête n'a pas abouti |
659
+ | `timeout` | La requête a dépassé son délai |
660
+
661
+ `rate_limited` laisse entière la possibilité que la recette existe. Attendez et
662
+ redemandez.
663
+
664
+ Un `parse_failure` signifie en général qu'une source a changé sa façon de
665
+ répondre. Cela vaut un signalement :
666
+ <https://github.com/smeet666/mcp-recipes/issues>
667
+
668
+ Si une recherche revient courte, lisez `per_source` avant d'en conclure quoi que
669
+ ce soit.
670
+
671
+ ---
672
+
673
+ ## Licence et crédit
674
+
675
+ Ce serveur est sous licence MIT. Ce qu'il renvoie ne l'est pas.
676
+
677
+ Les pages du Cookbook sont publiées sous **Creative Commons Attribution-Share
678
+ Alike 4.0**, qui exige l'attribution : chaque recette porte sa licence et son
679
+ lien, et les deux doivent voyager avec ce que vous reprenez.
680
+
681
+ Marmiton n'énonce aucune condition d'utilisation sur une page de recette. Le
682
+ silence n'est pas une autorisation. Créditez la source et liez la page.
683
+
684
+ Chaque résultat porte une `url`. Servez-vous-en.