diff --git a/CLAUDE.md b/CLAUDE.md index 9afc7a9..b87d1f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.7.5) +### Implementierte Tools (v0.8.0) | Kategorie | Tools | |---|---| @@ -33,7 +33,7 @@ und wird in Claude Desktop eingebunden. | Listen | `create_list`, `update_list`, `delete_list` | | Kategorien | `create_category`, `delete_category` | | Aktivitäten | `like_post` | -| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe` | +| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe`, `get_recipe_categories` | | Kreise | `create_circle`, `update_circle`, `delete_circle`, `add_member_to_circle` | ## Roadmap @@ -50,9 +50,10 @@ und wird in Claude Desktop eingebunden. - v0.7.2: delete_circle ✓ - v0.7.3: update_list (Umbenennen, emoji/color ändern) ✓ - v0.7.4: update_circle (Kreis umbenennen) ✓ -- v0.7.5: Primärkreis-Schutz in update_circle (isFirstFamily-Check) ✓ ← aktuell -- v0.7.6: mpadditemtolist (Zutaten → Einkaufsliste) +- v0.7.5: Primärkreis-Schutz in update_circle (isFirstFamily-Check) ✓ +- v0.8.0: Rezept-Kategorien (get_recipe_categories, create_recipe + category_ids, update_recipe + category_ids) ✓ ← aktuell - v0.8.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) +- v0.8.x: mpadditemtolist (Zutaten → Einkaufsliste) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) diff --git a/README.md b/README.md index 4bdbd1e..3aaf48b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude. -## Features (v0.7.5) +## Features (v0.8.0) ### Read @@ -13,7 +13,8 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `get_categories` -- list categories for a list (locale-filtered; custom categories always included; `custom` flag marks user-created ones) - `get_activities` -- list recent wall activities (author resolved to display name) - `get_recipes` -- list all family recipes (compact summary: id, name, prep/cook time, serves) -- `get_recipe` -- get a single recipe in full detail (ingredients, instructions, ingredients_parsed, etc.) +- `get_recipe` -- get a single recipe in full detail (ingredients, instructions, ingredients_parsed, category_ids, etc.) +- `get_recipe_categories` -- list all available recipe categories (extracts from existing recipes) ### Write (with confirmation prompt) @@ -27,8 +28,8 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `create_category` -- create a custom category for a shopping list (with optional icon) - `delete_category` -- delete a custom category (system categories are protected) - `like_post` -- like a wall post/activity -- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url); use `\n` to separate ingredient lines -- `update_recipe` -- update any field of an existing recipe (partial update — omitted fields unchanged) +- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url, category_ids); use `\n` to separate ingredient lines +- `update_recipe` -- update any field of an existing recipe (partial update — omitted fields unchanged; supports `category_ids` list to change categories) - `delete_recipe` -- permanently delete a recipe (only own recipes) - `create_circle` -- create a new Family Wall circle (group) - `update_circle` -- rename a circle (server capitalises first letter; only name is changeable via API; primary circle is protected) diff --git a/SPEC.md b/SPEC.md index 7c492f4..adf7c70 100644 --- a/SPEC.md +++ b/SPEC.md @@ -659,11 +659,30 @@ Löscht einen Kreis und alle zugehörigen Inhalte (Listen, Tasks, Rezepte, Wall- **Verifiziert am:** 2026-04-16 via FW_DEBUG=1 (family/23447371 erfolgreich gelöscht) +### Recipe Categories API Details + +Recipe categories are managed via `mprecipeput` endpoint. Each recipe can have zero or more +categories assigned via `recipe.recipeCategoryIdList` (sent multiple times for each category). + +**Response structure** (in `mprecipeput` response): + +``` +a00.r.r + .recipeCategoryIdList[] → List of assigned category IDs (e.g. ["category/23431854_2"]) + .recipeCategories[] → List of system names (e.g. ["KIDS_LOVE"]) +``` + +**Category IDs** have format `category/_`. Categories are family-wide. +Premium accounts have additional categories beyond the 5 available in free tier. + +**Limitation:** Currently no API endpoint to list all available categories. Categories are +discovered by examining existing recipes or hardcoding known IDs. + ## Offene Punkte - Unlike-Endpoint (Service Worker blockiert Analyse) - Erinnerungen (reminder) – nur Premium-Account - Wiederholungen (repeat) – nur Premium-Account -- Sortierung von Kategorien via API +- Rezept-Kategorien-Listing-Endpoint (derzeit keine API, müssen aus Rezepten extrahiert werden) - mpadditemtolist (Zutaten aus Rezept → Einkaufsliste) - Einladung bestehender FamilyWall-Nutzer (accinvite nur für neue Accounts) diff --git a/pyproject.toml b/pyproject.toml index fdef5d0..4d793ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.7.5" +version = "0.8.0" description = "MCP server for Family Wall — read your family's lists and tasks via Claude" readme = "README.md" requires-python = ">=3.12" diff --git a/src/mcp_familywall/__init__.py b/src/mcp_familywall/__init__.py index ab55bb1..777f190 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.7.5" +__version__ = "0.8.0" diff --git a/src/mcp_familywall/modules/recipes.py b/src/mcp_familywall/modules/recipes.py index 2aeed10..84a05a8 100644 --- a/src/mcp_familywall/modules/recipes.py +++ b/src/mcp_familywall/modules/recipes.py @@ -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 diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index bda9720..f06019e 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -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)