mcp-recipes 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,252 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 1.3.1
8
+
9
+ - The README carries the same badge row as every server here: npm, CI, the
10
+ licence, the MCP registry entry, the Glama score, and one-click installs for
11
+ Cursor and VS Code. Each install link encodes this package. npm serves the
12
+ README frozen at publish time, so a release is what puts it there.
13
+
14
+ ## 1.3.0
15
+
16
+ ### Added
17
+
18
+ - A question asked in a sentence is sent in shorter wordings as well as in the
19
+ words it was written in. These indexes answer the words they are handed: one
20
+ requires every word given to appear on the same page, so "comment faire un
21
+ ceviche de truite à l'origan" came back empty from a corpus holding four
22
+ ceviche recipes, and the other ranks on the words received, so the words
23
+ framing a question pulled up whatever shared them and pushed the dish out of
24
+ the list. A question now becomes an ordered ladder of at most three wordings,
25
+ tried in order and stopped as soon as enough rows have come back, and the rows
26
+ are the union deduplicated on the id naming its source. `per_source` lists
27
+ every wording, what each returned, and the ones held back with the reason.
28
+ Nothing is translated between the sources' languages.
29
+
30
+ - `search_recipes` takes `fan_out`, on unless a caller turns it off, which sends
31
+ exactly the words typed and still names the wordings that were withheld.
32
+
33
+ ### Changed
34
+
35
+ - A condition a question sets is handled as a condition rather than as a word to
36
+ find. "sans beurre", "gluten free" and "pour 6 personnes" stay in the wording
37
+ sent first, where a source whose index carries them can match, and are set
38
+ aside from the derived wordings, since no page is named for what it leaves out
39
+ and a number of eaters is not printed in a title. Every condition set aside is
40
+ named back in the notes with a reminder to read the ingredient list, because a
41
+ recipe found this way can hold exactly what was being avoided. A number of
42
+ servings is pointed at the `servings` argument.
43
+
44
+ - A condition is read by the role its words play rather than by the turn of
45
+ phrase they are written in. Reading it off the phrasings "sans X", "without X"
46
+ and "X-free" left an allergen a person had declared to become a positive
47
+ search term: "je suis allergique aux noix" put "noix" into a derived wording,
48
+ which pulled a coconut cake to the second row of a lasagne search with nothing
49
+ said about nuts. An exclusion written "no butter" or "pas de beurre" raised no
50
+ warning at all, and a diet named in one word was not treated as a condition,
51
+ so "recette vegane au foie gras" returned foie gras at the first row unmarked.
52
+ Three roles are now read: a negation, whichever of the closed class of
53
+ negating words carries it; a sentence about the eater rather than the dish,
54
+ which is named back as an allergy because that is the condition whose failure
55
+ is a matter of health; and a diet named in one word. All three are set aside
56
+ from the derived wordings and named back in the notes. A question can still
57
+ phrase one in a way this reading misses, and the notes say so instead of
58
+ presenting the conditions read as the only ones present. The reading is made
59
+ once for the whole question, so the words set aside as a condition and the
60
+ words searched for as a dish can no longer disagree.
61
+
62
+ - A quantity written across two measures is read as one quantity. "1 lb 4 oz
63
+ beef" doubled came back as "2 lb 4 oz beef", a tenth short of the answer and
64
+ marked as the exact product, and "1 kg 500 de farine" came back as "2 kg de
65
+ 500 de farine". Two measures off the same ladder written side by side, with
66
+ the second holding less than one of the first, are one amount: they give
67
+ "2.5 lb beef" and "3 kg de farine". The French form that leaves the smaller
68
+ unit unwritten is read the same way.
69
+
70
+ - A measure standing in front of a container is read as the size of one of them.
71
+ "12 oz can tomatoes" doubled came back as "1.5 lb can tomatoes", which asks
72
+ for a bigger tin where the recipe wants a second one. Lines of that shape come
73
+ back as published, with a note saying what the measure gives and that the line
74
+ states no count.
75
+
76
+ - A further quantity on a line is seen where the measure stands behind the word
77
+ introducing it. "150 g de sucre (soit 3/4 de tasse)" doubled came back as
78
+ "300 g de sucre (soit 3/4 de tasse)" marked as exact, so the line claimed
79
+ 300 g was three quarters of a cup. A quantity left as published beside a mass
80
+ or a volume now shows on the line's own `scaling`, which no longer reads
81
+ `scaled`. A container's stated capacity is not one of those: "2 boîtes de
82
+ 400 g" is exactly twice "1 boîte de 400 g" and stays `scaled`.
83
+
84
+ - A count read from a word that names no number is not reported as exact
85
+ arithmetic. "quelques feuilles de basilic" doubled gave six leaves marked
86
+ `scaled`, which put the weight of arithmetic behind a reading of "quelques".
87
+ The line comes back `rounded`, and the note says the figure is that reading
88
+ multiplied rather than a count the page gave.
89
+
90
+ - `compare_recipes` says when no version carries a word of the dish in its
91
+ title. One of these indexes answers almost any spelling with its closest row,
92
+ so a dish nobody publishes came back as a doughnut recipe presented as a
93
+ source's version of it, closing on an invitation to read it as what that
94
+ source publishes. That invitation is now withheld in exactly that case, and
95
+ the answer says the rows are candidates to check.
96
+
97
+ - A part of a recipe that comes back empty is reported as something this server
98
+ read nothing from rather than as something the page does not publish. Where
99
+ the page shows a sign of it anyway, a heading announcing that part or another
100
+ part of the recipe that was read, the answer states that this server failed to
101
+ read what the page carries. Where the page shows neither, the answer states
102
+ that a page publishing nothing and a layout this reader cannot follow are
103
+ indistinguishable from here. Both wordings close on the same warning: an empty
104
+ ingredient list is no evidence that an ingredient is absent from the dish.
105
+ This covers an empty method the same way. The note is only raised for a
106
+ section that was asked for.
107
+
108
+ ### Fixed
109
+
110
+ - An allergy stated with the food before the marker is read. Scoping a condition
111
+ forwards alone reads the French order, "allergique aux noix", and misses the
112
+ English one, so "cookies peanut allergy" searched for the nut, returned peanut
113
+ butter and said nothing about an allergy; "pancakes, I am lactose intolerant"
114
+ did the same with lactose. A marker now takes its food from whichever side of
115
+ it the sentence put one: a joining word straight after it says the food
116
+ follows, and with no such word the food is the one the marker was written onto
117
+ the end of.
118
+
119
+ - An exclusion and the dish no longer swap places. "a dessert free of dairy"
120
+ reported the dessert as the thing to leave out and searched for the dairy.
121
+ "free" is read the same way as any other marker, so "free of dairy" leaves out
122
+ the dairy and "gluten free" leaves out the gluten.
123
+
124
+ - A condition ends at the food it names. A fixed span of words let a condition
125
+ written at the head of a question swallow the dish behind it, so "sans gluten
126
+ gateau au chocolat" reported a recipe free of "gluten gateau au". A food's
127
+ name now runs on only through a word that carries it into a second noun, as in
128
+ "farine de blé", and stops everywhere else.
129
+
130
+ - A marker with a food on neither side of it is reported as a condition whose
131
+ food was not read, rather than passing unmentioned or taking a word at random.
132
+ Naming the wrong word hides a dish and searches for the food being avoided at
133
+ the same time, so the answer states that a condition was set and that this
134
+ server did not read what it covers.
135
+
136
+ - The note that says a diet was not filtered on reaches the text block. Notes
137
+ are dropped from that block when an answer carries more than it has room for,
138
+ and which ones could go was decided by matching their wording, so the diet
139
+ note went with the rest and "recette vegane au foie gras" rendered pan-fried
140
+ foie gras at the first row with nothing said about the diet. A note now
141
+ carries whether the answer misleads without it, set where the note is written,
142
+ and the block gives up body text before it gives up such a note.
143
+
144
+ - A comparison confronts two versions only where both titles carry the dish.
145
+ Sharing one word of a name is what makes a row worth offering and not what
146
+ makes it the dish, so "biscuits and gravy" put a tin of Christmas biscuits
147
+ beside the American dish and reported their yields, their times and their
148
+ ingredient counts as the difference between two traditions. A version whose
149
+ title leaves a word of the dish unanswered is now named, with the words it
150
+ does not carry and an invitation to read it as a candidate to check, and
151
+ nothing is set side by side until every version answers to the same name.
152
+
153
+ - A bracket the line closes on is scaled with the amount it restates.
154
+ "150 g de sucre (soit 3/4 de tasse)" doubled came back with the bracket as
155
+ published, so the line claimed that 300 g is three quarters of a cup. A
156
+ closing bracket that reads entirely as measures now moves with the figure it
157
+ restates, and it is read through the word that introduces it and through the
158
+ partitive between the number and the measure, so "soit 3/4 de tasse" is the
159
+ same quantity as "0,75 tasse". A slash inside a bracket separates two
160
+ statements of one quantity, and one sitting between two digits belongs to the
161
+ fraction it writes.
162
+
163
+ - A capacity in brackets counts containers rather than measuring one out.
164
+ "1 (14 oz) can" doubled came back as "2 (1.75 lb) can", which asks for a tin
165
+ no shop sells and converts a quantity into another system on the way. A
166
+ measure inside a bracket standing beside a container is what one of them
167
+ holds: it goes back exactly as published, the count is what the factor moves,
168
+ the container agrees with that count, and the line says which figure is
169
+ whose.
170
+
171
+ - A bracket a French line closes on moves with the rest of the line.
172
+ "1 demi verre de lait (10 cl)" tripled instructed the reader to take the ten
173
+ centilitres as published while asking for three times the milk.
174
+
175
+ - A measure written in abbreviations keeps what stands behind it.
176
+ "2 c.à.s d'huile" doubled came back as "4 cuillères à soupe", with the oil
177
+ gone from the recipe, and "2 c.à.s + 1 c.à.c d'huile" came back as
178
+ "4 cuillères à soupe de c.à.c d'huile". Normalising an abbreviation spells it
179
+ as several words where the line writes one, and that difference was consuming
180
+ words of the ingredient; the words the line wrote are now consumed until they
181
+ match the measure. Two measures joined by a sign or a word saying they add up
182
+ are read as one quantity, as two measures written side by side already were.
183
+
184
+ - A mass keeps the precision the arithmetic gave it. "1234 g" doubled came back
185
+ as "2,47 kg" marked as the exact product, where the exact product is 2468 g:
186
+ a whole number of grams was being snapped to a multiple of five, and the
187
+ result was then read in kilos, where a hundredth of the unit is ten grams and
188
+ every gap looks small. A whole number is what a scale shows and is left where
189
+ the arithmetic put it, a quantity moves up to a bigger unit only where that
190
+ unit states it as precisely, and whether a value landed exactly is judged in
191
+ the finer of the two units.
192
+
193
+ - A mark standing in front of the amount is not the amount. A line opening on a
194
+ bullet, a dash or a picture of the food was answered as a line carrying no
195
+ quantity, so 200 g of cheese stayed 200 g in a doubled recipe with nothing
196
+ said, and the mark also silenced the measure the language is read from. Such
197
+ a mark is set aside for reading and put back for writing.
198
+
199
+ - A text block that runs out of room says what it left out. Sentences
200
+ qualifying an answer keep their room, which leaves the result lines as the
201
+ thing that gives way, and a list of 119 ingredients gave way to nothing at
202
+ all in silence, which reads as a recipe needing no ingredients. A list now
203
+ keeps its opening whatever the room allows, states how many of how many lines
204
+ are shown, and points at the structured output for the rest. A recipe's
205
+ ingredients and its method share what is left rather than the first of them
206
+ filling the block.
207
+
208
+ - A length of time is no longer multiplied like an ingredient. Ingredient lists
209
+ carry a rest, a proof or a marinade among the quantities, and "2 h de repos"
210
+ tripled came back as "6 h de repos". A figure followed by a measure of time,
211
+ in either language, is returned as published with a note saying that a rest
212
+ takes as long for a large batch as for a small one.
213
+
214
+ - A word that names no number no longer produces one. "quelques feuilles de
215
+ basilic" tripled came back as "9 feuilles de basilic", while "a few basil
216
+ leaves" and "plusieurs feuilles de basilic" were returned as published, so
217
+ three ways of writing the same vagueness got three answers. All three are read
218
+ the same way now: the line carries no count, so it is repeated as published
219
+ and flagged as carrying nothing to multiply.
220
+
221
+ - The factor stated is the factor applied. It was shortened to three decimals
222
+ while the arithmetic was not, so `factor: 0.0001` rendered "Scaled by 0" and
223
+ put a zero in the payload beside quantities that were not zero. Below three
224
+ decimals the significant digits are stated instead.
225
+
226
+ - One quantity gives one answer however the page spelled it. "1234 g de farine"
227
+ and "1 kg 234 de farine" doubled came back 32 g apart, one of them called
228
+ exact and the other rounded, because the kilo cannot state 2468 g in the two
229
+ decimals a kitchen reads. A measured amount now moves down to the unit that
230
+ states it exactly, the way it already moved up only to a unit that could hold
231
+ it, so both lines answer "2468 g de farine".
232
+
233
+ - `compare_recipes` states differences about the parts it returned. It compared
234
+ the times of versions whose times the same answer said were not requested and
235
+ not returned, leaving a reader with a claim they had no way to check; a
236
+ difference is now stated only about a section the call asked for. Each
237
+ version's yield is also written the way the rest of the answer writes one, as
238
+ published and with the factor and the number of servings it was rescaled for,
239
+ rather than as a bare published yield standing above a rescaled list.
240
+
241
+ - Four smaller repairs. A query carrying no letter and no digit is refused
242
+ rather than sent, since an index handed nothing to match on answers with the
243
+ page it shows by default and those rows came back as recipes for "!!!". A
244
+ failure raised while reading a source is restated in this server's own words,
245
+ with what the site said quoted and credited to it and with any advice it
246
+ carried dropped, rather than served as this server's own sentence pointing
247
+ somewhere this server cannot answer for. A figure glued to an ordinal ending
248
+ is a rank rather than an amount, so "1er choix de boeuf" doubled no longer
249
+ reads "2 er choix de boeuf". And a rewritten line keeps the case the page
250
+ used, "1 TOMATO" tripled giving "3 TOMATOES", while a French noun in -ou takes
251
+ the plural French gives it: three choux, not three chous.
252
+
7
253
  ## [1.2.0] - 2026-08-07
