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:
+1
-1
@@ -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 @@
|
|||||||
__version__ = "0.8.0"
|
__version__ = "0.8.1"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user