fix(recipes): normalize newlines + add update_recipe (v0.6.1)
Bug fix: literal backslash-n sequences in ingredients/instructions are now converted to real newline characters before sending to the API, so the server correctly splits ingredient lines into ingredientsList[]. New tool: update_recipe — partial update via mprecipeput with recipe.metaId; fetches current recipe in the same session to verify can_update and supply name fallback. Verified: recipe.metaId triggers update (not create). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||
from mcp_familywall.modules.lists import translate_name
|
||||
from mcp_familywall.modules.recipes import (
|
||||
build_create_params,
|
||||
build_update_params,
|
||||
parse_recipe_full,
|
||||
parse_recipe_summary,
|
||||
)
|
||||
@@ -1337,6 +1338,142 @@ def delete_recipe(recipe_id: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: update_recipe
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_recipe(
|
||||
recipe_id: str,
|
||||
name: str | None = None,
|
||||
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,
|
||||
) -> str:
|
||||
"""Update an existing recipe in the Family Wall recipe box.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
|
||||
At least one field besides *recipe_id* must be provided. Fields that are
|
||||
omitted are left unchanged on the server.
|
||||
|
||||
Args:
|
||||
recipe_id: Recipe metaId from get_recipes
|
||||
(e.g. ``"recipe/23431854_10968866"``).
|
||||
name: New recipe title (omit to keep existing).
|
||||
description: New description (omit to keep existing).
|
||||
ingredients: New ingredient list as free text (omit to keep existing).
|
||||
Use newlines (``\\n``) to separate items.
|
||||
Example: ``"200g Mehl\\n3 Eier\\n100ml Milch"``
|
||||
instructions: New cooking instructions as free text (omit to keep
|
||||
existing). Use newlines (``\\n``) to separate steps.
|
||||
prep_time_minutes: New preparation time in minutes (omit to keep
|
||||
existing).
|
||||
cook_time_minutes: New cooking/baking time in minutes (omit to keep
|
||||
existing).
|
||||
serves: New number of servings (omit to keep existing).
|
||||
url: New external URL (omit to keep existing).
|
||||
|
||||
Returns:
|
||||
JSON with the updated recipe's full fields on success, or an error
|
||||
message.
|
||||
"""
|
||||
if all(
|
||||
v is None
|
||||
for v in (
|
||||
name,
|
||||
description,
|
||||
ingredients,
|
||||
instructions,
|
||||
prep_time_minutes,
|
||||
cook_time_minutes,
|
||||
serves,
|
||||
url,
|
||||
)
|
||||
):
|
||||
return (
|
||||
"Error: At least one of 'name', 'description', 'ingredients', 'instructions',"
|
||||
" 'prep_time_minutes', 'cook_time_minutes', 'serves', or 'url' must be provided."
|
||||
)
|
||||
|
||||
# Single session: fetch current recipe (verify + get name fallback) → update.
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
except RuntimeError as exc:
|
||||
return f"Error: {exc}"
|
||||
|
||||
try:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
|
||||
# Fetch all recipes to verify the target exists and is updatable.
|
||||
raw_data = client.call("metasync", {"id": "recipe"})
|
||||
try:
|
||||
items: list[dict[str, Any]] = raw_data["a00"]["r"]["r"]["updatedCreated"]
|
||||
except (KeyError, TypeError):
|
||||
items = []
|
||||
|
||||
current = next((r for r in items if r.get("metaId") == recipe_id), None)
|
||||
if current is None:
|
||||
client.logout()
|
||||
return f"Error: Recipe '{recipe_id}' not found."
|
||||
|
||||
can_update: str | None = (current.get("rights") or {}).get("canUpdate")
|
||||
if can_update != "true":
|
||||
client.logout()
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "Recipe cannot be updated.",
|
||||
"id": recipe_id,
|
||||
"name": current.get("name"),
|
||||
"hint": "Only recipes you created (can_update=true) can be updated.",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# Build params — current name is used as fallback when caller omits name.
|
||||
params = build_update_params(
|
||||
recipe_id=recipe_id,
|
||||
current_name=current.get("name", ""),
|
||||
name=name,
|
||||
description=description,
|
||||
ingredients=ingredients,
|
||||
instructions=instructions,
|
||||
prep_time_minutes=prep_time_minutes,
|
||||
cook_time_minutes=cook_time_minutes,
|
||||
serves=serves,
|
||||
url=url,
|
||||
)
|
||||
|
||||
resp = client.call("mprecipeput", params)
|
||||
client.logout()
|
||||
except FamilyWallError as exc:
|
||||
return f"Error: Family Wall API error: {exc}"
|
||||
except Exception as exc:
|
||||
return f"Error: Connection error: {exc}"
|
||||
|
||||
try:
|
||||
recipe_obj = resp["a00"]["r"]["r"]
|
||||
if not isinstance(recipe_obj, dict) or "metaId" not in recipe_obj:
|
||||
raise TypeError("unexpected shape")
|
||||
except (KeyError, TypeError):
|
||||
return json.dumps(
|
||||
{"warning": "Unexpected mprecipeput response structure", "raw": resp},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
result = parse_recipe_full(recipe_obj)
|
||||
result["updated"] = True
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Factory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user