mcp: remove stale slice wording

Amolith created

Change summary

cmd/cooked-mcp/main.go              | 12 +++
cmd/cooked-mcp/main_test.go         | 18 ++++++
internal/mcp/server.go              | 20 ++++--
internal/mcp/server_test.go         | 25 +++-----
internal/mcp/server_wording_test.go | 83 +++++++++++++++++++++++++++++++
5 files changed, 134 insertions(+), 24 deletions(-)

Detailed changes

cmd/cooked-mcp/main.go 🔗

@@ -41,8 +41,8 @@ func run() error {
 	transport := flag.String("transport", "stdio", "MCP transport: stdio")
 	flag.Parse()
 
-	if *transport != "stdio" {
-		return fmt.Errorf("unsupported transport %q in this slice", *transport)
+	if err := validateTransport(*transport); err != nil {
+		return err
 	}
 
 	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
@@ -69,6 +69,14 @@ func run() error {
 	return nil
 }
 
+func validateTransport(transport string) error {
+	if transport == "stdio" {
+		return nil
+	}
+
+	return fmt.Errorf("unsupported transport %q (supported: stdio)", transport)
+}
+
 func builtVersion() string {
 	info, ok := debug.ReadBuildInfo()
 	if !ok {

cmd/cooked-mcp/main_test.go 🔗

@@ -9,6 +9,24 @@ import (
 	"testing"
 )
 
+// server.TRANSPORT.4
+func TestValidateTransportACIDServerTransport4AcceptsStdio(t *testing.T) {
+	if err := validateTransport("stdio"); err != nil {
+		t.Fatalf("validateTransport() error = %v, want nil", err)
+	}
+}
+
+// server.TRANSPORT.4
+func TestValidateTransportACIDServerTransport4RejectsUnknownTransport(t *testing.T) {
+	err := validateTransport("bogus")
+	if err == nil {
+		t.Fatal("validateTransport() error = nil, want unsupported transport error")
+	}
+	if err.Error() != `unsupported transport "bogus" (supported: stdio)` {
+		t.Fatalf("validateTransport() error = %q", err.Error())
+	}
+}
+
 func TestVersionFromBuildInfoPrefersModuleVersion(t *testing.T) {
 	got := versionFromBuildInfo(&debug.BuildInfo{Main: debug.Module{Version: "1.2.3"}})
 	if got != "1.2.3" {

internal/mcp/server.go 🔗

@@ -95,7 +95,10 @@ func (s *Server) callReadTool(
 	case "recipes":
 		return s.callRecipesReadTool(ctx, arguments)
 	default:
-		return nil, ReadOutput{}, fmt.Errorf("unsupported read target %q in this slice", arguments.Target)
+		return nil, ReadOutput{}, fmt.Errorf(
+			"unsupported read target %q (supported: shopping_list, recipes, recipe)",
+			arguments.Target,
+		)
 	}
 }
 
@@ -235,7 +238,10 @@ func (s *Server) callSaveRecipeTool(
 	case "":
 		return nil, SaveRecipeOutput{}, fmt.Errorf("source is required")
 	default:
-		return nil, SaveRecipeOutput{}, fmt.Errorf("unsupported save_recipe source %q in this slice", source)
+		return nil, SaveRecipeOutput{}, fmt.Errorf(
+			"unsupported save_recipe source %q (supported: raw_text, prepared, url, draft, existing)",
+			source,
+		)
 	}
 }
 
@@ -448,7 +454,7 @@ func (s *Server) callChangeShoppingListTool(
 		return nil, ChangeShoppingListOutput{}, fmt.Errorf("action is required")
 	default:
 		return nil, ChangeShoppingListOutput{}, fmt.Errorf(
-			"unsupported change_shopping_list action %q in this slice",
+			"unsupported change_shopping_list action %q (supported: add, update_item, replace_selection, add_selection, remove_selection, remove, clear)",
 			action,
 		)
 	}
@@ -682,7 +688,7 @@ func saveRecipeTool() *sdk.Tool {
 	return &sdk.Tool{
 		Name:        saveRecipeToolName,
 		Title:       "Save recipe",
-		Description: "Save a recipe. This slice supports source raw_text with title and text, source prepared with title, markdown, and portions, source url with url, source draft with draft_id, markdown, and portions, and source existing with recipe_id, markdown, and portions.",
+		Description: "Save a recipe from raw text, prepared markdown, a URL import, a reviewed URL-import draft, or an existing recipe update. URL imports may return a draft_id for review instead of saving immediately.",
 		Annotations: &sdk.ToolAnnotations{
 			Title:         "Save recipe",
 			OpenWorldHint: &openWorld,
@@ -711,7 +717,7 @@ func changeShoppingListTool() *sdk.Tool {
 	return &sdk.Tool{
 		Name:        changeShoppingListToolName,
 		Title:       "Change shopping list",
-		Description: "Change the Cooked shopping list. This slice supports action add with newline-separated ingredients and optional recipe_id, action remove, replace_selection, add_selection, or remove_selection with product_group_ids, action update_item with product_group_id and optional name, quantity, aisle_id, or selected, and action clear. Remove and clear are destructive. Other actions are reserved for later slices.",
+		Description: "Change the Cooked shopping list. Supports add, update_item, replace_selection, add_selection, remove_selection, remove, and clear. Add accepts newline-separated ingredients and optional recipe_id. Remove and clear are destructive.",
 		Annotations: &sdk.ToolAnnotations{
 			Title:           "Change shopping list",
 			DestructiveHint: &destructive,
@@ -1016,7 +1022,7 @@ type PreviewRecipeTextOutput struct {
 
 // SaveRecipeArguments contains save_recipe tool arguments.
 type SaveRecipeArguments struct {
-	Source   string `json:"source"              jsonschema:"Recipe save source. Supported now: raw_text, prepared, url, draft, and existing."`
+	Source   string `json:"source"              jsonschema:"Recipe save source: raw_text, prepared, url, draft, or existing."`
 	RecipeID string `json:"recipe_id,omitempty" jsonschema:"Recipe ID for existing recipe updates."`
 	DraftID  string `json:"draft_id,omitempty"  jsonschema:"Draft ID for reviewed URL import draft saves."`
 	Title    string `json:"title,omitempty"     jsonschema:"Recipe title for raw_text or prepared saves."`
@@ -1047,7 +1053,7 @@ type DeleteRecipeOutput struct {
 
 // ChangeShoppingListArguments contains change_shopping_list tool arguments.
 type ChangeShoppingListArguments struct {
-	Action          string   `json:"action"                      jsonschema:"Shopping-list action. Supported now: add, remove, replace_selection, add_selection, remove_selection, update_item, and clear."`
+	Action          string   `json:"action"                      jsonschema:"Shopping-list action: add, remove, replace_selection, add_selection, remove_selection, update_item, or 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."`
 	ProductGroupIDs []string `json:"product_group_ids,omitempty" jsonschema:"Product group IDs for action remove, replace_selection, add_selection, or remove_selection."`

internal/mcp/server_test.go 🔗

@@ -360,19 +360,6 @@ func TestCallToolACIDRecipesSave2_1To2_3RequiresPreparedFields(t *testing.T) {
 	}
 }
 
-func TestCallToolACIDRecipesSave10RejectsUnsupportedSourceBeforeCallingCooked(t *testing.T) {
-	backend := &fakeBackend{}
-	server := NewServer(backend, "test")
-
-	_, _, err := server.callSaveRecipeTool(context.Background(), nil, SaveRecipeArguments{Source: "unsupported"})
-	if err == nil {
-		t.Fatal("callSaveRecipeTool() error = nil, want unsupported source error")
-	}
-	if backend.saveCalls != 0 || backend.draftSaveCalls != 0 {
-		t.Fatalf("save calls = prepared %d draft %d, want none", backend.saveCalls, backend.draftSaveCalls)
-	}
-}
-
 func TestCallToolACIDRecipesSave1And11SavesRawTextRecipe(t *testing.T) {
 	backend := &fakeBackend{
 		preview: cooked.RecipeTextPreview{
@@ -595,9 +582,14 @@ func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked
 	tests := []struct {
 		name      string
 		arguments ChangeShoppingListArguments
+		wantError string
 	}{
-		{name: "empty", arguments: ChangeShoppingListArguments{Action: "   "}},
-		{name: "unknown", arguments: ChangeShoppingListArguments{Action: "dance"}},
+		{name: "empty", arguments: ChangeShoppingListArguments{Action: "   "}, wantError: "action is required"},
+		{
+			name:      "unknown",
+			arguments: ChangeShoppingListArguments{Action: "dance"},
+			wantError: `unsupported change_shopping_list action "dance" (supported: add, update_item, replace_selection, add_selection, remove_selection, remove, clear)`,
+		},
 	}
 
 	for _, tt := range tests {
@@ -609,6 +601,9 @@ func TestCallToolACIDShoppingListActions2RejectsUnknownActionBeforeCallingCooked
 			if err == nil {
 				t.Fatal("callChangeShoppingListTool() error = nil, want action error")
 			}
+			if err.Error() != tt.wantError {
+				t.Fatalf("callChangeShoppingListTool() error = %q", err.Error())
+			}
 			if backend.clearShoppingListCalls != 0 {
 				t.Fatalf("clear calls = %d, want none", backend.clearShoppingListCalls)
 			}

internal/mcp/server_wording_test.go 🔗

@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
+
+package mcp
+
+import (
+	"context"
+	"strings"
+	"testing"
+)
+
+// tools.READ_TOOL.1 tools.SCHEMA.4 tools.RESPONSES.6
+func TestCallToolACIDToolsReadTool1RejectsUnsupportedTargetBeforeCallingCooked(t *testing.T) {
+	backend := &fakeBackend{}
+	server := NewServer(backend, "test")
+
+	_, err := server.CallReadTool(context.Background(), ReadArguments{Target: "meal_plan"})
+	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)` {
+		t.Fatalf("CallReadTool() error = %q", err.Error())
+	}
+	if backend.readShoppingListCalls != 0 || backend.metadataCalls != 0 || backend.contentCalls != 0 {
+		t.Fatalf(
+			"backend calls = shopping list %d metadata %d content %d, want none",
+			backend.readShoppingListCalls,
+			backend.metadataCalls,
+			backend.contentCalls,
+		)
+	}
+}
+
+// recipes.SAVE.10 tools.SCHEMA.4 tools.RESPONSES.6
+func TestCallToolACIDRecipesSave10RejectsUnsupportedSourceBeforeCallingCooked(t *testing.T) {
+	backend := &fakeBackend{}
+	server := NewServer(backend, "test")
+
+	_, _, err := server.callSaveRecipeTool(context.Background(), nil, SaveRecipeArguments{Source: "unsupported"})
+	if err == nil {
+		t.Fatal("callSaveRecipeTool() error = nil, want unsupported source error")
+	}
+	if err.Error() != `unsupported save_recipe source "unsupported" (supported: raw_text, prepared, url, draft, existing)` {
+		t.Fatalf("callSaveRecipeTool() error = %q", err.Error())
+	}
+	if backend.saveCalls != 0 || backend.draftSaveCalls != 0 {
+		t.Fatalf("save calls = prepared %d draft %d, want none", backend.saveCalls, backend.draftSaveCalls)
+	}
+}
+
+// tools.SCHEMA.3 tools.SCHEMA.5 tools.SAVE_RECIPE_TOOL.1 tools.CHANGE_SHOPPING_LIST_TOOL.1
+func TestToolDescriptionsACIDToolsSchema3And5DescribeCurrentLimits(t *testing.T) {
+	save := saveRecipeTool()
+	for _, want := range []string{
+		"raw text",
+		"prepared markdown",
+		"URL import",
+		"reviewed URL-import draft",
+		"existing recipe update",
+		"draft_id",
+	} {
+		if !strings.Contains(save.Description, want) {
+			t.Fatalf("save_recipe description missing %q: %q", want, save.Description)
+		}
+	}
+
+	shopping := changeShoppingListTool()
+	for _, want := range []string{
+		"add",
+		"update_item",
+		"replace_selection",
+		"add_selection",
+		"remove_selection",
+		"remove",
+		"clear",
+		"Remove and clear are destructive",
+	} {
+		if !strings.Contains(shopping.Description, want) {
+			t.Fatalf("change_shopping_list description missing %q: %q", want, shopping.Description)
+		}
+	}
+}