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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +684 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3037 -0
- package/dist/index.js.map +1 -0
- package/dist/language-BgfmjwtF.d.ts +14 -0
- package/dist/recipe/scale.d.ts +86 -0
- package/dist/recipe/scale.js +1367 -0
- package/dist/recipe/scale.js.map +1 -0
- package/dist/sources/client.d.ts +448 -0
- package/dist/sources/client.js +697 -0
- package/dist/sources/client.js.map +1 -0
- package/package.json +79 -0
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.
|