8
254
 
9
255
  ### Changed
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # mcp-recipes
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/mcp-recipes.svg)](https://www.npmjs.com/package/mcp-recipes)
4
+ [![CI](https://github.com/smeet666/mcp-recipes/actions/workflows/ci.yml/badge.svg)](https://github.com/smeet666/mcp-recipes/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/mcp-recipes.svg)](./LICENSE)
6
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-6E56CF)](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.smeet666/mcp-recipes)
7
+ [![Glama](https://glama.ai/mcp/servers/smeet666/mcp-recipes/badges/score.svg)](https://glama.ai/mcp/servers/smeet666/mcp-recipes)
8
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=recipes&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm1jcC1yZWNpcGVzIl19)
9
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=recipes&config=%7B%22name%22%3A%22recipes%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22mcp-recipes%22%5D%7D)
10
+
3
11
  One question, several kitchens. An MCP server that asks every recipe source it
4
12
  reads at the same time, merges what they say, and scales quantities in French
5
13
  and English.
@@ -94,6 +102,17 @@ clou de girofle, and a count of those lands on a whole number.
94
102
  other is the page's own claim.
95
103
  - `4 tablespoons` at a tenth comes down to **1.2 teaspoons**. A measurement moves
96
104
  to a smaller unit _before_ it is rounded, so a small share survives.
105
+ - `1 lb 4 oz beef` doubled gives **2.5 lb beef**, and `1 kg 500 de farine` gives
106
+ **3 kg de farine**. Two measures off one ladder written side by side are one
107
+ quantity, and the smaller one can be left unwritten.
108
+ - `12 oz can tomatoes` comes back as published, with a note saying the measure
109
+ gives what one tin holds and that the line states no count of them.
110
+ - `2 h de repos` comes back as published. A rest, a proof or a bake takes as long
111
+ for a large batch as for a small one, so a length of time is not a proportion.
112
+ - `quelques feuilles de basilic` comes back as published, as do `a few basil
113
+ leaves` and the same line with `plusieurs`. A word saying there are several
114
+ states no count, and any figure put behind it would be this server's reading
115
+ rather than the page's quantity.
97
116
 
98
117
  Every line comes back with a `scaling` field: `scaled` when the arithmetic
99
118
  landed on the exact product, `rounded` when something had to move for the line
@@ -145,12 +164,70 @@ one costs.
145
164
  | `query` | string | A dish or an ingredient, in any language a source publishes in |
146
165
  | `limit_per_source` | integer, 1 to 25, default 5 | Rows to take from each source |
147
166
  | `sources` | array of source ids, optional | Left out, every source is asked |
167
+ | `fan_out` | boolean, default true | Whether shorter wordings derived from the question go out too |
148
168
 
149
169
  The query goes to each source's own search as free text. There is no filtering:
150
170
  a word naming a diet, a time or a calorie count matches only where that source's
151
171
  index happens to carry it, and a question long enough to read like a request for
152
172
  conditions gets a note saying so.
153
173
 
174
+ **A question asked in a sentence is also sent in shorter wordings.** These
175
+ indexes answer the words they are handed. One of them requires every word given
176
+ to appear on the same page, so "comment faire un ceviche de truite à l'origan"
177
+ comes back empty from a corpus holding four ceviche recipes; the other ranks on
178
+ the words it was handed, so "comment faire" pulls up whatever shares those words
179
+ and the dish falls out of the list. Both reach a reader looking like a statement
180
+ about a corpus when they are statements about a wording.
181
+
182
+ So a question is turned into an ordered ladder of at most three wordings: the
183
+ words as asked, the words naming the dish with the framing words set aside, and
184
+ the leading dish word on its own. They go out in that order, one at a time, and
185
+ stop as soon as enough rows have come back, so a query that already names a dish
186
+ costs one request. What comes back is unioned and deduplicated on the id naming
187
+ its source. `per_source` lists every wording, what each returned, and the ones
188
+ held back and why. Nothing is translated between the sources' languages: a
189
+ guessed translation is a word nobody wrote, and a source that publishes in
190
+ another language and holds nothing under the words written is an honest absence.
191
+
192
+ **A condition is not a keyword.** "sans beurre", "gluten free" and "pour 6
193
+ personnes" say what a recipe must avoid or serve. No page is named for what it
194
+ leaves out, and the number of people at a table is not printed in a title, so
195
+ searching for those words buries the dish. They stay in the wording sent first,
196
+ where a source whose index carries them can still match, and they are set aside
197
+ from the derived wordings. Dropping them quietly would be the worse move, since
198
+ a recipe found this way can hold exactly what the person was avoiding, so every
199
+ condition set aside is named back in the notes with a reminder to check the
200
+ ingredient list. A number of servings is pointed at the `servings` argument,
201
+ which is what actually rescales a recipe.
202
+
203
+ A condition is read by the role its words play rather than by the turn of phrase
204
+ they are written in, because a list of phrasings has a hole at the first one
205
+ nobody thought of. Three roles put a food out of a dish: a negation, whatever
206
+ word carries it, since negations are a closed class of function words and
207
+ "without butter", "no butter" and "pas de beurre" are all built on one; a
208
+ sentence about the eater rather than the dish, as in "je suis allergique aux
209
+ noix", which is named back as an allergy because that is the condition whose
210
+ failure is a matter of health; and a diet named in one word, which is a class of
211
+ recipes defined by what it leaves out and so states an exclusion with no
212
+ negation written anywhere. A question can still phrase one in a way this reading
213
+ does not catch, and the answer says so rather than presenting the ones it caught
214
+ as all there were.
215
+
216
+ Which words a condition covers is read off the sentence. A joining word straight
217
+ after the marker says its food follows, as in "allergique aux noix", "allergic
218
+ to peanuts" or "free of dairy"; with no such word, the food is the one the
219
+ marker was written onto the end of, as in "peanut allergy", "lactose intolerant"
220
+ or "gluten free". Both orders are needed, since the two languages these sources
221
+ publish in put the food on opposite sides. A food's name ends where nothing
222
+ carries it further, so "sans gluten gateau au chocolat" leaves out gluten and
223
+ looks for the cake. Where a marker has a food on neither side, the answer says a
224
+ condition was stated and that its food was not read: naming the wrong word hides
225
+ a dish and leaves the food being avoided in the search at the same time, and an
226
+ allergen is not something to guess at.
227
+
228
+ Setting `fan_out` to false sends exactly the words typed. The wordings that were
229
+ not sent are still named, so a caller can see what turning it back on would buy.
230
+
154
231
  Rows are interleaved one source at a time. No score orders them against each
155
232
  other, because the sources share none: one carries reader ratings and another
156
233
  has no author and no rating by nature. The answer says how the order was built.
@@ -188,8 +265,17 @@ the lower end, and the answer gives the factor the upper end would have needed.
188
265
 
189
266
  Some sources keep recipes and reference pages under one namespace, so a search
190
267
  row can be a page _about_ a dish rather than a recipe _for_ it. Only reading the
191
- page tells them apart, and this tool says when a page carries no ingredient
192
- list.
268
+ page tells them apart.
269
+
270
+ A part of a recipe this answer holds nothing for is never reported as an absence
271
+ on the page, because reading nothing is a fact about this server and only the
272
+ page can say whether there was anything to read. Where the page shows a sign of
273
+ the part all the same, a heading announcing it or another part of the recipe
274
+ that was read, the answer states that this server failed to read what the page
275
+ carries. Where the page shows neither, it states that the two cannot be told
276
+ apart from here. Either way an empty ingredient list is never evidence that an
277
+ ingredient is absent from the dish: someone avoiding butter has to open the page
278
+ before ruling it out.
193
279
 
194
280
  ### `scale_ingredients`
195
281
 
@@ -466,6 +552,18 @@ d'œuf se compte entier, le blanc de poulet est une viande et se coupe en deux.
466
552
  vaut l'autre, c'est la page qui l'affirme.
467
553
  - `4 tablespoons` au dixième descend à **1.2 teaspoons**. Une mesure passe à une
468
554
  unité plus petite _avant_ d'être arrondie, pour qu'une petite part survive.
555
+ - `1 lb 4 oz beef` doublé donne **2.5 lb beef**, et `1 kg 500 de farine` donne
556
+ **3 kg de farine**. Deux mesures d'une même échelle écrites côte à côte sont
557
+ une seule quantité, et la plus petite peut rester sous-entendue.
558
+ - `12 oz can tomatoes` revient telle que publiée, avec la mention que la mesure
559
+ donne ce que tient une boîte et que la ligne n'énonce aucun nombre de boîtes.
560
+ - `2 h de repos` revient telle que publiée. Un repos, une pousse ou une cuisson
561
+ durent autant pour une grande fournée que pour une petite : une durée n'est pas
562
+ une proportion.
563
+ - `quelques feuilles de basilic` revient telle que publiée, comme `a few basil
564
+ leaves` et comme la même ligne avec `plusieurs`. Un mot qui dit qu'il y en a
565
+ plusieurs n'énonce aucun compte, et tout chiffre placé derrière lui serait une
566
+ lecture de ce serveur plutôt qu'une quantité de la page.
469
567
 
470
568
  Chaque ligne revient avec un champ `scaling` : `scaled` quand le calcul est tombé
471
569
  juste, `rounded` quand quelque chose a dû bouger pour que la ligne reste
@@ -504,17 +602,78 @@ npx -y mcp-recipes
504
602
  Interroge toutes les sources en parallèle : les demander toutes coûte à peu près
505
603
  ce que coûte en demander une.
506
604
 
507
- | Argument | Type | Sens |
508
- | ------------------ | ------------------------------------------- | ---------------------------------------------------------- |
509
- | `query` | string | Un plat ou un ingrédient, dans une des langues des sources |
510
- | `limit_per_source` | entier, 1 à 25, défaut 5 | Lignes prises à chaque source |
511
- | `sources` | tableau d'identifiants de source, optionnel | Absent, toutes sont interrogées |
605
+ | Argument | Type | Sens |
606
+ | ------------------ | ------------------------------------------- | ------------------------------------------------------------------- |
607
+ | `query` | string | Un plat ou un ingrédient, dans une des langues des sources |
608
+ | `limit_per_source` | entier, 1 à 25, défaut 5 | Lignes prises à chaque source |
609
+ | `sources` | tableau d'identifiants de source, optionnel | Absent, toutes sont interrogées |
610
+ | `fan_out` | booléen, défaut true | Envoyer aussi des formulations plus courtes dérivées de la question |
512
611
 
513
612
  La requête part vers la recherche de chaque source en texte libre. Il n'y a
514
613
  aucun filtrage : un mot désignant un régime, un temps ou un nombre de calories
515
614
  ne compte que si l'index de la source le porte, et une question assez longue
516
615
  pour ressembler à une demande de conditions reçoit une note qui le dit.
517
616
 
617
+ **Une question posée en phrase part aussi sous des formulations plus courtes.**
618
+ Ces index répondent aux mots qu'on leur donne. L'un exige que chaque mot fourni
619
+ figure sur la même page : « comment faire un ceviche de truite à l'origan » en
620
+ revient vide alors que le corpus tient quatre recettes de ceviche. L'autre
621
+ classe sur les mots reçus : « comment faire » remonte tout ce qui les partage et
622
+ le plat sort de la liste. Dans les deux cas, le lecteur reçoit ce qui ressemble
623
+ à un constat sur un corpus alors que c'est un constat sur une formulation.
624
+
625
+ Une question devient donc une échelle ordonnée d'au plus trois formulations :
626
+ les mots tels quels, les mots qui nomment le plat une fois écartés ceux qui
627
+ encadrent la question, puis le premier mot du plat seul. Elles partent dans cet
628
+ ordre, une à la fois, et s'arrêtent dès qu'il y a assez de lignes : une requête
629
+ qui nomme déjà un plat coûte une requête. Ce qui revient est réuni et dédupliqué
630
+ sur l'identifiant qui nomme sa source. `per_source` liste chaque formulation, ce
631
+ qu'elle a rendu, et celles qui ont été retenues avec la raison. Rien n'est
632
+ traduit d'une langue de source vers l'autre : une traduction devinée est un mot
633
+ que personne n'a écrit, et une source qui publie dans une autre langue et ne
634
+ tient rien sous les mots écrits est une absence honnête.
635
+
636
+ **Une contrainte n'est pas un mot-clé.** « sans beurre », « gluten free » et
637
+ « pour 6 personnes » disent ce que la recette doit éviter ou servir. Aucune page
638
+ n'est nommée d'après ce qu'elle exclut, et le nombre de convives ne figure pas
639
+ dans un titre : chercher ces mots enterre le plat. Ils restent dans la
640
+ formulation envoyée en premier, où une source dont l'index les porte peut encore
641
+ les faire correspondre, et ils sont écartés des formulations dérivées. Les
642
+ laisser tomber en silence serait pire, puisque la recette trouvée peut contenir
643
+ exactement ce que la personne évitait : chaque contrainte écartée est donc
644
+ rappelée dans les notes, avec l'invitation à vérifier la liste d'ingrédients. Un
645
+ nombre de parts est renvoyé vers l'argument `servings`, qui est ce qui remet
646
+ réellement une recette à l'échelle.
647
+
648
+ Une contrainte se lit au rôle que jouent ses mots, pas à la tournure dans
649
+ laquelle ils sont écrits : une liste de tournures se troue au premier cas non
650
+ prévu. Trois rôles sortent un aliment du plat. Une négation, quel que soit le
651
+ mot qui la porte, puisque les négations forment une classe fermée de mots
652
+ grammaticaux et que « sans beurre », « no butter » et « pas de beurre » reposent
653
+ toutes sur l'une d'elles. Une phrase sur la personne plutôt que sur le plat,
654
+ comme « je suis allergique aux noix », rappelée comme une allergie parce que
655
+ c'est la contrainte dont l'échec relève de la santé. Et un régime nommé en un
656
+ mot, qui est une classe de recettes définie par ce qu'elle exclut et énonce donc
657
+ une exclusion sans qu'aucune négation soit écrite. Une question peut encore
658
+ formuler une contrainte que cette lecture ne saisit pas, et la réponse le dit
659
+ plutôt que de présenter les contraintes lues comme les seules présentes.
660
+
661
+ La portée d'une contrainte se lit dans la phrase. Un mot de liaison juste après
662
+ le marqueur dit que l'aliment le suit : « allergique aux noix », « allergic to
663
+ peanuts », « free of dairy ». Sans ce mot, l'aliment est celui sur lequel le
664
+ marqueur a été accolé : « peanut allergy », « lactose intolerant », « gluten
665
+ free ». Les deux ordres sont nécessaires, puisque les deux langues que publient
666
+ ces sources placent l'aliment de part et d'autre. Le nom d'un aliment s'arrête
667
+ là où plus rien ne le prolonge, si bien que « sans gluten gateau au chocolat »
668
+ écarte le gluten et cherche le gâteau. Quand un marqueur n'a d'aliment d'aucun
669
+ côté, la réponse dit qu'une contrainte a été énoncée et que son aliment n'a pas
670
+ été lu : nommer le mauvais mot cache un plat et laisse en même temps dans la
671
+ recherche l'aliment que l'on évite, et un allergène ne se devine pas.
672
+
673
+ Mettre `fan_out` à false envoie exactement les mots saisis. Les formulations non
674
+ envoyées sont quand même nommées, pour qu'un appelant voie ce que le réactiver
675
+ lui apporterait.
676
+
518
677
  Les lignes alternent d'une source à l'autre. Aucune note ne les classe les unes
519
678
  contre les autres, faute d'échelle commune : l'une porte des avis de lecteurs,
520
679
  l'autre n'a par nature ni auteur ni note. La réponse dit comment l'ordre a été
@@ -552,6 +711,16 @@ disant pourquoi. Une page qui annonce une fourchette, « 4 à 6 personnes », es
552
711
  recalculée depuis la borne basse, et la réponse donne le facteur qu'aurait
553
712
  demandé la borne haute.
554
713
 
714
+ Une partie de recette dont cette réponse ne tient rien n'est jamais rapportée
715
+ comme une absence sur la page : n'avoir rien lu est un fait sur ce serveur, et
716
+ seule la page peut dire s'il y avait quelque chose à lire. Quand la page en
717
+ montre malgré tout un signe, un titre de section qui l'annonce ou une autre
718
+ partie de la recette qui a été lue, la réponse dit que ce serveur n'a pas su
719
+ lire ce que la page porte. Quand la page n'en montre aucun, elle dit que les
720
+ deux cas sont indiscernables d'ici. Dans les deux cas, une liste d'ingrédients
721
+ vide ne prouve jamais qu'un ingrédient est absent du plat : qui évite le beurre
722
+ doit ouvrir la page avant de l'écarter.
723
+
555
724
  ### `scale_ingredients`
556
725
 
557
726
  Hors ligne. Rien n'est téléchargé. La liste peut venir de n'importe quelle