mcp-recipes 2.0.0 → 3.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.
@@ -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
  /**