feat(recipes): add recipe categories support (v0.8.0)
- New tool: get_recipe_categories() lists available recipe category IDs - Enhanced create_recipe and update_recipe with optional category_ids parameter - Extended get_recipe and get_recipes to include category_ids in response - Updated parse_recipe_full() to extract recipeCategoryIdList from API response - Extended build_create_params() and build_update_params() to handle category_ids Recipe categories are managed via recipe.recipeCategoryIdList in mprecipeput API. Categories are represented as a list of category IDs (e.g. ["category/23431854_2"]). No direct API endpoint exists for listing all categories; they are discovered from existing recipes or must be known in advance. Version: 0.7.5 → 0.8.0 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
__version__ = "0.7.5"
|
||||
__version__ = "0.8.0"
|
||||
|
||||
@@ -75,6 +75,15 @@ def parse_recipe_full(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
ingredients_list_raw: list[dict[str, Any]] = raw.get("ingredientsList") or []
|
||||
ingredients_parsed = [item.get("name") for item in ingredients_list_raw if item.get("name")]
|
||||
|
||||
# Extract recipe categories from API response.
|
||||
# The API provides both recipeCategoryIdList (actual IDs) and recipeCategories
|
||||
# (system name strings). We use the ID list as the authoritative source.
|
||||
categories: list[str] = []
|
||||
category_ids: list[str] = raw.get("recipeCategoryIdList") or []
|
||||
for cat_id in category_ids:
|
||||
if cat_id:
|
||||
categories.append(cat_id)
|
||||
|
||||
return {
|
||||
"id": raw.get("metaId"),
|
||||
"name": raw.get("name"),
|
||||
@@ -91,6 +100,7 @@ def parse_recipe_full(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"can_update": raw.get("rights", {}).get("canUpdate") == "true",
|
||||
"created_at": raw.get("creationDate"),
|
||||
"account_id": raw.get("accountId"),
|
||||
"category_ids": categories,
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +113,8 @@ def build_create_params(
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
category_ids: list[str] | None = None,
|
||||
) -> dict[str, str | list[str]]:
|
||||
"""Build the form parameters for a mprecipeput create call.
|
||||
|
||||
The Family Wall API requires the 'recipe.' prefix for all recipe fields.
|
||||
@@ -124,11 +135,13 @@ def build_create_params(
|
||||
cook_time_minutes: Optional cooking time in minutes.
|
||||
serves: Optional number of servings.
|
||||
url: Optional external URL (e.g. original recipe source).
|
||||
category_ids: Optional list of recipe category IDs
|
||||
(e.g. ``["category/23431854_2"]``).
|
||||
|
||||
Returns:
|
||||
Dict of form parameters ready to send to mprecipeput.
|
||||
"""
|
||||
params: dict[str, str] = {
|
||||
params: dict[str, str | list[str]] = {
|
||||
"recipe.name": name,
|
||||
"recipe.isRecipe": "true",
|
||||
}
|
||||
@@ -146,6 +159,8 @@ def build_create_params(
|
||||
params["recipe.serves"] = str(serves)
|
||||
if url is not None:
|
||||
params["recipe.url"] = url
|
||||
if category_ids is not None:
|
||||
params["recipe.recipeCategoryIdList"] = category_ids
|
||||
return params
|
||||
|
||||
|
||||
@@ -160,7 +175,8 @@ def build_update_params(
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
category_ids: list[str] | None = None,
|
||||
) -> dict[str, str | list[str]]:
|
||||
"""Build the form parameters for a mprecipeput update call.
|
||||
|
||||
Identical to :func:`build_create_params` except that ``recipe.metaId`` is
|
||||
@@ -183,11 +199,13 @@ def build_update_params(
|
||||
cook_time_minutes: New cooking time in minutes (omit to keep existing).
|
||||
serves: New number of servings (omit to keep existing).
|
||||
url: New external URL (omit to keep existing).
|
||||
category_ids: New list of recipe category IDs (omit to keep existing).
|
||||
Pass empty list to remove all categories.
|
||||
|
||||
Returns:
|
||||
Dict of form parameters ready to send to mprecipeput.
|
||||
"""
|
||||
params: dict[str, str] = {
|
||||
params: dict[str, str | list[str]] = {
|
||||
"recipe.metaId": recipe_id,
|
||||
"recipe.name": name if name is not None else current_name,
|
||||
"recipe.isRecipe": "true",
|
||||
@@ -206,4 +224,6 @@ def build_update_params(
|
||||
params["recipe.serves"] = str(serves)
|
||||
if url is not None:
|
||||
params["recipe.url"] = url
|
||||
if category_ids is not None:
|
||||
params["recipe.recipeCategoryIdList"] = category_ids
|
||||
return params
|
||||
|
||||
@@ -1715,6 +1715,62 @@ def _get_raw_recipes() -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: get_recipe_categories
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
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.
|
||||
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.
|
||||
try:
|
||||
data = _authenticated_call("metasync", {"id": "recipeCategory"})
|
||||
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}"
|
||||
|
||||
# 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)
|
||||
|
||||
# If metasync succeeded, extract categories from response.
|
||||
try:
|
||||
items: list[dict[str, Any]] = data["a00"]["r"]["r"]["updatedCreated"]
|
||||
except (KeyError, TypeError):
|
||||
items = []
|
||||
|
||||
if not isinstance(items, list):
|
||||
return json.dumps({"error": "No recipe categories available"}, ensure_ascii=False)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: get_recipes
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1783,6 +1839,7 @@ def create_recipe(
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
category_ids: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Create a new recipe in the Family Wall recipe box.
|
||||
|
||||
@@ -1801,6 +1858,8 @@ def create_recipe(
|
||||
cook_time_minutes: Optional cooking/baking time in minutes.
|
||||
serves: Optional number of servings.
|
||||
url: Optional external URL (e.g. original recipe source).
|
||||
category_ids: Optional list of recipe category IDs
|
||||
(e.g. ``["category/23431854_2"]``). Use get_recipe_categories to find IDs.
|
||||
|
||||
Returns:
|
||||
JSON with the new recipe's full fields on success, or an error message.
|
||||
@@ -1814,6 +1873,7 @@ def create_recipe(
|
||||
cook_time_minutes=cook_time_minutes,
|
||||
serves=serves,
|
||||
url=url,
|
||||
category_ids=category_ids,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -1926,6 +1986,7 @@ def update_recipe(
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
category_ids: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Update an existing recipe in the Family Wall recipe box.
|
||||
|
||||
@@ -1950,6 +2011,8 @@ def update_recipe(
|
||||
existing).
|
||||
serves: New number of servings (omit to keep existing).
|
||||
url: New external URL (omit to keep existing).
|
||||
category_ids: New list of recipe category IDs (omit to keep existing).
|
||||
Pass empty list to remove all categories.
|
||||
|
||||
Returns:
|
||||
JSON with the updated recipe's full fields on success, or an error
|
||||
@@ -1966,11 +2029,13 @@ def update_recipe(
|
||||
cook_time_minutes,
|
||||
serves,
|
||||
url,
|
||||
category_ids,
|
||||
)
|
||||
):
|
||||
return (
|
||||
"Error: At least one of 'name', 'description', 'ingredients', 'instructions',"
|
||||
" 'prep_time_minutes', 'cook_time_minutes', 'serves', or 'url' must be provided."
|
||||
" 'prep_time_minutes', 'cook_time_minutes', 'serves', 'url', or 'category_ids'"
|
||||
" must be provided."
|
||||
)
|
||||
|
||||
# Single session: fetch current recipe (verify + get name fallback) → update.
|
||||
@@ -2021,6 +2086,7 @@ def update_recipe(
|
||||
cook_time_minutes=cook_time_minutes,
|
||||
serves=serves,
|
||||
url=url,
|
||||
category_ids=category_ids,
|
||||
)
|
||||
|
||||
resp = client.call("mprecipeput", params)
|
||||
|
||||
Reference in New Issue
Block a user