package tools import ( "context" "strings" "time" "github.com/google/uuid" "github.com/modelcontextprotocol/go-sdk/mcp" "git.warky.dev/wdevs/amcs/internal/store" ext "git.warky.dev/wdevs/amcs/internal/types" ) type MealsTool struct { store *store.DB } func NewMealsTool(db *store.DB) *MealsTool { return &MealsTool{store: db} } // add_recipe type AddRecipeInput struct { Name string `json:"name" jsonschema:"recipe name"` Cuisine string `json:"cuisine,omitempty"` PrepTimeMinutes *int `json:"prep_time_minutes,omitempty"` CookTimeMinutes *int `json:"cook_time_minutes,omitempty"` Servings *int `json:"servings,omitempty"` Ingredients []ext.Ingredient `json:"ingredients,omitempty" jsonschema:"list of ingredients with name, quantity, unit"` Instructions []string `json:"instructions,omitempty" jsonschema:"ordered list of steps"` Tags []string `json:"tags,omitempty"` Rating *int `json:"rating,omitempty" jsonschema:"1-5 rating"` Notes string `json:"notes,omitempty"` } type AddRecipeOutput struct { Recipe ext.Recipe `json:"recipe"` } func (t *MealsTool) AddRecipe(ctx context.Context, _ *mcp.CallToolRequest, in AddRecipeInput) (*mcp.CallToolResult, AddRecipeOutput, error) { if strings.TrimSpace(in.Name) == "" { return nil, AddRecipeOutput{}, errRequiredField("name") } if in.Ingredients == nil { in.Ingredients = []ext.Ingredient{} } if in.Instructions == nil { in.Instructions = []string{} } if in.Tags == nil { in.Tags = []string{} } recipe, err := t.store.AddRecipe(ctx, ext.Recipe{ Name: strings.TrimSpace(in.Name), Cuisine: strings.TrimSpace(in.Cuisine), PrepTimeMinutes: in.PrepTimeMinutes, CookTimeMinutes: in.CookTimeMinutes, Servings: in.Servings, Ingredients: in.Ingredients, Instructions: in.Instructions, Tags: in.Tags, Rating: in.Rating, Notes: strings.TrimSpace(in.Notes), }) if err != nil { return nil, AddRecipeOutput{}, err } return nil, AddRecipeOutput{Recipe: recipe}, nil } // search_recipes type SearchRecipesInput struct { Query string `json:"query,omitempty" jsonschema:"search by recipe name"` Cuisine string `json:"cuisine,omitempty"` Tags []string `json:"tags,omitempty" jsonschema:"filter by tags (all must match)"` Ingredient string `json:"ingredient,omitempty" jsonschema:"filter by ingredient name"` } type SearchRecipesOutput struct { Recipes []ext.Recipe `json:"recipes"` } func (t *MealsTool) SearchRecipes(ctx context.Context, _ *mcp.CallToolRequest, in SearchRecipesInput) (*mcp.CallToolResult, SearchRecipesOutput, error) { recipes, err := t.store.SearchRecipes(ctx, in.Query, in.Cuisine, in.Tags, in.Ingredient) if err != nil { return nil, SearchRecipesOutput{}, err } if recipes == nil { recipes = []ext.Recipe{} } return nil, SearchRecipesOutput{Recipes: recipes}, nil } // update_recipe type UpdateRecipeInput struct { ID uuid.UUID `json:"id" jsonschema:"recipe id to update"` Name string `json:"name" jsonschema:"recipe name"` Cuisine string `json:"cuisine,omitempty"` PrepTimeMinutes *int `json:"prep_time_minutes,omitempty"` CookTimeMinutes *int `json:"cook_time_minutes,omitempty"` Servings *int `json:"servings,omitempty"` Ingredients []ext.Ingredient `json:"ingredients,omitempty"` Instructions []string `json:"instructions,omitempty"` Tags []string `json:"tags,omitempty"` Rating *int `json:"rating,omitempty"` Notes string `json:"notes,omitempty"` } type UpdateRecipeOutput struct { Recipe ext.Recipe `json:"recipe"` } func (t *MealsTool) UpdateRecipe(ctx context.Context, _ *mcp.CallToolRequest, in UpdateRecipeInput) (*mcp.CallToolResult, UpdateRecipeOutput, error) { if strings.TrimSpace(in.Name) == "" { return nil, UpdateRecipeOutput{}, errRequiredField("name") } if in.Ingredients == nil { in.Ingredients = []ext.Ingredient{} } if in.Instructions == nil { in.Instructions = []string{} } if in.Tags == nil { in.Tags = []string{} } recipe, err := t.store.UpdateRecipe(ctx, in.ID, ext.Recipe{ Name: strings.TrimSpace(in.Name), Cuisine: strings.TrimSpace(in.Cuisine), PrepTimeMinutes: in.PrepTimeMinutes, CookTimeMinutes: in.CookTimeMinutes, Servings: in.Servings, Ingredients: in.Ingredients, Instructions: in.Instructions, Tags: in.Tags, Rating: in.Rating, Notes: strings.TrimSpace(in.Notes), }) if err != nil { return nil, UpdateRecipeOutput{}, err } return nil, UpdateRecipeOutput{Recipe: recipe}, nil } // create_meal_plan type CreateMealPlanInput struct { WeekStart time.Time `json:"week_start" jsonschema:"Monday of the week to plan"` Meals []ext.MealPlanInput `json:"meals" jsonschema:"list of meal entries for the week"` } type CreateMealPlanOutput struct { Entries []ext.MealPlanEntry `json:"entries"` } func (t *MealsTool) CreateMealPlan(ctx context.Context, _ *mcp.CallToolRequest, in CreateMealPlanInput) (*mcp.CallToolResult, CreateMealPlanOutput, error) { if len(in.Meals) == 0 { return nil, CreateMealPlanOutput{}, errInvalidInput("meals are required") } entries, err := t.store.CreateMealPlan(ctx, in.WeekStart, in.Meals) if err != nil { return nil, CreateMealPlanOutput{}, err } if entries == nil { entries = []ext.MealPlanEntry{} } return nil, CreateMealPlanOutput{Entries: entries}, nil } // get_meal_plan type GetMealPlanInput struct { WeekStart time.Time `json:"week_start" jsonschema:"Monday of the week to retrieve"` } type GetMealPlanOutput struct { Entries []ext.MealPlanEntry `json:"entries"` } func (t *MealsTool) GetMealPlan(ctx context.Context, _ *mcp.CallToolRequest, in GetMealPlanInput) (*mcp.CallToolResult, GetMealPlanOutput, error) { entries, err := t.store.GetMealPlan(ctx, in.WeekStart) if err != nil { return nil, GetMealPlanOutput{}, err } if entries == nil { entries = []ext.MealPlanEntry{} } return nil, GetMealPlanOutput{Entries: entries}, nil } // generate_shopping_list type GenerateShoppingListInput struct { WeekStart time.Time `json:"week_start" jsonschema:"Monday of the week to generate shopping list for"` } type GenerateShoppingListOutput struct { ShoppingList ext.ShoppingList `json:"shopping_list"` } func (t *MealsTool) GenerateShoppingList(ctx context.Context, _ *mcp.CallToolRequest, in GenerateShoppingListInput) (*mcp.CallToolResult, GenerateShoppingListOutput, error) { list, err := t.store.GenerateShoppingList(ctx, in.WeekStart) if err != nil { return nil, GenerateShoppingListOutput{}, err } return nil, GenerateShoppingListOutput{ShoppingList: list}, nil }