feat(tools): add maintenance and meal planning tools with CRUD operations

- Implement maintenance tool for adding, logging, and retrieving tasks
- Create meals tool for managing recipes, meal plans, and shopping lists
- Introduce reparse metadata tool for updating thought metadata
- Add household knowledge, home maintenance, family calendar, meal planning, and professional CRM database migrations
- Grant necessary permissions for new database tables
This commit is contained in:
2026-03-26 23:29:03 +02:00
parent b74d63c543
commit 0eb6ac7ee5
25 changed files with 2910 additions and 10 deletions

210
internal/tools/meals.go Normal file
View File

@@ -0,0 +1,210 @@
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{}, errInvalidInput("name is required")
}
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{}, errInvalidInput("name is required")
}
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
}