mcp-recipes 2.0.0 → 3.0.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 +51 -0
- package/PRIVACY.md +168 -0
- package/README.md +491 -708
- package/dist/index.js +594 -88
- package/dist/index.js.map +1 -1
- package/dist/sources/client.d.ts +344 -0
- package/dist/sources/client.js +448 -9
- package/dist/sources/client.js.map +1 -1
- package/package.json +14 -4
- package/server.json +5 -5
package/dist/sources/client.d.ts
CHANGED
|
@@ -30,6 +30,21 @@ interface Logger {
|
|
|
30
30
|
error: (message: string) => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Reading what a page's own headings announce.
|
|
35
|
+
*
|
|
36
|
+
* A heading is the one statement a page makes about its own layout, and it is
|
|
37
|
+
* the only evidence there is that a part of a recipe was published when nothing
|
|
38
|
+
* was read out of that part. That matters because reading nothing is a fact
|
|
39
|
+
* about this server, and only the page can say whether there was anything to
|
|
40
|
+
* read.
|
|
41
|
+
*
|
|
42
|
+
* Both languages the pages are written in are recognised here, since a heading
|
|
43
|
+
* belongs to a page rather than to the corpus it sits in.
|
|
44
|
+
*/
|
|
45
|
+
/** The parts of a recipe a page announces with a heading of its own. */
|
|
46
|
+
type PagePart = "ingredients" | "method";
|
|
47
|
+
|
|
33
48
|
/** The shapes the source layer produces. Nothing here knows about MCP. */
|
|
34
49
|
|
|
35
50
|
/**
|
|
@@ -115,6 +130,31 @@ interface RecipeDetail {
|
|
|
115
130
|
prepMinutes: number | null;
|
|
116
131
|
cookMinutes: number | null;
|
|
117
132
|
totalMinutes: number | null;
|
|
133
|
+
/**
|
|
134
|
+
* Time the recipe stands, from a source that publishes it apart from
|
|
135
|
+
* preparation and cooking. It belongs to no other source's cooking time, and
|
|
136
|
+
* a source publishing none says null, which is not a dish that needs no rest.
|
|
137
|
+
*/
|
|
138
|
+
restMinutes: number | null;
|
|
139
|
+
/**
|
|
140
|
+
* Whether the source publishes the method as one block of prose rather than
|
|
141
|
+
* as steps of its own. Null from a source that says neither, since a single
|
|
142
|
+
* entry can be one block or a method of one step and only the source knows.
|
|
143
|
+
*/
|
|
144
|
+
stepsAsOneBlock: boolean | null;
|
|
145
|
+
/**
|
|
146
|
+
* A part the source published nothing of because it keeps it for its
|
|
147
|
+
* subscribers, and the words that say so.
|
|
148
|
+
*
|
|
149
|
+
* Null from a source that withholds nothing. That is a different statement
|
|
150
|
+
* from an empty list: a part withheld is a part the page has, and an answer
|
|
151
|
+
* that read it as a part this server failed to read would say something the
|
|
152
|
+
* page never said.
|
|
153
|
+
*/
|
|
154
|
+
withheld: {
|
|
155
|
+
parts: PagePart[];
|
|
156
|
+
why: string;
|
|
157
|
+
} | null;
|
|
118
158
|
category: string | null;
|
|
119
159
|
author: string | null;
|
|
120
160
|
rating: Rating | null;
|
|
@@ -324,6 +364,106 @@ interface CookbookReader {
|
|
|
324
364
|
}>;
|
|
325
365
|
}
|
|
326
366
|
|
|
367
|
+
/**
|
|
368
|
+
* BBC Good Food, as a source.
|
|
369
|
+
*
|
|
370
|
+
* An English recipe site addressing a recipe by the path of its page. Two of
|
|
371
|
+
* its statements need care. Its total can sit on the largest number of rows one
|
|
372
|
+
* search will serve, where it states a floor rather than a count. And it keeps
|
|
373
|
+
* some recipes' ingredients and method for its subscribers while publishing the
|
|
374
|
+
* rest of the page, which is a part withheld rather than a part this server
|
|
375
|
+
* failed to read.
|
|
376
|
+
*
|
|
377
|
+
* A difficulty the site writes in a word of its own is not carried, since it
|
|
378
|
+
* publishes no scale for it. Its rendition for readers in the United States is
|
|
379
|
+
* not read either: taking lines from both would make a list in two measuring
|
|
380
|
+
* systems that the site never published as one.
|
|
381
|
+
*/
|
|
382
|
+
|
|
383
|
+
/** A search row, as BBC Good Food's own reader publishes one. */
|
|
384
|
+
interface GoodFoodRow {
|
|
385
|
+
id: string;
|
|
386
|
+
title: string;
|
|
387
|
+
url: string;
|
|
388
|
+
image_url: string | null;
|
|
389
|
+
rating: number | null;
|
|
390
|
+
rating_count: number | null;
|
|
391
|
+
/** True where the recipe sits behind the site's subscription. */
|
|
392
|
+
premium: boolean;
|
|
393
|
+
total_minutes: number | null;
|
|
394
|
+
difficulty: string | null;
|
|
395
|
+
author: string | null;
|
|
396
|
+
}
|
|
397
|
+
/** A listing, as that reader publishes one. */
|
|
398
|
+
interface GoodFoodReport {
|
|
399
|
+
query: string;
|
|
400
|
+
results: GoodFoodRow[];
|
|
401
|
+
result_count: number;
|
|
402
|
+
total_available: number | null;
|
|
403
|
+
/** True where the total sits on the largest number of rows one search serves. */
|
|
404
|
+
total_is_ceiling: boolean;
|
|
405
|
+
rows_seen: number;
|
|
406
|
+
restrictions_lifted: string[];
|
|
407
|
+
}
|
|
408
|
+
/** One line of an ingredient list, as that reader publishes it. */
|
|
409
|
+
interface GoodFoodIngredient {
|
|
410
|
+
text: string;
|
|
411
|
+
amount: number | null;
|
|
412
|
+
unit: string | null;
|
|
413
|
+
item: string;
|
|
414
|
+
note: string | null;
|
|
415
|
+
/** The site's own normalised name for it, which no other source publishes. */
|
|
416
|
+
term: string | null;
|
|
417
|
+
}
|
|
418
|
+
interface GoodFoodGroup {
|
|
419
|
+
heading: string | null;
|
|
420
|
+
ingredients: GoodFoodIngredient[];
|
|
421
|
+
}
|
|
422
|
+
/** A recipe, as that reader publishes one. */
|
|
423
|
+
interface GoodFoodRecipe {
|
|
424
|
+
id: string;
|
|
425
|
+
title: string;
|
|
426
|
+
url: string;
|
|
427
|
+
premium: boolean;
|
|
428
|
+
yield_text: string | null;
|
|
429
|
+
yield_count: number | null;
|
|
430
|
+
prep_minutes: number | null;
|
|
431
|
+
cook_minutes: number | null;
|
|
432
|
+
total_minutes: number | null;
|
|
433
|
+
difficulty: string | null;
|
|
434
|
+
diets: string[];
|
|
435
|
+
author: string | null;
|
|
436
|
+
rating: number | null;
|
|
437
|
+
rating_count: number | null;
|
|
438
|
+
description: string | null;
|
|
439
|
+
/** Empty where the recipe sits behind the subscription. */
|
|
440
|
+
ingredients: GoodFoodGroup[];
|
|
441
|
+
/** Empty where the recipe sits behind the subscription. */
|
|
442
|
+
steps: string[];
|
|
443
|
+
nutrition: Array<{
|
|
444
|
+
label: string;
|
|
445
|
+
value: number | null;
|
|
446
|
+
unit: string;
|
|
447
|
+
}>;
|
|
448
|
+
nutrition_per: string | null;
|
|
449
|
+
us_edition: unknown;
|
|
450
|
+
}
|
|
451
|
+
/** The part of BBC Good Food's client this server uses. */
|
|
452
|
+
interface GoodFoodReader {
|
|
453
|
+
searchRecipes: (query: string, options?: {
|
|
454
|
+
limit?: number;
|
|
455
|
+
}) => Promise<{
|
|
456
|
+
data: GoodFoodReport;
|
|
457
|
+
cached: boolean;
|
|
458
|
+
skipped?: string[];
|
|
459
|
+
}>;
|
|
460
|
+
getRecipe: (id: string) => Promise<{
|
|
461
|
+
data: GoodFoodRecipe;
|
|
462
|
+
cached: boolean;
|
|
463
|
+
skipped?: string[];
|
|
464
|
+
}>;
|
|
465
|
+
}
|
|
466
|
+
|
|
327
467
|
/**
|
|
328
468
|
* Marmiton, as a source.
|
|
329
469
|
*
|
|
@@ -378,6 +518,204 @@ interface MarmitonReader {
|
|
|
378
518
|
}>;
|
|
379
519
|
}
|
|
380
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Ptitchef, as a source.
|
|
523
|
+
*
|
|
524
|
+
* A French recipe site addressing a recipe by the path of its page. It answers
|
|
525
|
+
* a search in several ways and prints one total for all of them, so the same
|
|
526
|
+
* number counts a category on one answer and the rows served on another; what
|
|
527
|
+
* it counts is said in words beside it rather than left to be guessed.
|
|
528
|
+
*
|
|
529
|
+
* Two figures the site publishes are not carried. It estimates what a recipe
|
|
530
|
+
* costs in euros, which is a price where another source ranks a recipe inside
|
|
531
|
+
* its own list, and one field holding both would invite the two to be compared.
|
|
532
|
+
* It writes a difficulty in a word of its own and states no scale for it, so
|
|
533
|
+
* the word sits on no axis a caller could put beside another source's.
|
|
534
|
+
*/
|
|
535
|
+
|
|
536
|
+
/** How the site answered a wording, as its own reader reports it. */
|
|
537
|
+
type PtitchefKind = "topic" | "free_text" | "category" | "guide" | "standing" | "fridge" | "recipe" | "unmatched";
|
|
538
|
+
/** A search row, as Ptitchef's own reader publishes one. */
|
|
539
|
+
interface PtitchefRow {
|
|
540
|
+
id: string;
|
|
541
|
+
title: string;
|
|
542
|
+
url: string;
|
|
543
|
+
image_url: string | null;
|
|
544
|
+
rating: number | null;
|
|
545
|
+
/** How many readers rated it. */
|
|
546
|
+
rating_count: number | null;
|
|
547
|
+
/** How many wrote a review, which counts a different thing. */
|
|
548
|
+
review_count: number | null;
|
|
549
|
+
category: string | null;
|
|
550
|
+
difficulty: string | null;
|
|
551
|
+
total_minutes: number | null;
|
|
552
|
+
calories: string | null;
|
|
553
|
+
ingredients_preview: string | null;
|
|
554
|
+
}
|
|
555
|
+
/** A listing, as that reader publishes one. */
|
|
556
|
+
interface PtitchefListing {
|
|
557
|
+
asked: string;
|
|
558
|
+
kind: PtitchefKind;
|
|
559
|
+
topic_slug: string | null;
|
|
560
|
+
title: string | null;
|
|
561
|
+
results: PtitchefRow[];
|
|
562
|
+
result_count: number;
|
|
563
|
+
rows_seen: number;
|
|
564
|
+
folded: number;
|
|
565
|
+
total_available: number | null;
|
|
566
|
+
page: number;
|
|
567
|
+
single_page: boolean;
|
|
568
|
+
url: string;
|
|
569
|
+
}
|
|
570
|
+
/** A recipe, as that reader publishes one. */
|
|
571
|
+
interface PtitchefRecipe {
|
|
572
|
+
id: string;
|
|
573
|
+
title: string;
|
|
574
|
+
url: string;
|
|
575
|
+
description: string | null;
|
|
576
|
+
image_url: string | null;
|
|
577
|
+
category: string | null;
|
|
578
|
+
cuisine: string | null;
|
|
579
|
+
difficulty: string | null;
|
|
580
|
+
author: string | null;
|
|
581
|
+
author_url: string | null;
|
|
582
|
+
published: string | null;
|
|
583
|
+
modified: string | null;
|
|
584
|
+
rating: number | null;
|
|
585
|
+
rating_count: number | null;
|
|
586
|
+
review_count: number | null;
|
|
587
|
+
prep_minutes: number | null;
|
|
588
|
+
cook_minutes: number | null;
|
|
589
|
+
total_minutes: number | null;
|
|
590
|
+
yield_count: number | null;
|
|
591
|
+
yield_text: string | null;
|
|
592
|
+
yield_unit: string | null;
|
|
593
|
+
ingredients: string[];
|
|
594
|
+
steps: Array<{
|
|
595
|
+
text: string;
|
|
596
|
+
image_url: string | null;
|
|
597
|
+
}>;
|
|
598
|
+
/** True where the site published its method as one block of prose. */
|
|
599
|
+
steps_are_one_block: boolean;
|
|
600
|
+
/** The panel as the site prints it, slot by slot, unread slots left null. */
|
|
601
|
+
nutrition: unknown;
|
|
602
|
+
estimated_cost: string | null;
|
|
603
|
+
keywords: string[];
|
|
604
|
+
faq: unknown[];
|
|
605
|
+
translations: unknown[];
|
|
606
|
+
}
|
|
607
|
+
/** The part of Ptitchef's client this server uses. */
|
|
608
|
+
interface PtitchefReader {
|
|
609
|
+
searchRecipes: (query: string) => Promise<{
|
|
610
|
+
data: PtitchefListing;
|
|
611
|
+
cached: boolean;
|
|
612
|
+
skipped?: string[];
|
|
613
|
+
}>;
|
|
614
|
+
getRecipe: (id: string) => Promise<{
|
|
615
|
+
data: PtitchefRecipe;
|
|
616
|
+
cached: boolean;
|
|
617
|
+
skipped?: string[];
|
|
618
|
+
}>;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Supertoinette, as a source.
|
|
623
|
+
*
|
|
624
|
+
* A French recipe site addressing a recipe by a number. It prints no total on
|
|
625
|
+
* a search page and publishes no nutrition panel, and both of those are stated
|
|
626
|
+
* here as silence: a count nobody published is not a count of zero.
|
|
627
|
+
*/
|
|
628
|
+
|
|
629
|
+
/** A search row, as Supertoinette's own reader publishes one. */
|
|
630
|
+
interface SupertoinetteListingRow {
|
|
631
|
+
id: string;
|
|
632
|
+
title: string;
|
|
633
|
+
title_as_published: string;
|
|
634
|
+
url: string;
|
|
635
|
+
image_url: string | null;
|
|
636
|
+
description: string | null;
|
|
637
|
+
categories: string[];
|
|
638
|
+
}
|
|
639
|
+
/** One line of an ingredient list, as that reader publishes it. */
|
|
640
|
+
interface SupertoinetteIngredient {
|
|
641
|
+
amount_text: string | null;
|
|
642
|
+
label: string;
|
|
643
|
+
/** The whole line as published, quantity included. */
|
|
644
|
+
raw: string;
|
|
645
|
+
/** The ingredient page the site linked the words to. Not read: the site
|
|
646
|
+
* links the wrong page often enough that pairing recipes by it would pair
|
|
647
|
+
* the wrong recipes. */
|
|
648
|
+
sheet: unknown;
|
|
649
|
+
is_heading: boolean;
|
|
650
|
+
}
|
|
651
|
+
/** A recipe, as Supertoinette's own reader publishes one. */
|
|
652
|
+
interface SupertoinetteRecipe {
|
|
653
|
+
id: string;
|
|
654
|
+
title: string;
|
|
655
|
+
title_as_published: string;
|
|
656
|
+
url: string;
|
|
657
|
+
description: string | null;
|
|
658
|
+
yield_text: string | null;
|
|
659
|
+
ingredients: SupertoinetteIngredient[];
|
|
660
|
+
steps: string[];
|
|
661
|
+
intro: string | null;
|
|
662
|
+
prep_minutes: number | null;
|
|
663
|
+
cook_minutes: number | null;
|
|
664
|
+
total_minutes: number | null;
|
|
665
|
+
/** Time the recipe stands, which the site prints on its own. */
|
|
666
|
+
rest_minutes: number | null;
|
|
667
|
+
category: string | null;
|
|
668
|
+
author: string | null;
|
|
669
|
+
rating: {
|
|
670
|
+
value: number;
|
|
671
|
+
count: number;
|
|
672
|
+
scale: number;
|
|
673
|
+
} | null;
|
|
674
|
+
/** The site publishes none, and the field says so rather than being absent. */
|
|
675
|
+
nutrition: null;
|
|
676
|
+
difficulty: {
|
|
677
|
+
label: string;
|
|
678
|
+
} | null;
|
|
679
|
+
cost_level: {
|
|
680
|
+
label: string;
|
|
681
|
+
level: number;
|
|
682
|
+
scale: number;
|
|
683
|
+
} | null;
|
|
684
|
+
images: string[];
|
|
685
|
+
tags: unknown[];
|
|
686
|
+
ingredient_sheets: unknown[];
|
|
687
|
+
faq: unknown[];
|
|
688
|
+
}
|
|
689
|
+
interface SupertoinetteListing {
|
|
690
|
+
results: SupertoinetteListingRow[];
|
|
691
|
+
rows_published: number;
|
|
692
|
+
total_available: null;
|
|
693
|
+
last_page: number;
|
|
694
|
+
facets: unknown[];
|
|
695
|
+
matched_nothing: boolean;
|
|
696
|
+
url: string;
|
|
697
|
+
}
|
|
698
|
+
/** The part of Supertoinette's client this server uses. */
|
|
699
|
+
interface SupertoinetteReader {
|
|
700
|
+
searchRecipes: (request: {
|
|
701
|
+
query: string;
|
|
702
|
+
page: number;
|
|
703
|
+
category: string | null;
|
|
704
|
+
}) => Promise<{
|
|
705
|
+
data: {
|
|
706
|
+
listing: SupertoinetteListing;
|
|
707
|
+
dropped_category: string | null;
|
|
708
|
+
};
|
|
709
|
+
cached: boolean;
|
|
710
|
+
skipped?: string[];
|
|
711
|
+
}>;
|
|
712
|
+
getRecipe: (id: string) => Promise<{
|
|
713
|
+
data: SupertoinetteRecipe;
|
|
714
|
+
cached: boolean;
|
|
715
|
+
skipped?: string[];
|
|
716
|
+
}>;
|
|
717
|
+
}
|
|
718
|
+
|
|
381
719
|
/**
|
|
382
720
|
* The sources this server reads.
|
|
383
721
|
*
|
|
@@ -390,10 +728,16 @@ interface MarmitonReader {
|
|
|
390
728
|
* A reader may be supplied in place of the one that talks to a site, so a
|
|
391
729
|
* program embedding this server can put its own cache in front of a corpus, and
|
|
392
730
|
* a test can drive it from fixed answers.
|
|
731
|
+
*
|
|
732
|
+
* Each key is the id of the source it stands in for, so what a caller has
|
|
733
|
+
* replaced can be read off against what the registry builds.
|
|
393
734
|
*/
|
|
394
735
|
interface Readers {
|
|
395
736
|
marmiton?: MarmitonReader;
|
|
396
737
|
cookbook?: CookbookReader;
|
|
738
|
+
ptitchef?: PtitchefReader;
|
|
739
|
+
goodfood?: GoodFoodReader;
|
|
740
|
+
supertoinette?: SupertoinetteReader;
|
|
397
741
|
}
|
|
398
742
|
|
|
399
743
|
/**
|