feat(recipes): implement get_recipes, get_recipe, create_recipe, delete_recipe (v0.6.0)
Adds 4 new MCP tools for the Family Wall recipe box: - get_recipes: list all family recipes via metasync id='recipe' - get_recipe: fetch full recipe detail by id (filters from metasync response) - create_recipe: create a new recipe via mprecipeput (params use 'recipe.' prefix) - delete_recipe: delete a recipe via metadelete (same endpoint as tasks) Verified endpoints and parameter names via FW_DEBUG=1 probe scripts. All 4 tools pass the create → read → get_single → delete integration test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Recipe helper functions for the Family Wall recipe box.
|
||||
|
||||
Verified endpoints (2026-04-16 via FW_DEBUG=1):
|
||||
- Create: POST mprecipeput — params use 'recipe.' prefix (recipe.name, etc.)
|
||||
- Read all: POST metasync with id='recipe' — response at a00.r.r.updatedCreated[]
|
||||
- Delete: POST metadelete with id=<recipe_metaId>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_recipe_summary(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Extract a compact recipe summary from a raw API recipe object.
|
||||
|
||||
Args:
|
||||
raw: Raw recipe dict from the API (metasync or mprecipeput response).
|
||||
|
||||
Returns:
|
||||
Dict with keys: id, name, prep_time_minutes, cook_time_minutes, serves,
|
||||
description, can_delete.
|
||||
"""
|
||||
prep = raw.get("prepTime")
|
||||
cook = raw.get("cookTime")
|
||||
srv = raw.get("serves")
|
||||
return {
|
||||
"id": raw.get("metaId"),
|
||||
"name": raw.get("name"),
|
||||
"description": raw.get("description") or None,
|
||||
"prep_time_minutes": int(prep) if prep else None,
|
||||
"cook_time_minutes": int(cook) if cook else None,
|
||||
"serves": int(srv) if srv else None,
|
||||
"can_delete": raw.get("rights", {}).get("canDelete") == "true",
|
||||
}
|
||||
|
||||
|
||||
def parse_recipe_full(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Extract the full recipe from a raw API recipe object.
|
||||
|
||||
Args:
|
||||
raw: Raw recipe dict from the API (metasync or mprecipeput response).
|
||||
|
||||
Returns:
|
||||
Dict with all recipe fields including ingredients, instructions, etc.
|
||||
"""
|
||||
prep = raw.get("prepTime")
|
||||
cook = raw.get("cookTime")
|
||||
srv = raw.get("serves")
|
||||
|
||||
# ingredientsList is auto-parsed by the server; normalise to a plain list of names.
|
||||
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")]
|
||||
|
||||
return {
|
||||
"id": raw.get("metaId"),
|
||||
"name": raw.get("name"),
|
||||
"description": raw.get("description") or None,
|
||||
"ingredients": raw.get("ingredients") or None,
|
||||
"ingredients_parsed": ingredients_parsed,
|
||||
"instructions": raw.get("instructions") or None,
|
||||
"prep_time_minutes": int(prep) if prep else None,
|
||||
"cook_time_minutes": int(cook) if cook else None,
|
||||
"serves": int(srv) if srv else None,
|
||||
"url": raw.get("url") or None,
|
||||
"is_favorite": raw.get("isFavorite") == "true",
|
||||
"can_delete": raw.get("rights", {}).get("canDelete") == "true",
|
||||
"can_update": raw.get("rights", {}).get("canUpdate") == "true",
|
||||
"created_at": raw.get("creationDate"),
|
||||
"account_id": raw.get("accountId"),
|
||||
}
|
||||
|
||||
|
||||
def build_create_params(
|
||||
name: str,
|
||||
description: str | None = None,
|
||||
ingredients: str | None = None,
|
||||
instructions: str | None = None,
|
||||
prep_time_minutes: int | None = None,
|
||||
cook_time_minutes: int | None = None,
|
||||
serves: int | None = None,
|
||||
url: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Build the form parameters for a mprecipeput create call.
|
||||
|
||||
The Family Wall API requires the 'recipe.' prefix for all recipe fields.
|
||||
isRecipe='true' is always required.
|
||||
|
||||
Args:
|
||||
name: Recipe title (required).
|
||||
description: Optional description.
|
||||
ingredients: Optional free-text ingredients, lines separated by \\n.
|
||||
instructions: Optional free-text cooking instructions, lines separated by \\n.
|
||||
prep_time_minutes: Optional preparation time in minutes.
|
||||
cook_time_minutes: Optional cooking time in minutes.
|
||||
serves: Optional number of servings.
|
||||
url: Optional external URL (e.g. original recipe source).
|
||||
|
||||
Returns:
|
||||
Dict of form parameters ready to send to mprecipeput.
|
||||
"""
|
||||
params: dict[str, str] = {
|
||||
"recipe.name": name,
|
||||
"recipe.isRecipe": "true",
|
||||
}
|
||||
if description is not None:
|
||||
params["recipe.description"] = description
|
||||
if ingredients is not None:
|
||||
params["recipe.ingredients"] = ingredients
|
||||
if instructions is not None:
|
||||
params["recipe.instructions"] = instructions
|
||||
if prep_time_minutes is not None:
|
||||
params["recipe.prepTime"] = str(prep_time_minutes)
|
||||
if cook_time_minutes is not None:
|
||||
params["recipe.cookTime"] = str(cook_time_minutes)
|
||||
if serves is not None:
|
||||
params["recipe.serves"] = str(serves)
|
||||
if url is not None:
|
||||
params["recipe.url"] = url
|
||||
return params
|
||||
Reference in New Issue
Block a user