@@ -36,6 +36,7 @@ type Backend interface {
UpdateRecipeContent(ctx context.Context, recipeID, markdown string, portions int) error
DeleteRecipe(ctx context.Context, recipeID string) error
ClearShoppingList(ctx context.Context) error
+ AddShoppingListIngredients(ctx context.Context, ingredients, recipeID string) (cooked.AddShoppingListResult, error)
}
// Server exposes Cooked as an MCP server.
@@ -335,6 +336,28 @@ func (s *Server) callChangeShoppingListTool(
) (*sdk.CallToolResult, ChangeShoppingListOutput, error) {
action := strings.TrimSpace(arguments.Action)
switch action {
+ case "add":
+ if strings.TrimSpace(arguments.Ingredients) == "" {
+ return nil, ChangeShoppingListOutput{}, fmt.Errorf("ingredients is required when action is add")
+ }
+
+ result, err := s.backend.AddShoppingListIngredients(
+ ctx,
+ arguments.Ingredients,
+ strings.TrimSpace(arguments.RecipeID),
+ )
+ if err != nil {
+ return nil, ChangeShoppingListOutput{}, err
+ }
+
+ output := ChangeShoppingListOutput{AddedCount: result.AddedCount, Ingredients: result.Ingredients}
+
+ return &sdk.CallToolResult{
+ Content: []sdk.Content{&sdk.TextContent{Text: fmt.Sprintf(
+ "Added %d shopping-list ingredients.",
+ result.AddedCount,
+ )}},
+ }, output, nil
case "clear":
if err := s.backend.ClearShoppingList(ctx); err != nil {
return nil, ChangeShoppingListOutput{}, err
@@ -417,7 +440,7 @@ func changeShoppingListTool() *sdk.Tool {
return &sdk.Tool{
Name: changeShoppingListToolName,
Title: "Change shopping list",
- Description: "Change the Cooked shopping list. This slice supports only action clear, which is destructive and deletes all shopping-list items. Other actions are reserved for later slices.",
+ Description: "Change the Cooked shopping list. This slice supports action add with newline-separated ingredients and optional recipe_id, and action clear, which is destructive and deletes all shopping-list items. Other actions are reserved for later slices.",
Annotations: &sdk.ToolAnnotations{
Title: "Change shopping list",
DestructiveHint: &destructive,
@@ -639,10 +662,14 @@ type DeleteRecipeOutput struct {
// ChangeShoppingListArguments contains change_shopping_list tool arguments.
type ChangeShoppingListArguments struct {
- Action string `json:"action" jsonschema:"Shopping-list action. Supported now: clear."`
+ Action string `json:"action" jsonschema:"Shopping-list action. Supported now: add and clear."`
+ Ingredients string `json:"ingredients,omitempty" jsonschema:"Newline-separated ingredients for action add."`
+ RecipeID string `json:"recipe_id,omitempty" jsonschema:"Optional saved recipe ID or import draft ID for action add."`
}
// ChangeShoppingListOutput is the structured output for change_shopping_list.
type ChangeShoppingListOutput struct {
- Cleared bool `json:"cleared,omitempty"`
+ Cleared bool `json:"cleared,omitempty"`
+ AddedCount int `json:"added_count,omitempty"`
+ Ingredients []string `json:"ingredients,omitempty"`
}
@@ -596,7 +596,7 @@ func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked
arguments ChangeShoppingListArguments
}{
{name: "empty", arguments: ChangeShoppingListArguments{Action: " "}},
- {name: "unknown", arguments: ChangeShoppingListArguments{Action: "add"}},
+ {name: "unknown", arguments: ChangeShoppingListArguments{Action: "dance"}},
}
for _, tt := range tests {
@@ -615,6 +615,60 @@ func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked
}
}
+func TestCallToolACIDShoppingListAdd1To4AddsIngredients(t *testing.T) {
+ backend := &fakeBackend{addShoppingListResult: cooked.AddShoppingListResult{
+ AddedCount: 2,
+ Ingredients: []string{"200g pasta", "1 cup tomato sauce"},
+ }}
+ server := NewServer(backend, "test")
+
+ _, output, err := server.callChangeShoppingListTool(
+ context.Background(),
+ nil,
+ ChangeShoppingListArguments{
+ Action: " add ",
+ Ingredients: "200g pasta\n1 cup tomato sauce",
+ RecipeID: " recipe-1 ",
+ },
+ )
+ if err != nil {
+ t.Fatalf("callChangeShoppingListTool() error = %v", err)
+ }
+
+ if backend.addShoppingListCalls != 1 {
+ t.Fatalf("add calls = %d, want 1", backend.addShoppingListCalls)
+ }
+ if backend.addIngredients != "200g pasta\n1 cup tomato sauce" {
+ t.Fatalf("backend ingredients = %q, want raw multiline ingredients", backend.addIngredients)
+ }
+ if backend.addRecipeID != "recipe-1" {
+ t.Fatalf("backend recipe ID = %q, want recipe-1", backend.addRecipeID)
+ }
+ if output.AddedCount != 2 {
+ t.Fatalf("output added count = %d, want 2", output.AddedCount)
+ }
+ if len(output.Ingredients) != 2 || output.Ingredients[0] != "200g pasta" {
+ t.Fatalf("output ingredients = %#v, want added ingredients", output.Ingredients)
+ }
+}
+
+func TestCallToolACIDToolsChangeShoppingListTool2RequiresIngredients(t *testing.T) {
+ backend := &fakeBackend{}
+ server := NewServer(backend, "test")
+
+ _, _, err := server.callChangeShoppingListTool(
+ context.Background(),
+ nil,
+ ChangeShoppingListArguments{Action: "add", Ingredients: " "},
+ )
+ if err == nil {
+ t.Fatal("callChangeShoppingListTool() error = nil, want missing ingredients error")
+ }
+ if backend.addShoppingListCalls != 0 {
+ t.Fatalf("add calls = %d, want none", backend.addShoppingListCalls)
+ }
+}
+
func TestChangeShoppingListToolACIDToolsAnnotations2And4To6MarksDestructiveOpenWorldTool(t *testing.T) {
tool := changeShoppingListTool()
if tool.Name != changeShoppingListToolName {
@@ -663,6 +717,9 @@ type fakeBackend struct {
updateMarkdown string
updatePortions int
deleteRecipeID string
+ addShoppingListResult cooked.AddShoppingListResult
+ addIngredients string
+ addRecipeID string
metadataCalls int
contentCalls int
previewCalls int
@@ -670,6 +727,7 @@ type fakeBackend struct {
updateCalls int
deleteCalls int
clearShoppingListCalls int
+ addShoppingListCalls int
}
func (f *fakeBackend) ReadShoppingList(context.Context) (cooked.ShoppingList, error) {
@@ -745,3 +803,14 @@ func (f *fakeBackend) ClearShoppingList(context.Context) error {
return nil
}
+
+func (f *fakeBackend) AddShoppingListIngredients(
+ _ context.Context,
+ ingredients, recipeID string,
+) (cooked.AddShoppingListResult, error) {
+ f.addIngredients = ingredients
+ f.addRecipeID = recipeID
+ f.addShoppingListCalls++
+
+ return f.addShoppingListResult, nil
+}