mcp-recipes 4.0.0 → 4.1.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 +9 -0
- package/README.md +26 -26
- package/dist/index.js +84 -41
- package/dist/index.js.map +1 -1
- package/dist/recipe/scale.d.ts +2 -1
- package/dist/recipe/scale.js.map +1 -1
- package/dist/sources/client.d.ts +5 -4
- package/dist/sources/client.js +29 -12
- package/dist/sources/client.js.map +1 -1
- package/package.json +15 -15
- package/server.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
- **Node.js 24.22.1 or later**, inside the Node.js 24 line. The package, the
|
|
6
|
+
extension manifest and the image all declare it.
|
|
7
|
+
- **zod `^4.6.7`.** Below 4.6.6, zod can exhaust memory while
|
|
8
|
+
validating oversized constrained collections, and the range accepts none of
|
|
9
|
+
those versions.
|
|
10
|
+
- **Dependency floors:** `@modelcontextprotocol/sdk` `^1.32.2`, `mcp-bbc-goodfood` `^1.1.0`, `mcp-marmiton` `^2.1.0`, `mcp-pequerecetas` `^1.1.0`, `mcp-supertoinette` `^1.1.0`, `mcp-wikibooks-cookbook` `^2.1.0`.
|
|
11
|
+
|
|
3
12
|
## 4.0.0
|
|
4
13
|
|
|
5
14
|
- **The published client entry renames a field on `SourceProfile` and
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Node 24 or later is required, and no environment variable has to be set.
|
|
|
65
65
|
"mcpServers": {
|
|
66
66
|
"recipes": {
|
|
67
67
|
"command": "docker",
|
|
68
|
-
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:4.
|
|
68
|
+
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:4.1.0"]
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -79,7 +79,7 @@ nothing else: no volume, no port, no credential.
|
|
|
79
79
|
|
|
80
80
|
### Bundle, without npm
|
|
81
81
|
|
|
82
|
-
Download `mcp-recipes-4.
|
|
82
|
+
Download `mcp-recipes-4.1.0.mcpb` from
|
|
83
83
|
[the latest release](https://github.com/smeet666/mcp-recipes/releases/latest) and
|
|
84
84
|
open it. A client that supports MCP bundles installs it on its own, with no npm
|
|
85
85
|
and no configuration file to edit. The bundle carries its dependencies, so
|
|
@@ -133,12 +133,12 @@ search says so, and `get_recipe` says what it read off the page.
|
|
|
133
133
|
|
|
134
134
|
Searches every source with one question.
|
|
135
135
|
|
|
136
|
-
| Argument | Type | Required | What it does
|
|
137
|
-
| ------------------ | ----------------------------- | -------- |
|
|
138
|
-
| `query` | string, 1 to 200 characters | yes | The dish or the ingredient to look for.
|
|
139
|
-
| `limit_per_source` | integer, 1 to 25, default `5` | no | Rows to keep from each source.
|
|
140
|
-
| `sources` | array of source ids | no | Ask these sources alone.
|
|
141
|
-
| `fan_out` | boolean, default `true` | no |
|
|
136
|
+
| Argument | Type | Required | What it does |
|
|
137
|
+
| ------------------ | ----------------------------- | -------- | ----------------------------------------------------------------- |
|
|
138
|
+
| `query` | string, 1 to 200 characters | yes | The dish or the ingredient to look for. |
|
|
139
|
+
| `limit_per_source` | integer, 1 to 25, default `5` | no | Rows to keep from each source. |
|
|
140
|
+
| `sources` | array of source ids | no | Ask these sources alone. |
|
|
141
|
+
| `fan_out` | boolean, default `true` | no | Also send each source shorter wordings derived from the question. |
|
|
142
142
|
|
|
143
143
|
**In return:** `results`, rows carrying `id`, which `get_recipe` takes;
|
|
144
144
|
`source` and `source_name` saying which site published the row; `title`; `url`;
|
|
@@ -314,11 +314,11 @@ Each site keeps its own pace, and the floors hold here as well.
|
|
|
314
314
|
|
|
315
315
|
## Pacing and attribution
|
|
316
316
|
|
|
317
|
-
Each site is paced on its own, one request at a time
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
and
|
|
321
|
-
and never lower it. Asking every site at once therefore costs each of them one request,
|
|
317
|
+
Each site is paced on its own, one request at a time. Every site holds a floor
|
|
318
|
+
of its own that no configuration goes under: half a second for Marmiton and the
|
|
319
|
+
Cookbook, one second for Ptitchef and BBC Good Food, three seconds for
|
|
320
|
+
Supertoinette and Pequerecetas. A setting posted for every source can raise a
|
|
321
|
+
site's spacing and never lower it. Asking every site at once therefore costs each of them one request,
|
|
322
322
|
never two. The `User-Agent` always ends with the project identity and an address
|
|
323
323
|
where a person can be reached.
|
|
324
324
|
|
|
@@ -438,7 +438,7 @@ renseigner.
|
|
|
438
438
|
"mcpServers": {
|
|
439
439
|
"recipes": {
|
|
440
440
|
"command": "docker",
|
|
441
|
-
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:4.
|
|
441
|
+
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-recipes:4.1.0"]
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
}
|
|
@@ -452,7 +452,7 @@ rien d'autre : aucun volume, aucun port, aucun identifiant.
|
|
|
452
452
|
|
|
453
453
|
### Bundle, sans npm
|
|
454
454
|
|
|
455
|
-
Téléchargez `mcp-recipes-4.
|
|
455
|
+
Téléchargez `mcp-recipes-4.1.0.mcpb` depuis
|
|
456
456
|
[la dernière publication](https://github.com/smeet666/mcp-recipes/releases/latest)
|
|
457
457
|
et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
|
|
458
458
|
sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
|
|
@@ -508,12 +508,12 @@ page.
|
|
|
508
508
|
|
|
509
509
|
Cherche dans toutes les sources avec une seule question.
|
|
510
510
|
|
|
511
|
-
| Argument | Type | Requis | Ce qu'il fait
|
|
512
|
-
| ------------------ | -------------------------------- | ------ |
|
|
513
|
-
| `query` | chaîne, 1 à 200 caractères | oui | Le plat ou l'ingrédient cherché.
|
|
514
|
-
| `limit_per_source` | entier, 1 à 25, défaut `5` | non | Lignes à garder de chaque source.
|
|
515
|
-
| `sources` | tableau d'identifiants de source | non | N'interroger que ces sources.
|
|
516
|
-
| `fan_out` | booléen, défaut `true` | non |
|
|
511
|
+
| Argument | Type | Requis | Ce qu'il fait |
|
|
512
|
+
| ------------------ | -------------------------------- | ------ | ---------------------------------------------------------------------------------- |
|
|
513
|
+
| `query` | chaîne, 1 à 200 caractères | oui | Le plat ou l'ingrédient cherché. |
|
|
514
|
+
| `limit_per_source` | entier, 1 à 25, défaut `5` | non | Lignes à garder de chaque source. |
|
|
515
|
+
| `sources` | tableau d'identifiants de source | non | N'interroger que ces sources. |
|
|
516
|
+
| `fan_out` | booléen, défaut `true` | non | Envoyer aussi à chaque source des formulations plus courtes tirées de la question. |
|
|
517
517
|
|
|
518
518
|
**En retour :** `results`, des lignes portant `id`, que `get_recipe` reprend ;
|
|
519
519
|
`source` et `source_name` qui disent quel site a publié la ligne ; `title` ;
|
|
@@ -695,11 +695,11 @@ Chaque site garde son propre rythme, et les planchers tiennent également ici.
|
|
|
695
695
|
|
|
696
696
|
## Rythme et attribution
|
|
697
697
|
|
|
698
|
-
Chaque site est cadencé pour lui-même, une requête à la fois
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
élargir l'écart d'un site, jamais le réduire. Les interroger toutes à la fois coûte donc à chacune une requête,
|
|
698
|
+
Chaque site est cadencé pour lui-même, une requête à la fois. Chaque site tient
|
|
699
|
+
un plancher qui lui est propre et sous lequel aucune configuration ne descend :
|
|
700
|
+
une demi-seconde pour Marmiton et le Cookbook, une seconde pour Ptitchef et BBC
|
|
701
|
+
Good Food, trois secondes pour Supertoinette et Pequerecetas. Un réglage posé
|
|
702
|
+
pour toutes les sources peut élargir l'écart d'un site, jamais le réduire. Les interroger toutes à la fois coûte donc à chacune une requête,
|
|
703
703
|
jamais deux. Le `User-Agent` se termine toujours par l'identité du projet et une
|
|
704
704
|
adresse où joindre une personne.
|
|
705
705
|
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
11
11
|
import process from "process";
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
|
-
var PKG_VERSION = "4.
|
|
14
|
+
var PKG_VERSION = "4.1.0";
|
|
15
15
|
var REPO_URL = "https://github.com/smeet666/mcp-recipes";
|
|
16
16
|
var ISSUES_URL = "https://github.com/smeet666/mcp-recipes/issues";
|
|
17
17
|
|
|
@@ -174,6 +174,7 @@ function fromSource(error, sourceName) {
|
|
|
174
174
|
|
|
175
175
|
// src/sources/adapter.ts
|
|
176
176
|
var NUMBER_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(.*)$/;
|
|
177
|
+
var NUMBER_INSIDE = /^\D*?(\d+(?:[.,]\d+)?)\s*(.*)$/;
|
|
177
178
|
var RANGE_AND_REST = /^\s*(\d+(?:[.,]\d+)?)\s*(?:à|a|to|-|–|—|ou|or)\s*(\d+(?:[.,]\d+)?)\s*(.*)$/i;
|
|
178
179
|
function text(value) {
|
|
179
180
|
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
@@ -237,6 +238,14 @@ function readYieldSpan(published) {
|
|
|
237
238
|
return { count: value, max: null, unit: text(single[2]) };
|
|
238
239
|
}
|
|
239
240
|
}
|
|
241
|
+
const inside = NUMBER_INSIDE.exec(published);
|
|
242
|
+
if (inside) {
|
|
243
|
+
const [figure = ""] = inside.slice(1);
|
|
244
|
+
const value = Number(figure.replace(",", "."));
|
|
245
|
+
if (Number.isFinite(value)) {
|
|
246
|
+
return { count: value, max: null, unit: text(inside[2]) };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
240
249
|
return { count: null, max: null, unit: null };
|
|
241
250
|
}
|
|
242
251
|
function detailBase(source) {
|
|
@@ -609,8 +618,10 @@ function marmitonAdapter(reader) {
|
|
|
609
618
|
skipped,
|
|
610
619
|
// Rows on the page, including the ones this server could not read: the
|
|
611
620
|
// site saw them, and reporting only the readable ones would understate
|
|
612
|
-
// what it holds.
|
|
613
|
-
|
|
621
|
+
// what it holds. A page carrying none is a page this server counted
|
|
622
|
+
// nothing on, and "Marmiton reported 0" is a sentence Marmiton never
|
|
623
|
+
// said.
|
|
624
|
+
reportedTotal: rows.length + skipped === 0 ? null : rows.length + skipped,
|
|
614
625
|
reportedTotalMeans: "rows on the single page of results Marmiton serves; the site disallows paging, so this is not a catalogue count",
|
|
615
626
|
cached: outcome.cached
|
|
616
627
|
};
|
|
@@ -1096,7 +1107,8 @@ function supertoinetteDetail(payload) {
|
|
|
1096
1107
|
}
|
|
1097
1108
|
|
|
1098
1109
|
// src/sources/registry.ts
|
|
1099
|
-
var
|
|
1110
|
+
var MAX_BODY_BYTES = 8e6;
|
|
1111
|
+
var PTITCHEF_BUDGET_MS = 6e4;
|
|
1100
1112
|
var PACING_FLOOR_MS = {
|
|
1101
1113
|
marmiton: 500,
|
|
1102
1114
|
cookbook: 500,
|
|
@@ -1116,6 +1128,7 @@ function pacedConfig(config, source) {
|
|
|
1116
1128
|
minIntervalMs: pacingFor(source, config.minIntervalMs),
|
|
1117
1129
|
timeoutMs: config.timeoutMs,
|
|
1118
1130
|
maxRetries: config.maxRetries,
|
|
1131
|
+
maxBodyBytes: MAX_BODY_BYTES,
|
|
1119
1132
|
cacheTtlMs: config.cacheTtlMs,
|
|
1120
1133
|
cacheMaxEntries: config.cacheMaxEntries,
|
|
1121
1134
|
logLevel: config.logLevel
|
|
@@ -1126,7 +1139,7 @@ function buildSources(config, readers, logger) {
|
|
|
1126
1139
|
const marmitonOptions = { config: paced(MARMITON_PROFILE.id) };
|
|
1127
1140
|
const cookbookOptions = { config: paced(COOKBOOK_PROFILE.id) };
|
|
1128
1141
|
const ptitchefOptions = {
|
|
1129
|
-
config: { ...paced(PTITCHEF_PROFILE.id),
|
|
1142
|
+
config: { ...paced(PTITCHEF_PROFILE.id), budgetMs: PTITCHEF_BUDGET_MS },
|
|
1130
1143
|
logger
|
|
1131
1144
|
};
|
|
1132
1145
|
const goodfoodOptions = { config: paced(GOODFOOD_PROFILE.id), logger };
|
|
@@ -1806,13 +1819,16 @@ function deriveWordings(question) {
|
|
|
1806
1819
|
);
|
|
1807
1820
|
return wordings.slice(0, MAX_WORDINGS_PER_SOURCE);
|
|
1808
1821
|
}
|
|
1809
|
-
function
|
|
1822
|
+
function sharesDishWord(title, question) {
|
|
1810
1823
|
const words = namingWords(question);
|
|
1811
1824
|
if (words.length === 0) {
|
|
1812
1825
|
return true;
|
|
1813
1826
|
}
|
|
1814
1827
|
return dishWordsMissing(title, question).length < words.length;
|
|
1815
1828
|
}
|
|
1829
|
+
function carriesDish(title, question) {
|
|
1830
|
+
return dishWordsMissing(title, question).length === 0;
|
|
1831
|
+
}
|
|
1816
1832
|
function dishWordsMissing(title, question) {
|
|
1817
1833
|
const haystack = ` ${fold(title)} `;
|
|
1818
1834
|
return namingWords(question).filter((word) => !haystack.includes(` ${fold(word)}`));
|
|
@@ -1894,7 +1910,7 @@ function reportOf(attempt, rows, question) {
|
|
|
1894
1910
|
preferredByName: attempt.preferredByName,
|
|
1895
1911
|
// Counted over the rows this answer holds, so it is read against `count`
|
|
1896
1912
|
// and never against a number of rows nobody was shown.
|
|
1897
|
-
namesTheDish: rows.filter((row) =>
|
|
1913
|
+
namesTheDish: rows.filter((row) => carriesDish(row.title, question)).length
|
|
1898
1914
|
};
|
|
1899
1915
|
}
|
|
1900
1916
|
function heldBack(wording, because) {
|
|
@@ -1918,7 +1934,7 @@ function keepNewRows(read, seen, rows, question) {
|
|
|
1918
1934
|
seen.add(row.id);
|
|
1919
1935
|
rows.push(row);
|
|
1920
1936
|
added += 1;
|
|
1921
|
-
if (
|
|
1937
|
+
if (sharesDishWord(row.title, question)) {
|
|
1922
1938
|
onTopic += 1;
|
|
1923
1939
|
}
|
|
1924
1940
|
}
|
|
@@ -1982,8 +1998,9 @@ var RecipesClient = class {
|
|
|
1982
1998
|
*/
|
|
1983
1999
|
deadlineFor(source) {
|
|
1984
2000
|
const attempts = this.config.maxRetries + 1;
|
|
1985
|
-
const
|
|
1986
|
-
|
|
2001
|
+
const spacing = pacingFor(source, this.config.minIntervalMs) * attempts;
|
|
2002
|
+
const backoff = LONGEST_WAIT_BETWEEN_TRIES_MS * this.config.maxRetries;
|
|
2003
|
+
return this.config.timeoutMs * attempts + spacing + backoff;
|
|
1987
2004
|
}
|
|
1988
2005
|
/**
|
|
1989
2006
|
* Ask every source the same question and merge the rows.
|
|
@@ -2123,8 +2140,8 @@ var RecipesClient = class {
|
|
|
2123
2140
|
failure = reason;
|
|
2124
2141
|
}
|
|
2125
2142
|
}
|
|
2126
|
-
const onTopicRows = rows.filter((row) =>
|
|
2127
|
-
const ordered = [...onTopicRows, ...rows.filter((row) => !
|
|
2143
|
+
const onTopicRows = rows.filter((row) => sharesDishWord(row.title, question));
|
|
2144
|
+
const ordered = [...onTopicRows, ...rows.filter((row) => !sharesDishWord(row.title, question))];
|
|
2128
2145
|
return {
|
|
2129
2146
|
source,
|
|
2130
2147
|
rows: ordered,
|
|
@@ -5058,9 +5075,17 @@ function labelNote(label, note) {
|
|
|
5058
5075
|
function answeredNotes(report) {
|
|
5059
5076
|
const notes = [];
|
|
5060
5077
|
const sent = report.wordings.filter((attempt) => attempt.ran && attempt.error === null);
|
|
5078
|
+
const failed = report.wordings.filter((attempt) => attempt.error !== null);
|
|
5061
5079
|
if (report.count === 0) {
|
|
5062
5080
|
notes.push(nothingOfferedNote(report, sent));
|
|
5063
5081
|
}
|
|
5082
|
+
if (failed.length > 0) {
|
|
5083
|
+
notes.push(
|
|
5084
|
+
mustKeep(
|
|
5085
|
+
`${report.name} did not answer ${failed.map((attempt) => `"${quoteForeign(attempt.query)}" ([${attempt.error?.code}])`).join(", ")}, so nothing here is a statement about ${failed.length > 1 ? "those wordings" : "that wording"}.`
|
|
5086
|
+
)
|
|
5087
|
+
);
|
|
5088
|
+
}
|
|
5064
5089
|
if (sent.length > 1) {
|
|
5065
5090
|
notes.push(
|
|
5066
5091
|
`${report.name} was asked ${sent.length} wordings and these rows are their union: ${sent.map((attempt) => `"${quoteForeign(attempt.query)}" (${attempt.count ?? 0})`).join(", ")}. Those counts are per wording and are never added.`
|
|
@@ -5225,7 +5250,7 @@ function truncate(text2, maxChars) {
|
|
|
5225
5250
|
}
|
|
5226
5251
|
function renderRows(rows) {
|
|
5227
5252
|
return rows.map((row, index) => {
|
|
5228
|
-
const head = `${index + 1}. ${quoteForeign(row.title)} \xB7 ${quoteForeign(row.source_name)} \xB7 id: ${row.id}`;
|
|
5253
|
+
const head = `${index + 1}. ${quoteForeign(row.title)} \xB7 ${quoteForeign(row.source_name)} \xB7 id: ${quoteForeign(row.id)}`;
|
|
5229
5254
|
return `${head}
|
|
5230
5255
|
${quoteForeign(row.url)}`;
|
|
5231
5256
|
}).join("\n");
|
|
@@ -5235,7 +5260,7 @@ function creditLine(contributors) {
|
|
|
5235
5260
|
return "No source contributed to this answer.";
|
|
5236
5261
|
}
|
|
5237
5262
|
const names2 = contributors.map(
|
|
5238
|
-
(entry) => entry.url ? `${entry.attribution}
|
|
5263
|
+
(entry) => entry.url ? `${entry.attribution} (${quoteForeign(entry.url)})` : entry.attribution
|
|
5239
5264
|
);
|
|
5240
5265
|
return names2.join("\n");
|
|
5241
5266
|
}
|
|
@@ -5538,16 +5563,21 @@ function buildRecipeView(recipe, options) {
|
|
|
5538
5563
|
attribution: recipe.attribution,
|
|
5539
5564
|
sections_returned: [...options.sections],
|
|
5540
5565
|
sections_omitted: omitted,
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
//
|
|
5546
|
-
|
|
5566
|
+
// The counts are over the lines this answer carries. Counting the lines a
|
|
5567
|
+
// recipe holds while the list is empty would say the answer scaled and
|
|
5568
|
+
// returned them.
|
|
5569
|
+
scaling_summary: wants("ingredients") ? {
|
|
5570
|
+
// A tool line is counted apart, so the four counts add up to the lines
|
|
5571
|
+
// in the list. It carries "unscaled" like a line with no figure on it,
|
|
5572
|
+
// and merging the two would say this server found no quantity where it
|
|
5573
|
+
// found one and declined it.
|
|
5574
|
+
scaled_count: ingredients.filter(
|
|
5575
|
+
(entry) => entry.scaling === "scaled" && !entry.isEquipment
|
|
5576
|
+
).length,
|
|
5547
5577
|
rounded_count: rounded.length,
|
|
5548
5578
|
unscaled_count: unscaled.length,
|
|
5549
5579
|
equipment_count: equipment.length
|
|
5550
|
-
}
|
|
5580
|
+
} : { scaled_count: 0, rounded_count: 0, unscaled_count: 0, equipment_count: 0 }
|
|
5551
5581
|
};
|
|
5552
5582
|
if (options.label) {
|
|
5553
5583
|
const label = quoteForeign(options.label);
|
|
@@ -5645,10 +5675,13 @@ function editDistance(left, right) {
|
|
|
5645
5675
|
}
|
|
5646
5676
|
|
|
5647
5677
|
// src/tools/compareRecipes.ts
|
|
5648
|
-
function nothingWasCompared(someWereUnread, dish) {
|
|
5678
|
+
function nothingWasCompared(someWereUnread, dish, answeredCount) {
|
|
5649
5679
|
if (someWereUnread) {
|
|
5650
5680
|
return `Every version of "${quoteForeign(dish)}" that was offered could not be read, so nothing was compared.`;
|
|
5651
5681
|
}
|
|
5682
|
+
if (answeredCount === 0) {
|
|
5683
|
+
return `No source answered for "${quoteForeign(dish)}", so nothing here says whether such a recipe exists.`;
|
|
5684
|
+
}
|
|
5652
5685
|
return `No source offered a recipe for "${quoteForeign(dish)}".`;
|
|
5653
5686
|
}
|
|
5654
5687
|
var compareRecipesDescription = [
|
|
@@ -5656,7 +5689,7 @@ var compareRecipesDescription = [
|
|
|
5656
5689
|
"Each source's closest match is read in full, and all of them can be rescaled to the same number of servings so the ingredient lists stand comparison.",
|
|
5657
5690
|
"The answer states what differs and leaves it there: the quantities each asks for, what each measures in, what each yields, and which fields each source publishes at all.",
|
|
5658
5691
|
"No version is ranked above another. Some sources carry reader ratings and some have no author and no rating by nature, so there is no score they share.",
|
|
5659
|
-
"When only one source answers, the answer
|
|
5692
|
+
"When only one source answers, the answer names that source and shows its version, so a single version is read as the one it is."
|
|
5660
5693
|
].join(" ");
|
|
5661
5694
|
var compareRecipesInput = strictInput({
|
|
5662
5695
|
dish: z4.string().min(1).max(200).describe("The dish to compare, in any of the languages the sources publish in."),
|
|
@@ -5685,7 +5718,7 @@ function weighTitlesAgainstTheDish(payloads, dish, notes) {
|
|
|
5685
5718
|
payload,
|
|
5686
5719
|
missing: dishWordsMissing(payload.title, dish)
|
|
5687
5720
|
}));
|
|
5688
|
-
const
|
|
5721
|
+
const carriesDish2 = weighed.some((one) => one.missing.length === 0);
|
|
5689
5722
|
for (const { payload, missing } of weighed) {
|
|
5690
5723
|
if (missing.length === 0) {
|
|
5691
5724
|
continue;
|
|
@@ -5696,12 +5729,12 @@ function weighTitlesAgainstTheDish(payloads, dish, notes) {
|
|
|
5696
5729
|
)
|
|
5697
5730
|
);
|
|
5698
5731
|
}
|
|
5699
|
-
if (payloads.length > 0 && !
|
|
5732
|
+
if (payloads.length > 0 && !carriesDish2) {
|
|
5700
5733
|
notes.push(
|
|
5701
5734
|
`No version here carries the whole of "${quoteForeign(dish)}" in its title. These are the closest rows each source returned for that spelling, so read them as candidates to check rather than as that dish.`
|
|
5702
5735
|
);
|
|
5703
5736
|
}
|
|
5704
|
-
if (payloads.length === 1 &&
|
|
5737
|
+
if (payloads.length === 1 && carriesDish2) {
|
|
5705
5738
|
notes.push(
|
|
5706
5739
|
mustKeep(
|
|
5707
5740
|
"This is one version rather than a comparison. Read it as what that one source publishes."
|
|
@@ -5922,7 +5955,11 @@ ${shownDifferences.map((line) => `- ${line}`).join("\n")}${moreDifferences > 0 ?
|
|
|
5922
5955
|
}
|
|
5923
5956
|
return lines.join("\n");
|
|
5924
5957
|
});
|
|
5925
|
-
const body = payloads.length === 0 ? nothingWasCompared(
|
|
5958
|
+
const body = payloads.length === 0 ? nothingWasCompared(
|
|
5959
|
+
unread.size > 0,
|
|
5960
|
+
args.dish,
|
|
5961
|
+
merged.reports.filter((report) => report.error === null).length
|
|
5962
|
+
) : versionBlocks.join("\n\n");
|
|
5926
5963
|
const summary = `${differencesBlock}${body}`;
|
|
5927
5964
|
return ok(
|
|
5928
5965
|
{
|
|
@@ -5957,7 +5994,7 @@ import { z as z5 } from "zod";
|
|
|
5957
5994
|
var getRecipeDescription = [
|
|
5958
5995
|
"Read one recipe in full: its ingredients, its steps, what it yields, and whatever times, rating and nutrition its source publishes.",
|
|
5959
5996
|
"'id' must come from search_recipes. It names the source, so this reads the right one without guessing; an identifier no source would have minted is refused, because sending it anywhere would answer about the wrong dish.",
|
|
5960
|
-
"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable
|
|
5997
|
+
"Pass 'servings' to rescale. Quantities land where a kitchen can follow them: an egg stays whole, anything that pours or cuts can halve, a small measurement moves to a smaller unit before it is rounded, and anything unmultipliable comes back flagged.",
|
|
5961
5998
|
"A page that states no number of servings comes back as published and says so, because dividing by a yield nobody wrote would answer for a number of people the page never claimed.",
|
|
5962
5999
|
"Read 'kind' first. Some sources publish articles gathering other recipes at the same kind of address as a recipe, and such an answer carries 'collection' with the recipes it points at and no 'recipe' at all: there is nothing to cook from that page, and the recipes it lists are read with get_recipe.",
|
|
5963
6000
|
"A part this answer holds nothing for says which of two things happened: the page showed no sign of it, or this server failed to read what the page carries. An empty ingredient list is never evidence that an ingredient is absent from the dish.",
|
|
@@ -6024,7 +6061,10 @@ async function runGetRecipe(client, args) {
|
|
|
6024
6061
|
if (entry.is_equipment) {
|
|
6025
6062
|
return `- ${quoteForeign(entry.text)} (a tool)`;
|
|
6026
6063
|
}
|
|
6027
|
-
|
|
6064
|
+
if (entry.scaling !== "unscaled") {
|
|
6065
|
+
return `- ${quoteForeign(entry.text)}`;
|
|
6066
|
+
}
|
|
6067
|
+
return entry.note ? `- ${quoteForeign(entry.text)} (${quoteForeign(entry.note)})` : `- ${quoteForeign(entry.text)} (no quantity)`;
|
|
6028
6068
|
});
|
|
6029
6069
|
const stepLines = payload.steps.map((step, index) => `${index + 1}. ${quoteForeign(step)}`);
|
|
6030
6070
|
const room = roomForBody({ notes, credit }) - head.join("\n").length;
|
|
@@ -6110,9 +6150,7 @@ var scaleIngredientsDescription = [
|
|
|
6110
6150
|
"No quantity is converted between measuring systems: grams stay grams and cups stay cups, because a conversion changes what the recipe said."
|
|
6111
6151
|
].join(" ");
|
|
6112
6152
|
var scaleIngredientsInput = strictInput({
|
|
6113
|
-
ingredients: z6.array(z6.string().max(500)).min(1).max(200).describe(
|
|
6114
|
-
"The lines as written, one ingredient each, such as '200 g de farine' or '3 eggs'. One line, not a whole recipe."
|
|
6115
|
-
),
|
|
6153
|
+
ingredients: z6.array(z6.string().max(500)).min(1).max(200).describe("The lines as written, one ingredient each, such as '200 g de farine' or '3 eggs'."),
|
|
6116
6154
|
factor: z6.number().gt(0).max(1e3).optional().describe(
|
|
6117
6155
|
"What to multiply by. Give exactly one of: 'factor', or both 'from_servings' and 'to_servings'."
|
|
6118
6156
|
),
|
|
@@ -6185,7 +6223,7 @@ function runScaleIngredients(args) {
|
|
|
6185
6223
|
}
|
|
6186
6224
|
if (counts.unscaled_count > 0) {
|
|
6187
6225
|
notes.push(
|
|
6188
|
-
`${counts.unscaled_count} line(s) carry no
|
|
6226
|
+
`${counts.unscaled_count} line(s) came back as given: they carry no figure to multiply, or one this server declined to multiply and named on the line itself.`
|
|
6189
6227
|
);
|
|
6190
6228
|
}
|
|
6191
6229
|
if (counts.equipment_count > 0) {
|
|
@@ -6209,7 +6247,10 @@ function runScaleIngredients(args) {
|
|
|
6209
6247
|
const body = scaled.map((entry, index) => `${index + 1}. ${quoteForeign(entry.text)}`).join("\n");
|
|
6210
6248
|
return ok(
|
|
6211
6249
|
{
|
|
6212
|
-
|
|
6250
|
+
// The multiplier the quantities were worked out with, so a caller
|
|
6251
|
+
// recomputing one from it lands where this answer landed. The block
|
|
6252
|
+
// below states a shortened one, which is for reading.
|
|
6253
|
+
factor,
|
|
6213
6254
|
language: args.language,
|
|
6214
6255
|
ingredients: scaled.map(toIngredientPayload),
|
|
6215
6256
|
...counts,
|
|
@@ -6237,10 +6278,11 @@ function howTheRowsAreOrdered(contributed) {
|
|
|
6237
6278
|
return "No source contributed a row.";
|
|
6238
6279
|
}
|
|
6239
6280
|
function nothingCameBack(answeredCount, query, alsoSent) {
|
|
6281
|
+
const asked = quoteForeign(query);
|
|
6240
6282
|
if (answeredCount === 0) {
|
|
6241
|
-
return `No source answered for "${
|
|
6283
|
+
return `No source answered for "${asked}", so nothing here says whether such a recipe exists.`;
|
|
6242
6284
|
}
|
|
6243
|
-
return `Nothing came back for "${
|
|
6285
|
+
return `Nothing came back for "${asked}".${alsoSent}`;
|
|
6244
6286
|
}
|
|
6245
6287
|
var searchRecipesDescription = [
|
|
6246
6288
|
"Search every recipe source this server reads, at the same time, for a dish or an ingredient, and get one merged list.",
|
|
@@ -6251,7 +6293,7 @@ var searchRecipesDescription = [
|
|
|
6251
6293
|
"Ask in a whole sentence if that is the question. These indexes answer the words they are handed, so a sentence is also sent as the words naming the dish and as the dish word alone, and the rows are the union; 'per_source' lists every wording and what it returned.",
|
|
6252
6294
|
"What the question says the recipe must not hold is set aside from those shorter wordings rather than searched for, and named back in the notes: a negation, an allergy stated as one, a diet named in one word, and the number of people at the table. No source filters on any of it, so open a row with get_recipe and read the ingredient list before calling it suitable.",
|
|
6253
6295
|
"A condition is read with the food on whichever side of it the sentence put one, so 'allergique aux noix' and 'peanut allergy' both name the nut. Where a sentence puts a food on neither side, the notes say a condition was stated and that its food was not read, because naming the wrong word would hide a dish and search for the food being avoided at once.",
|
|
6254
|
-
"Rows are interleaved one source at a time
|
|
6296
|
+
"Rows are interleaved one source at a time, since the sources share no score that could order them against each other."
|
|
6255
6297
|
].join(" ");
|
|
6256
6298
|
var searchRecipesInput = strictInput({
|
|
6257
6299
|
query: z7.string().min(1).max(200).describe("A dish or an ingredient, in any of the languages the sources publish in."),
|
|
@@ -6342,12 +6384,13 @@ async function runSearchRecipes(client, args) {
|
|
|
6342
6384
|
)
|
|
6343
6385
|
);
|
|
6344
6386
|
}
|
|
6345
|
-
if (results.length > 0 && !results.some((row) =>
|
|
6387
|
+
if (results.length > 0 && !results.some((row) => sharesDishWord(row.title, args.query))) {
|
|
6346
6388
|
notes.push(
|
|
6347
|
-
`No title here carries a word of "${args.query}". These rows are what the sources ranked for that spelling, so read them as candidates to check rather than as recipes for the dish.`
|
|
6389
|
+
`No title here carries a word of "${quoteForeign(args.query)}". These rows are what the sources ranked for that spelling, so read them as candidates to check rather than as recipes for the dish.`
|
|
6348
6390
|
);
|
|
6349
6391
|
}
|
|
6350
|
-
|
|
6392
|
+
const allReadable = merged.reports.every((report) => report.skipped === 0);
|
|
6393
|
+
if (results.length === 0 && answered.length === merged.reports.length && allReadable) {
|
|
6351
6394
|
notes.push(
|
|
6352
6395
|
mustKeep(
|
|
6353
6396
|
"Every source answered and none holds anything under this wording. Try the dish's name in another language, or name a main ingredient instead."
|
|
@@ -6364,7 +6407,7 @@ async function runSearchRecipes(client, args) {
|
|
|
6364
6407
|
const alsoSent = derived.length > 0 ? `
|
|
6365
6408
|
Also searched for: ${derived.map((wording) => `"${quoteForeign(wording)}"`).join(", ")}.` : "";
|
|
6366
6409
|
const anyNamesTheDish = merged.reports.some((report) => report.namesTheDish > 0);
|
|
6367
|
-
const body = results.length > 0 ? `${results.length} ${anyNamesTheDish ? "recipes" : "rows"} for "${args.query}":
|
|
6410
|
+
const body = results.length > 0 ? `${results.length} ${anyNamesTheDish ? "recipes" : "rows"} for "${quoteForeign(args.query)}":
|
|
6368
6411
|
${renderRows(results)}${alsoSent}` : nothingCameBack(answered.length, args.query, alsoSent);
|
|
6369
6412
|
return ok(
|
|
6370
6413
|
{
|