feat(meal-planner): merge mealList[] into get_meal_plan output (v0.10.2)

- dish entries (list[]) and meal entries (mealList[]) are now merged and
  returned together, sorted by date then type (BREAKFAST→LUNCH→SNACK→DINNER)
- New output fields: note (free-text, meal entries only) and serves (int)
- SPEC.md: document verified mealList[] response structure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 11:26:21 +02:00
parent a7b21c1ede
commit 7d912beb5f
6 changed files with 67 additions and 18 deletions
+3 -2
View File
@@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden.
## Aktueller Stand ## Aktueller Stand
### Implementierte Tools (v0.10.1) ### Implementierte Tools (v0.10.2)
| Kategorie | Tools | | Kategorie | Tools |
|---|---| |---|---|
@@ -59,7 +59,8 @@ und wird in Claude Desktop eingebunden.
- v0.9.0: get_tasks liefert recurrency, recurrency_interval, rrule, reminder (read-only) ✓ - v0.9.0: get_tasks liefert recurrency, recurrency_interval, rrule, reminder (read-only) ✓
- v0.9.1: Bugfix reminder-Mapping (reminderUnit/reminderValue statt unit/value; value=0 ist gültig) ✓ - 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.0: get_meal_plan (read-only, raw JSON; Premium-Feature Essensplaner) ✓
- v0.10.1: get_meal_plan strukturierter Output + SPEC.md mplistinterval Response verifiziert ✓ ← aktuell - 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
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
+2 -2
View File
@@ -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. 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.1) ## Features (v0.10.2)
### Read ### 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_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` -- 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_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; returns structured list with id, date, type, name, recipe_id, 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, note, serves, can_update, can_delete)
### Write (with confirmation prompt) ### Write (with confirmation prompt)
+10 -2
View File
@@ -697,7 +697,14 @@ a00.r.r
.rights.canUpdate → "true" wenn bearbeitbar .rights.canUpdate → "true" wenn bearbeitbar
.rights.canDelete → "true" wenn löschbar .rights.canDelete → "true" wenn löschbar
.recipeList[] → vollständige Rezept-Objekte der verknüpften Rezepte .recipeList[] → vollständige Rezept-Objekte der verknüpften Rezepte
.mealList[] → immer leer (Bedeutung unbekannt) .mealList[] → Freitext-Notizen (meal-Objekte)
.metaId → "meal/<family_num>_<meal_num>"
.date → Datum der Mahlzeit (z.B. "2026-04-17")
.type → Mahlzeiten-Typ (BREAKFAST/LUNCH/SNACK/DINNER)
.note → Freitext-Notiz (z.B. "Test")
.serves → Portionen als String (z.B. "1")
.rights.canUpdate → "true" wenn bearbeitbar
.rights.canDelete → "true" wenn löschbar
``` ```
**Mahlzeiten-Typen:** **Mahlzeiten-Typen:**
@@ -706,8 +713,9 @@ a00.r.r
- `SNACK` Snack - `SNACK` Snack
- `DINNER` Abendessen - `DINNER` Abendessen
**Hinweis:** `recipeList` und `mealList` werden vom MCP-Tool nicht zurückgegeben. **Hinweis:** `recipeList` wird vom MCP-Tool nicht zurückgegeben.
Rezept-Details bei Bedarf separat via `get_recipe` abrufen. Rezept-Details bei Bedarf separat via `get_recipe` abrufen.
`list[]` (dish) und `mealList[]` (meal) werden gemergt und nach Datum + Typ sortiert zurückgegeben.
**Verifiziert am:** 2026-04-17 via FW_DEBUG=1 **Verifiziert am:** 2026-04-17 via FW_DEBUG=1
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mcp-familywall" name = "mcp-familywall"
version = "0.10.1" version = "0.10.2"
description = "MCP server for Family Wall — read your family's lists and tasks via Claude" description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.10.1" __version__ = "0.10.2"
+50 -10
View File
@@ -2218,16 +2218,29 @@ def update_recipe(
def get_meal_plan(date_from: str, date_to: str) -> str: def get_meal_plan(date_from: str, date_to: str) -> str:
"""Return meal plan entries for a date range. """Return meal plan entries for a date range.
The response merges two API lists:
* ``dish`` entries — planned meals, optionally linked to a recipe.
* ``meal`` entries — free-text notes with an optional serving count.
Args: Args:
date_from: Start date in ISO format (e.g. ``"2026-04-13"``). date_from: Start date in ISO format (e.g. ``"2026-04-13"``).
date_to: End date in ISO format (e.g. ``"2026-04-19"``). date_to: End date in ISO format (e.g. ``"2026-04-19"``).
Returns: Returns:
JSON list of meal plan entries with keys: id, date, type, name, JSON list of meal plan entries sorted by date then meal type
recipe_id, can_update, can_delete. ``type`` is one of (BREAKFAST → LUNCH → SNACK → DINNER), with keys:
``BREAKFAST``, ``LUNCH``, ``SNACK``, ``DINNER``.
``recipe_id`` is the linked recipe metaId or ``null`` when the - ``id`` — metaId (``dish/…`` or ``meal/…``)
entry is a free-text dish (not linked to a recipe). - ``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
Returns an error message string on failure. Returns an error message string on failure.
""" """
try: try:
@@ -2236,9 +2249,9 @@ def get_meal_plan(date_from: str, date_to: str) -> str:
return f"Error: {exc}" return f"Error: {exc}"
try: try:
raw_list = data["a00"]["r"]["r"]["list"] payload = data["a00"]["r"]["r"]
if not isinstance(raw_list, list): if not isinstance(payload, dict):
raise TypeError("a00.r.r.list is not a list") raise TypeError("a00.r.r is not a dict")
except (KeyError, TypeError): except (KeyError, TypeError):
return json.dumps( return json.dumps(
{"warning": "Unexpected mplistinterval response structure", "raw": data}, {"warning": "Unexpected mplistinterval response structure", "raw": data},
@@ -2246,8 +2259,14 @@ def get_meal_plan(date_from: str, date_to: str) -> str:
indent=2, indent=2,
) )
result = [] raw_dishes: list[dict[str, Any]] = payload.get("list") or []
for dish in raw_list: raw_meals: list[dict[str, Any]] = payload.get("mealList") or []
_type_order = {"BREAKFAST": 0, "LUNCH": 1, "SNACK": 2, "DINNER": 3}
result: list[dict[str, Any]] = []
for dish in raw_dishes:
rights = dish.get("rights") or {} rights = dish.get("rights") or {}
result.append( result.append(
{ {
@@ -2256,11 +2275,32 @@ def get_meal_plan(date_from: str, date_to: str) -> str:
"type": dish.get("type"), "type": dish.get("type"),
"name": dish.get("name"), "name": dish.get("name"),
"recipe_id": dish.get("recipeId") or None, "recipe_id": dish.get("recipeId") or None,
"note": None,
"serves": None,
"can_update": rights.get("canUpdate") == "true", "can_update": rights.get("canUpdate") == "true",
"can_delete": rights.get("canDelete") == "true", "can_delete": rights.get("canDelete") == "true",
} }
) )
for meal in raw_meals:
rights = meal.get("rights") or {}
raw_serves = meal.get("serves")
result.append(
{
"id": meal.get("metaId"),
"date": meal.get("date"),
"type": meal.get("type"),
"name": None,
"recipe_id": 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",
"can_delete": rights.get("canDelete") == "true",
}
)
result.sort(key=lambda e: (e.get("date") or "", _type_order.get(e.get("type") or "", 99)))
return json.dumps(result, ensure_ascii=False, indent=2) return json.dumps(result, ensure_ascii=False, indent=2)