Detailed changes
@@ -12,15 +12,16 @@ components:
5: The server exposes a shopping-list mutation tool named change_shopping_list.
6: The server does not expose one MCP tool per Cooked API endpoint.
7: The server does not expose MCP resources.
- 8: The read tool supports recipe lists, single recipes, and the shopping list through explicit target values.
+ 8: The read tool supports recipe lists, single recipes, recipe format examples, and the shopping list through explicit target values.
READ_TOOL:
requirements:
- 1: The read tool target values are recipes, recipe, and shopping_list.
+ 1: The read tool target values are recipes, recipe, example_recipes, and shopping_list.
2: The read tool accepts recipe_id when target is recipe.
3: The read tool requires recipe_id when target is recipe.
4: The read tool accepts query when target is recipes.
5: The read tool accepts page with default 1 and minimum 1.
6: The read tool accepts limit with default 10, minimum 1, and maximum 30.
+ 7: The read tool returns Cooked recipe markdown examples when target is example_recipes.
PREVIEW_RECIPE_TEXT_TOOL:
requirements:
1: The preview_recipe_text tool requires title.
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
+
+package mcp
+
+import sdk "github.com/modelcontextprotocol/go-sdk/mcp"
+
+func callExampleRecipesReadTool() (*sdk.CallToolResult, ReadOutput, error) {
+ output := ReadOutput{ExampleRecipes: exampleRecipesMarkdown}
+
+ return newToolResult(exampleRecipesMarkdown, output), output, nil
+}
+
+const exampleRecipesMarkdown = `Cooked recipe markdown examples
+
+Title and portions are separate fields. Recipe markdown starts with ingredient bullets, then uses # headings for instruction sections. Ordered-list items are steps. Paragraphs are notes and may appear between steps.
+
+Example: Shokupan w/ yudane improver
+
+- 45g strong bread flour (for yudane)
+- 45g boiling water (for yudane)
+- 285g strong bread flour (for bread)
+- 4.5g instant dry yeast or 5.4g active dry or 13.5g fresh
+- 6g salt
+- 15g sugar
+- 30g soft butter
+- 172.5g milk (or alternative, or water)
+
+# Yudane dough improver
+
+1. Get the flour-for-yudane in a bowl, then pour the boiling-water-for-yudane in and thoroughly mix. It'll still be very sticky; the goal is incorporating all the flour.
+2. Cover immediately and leave to cool completely. Can be used once cool, or refrigerated and used the next day.
+
+# Shokupan
+
+1. Combine milk, butter, yeast, and yudane from previous section in a large bowl.
+2. Add salt and sugar, then mix well.
+3. Add the flour-for-bread, then mix to a dough.
+
+This is a note between steps.
+
+4. Tip the dough out on the table and knead for 5-10 minutes.
+
+Example: Simple toast
+
+- 1 slice shokupan
+- 1 tbsp brown sugar
+- 1 tbsp butter
+
+# Toast
+
+1. Toast the bread until golden.
+2. Stir the brown sugar and butter together.
+3. Spread on the toast while warm.`
@@ -96,9 +96,11 @@ func (s *Server) callReadTool(
return s.callRecipeReadTool(ctx, arguments.RecipeID)
case "recipes":
return s.callRecipesReadTool(ctx, arguments)
+ case "example_recipes":
+ return callExampleRecipesReadTool()
default:
return nil, ReadOutput{}, fmt.Errorf(
- "unsupported read target %q (supported: shopping_list, recipes, recipe)",
+ "unsupported read target %q (supported: shopping_list, recipes, recipe, example_recipes)",
arguments.Target,
)
}
@@ -652,19 +654,12 @@ func (s *Server) updateShoppingListProductGroup(
return newToolResult(text, output), output, nil
}
-func newToolResult[Output any](text string, output Output) *sdk.CallToolResult {
- return &sdk.CallToolResult{
- Content: []sdk.Content{&sdk.TextContent{Text: text}},
- StructuredContent: output,
- }
-}
-
func readTool() *sdk.Tool {
openWorld := true
return &sdk.Tool{
Name: readToolName,
Title: "Read Cooked data",
- Description: "Read Cooked data. Use target shopping_list for the current shopping list, target recipes to list or search saved recipe IDs and titles, or target recipe with recipe_id to read a single recipe. Recipe results omit images and thumbnails.",
+ Description: "Read Cooked data. Use target shopping_list for the current shopping list, recipes to list or search saved recipe IDs and titles, recipe with recipe_id to read a single recipe, or example_recipes for Cooked recipe markdown examples. Recipe results omit images and thumbnails.",
Annotations: &sdk.ToolAnnotations{
Title: "Read Cooked data",
ReadOnlyHint: true,
@@ -1088,7 +1083,7 @@ func recipeSummaries(recipes []cooked.RecipeCard) []RecipeSummary {
// ReadArguments contains read tool arguments.
type ReadArguments struct {
- Target string `json:"target" jsonschema:"Cooked object to read. Use shopping_list, recipes, or recipe."`
+ Target string `json:"target" jsonschema:"Cooked object to read. Use shopping_list, recipes, recipe, or example_recipes."`
RecipeID string `json:"recipe_id,omitempty" jsonschema:"Recipe ID for target recipe."`
Query string `json:"query,omitempty" jsonschema:"Search query for target recipes. Omit to list saved recipes."`
Page int `json:"page,omitempty" jsonschema:"Page number for target recipes. Defaults to 1."`
@@ -1101,6 +1096,7 @@ type ReadOutput struct {
ShoppingListRecipes []string `json:"shopping_list_recipes,omitempty"`
Recipes []RecipeSummary `json:"recipes,omitempty"`
Recipe *RecipeDetail `json:"recipe,omitempty"`
+ ExampleRecipes string `json:"example_recipes,omitempty"`
}
// RecipeSummary is the MCP-safe saved recipe summary.
@@ -1140,7 +1136,7 @@ type SaveRecipeArguments struct {
Title string `json:"title,omitempty" jsonschema:"Recipe title for raw_text or prepared saves."`
Text string `json:"text,omitempty" jsonschema:"Raw recipe text for raw_text saves."`
URL string `json:"url,omitempty" jsonschema:"Recipe URL for url imports."`
- Markdown string `json:"markdown,omitempty" jsonschema:"Recipe markdown for prepared saves, reviewed URL import drafts, or existing recipe updates."`
+ Markdown string `json:"markdown,omitempty" jsonschema:"Recipe markdown for prepared saves, reviewed URL import drafts, or existing recipe updates. Read target example_recipes first for Cooked's markdown dialect and examples."`
Portions float64 `json:"portions,omitempty" jsonschema:"Recipe portions for prepared saves, reviewed URL import drafts, or existing recipe updates."`
}
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
+
+package mcp
+
+import (
+ "context"
+ "strings"
+ "testing"
+)
+
+// tools.READ_TOOL.7 tools.SCHEMA.3
+func TestCallToolACIDToolsReadTool7ReadsExampleRecipes(t *testing.T) {
+ backend := &fakeBackend{}
+ server := NewServer(backend, "test")
+
+ result, output, err := server.callReadTool(context.Background(), nil, ReadArguments{Target: "example_recipes"})
+ if err != nil {
+ t.Fatalf("callReadTool() error = %v", err)
+ }
+
+ text := requireTextContent(t, result)
+ for _, want := range []string{
+ "Title and portions are separate fields",
+ "# Yudane dough improver",
+ "This is a note between steps",
+ "# Toast",
+ } {
+ if !strings.Contains(text, want) {
+ t.Fatalf("example_recipes text missing %q: %q", want, text)
+ }
+ if !strings.Contains(output.ExampleRecipes, want) {
+ t.Fatalf("example_recipes output missing %q: %q", want, output.ExampleRecipes)
+ }
+ }
+ if backend.readShoppingListCalls != 0 || backend.page != 0 || backend.limit != 0 || backend.metadataCalls != 0 {
+ t.Fatalf(
+ "backend calls = shopping list %d list recipes page/limit %d/%d metadata %d, want none",
+ backend.readShoppingListCalls,
+ backend.page,
+ backend.limit,
+ backend.metadataCalls,
+ )
+ }
+}
@@ -6,6 +6,7 @@ package mcp
import (
"context"
+ "reflect"
"strings"
"testing"
)
@@ -19,7 +20,7 @@ func TestCallToolACIDToolsReadTool1RejectsUnsupportedTargetBeforeCallingCooked(t
if err == nil {
t.Fatal("CallReadTool() error = nil, want unsupported target error")
}
- if err.Error() != `unsupported read target "meal_plan" (supported: shopping_list, recipes, recipe)` {
+ if err.Error() != `unsupported read target "meal_plan" (supported: shopping_list, recipes, recipe, example_recipes)` {
t.Fatalf("CallReadTool() error = %q", err.Error())
}
if backend.readShoppingListCalls != 0 || backend.metadataCalls != 0 || backend.contentCalls != 0 {
@@ -51,6 +52,19 @@ func TestCallToolACIDRecipesSave10RejectsUnsupportedSourceBeforeCallingCooked(t
// tools.SCHEMA.3 tools.SCHEMA.5 tools.SAVE_RECIPE_TOOL.1 tools.CHANGE_SHOPPING_LIST_TOOL.1
func TestToolDescriptionsACIDToolsSchema3And5DescribeCurrentLimits(t *testing.T) {
+ read := readTool()
+ for _, want := range []string{
+ "shopping_list",
+ "recipes",
+ "recipe with recipe_id",
+ "example_recipes",
+ "Recipe results omit images and thumbnails",
+ } {
+ if !strings.Contains(read.Description, want) {
+ t.Fatalf("read description missing %q: %q", want, read.Description)
+ }
+ }
+
save := saveRecipeTool()
for _, want := range []string{
"raw text",
@@ -83,3 +97,24 @@ func TestToolDescriptionsACIDToolsSchema3And5DescribeCurrentLimits(t *testing.T)
}
}
}
+
+// tools.READ_TOOL.1 tools.SAVE_RECIPE_TOOL.3 tools.SAVE_RECIPE_TOOL.5 tools.SAVE_RECIPE_TOOL.6
+func TestArgumentDescriptionsACIDToolsReadTool1AndSaveRecipeTool3And5And6PointToExampleRecipes(t *testing.T) {
+ readTarget, ok := reflect.TypeFor[ReadArguments]().FieldByName("Target")
+ if !ok {
+ t.Fatal("ReadArguments.Target field missing")
+ }
+ if !strings.Contains(string(readTarget.Tag), "example_recipes") {
+ t.Fatalf("ReadArguments.Target tag = %q, want example_recipes", readTarget.Tag)
+ }
+
+ saveMarkdown, ok := reflect.TypeFor[SaveRecipeArguments]().FieldByName("Markdown")
+ if !ok {
+ t.Fatal("SaveRecipeArguments.Markdown field missing")
+ }
+ for _, want := range []string{"example_recipes", "Cooked's markdown dialect"} {
+ if !strings.Contains(string(saveMarkdown.Tag), want) {
+ t.Fatalf("SaveRecipeArguments.Markdown tag missing %q: %q", want, saveMarkdown.Tag)
+ }
+ }
+}
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
+
+package mcp
+
+import sdk "github.com/modelcontextprotocol/go-sdk/mcp"
+
+func newToolResult[Output any](text string, output Output) *sdk.CallToolResult {
+ return &sdk.CallToolResult{
+ Content: []sdk.Content{&sdk.TextContent{Text: text}},
+ StructuredContent: output,
+ }
+}