From d144a77662bbcbcc6cce9abf9aa85e795e401416 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Thu, 16 Apr 2026 20:12:28 +0200 Subject: [PATCH] feat(lists): add update_list tool (v0.7.3) Implements taskupdatelist endpoint (verified via FW_DEBUG=1): - Parameter 'metaId' (not 'id') identifies the list - Partial update: only provided fields (name/color/emoji) are changed - Reads rights.canUpdate before calling the endpoint (single session) - System lists (canUpdate != 'true') are rejected with a clear error - Scope derived from list metaId for secondary-circle support Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 11 +-- README.md | 3 +- SPEC.md | 33 +++++++++ pyproject.toml | 2 +- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/server.py | 126 +++++++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index de0214e..c1bdf89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,13 +24,13 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.7.2) +### Implementierte Tools (v0.7.3) | Kategorie | Tools | |---|---| | Lesen | `get_circles`, `get_members`, `get_lists`, `get_tasks`, `get_categories`, `get_activities` | | Tasks | `create_task`, `update_task`, `toggle_task`, `delete_task` | -| Listen | `create_list`, `delete_list` | +| 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` | @@ -47,9 +47,9 @@ und wird in Claude Desktop eingebunden. - v0.6.1: update_recipe + Bugfix Zeilenumbrüche in create_recipe ✓ - v0.7.0: create_circle + add_member_to_circle ✓ - v0.7.1: get_lists scope fix + create_list circle_id + delete_list scope ✓ -- v0.7.2: delete_circle ✓ ← aktuell -- v0.7.3: mpadditemtolist (Zutaten → Einkaufsliste) -- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung +- v0.7.2: delete_circle ✓ +- v0.7.3: update_list (Umbenennen, emoji/color ändern) ✓ ← aktuell +- v0.7.4: mpadditemtolist (Zutaten → Einkaufsliste) - v0.8.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) @@ -141,6 +141,7 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level: | `taskcategorydelete` | `id` | metaId der Kategorie | | `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji`, `scope` | `scope`: Kreis-metaId für nicht-primäre Kreise | | `taskgettasklists` | `scope` | Kreis-metaId; ohne scope → primärer Kreis | +| `taskupdatelist` | `metaId`, `name`, `color`, `emoji` | `metaId` ⚠️ nicht `id`!; Partial Update | | `taskdeletelist` | `id`, `scope` | `scope`: Kreis-metaId für sekundäre Kreise | | `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! | | `mprecipeput` (Update) | zusätzlich `recipe.metaId` | Vorhandene ID → Update statt Create | diff --git a/README.md b/README.md index 1b716cd..64f2a0f 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.2) +## Features (v0.7.3) ### Read @@ -22,6 +22,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `toggle_task` -- mark a task complete or reopen it - `delete_task` -- permanently delete a task - `create_list` -- create a new task list (SHOPPING_LIST or TODOS; optional `emoji`, `color`, and `circle_id` to target a specific circle) +- `update_list` -- rename a list or change its emoji/color (partial update — omitted fields unchanged; system lists are protected) - `delete_list` -- permanently delete a list and all its tasks (system lists are protected) - `create_category` -- create a custom category for a shopping list (with optional icon) - `delete_category` -- delete a custom category (system categories are protected) diff --git a/SPEC.md b/SPEC.md index 166c242..33536dd 100644 --- a/SPEC.md +++ b/SPEC.md @@ -315,6 +315,39 @@ a00.r.r → vollständiges Listen-Objekt **Verifiziert am:** 2026-04-16 via FW_DEBUG=1 +### `taskupdatelist` – Liste aktualisieren +POST https://api.familywall.com/api/taskupdatelist + +**Body-Parameter:** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `metaId` | ja | Listen-metaId ⚠️ nicht `id`! | +| `name` | nein | Neuer Listen-Name | +| `color` | nein | Hex-Farbwert z.B. `"#E53935"` | +| `emoji` | nein | Unicode-Emoji z.B. `"🧪"` | + +**Hinweis:** Nur übergebene Felder werden geändert (Partial Update). +Felder die nicht mitgeschickt werden bleiben auf dem Server unverändert. +System-Listen (`rights.canUpdate` fehlt oder `!= "true"`) können nicht geändert werden. +MCP-Server prüft `rights.canUpdate` vor dem Update via `taskgettasklists`. + +**Response:** +``` +a00.r.r → vollständiges Listen-Objekt (analog taskcreatelist) + .metaId → Listen-ID + .name → (aktualisierter) Listen-Name + .taskListType → SHOPPING_LIST oder TODOS + .emoji → (aktualisiertes) Unicode-Emoji + .color → (aktualisierter) Hex-Farbwert + .familyId → Kreis-metaId + .rights.canUpdate → "true" für bearbeitbare Listen + .rights.canDelete → "true" für löschbare Listen + .lastAction → "UPDATED" +``` + +**Verifiziert am:** 2026-04-16 via FW_DEBUG=1 + ### `taskdeletelist` – Liste löschen POST https://api.familywall.com/api/taskdeletelist diff --git a/pyproject.toml b/pyproject.toml index 40bdc25..c97658c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.7.2" +version = "0.7.3" 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 bc8c296..4910b9e 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.7.2" +__version__ = "0.7.3" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index 924d4f4..b09d24b 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -1140,6 +1140,132 @@ def delete_list(list_id: str) -> str: ) +# --------------------------------------------------------------------------- +# Tool: update_list +# --------------------------------------------------------------------------- + + +@mcp.tool() +def update_list( + list_id: str, + name: str | None = None, + color: str | None = None, + emoji: str | None = None, +) -> str: + """Update a task list's name, color, or emoji. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Performs a partial update — only the fields you provide are changed. + The current values for any omitted fields are preserved on the server + (the server keeps them; no need to fetch and re-send them). + + Only user-created lists with ``rights.canUpdate="true"`` can be updated. + System lists are protected and this tool will refuse to update them. + + Args: + list_id: List metaId from get_lists + (e.g. ``"taskList/23431854_29759623"``). + name: New display name (omit to keep existing). + color: New background colour as a hex string (e.g. ``"#E53935"``). + Omit to keep existing. + emoji: New Unicode emoji icon (e.g. ``"🧪"``). + Omit to keep existing. + + Returns: + JSON with the updated list object on success, or an error message. + """ + if name is None and color is None and emoji is None: + return "Error: At least one of 'name', 'color', or 'emoji' must be provided." + + try: + email, password = get_credentials() + except RuntimeError as exc: + return f"Error: {exc}" + + # Derive the owning circle from the list metaId (same as delete_list). + circle_scope = _circle_id_from_list_id(list_id) + + list_obj: dict[str, Any] | None = None + try: + with FamilyWallClient() as client: + client.login(email, password) + + # Fetch list to verify rights.canUpdate before updating. + get_params: dict[str, Any] = {} + if circle_scope: + get_params["scope"] = circle_scope + raw = client.call("taskgettasklists", get_params) + try: + raw_lists: list[dict[str, Any]] = raw["a00"]["r"]["r"] + if not isinstance(raw_lists, list): + raw_lists = [] + except (KeyError, TypeError): + raw_lists = [] + + list_obj = next((lst for lst in raw_lists if lst.get("metaId") == list_id), None) + if list_obj is None: + client.logout() + return f"Error: List '{list_id}' not found." + + can_update: str | None = (list_obj.get("rights") or {}).get("canUpdate") + if can_update != "true": + client.logout() + return json.dumps( + { + "error": "System lists cannot be updated.", + "id": list_id, + "name": list_obj.get("name"), + "hint": "Only user-created lists (rights.canUpdate=true) can be updated.", + }, + ensure_ascii=False, + indent=2, + ) + + # Build update params — only include provided fields. + # Verified: taskupdatelist uses 'metaId' (not 'id') as the list identifier. + upd_params: dict[str, Any] = {"metaId": list_id} + if name is not None: + upd_params["name"] = name + if color is not None: + upd_params["color"] = color + if emoji is not None: + upd_params["emoji"] = emoji + + resp = client.call("taskupdatelist", upd_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: + updated_obj: dict[str, Any] = resp["a00"]["r"]["r"] + if not isinstance(updated_obj, dict) or "metaId" not in updated_obj: + raise TypeError("unexpected shape") + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected taskupdatelist response structure", "raw": resp}, + ensure_ascii=False, + indent=2, + ) + + raw_emoji: str = updated_obj.get("emoji", "") + return json.dumps( + { + "updated": True, + "id": updated_obj.get("metaId"), + "name": translate_name(updated_obj.get("name", "")), + "type": updated_obj.get("taskListType"), + "emoji": raw_emoji if raw_emoji else None, + "color": updated_obj.get("color") or None, + "circle_id": updated_obj.get("familyId"), + }, + ensure_ascii=False, + indent=2, + ) + + # --------------------------------------------------------------------------- # Tool: create_circle # ---------------------------------------------------------------------------