fix(recipes): two bugs in recipe categories (v0.8.1)

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/<familyId>_2 through _6

Version: 0.8.0 → 0.8.1
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 06:30:09 +02:00
parent 4c60b5b5fa
commit 74a8b83fde
4 changed files with 71 additions and 39 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mcp-familywall" 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" 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.8.0" __version__ = "0.8.1"
+10 -2
View File
@@ -160,7 +160,9 @@ def build_create_params(
if url is not None: if url is not None:
params["recipe.url"] = url params["recipe.url"] = url
if category_ids is not None: 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 return params
@@ -225,5 +227,11 @@ def build_update_params(
if url is not None: if url is not None:
params["recipe.url"] = url params["recipe.url"] = url
if category_ids is not None: 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 return params
+59 -35
View File
@@ -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]]: def _get_raw_recipes() -> list[dict[str, Any]]:
"""Login, call metasync with id='recipe', logout and return the raw recipe list. """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. """Return all available recipe categories for the family.
Returns: 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. Returns an error message string on failure.
""" """
# Attempt to get categories via metasync call (similar to recipes). # Get the family ID to construct standard category IDs
# If the endpoint supports it, use that. Otherwise fallback to extracting family_id: str | None = None
# categories from existing recipes.
try: try:
data = _authenticated_call("metasync", {"id": "recipeCategory"}) family_id = _get_family_id()
except RuntimeError: except RuntimeError:
# Endpoint doesn't exist or failed. Fall back to extracting from recipes. pass
try:
raw_recipes = _get_raw_recipes()
except RuntimeError as exc:
return f"Error: {exc}"
# Collect unique category IDs from all recipes. # Collect all category IDs: standard categories + categories found in recipes
seen_ids: set[str] = set() seen_ids: set[str] = set()
category_ids: list[str] = [] 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)
# 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: try:
items: list[dict[str, Any]] = data["a00"]["r"]["r"]["updatedCreated"] raw_recipes = _get_raw_recipes()
except (KeyError, TypeError): except RuntimeError:
items = [] raw_recipes = []
if not isinstance(items, list): for recipe in raw_recipes:
return json.dumps({"error": "No recipe categories available"}, ensure_ascii=False) 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] = [] return json.dumps(category_ids, ensure_ascii=False, indent=2)
for cat_id in items:
if isinstance(cat_id, str):
result.append(cat_id)
return json.dumps(result, ensure_ascii=False, indent=2)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------