From bf086a4f847bccdf05e61b397dc5cd9e7ecdff6a Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 17 Apr 2026 11:31:51 +0200 Subject: [PATCH] feat(meal-planner): add is_from_recipe_box field to get_meal_plan (v0.10.3) Join recipeList[] from API response as a lookup table: isRecipe="true" means a real recipe from the recipe box, "false" is a free-text stub. Dish entries get is_from_recipe_box=true/false; meal entries get null. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 5 +++-- README.md | 4 ++-- SPEC.md | 5 ++++- pyproject.toml | 2 +- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/server.py | 36 ++++++++++++++++++++++++---------- 6 files changed, 37 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3b9fc4f..43b2968 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.10.2) +### Implementierte Tools (v0.10.3) | Kategorie | Tools | |---|---| @@ -60,7 +60,8 @@ und wird in Claude Desktop eingebunden. - v0.9.1: Bugfix reminder-Mapping (reminderUnit/reminderValue statt unit/value; value=0 ist gültig) ✓ - v0.10.0: get_meal_plan (read-only, raw JSON; Premium-Feature Essensplaner) ✓ - v0.10.1: get_meal_plan strukturierter Output + SPEC.md mplistinterval Response verifiziert ✓ -- v0.10.2: get_meal_plan mealList[] einbinden (Freitext-Notizen + Portionen), merged + sortiert ✓ ← aktuell +- v0.10.2: get_meal_plan mealList[] einbinden (Freitext-Notizen + Portionen), merged + sortiert ✓ +- v0.10.3: get_meal_plan is_from_recipe_box Feld (recipeList[].isRecipe Lookup) ✓ ← aktuell - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) diff --git a/README.md b/README.md index 7f8c942..8e37630 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude. -## Features (v0.10.2) +## Features (v0.10.3) ### Read @@ -15,7 +15,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `get_recipes` -- list all family recipes (compact summary: id, name, prep/cook time, serves) - `get_recipe` -- get a single recipe in full detail (ingredients, instructions, ingredients_parsed, category_ids, etc.) - `get_recipe_categories` -- list all available recipe categories (always returns all 5 standard categories: Bei Kindern beliebt, Wirklich einfach, Nachspeisen, Schmeckt toll, Gemüse; plus any additional categories found in existing recipes) -- `get_meal_plan` -- get meal plan entries for a date range (Premium feature; merges dish and meal entries, sorted by date + type; fields: id, date, type, name, recipe_id, note, serves, can_update, can_delete) +- `get_meal_plan` -- get meal plan entries for a date range (Premium feature; merges dish and meal entries, sorted by date + type; fields: id, date, type, name, recipe_id, is_from_recipe_box, note, serves, can_update, can_delete) ### Write (with confirmation prompt) diff --git a/SPEC.md b/SPEC.md index c571e75..95dbff6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -696,7 +696,10 @@ a00.r.r .sortingIndex → Sortierung (numerischer Timestamp als String) .rights.canUpdate → "true" wenn bearbeitbar .rights.canDelete → "true" wenn löschbar - .recipeList[] → vollständige Rezept-Objekte der verknüpften Rezepte + .recipeList[] → Rezept-Objekte der verknüpften Gerichte + .metaId → Rezept-ID (z.B. "recipe/16282169_7932720") + .isRecipe → "true" = echtes Rezept aus der Rezeptbox (hat Zutaten etc.) + "false" = Freitext-Stub (nur Name, keine Zutaten) .mealList[] → Freitext-Notizen (meal-Objekte) .metaId → "meal/_" .date → Datum der Mahlzeit (z.B. "2026-04-17") diff --git a/pyproject.toml b/pyproject.toml index 3255243..e4a9598 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.10.2" +version = "0.10.3" description = "MCP server for Family Wall — read your family's lists and tasks via Claude" readme = "README.md" requires-python = ">=3.12" diff --git a/src/mcp_familywall/__init__.py b/src/mcp_familywall/__init__.py index 17c1a62..b2385cb 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.10.2" +__version__ = "0.10.3" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index b368de1..9be52e6 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -2231,15 +2231,18 @@ def get_meal_plan(date_from: str, date_to: str) -> str: JSON list of meal plan entries sorted by date then meal type (BREAKFAST → LUNCH → SNACK → DINNER), with keys: - - ``id`` — metaId (``dish/…`` or ``meal/…``) - - ``date`` — ISO date string - - ``type`` — ``BREAKFAST``, ``LUNCH``, ``SNACK``, or ``DINNER`` - - ``name`` — dish name, or ``null`` for meal entries - - ``recipe_id`` — linked recipe metaId, or ``null`` - - ``note`` — free-text note (meal entries only), or ``null`` - - ``serves`` — number of servings as int (meal entries only), or ``null`` - - ``can_update`` — whether the entry can be updated - - ``can_delete`` — whether the entry can be deleted + - ``id`` — metaId (``dish/…`` or ``meal/…``) + - ``date`` — ISO date string + - ``type`` — ``BREAKFAST``, ``LUNCH``, ``SNACK``, or ``DINNER`` + - ``name`` — dish name, or ``null`` for meal entries + - ``recipe_id`` — linked recipe metaId, or ``null`` + - ``is_from_recipe_box`` — ``true`` if the linked recipe was pulled from the + recipe box; ``false`` if it is a free-text stub (name only, no ingredients); + ``null`` for meal entries or when no recipe is linked + - ``note`` — free-text note (meal entries only), or ``null`` + - ``serves`` — number of servings as int (meal entries only), or ``null`` + - ``can_update`` — whether the entry can be updated + - ``can_delete`` — whether the entry can be deleted Returns an error message string on failure. """ @@ -2261,6 +2264,16 @@ def get_meal_plan(date_from: str, date_to: str) -> str: raw_dishes: list[dict[str, Any]] = payload.get("list") or [] raw_meals: list[dict[str, Any]] = payload.get("mealList") or [] + raw_recipe_list: list[dict[str, Any]] = payload.get("recipeList") or [] + + # Build lookup: recipeId → is_from_recipe_box + # recipeList[] contains one entry per linked recipe; isRecipe="true" means it is + # a real recipe from the recipe box, "false" means a free-text stub (name only). + recipe_lookup: dict[str, bool] = { + r["metaId"]: r.get("isRecipe") == "true" + for r in raw_recipe_list + if isinstance(r, dict) and "metaId" in r + } _type_order = {"BREAKFAST": 0, "LUNCH": 1, "SNACK": 2, "DINNER": 3} @@ -2268,13 +2281,15 @@ def get_meal_plan(date_from: str, date_to: str) -> str: for dish in raw_dishes: rights = dish.get("rights") or {} + recipe_id: str | None = dish.get("recipeId") or None result.append( { "id": dish.get("metaId"), "date": dish.get("date"), "type": dish.get("type"), "name": dish.get("name"), - "recipe_id": dish.get("recipeId") or None, + "recipe_id": recipe_id, + "is_from_recipe_box": recipe_lookup.get(recipe_id) if recipe_id else None, "note": None, "serves": None, "can_update": rights.get("canUpdate") == "true", @@ -2292,6 +2307,7 @@ def get_meal_plan(date_from: str, date_to: str) -> str: "type": meal.get("type"), "name": None, "recipe_id": None, + "is_from_recipe_box": None, "note": meal.get("note") or None, "serves": int(raw_serves) if raw_serves is not None else None, "can_update": rights.get("canUpdate") == "true",