Files
amcs/internal/tools/meals.go
Hein f41c512f36 test(tools): add unit tests for error handling functions
* Implement tests for error functions like errRequiredField, errInvalidField, and errEntityNotFound.
* Ensure proper metadata is returned for various error scenarios.
* Validate error handling in CRM, Files, and other tools.
* Introduce tests for parsing stored file IDs and UUIDs.
* Enhance coverage for helper functions related to project resolution and session management.
2026-03-31 15:10:07 +02:00

211 lines
6.8 KiB
Go

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
}