@@ -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 {
@@ -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()