From 74a8b83fde9792c66dfe7cc174ef3df3bb61ddac Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 17 Apr 2026 06:30:09 +0200 Subject: [PATCH] fix(recipes): two bugs in recipe categories (v0.8.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: update_recipe(category_ids=[]) didn't remove categories - Fixed: send empty string "" to API to remove all categories - Non-empty lists continue to work as expected - Verified via FW_DEBUG=1 testing Bug 2: get_recipe_categories() only showed used categories - Fixed: always return 5 free-tier standard categories - Added _get_family_id() helper to extract family ID - Now returns all available category IDs even on new accounts - Standard categories: category/_2 through _6 Version: 0.8.0 → 0.8.1 Co-Authored-By: Claude Haiku 4.5 --- pyproject.toml | 2 +- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/modules/recipes.py | 12 +++- src/mcp_familywall/server.py | 94 +++++++++++++++++---------- 4 files changed, 71 insertions(+), 39 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4d793ca..472e448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.8.0" +version = "0.8.1" 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 777f190..8088f75 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.8.1" diff --git a/src/mcp_familywall/modules/recipes.py b/src/mcp_familywall/modules/recipes.py index 84a05a8..8adda35 100644 --- a/src/mcp_familywall/modules/recipes.py +++ b/src/mcp_familywall/modules/recipes.py @@ -160,7 +160,9 @@ def build_create_params( if url is not None: params["recipe.url"] = url if category_ids is not None: - params["recipe.recipeCategoryIdList"] = category_ids + # For create: empty list just means no categories (don't send parameter). + if len(category_ids) > 0: + params["recipe.recipeCategoryIdList"] = category_ids return params @@ -225,5 +227,11 @@ def build_update_params( if url is not None: params["recipe.url"] = url if category_ids is not None: - params["recipe.recipeCategoryIdList"] = category_ids + # Empty list: send empty string to remove all categories. + # Non-empty list: send as-is (httpx sends list values multiple times). + # Verified via testing: empty string "" removes categories, empty list [] does not. + if len(category_ids) == 0: + params["recipe.recipeCategoryIdList"] = "" + else: + params["recipe.recipeCategoryIdList"] = category_ids return params diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index f06019e..0a5968c 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -1699,6 +1699,27 @@ def like_post(post_id: str, like: bool = True) -> str: # --------------------------------------------------------------------------- +def _get_family_id() -> str: + """Extract the primary family ID from famlistfamily response. + + Returns: + The family ID as a string (e.g. "23431854"). + + Raises: + RuntimeError: On credential or API errors. + """ + try: + families = _famlistfamily() + if families and isinstance(families, list) and len(families) > 0: + primary = families[0] + metaid = primary.get("metaId") + if isinstance(metaid, str) and metaid.startswith("family/"): + return metaid.split("/", 1)[1] + except RuntimeError: + pass + raise RuntimeError("Could not determine family ID") + + def _get_raw_recipes() -> list[dict[str, Any]]: """Login, call metasync with id='recipe', logout and return the raw recipe list. @@ -1725,50 +1746,53 @@ def get_recipe_categories() -> str: """Return all available recipe categories for the family. Returns: - JSON list of recipe category objects with keys: id, name, emoji. + JSON list of category IDs (e.g. ["category/23431854_2", ...]). + Always includes the 5 free-tier standard categories if family ID is available. Returns an error message string on failure. """ - # Attempt to get categories via metasync call (similar to recipes). - # If the endpoint supports it, use that. Otherwise fallback to extracting - # categories from existing recipes. + # Get the family ID to construct standard category IDs + family_id: str | None = None try: - data = _authenticated_call("metasync", {"id": "recipeCategory"}) + family_id = _get_family_id() except RuntimeError: - # Endpoint doesn't exist or failed. Fall back to extracting from recipes. - try: - raw_recipes = _get_raw_recipes() - except RuntimeError as exc: - return f"Error: {exc}" + pass - # Collect unique category IDs from all recipes. - seen_ids: set[str] = set() - category_ids: list[str] = [] - for recipe in raw_recipes: - if not isinstance(recipe, dict): - continue - recipe_cat_ids = recipe.get("recipeCategoryIdList") - if not isinstance(recipe_cat_ids, list): - continue - for cat_id in recipe_cat_ids: - if isinstance(cat_id, str) and cat_id and cat_id not in seen_ids: - seen_ids.add(cat_id) - category_ids.append(cat_id) - return json.dumps(category_ids, ensure_ascii=False, indent=2) + # Collect all category IDs: standard categories + categories found in recipes + seen_ids: set[str] = set() + category_ids: list[str] = [] - # If metasync succeeded, extract categories from response. + # Add the 5 free-tier standard categories if we have the family ID + if family_id: + standard_cat_ids = [ + f"category/{family_id}_2", # Bei Kindern beliebt (KIDS_LOVE) + f"category/{family_id}_3", # Wirklich einfach (EASY) + f"category/{family_id}_4", # Nachspeisen (DESSERT) + f"category/{family_id}_5", # Schmeckt toll (DELICIOUS) + f"category/{family_id}_6", # Gemüse (VEGETABLES) + ] + for cat_id in standard_cat_ids: + if cat_id not in seen_ids: + seen_ids.add(cat_id) + category_ids.append(cat_id) + + # Add any additional categories found in recipes (e.g., premium categories) try: - items: list[dict[str, Any]] = data["a00"]["r"]["r"]["updatedCreated"] - except (KeyError, TypeError): - items = [] + raw_recipes = _get_raw_recipes() + except RuntimeError: + raw_recipes = [] - if not isinstance(items, list): - return json.dumps({"error": "No recipe categories available"}, ensure_ascii=False) + for recipe in raw_recipes: + if not isinstance(recipe, dict): + continue + recipe_cat_ids = recipe.get("recipeCategoryIdList") + if not isinstance(recipe_cat_ids, list): + continue + for cat_id in recipe_cat_ids: + if isinstance(cat_id, str) and cat_id and cat_id not in seen_ids: + seen_ids.add(cat_id) + category_ids.append(cat_id) - result: list[str] = [] - for cat_id in items: - if isinstance(cat_id, str): - result.append(cat_id) - return json.dumps(result, ensure_ascii=False, indent=2) + return json.dumps(category_ids, ensure_ascii=False, indent=2) # ---------------------------------------------------------------------------