From 8de87045d5480ae5b077c70291e7e9969bca4dae Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 10 Jun 2026 19:34:32 -0600 Subject: [PATCH] mcp: summarize shopping list text --- internal/mcp/server.go | 91 +++++++++++++++++++------- internal/mcp/server_structured_test.go | 69 +++++++++++++++++++ 2 files changed, 137 insertions(+), 23 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 35a572584faf1f55572ce93d7b897926f96136e5..e08b4b3f3d19affa7cba1b72e87b51be2e9e19a1 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -17,6 +17,7 @@ import ( const ( serverName = "Cooked" + maxShoppingListTextItems = 30 readToolName = "read" previewRecipeTextToolName = "preview_recipe_text" saveRecipeToolName = "save_recipe" @@ -831,36 +832,80 @@ func formatShoppingList(shoppingList cooked.ShoppingList) string { return "Shopping list is empty." } + totalItems := shoppingListProductGroupCount(shoppingList) var builder strings.Builder - builder.WriteString("Shopping list:\n") + if totalItems > maxShoppingListTextItems { + fmt.Fprintf(&builder, "Shopping list (showing first %d of %d items):\n", + maxShoppingListTextItems, + totalItems) + } else { + builder.WriteString("Shopping list:\n") + } + + shownItems := 0 for _, aisle := range shoppingList.Aisles { - builder.WriteString("- ") - builder.WriteString(aisle.Name) - builder.WriteString(":") - if len(aisle.ProductGroups) == 0 { - builder.WriteString(" no items\n") - continue + if shownItems >= maxShoppingListTextItems { + break } - builder.WriteByte('\n') + shownItems = writeShoppingListAisle(&builder, aisle, shownItems) + } + if totalItems > maxShoppingListTextItems { + fmt.Fprintf( + &builder, + "Additional shopping-list items are available in structured_content from this tool response: %d.\n", + totalItems-maxShoppingListTextItems, + ) + } - for _, product := range aisle.ProductGroups { - builder.WriteString(" - ") - builder.WriteString(product.Name) - if product.Quantity != "" { - builder.WriteString(" — ") - builder.WriteString(product.Quantity) - } - builder.WriteString(" (id: ") - builder.WriteString(product.ID) - builder.WriteString(")") - if product.Selected { - builder.WriteString(" selected") - } - builder.WriteByte('\n') + return strings.TrimRight(builder.String(), "\n") +} + +func writeShoppingListAisle(builder *strings.Builder, aisle cooked.Aisle, shownItems int) int { + builder.WriteString("- ") + builder.WriteString(aisle.Name) + builder.WriteString(":") + if len(aisle.ProductGroups) == 0 { + builder.WriteString(" no items\n") + + return shownItems + } + builder.WriteByte('\n') + + for _, product := range aisle.ProductGroups { + if shownItems >= maxShoppingListTextItems { + break } + + writeShoppingListProductGroup(builder, product) + shownItems++ } - return strings.TrimRight(builder.String(), "\n") + return shownItems +} + +func writeShoppingListProductGroup(builder *strings.Builder, product cooked.ProductGroup) { + builder.WriteString(" - ") + builder.WriteString(product.Name) + if product.Quantity != "" { + builder.WriteString(" — ") + builder.WriteString(product.Quantity) + } + builder.WriteString(" (id: ") + builder.WriteString(product.ID) + builder.WriteString(")") + if product.Selected { + builder.WriteString(" selected") + } + builder.WriteByte('\n') +} + +func shoppingListProductGroupCount(shoppingList cooked.ShoppingList) int { + count := 0 + for _, aisle := range shoppingList.Aisles { + count += len(aisle.ProductGroups) + } + + return count } func formatRecipes(recipes []cooked.RecipeCard) string { diff --git a/internal/mcp/server_structured_test.go b/internal/mcp/server_structured_test.go index b5055cbf6a2ac5c53d895fde619919156060fd76..122435c2eb9d784cbbeb779012d5ebd62411a236 100644 --- a/internal/mcp/server_structured_test.go +++ b/internal/mcp/server_structured_test.go @@ -5,12 +5,81 @@ package mcp import ( + "context" "encoding/json" + "fmt" + "strings" "testing" sdk "github.com/modelcontextprotocol/go-sdk/mcp" + + "git.secluded.site/cooked-mcp/internal/cooked" ) +func TestCallToolACIDAPIClientLimits2SummarizesLongShoppingListText(t *testing.T) { + productGroups := make([]cooked.ProductGroup, maxShoppingListTextItems+1) + for index := range productGroups { + productGroups[index] = cooked.ProductGroup{ + ID: fmt.Sprintf("item-%02d", index+1), + Name: fmt.Sprintf("Item %02d", index+1), + } + } + + backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{ + ID: "pantry", + Name: "Pantry", + ProductGroups: productGroups, + }}}} + server := NewServer(backend, "test") + + result, output, err := server.callShoppingListReadTool(context.Background()) + if err != nil { + t.Fatalf("callShoppingListReadTool() error = %v", err) + } + + text := requireTextContent(t, result) + if !strings.Contains(text, "Shopping list (showing first 30 of 31 items):") { + t.Fatalf("text output missing shopping-list summary: %q", text) + } + if strings.Contains(text, "Item 31") || strings.Contains(text, "item-31") { + t.Fatalf("text output included truncated item: %q", text) + } + if !strings.Contains(text, "Additional shopping-list items are available in structured_content") { + t.Fatalf("text output missing structured_content hint: %q", text) + } + if len(output.Aisles) != 1 || len(output.Aisles[0].ProductGroups) != maxShoppingListTextItems+1 { + t.Fatalf("structured shopping-list item count = %#v, want all items", output.Aisles) + } + requireStructuredContent(t, result, output) +} + +func TestCallToolACIDAPIClientLimits2PreservesShortShoppingListText(t *testing.T) { + backend := &fakeBackend{shoppingList: cooked.ShoppingList{Aisles: []cooked.Aisle{{ + ID: "pantry", + Name: "Pantry", + ProductGroups: []cooked.ProductGroup{{ + ID: "pasta", + Name: "Pasta", + Quantity: "200g", + }}, + }}}} + server := NewServer(backend, "test") + + result, _, err := server.callShoppingListReadTool(context.Background()) + if err != nil { + t.Fatalf("callShoppingListReadTool() error = %v", err) + } + + text := requireTextContent(t, result) + want := "Shopping list:\n- Pantry:\n - Pasta — 200g (id: pasta)" + if text != want { + t.Fatalf("short shopping-list text = %q, want %q", text, want) + } + if strings.Contains(text, "structured_content") || strings.Contains(text, "showing first") { + t.Fatalf("short shopping-list text included truncation hint: %q", text) + } +} + func requireTextContent(t *testing.T, result *sdk.CallToolResult) string { t.Helper()