From 3f20b6eda3293369e285402dd9f6427053a30dd1 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 17 Apr 2026 12:33:31 +0200 Subject: [PATCH] feat(meal-planner): add add_meal_note tool (v0.11.5) New write tool using mpmealput endpoint to create meal/ note entries with optional free-text and serving count. Response structure verified from JS-bundle (Sg class); a00.r.r is a plain object (unlike mpcreate). Structured output matches get_meal_plan meal entry format. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 7 ++-- README.md | 3 +- SPEC.md | 33 ++++++++++++++- pyproject.toml | 2 +- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/server.py | 73 ++++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e45ed77..d86c242 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.11.4) +### Implementierte Tools (v0.11.5) | Kategorie | Tools | |---|---| @@ -34,7 +34,7 @@ und wird in Claude Desktop eingebunden. | Kategorien | `create_category`, `delete_category` | | Aktivitäten | `like_post` | | Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe`, `get_recipe_categories` | -| Essensplaner | `get_meal_plan`, `add_recipe_to_meal_plan`, `add_meal_to_meal_plan`, `delete_meal_plan_entry` | +| Essensplaner | `get_meal_plan`, `add_recipe_to_meal_plan`, `add_meal_to_meal_plan`, `add_meal_note`, `delete_meal_plan_entry` | | Kreise | `create_circle`, `update_circle`, `delete_circle`, `add_member_to_circle` | ## Roadmap @@ -67,7 +67,8 @@ und wird in Claude Desktop eingebunden. - v0.11.1: add_recipe_to_meal_plan strukturierter Output (Response verifiziert) ✓ - v0.11.2: add_meal_to_meal_plan (mpcreate; Freitext; raw response bis Struktur verifiziert) ✓ - v0.11.3: add_meal_to_meal_plan strukturierter Output (a00.r.r ist Array, nicht Objekt) ✓ -- v0.11.4: delete_meal_plan_entry (metadelete für dish/ und meal/-Objekte) ✓ ← aktuell +- v0.11.4: delete_meal_plan_entry (metadelete für dish/ und meal/-Objekte) ✓ +- v0.11.5: add_meal_note (mpmealput; Notiz + Portionen; strukturierter Output) ✓ ← aktuell - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) diff --git a/README.md b/README.md index e8ef05a..66c5dd2 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.11.4) +## Features (v0.11.5) ### Read @@ -34,6 +34,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `delete_recipe` -- permanently delete a recipe (only own recipes) - `add_recipe_to_meal_plan` -- add a recipe from the recipe box to the meal plan for a specific date and meal slot (BREAKFAST/LUNCH/SNACK/DINNER) - `add_meal_to_meal_plan` -- add a free-text meal entry (no recipe) to the meal plan for a specific date and meal slot +- `add_meal_note` -- add a note and/or serving count to a meal plan slot (creates a `meal/` entry; at least one of `note` or `serves` required) - `delete_meal_plan_entry` -- permanently delete a meal plan entry (works for both `dish/…` and `meal/…` entries) - `create_circle` -- create a new Family Wall circle (group) - `update_circle` -- rename a circle (server capitalises first letter; only name is changeable via API; primary circle is protected) diff --git a/SPEC.md b/SPEC.md index 1a8edd3..cc8b536 100644 --- a/SPEC.md +++ b/SPEC.md @@ -788,11 +788,42 @@ verknüpft es mit dem Dish-Objekt. `is_from_recipe_box` ist daher `false`. **Verifiziert am:** 2026-04-17 via FW_DEBUG=1 +### `mpmealput` – Meal-Notiz erstellen/aktualisieren +POST https://api.familywall.com/api/mpmealput + +**Body-Parameter (verifiziert aus JS-Bundle, Encoder `UI(O)`):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `date` | ja | Ziel-Datum ISO 8601 (z.B. `"2026-04-20"`) | +| `type` | ja | Mahlzeiten-Typ: `BREAKFAST`, `LUNCH`, `SNACK`, `DINNER` | +| `note` | nein | Freitext-Notiz (z.B. `"Bitte ohne Zwiebeln"`) | +| `serves` | nein | Portionen als String (z.B. `"4"`) — Vt = int→string Konverter | +| `metaId` | nein | metaId eines bestehenden meal/-Objekts → Update; ohne → Create | + +**Hinweis:** Ohne `metaId` wird ein neues `meal/`-Objekt erstellt. +Mit `metaId` wird ein bestehendes aktualisiert (Update, noch nicht implementiert). + +**Response-Struktur:** +``` +a00.r.r → meal-Objekt (Objekt, nicht Array) + .metaId → neue/aktualisierte Meal-ID (z.B. "meal/16282169_...") + .date → Datum + .type → Mahlzeiten-Typ + .note → Freitext-Notiz + .serves → Portionen als String (z.B. "1") + .familyId → Kreis-metaId + .accountId → Ersteller-accountId + .rights.canUpdate → "true" + .rights.canDelete → "true" +``` + +**Verifiziert am:** 2026-04-17 (Parameter aus JS-Bundle; Response-Struktur aus `Sg`-Klasse im Bundle) + ### Weitere Meal Planner Endpoints (nicht implementiert) | Endpoint | Parameter | Bedeutung | |---|---|---| -| `mpmealput` | Mahlzeiten-Objekt (encoded) | Mahlzeit aktualisieren | | `mpmove` | `metaId`, `date`, `type`, `clientOpId` | Mahlzeit zu anderem Datum/Typ verschieben | | `mpdelete` | `metaId` | Mahlzeit löschen | | `mpsettings` | – | Einstellungen lesen | diff --git a/pyproject.toml b/pyproject.toml index 820d26e..f175a5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.11.4" +version = "0.11.5" 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 350cbe9..91df974 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.11.4" +__version__ = "0.11.5" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index 98daacd..032cbd6 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -2499,6 +2499,79 @@ def delete_meal_plan_entry(entry_id: str) -> str: return json.dumps({"deleted": True, "id": entry_id}, ensure_ascii=False, indent=2) +# --------------------------------------------------------------------------- +# Tool: add_meal_note +# --------------------------------------------------------------------------- + + +@mcp.tool() +def add_meal_note( + date: str, + meal_type: str, + note: str | None = None, + serves: int | None = None, +) -> str: + """Add a note and/or serving count to a meal plan slot. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Creates a ``meal/`` entry for the given date and meal type. + At least one of ``note`` or ``serves`` must be provided. + + Args: + date: Target date in ISO format (e.g. ``"2026-04-20"``). + meal_type: Meal slot — one of ``"BREAKFAST"``, ``"LUNCH"``, + ``"SNACK"``, or ``"DINNER"``. + note: Optional free-text note (e.g. ``"Bitte ohne Zwiebeln"``). + serves: Optional number of servings as integer (e.g. ``4``). + + Returns: + JSON with the new meal entry on success, or an error message. + """ + if meal_type not in ("BREAKFAST", "LUNCH", "SNACK", "DINNER"): + return "Error: meal_type must be one of 'BREAKFAST', 'LUNCH', 'SNACK', 'DINNER'." + if note is None and serves is None: + return "Error: At least one of 'note' or 'serves' must be provided." + + params: dict[str, Any] = {"date": date, "type": meal_type} + if note is not None: + params["note"] = note + if serves is not None: + params["serves"] = str(serves) # API expects string (int→string) + + try: + data = _authenticated_call("mpmealput", params) + except RuntimeError as exc: + return f"Error: {exc}" + + try: + meal = data["a00"]["r"]["r"] + if not isinstance(meal, dict) or "metaId" not in meal: + raise TypeError("unexpected shape") + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected mpmealput response structure", "raw": data}, + ensure_ascii=False, + indent=2, + ) + + rights = meal.get("rights") or {} + raw_serves = meal.get("serves") + result: dict[str, Any] = { + "id": meal.get("metaId"), + "date": meal.get("date"), + "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", + "can_delete": rights.get("canDelete") == "true", + } + return json.dumps(result, ensure_ascii=False, indent=2) + + # --------------------------------------------------------------------------- # Factory # ---------------------------------------------------------------------------