diff --git a/CLAUDE.md b/CLAUDE.md index eacc962..e38f9c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,19 +24,22 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.4.x) +### Implementierte Tools (v0.5.x) | 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` | | Kategorien | `create_category`, `delete_category` | | Aktivitäten | `like_post` | ## Roadmap -- v0.4.x: Kategorie-Management, Task-Felder (due_date, assignee, list_id) ← aktuell -- v0.5.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) +- v0.4.x: Kategorie-Management, Task-Felder (due_date, assignee, list_id) ✓ +- v0.5.x: Listen-Management (create_list, delete_list) ← aktuell +- v0.5.1: update_list (Umbenennen), Sharing-Verwaltung +- v0.6.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) @@ -121,6 +124,8 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level: | `wallmood` | `wall_message_id`, `moodType` | `"STAR"` für Like | | `taskcategoryput` | `name`, `emoji` | – | | `taskcategorydelete` | `id` | metaId der Kategorie | +| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji` | `taskListType`: `"SHOPPING_LIST"`/`"TODOS"` | +| `taskdeletelist` | `id` | metaId der Liste | ### Self-Like-Restriction Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts. diff --git a/SPEC.md b/SPEC.md index ed2b360..3023aba 100644 --- a/SPEC.md +++ b/SPEC.md @@ -280,6 +280,51 @@ POST https://api.familywall.com/api/taskcategorydelete gelöscht werden, sind dann aber dauerhaft weg und nicht wiederherstellbar. MCP-Server schützt dagegen durch Check auf `rights.canDelete`. +### `taskcreatelist` – Liste erstellen +POST https://api.familywall.com/api/taskcreatelist + +**Body-Parameter:** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `name` | ja | Listen-Name (max 200 Zeichen) | +| `taskListType` | ja | `"SHOPPING_LIST"` oder `"TODOS"` | +| `sharedToAll` | nein | `"true"` / `"false"` (default: `"true"`) | +| `color` | nein | Hex-Farbwert z.B. `"#4784EC"` | +| `emoji` | nein | Unicode-Emoji z.B. `"🛒"` | + +**Response:** +``` +a00.r.r → vollständiges Listen-Objekt + .metaId → neue Listen-ID (z.B. "taskList/23431854_29759623") + .name → Listen-Name + .taskListType → SHOPPING_LIST oder TODOS + .sharedToAll → "true" / "false" + .rights.canDelete → "true" (user-created lists) +``` + +**Verifiziert am:** 2026-04-16 via FW_DEBUG=1 + +### `taskdeletelist` – Liste löschen +POST https://api.familywall.com/api/taskdeletelist + +**Body-Parameter:** + +| Parameter | Wert | +|---|---| +| `id` | Listen-metaId ⚠️ nicht `listId` oder `taskListId`! | + +**Response:** +``` +a00.r.r → "true" (String) +``` + +**Hinweis:** Löscht die Liste und alle enthaltenen Tasks unwiderruflich. +System-Listen (`rights.canDelete` fehlt oder `null`) sind nicht löschbar. +MCP-Server prüft dies vor dem Löschen via `taskgettasklists`. + +**Verifiziert am:** 2026-04-16 via FW_DEBUG=1 + ### `taskgettasklists` – Listen abrufen (alternativ) POST https://api.familywall.com/api/taskgettasklists diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index ee19f05..a5f084f 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -869,6 +869,157 @@ def delete_task(task_id: str) -> str: return json.dumps({"deleted": True, "id": task_id}, ensure_ascii=False, indent=2) +# --------------------------------------------------------------------------- +# Tool: create_list +# --------------------------------------------------------------------------- + + +@mcp.tool() +def create_list( + name: str, + list_type: str, + shared_to_all: bool = True, + color: str | None = None, + emoji: str | None = None, +) -> str: + """Create a new task list in Family Wall. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Args: + name: Display name for the new list (max 200 characters). + list_type: List type — either ``"SHOPPING_LIST"`` or ``"TODOS"``. + shared_to_all: When ``True`` (default) the list is shared with all + circle members. When ``False`` it is private to the creator. + color: Optional background colour as a hex string (e.g. ``"#4784EC"``). + emoji: Optional Unicode emoji to use as the list icon (e.g. ``"🛒"``). + + Returns: + JSON with the new list object on success, or an error message. + """ + if list_type not in ("SHOPPING_LIST", "TODOS"): + return "Error: list_type must be 'SHOPPING_LIST' or 'TODOS'." + if len(name) > 200: + return "Error: name must not exceed 200 characters." + + params: dict[str, Any] = { + "name": name, + "taskListType": list_type, + "sharedToAll": "true" if shared_to_all else "false", + } + if color: + params["color"] = color + if emoji: + params["emoji"] = emoji + + try: + data = _authenticated_call("taskcreatelist", params) + except RuntimeError as exc: + return f"Error: {exc}" + + try: + list_obj = data["a00"]["r"]["r"] + meta_id: str = list_obj["metaId"] + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected taskcreatelist response structure", "raw": data}, + ensure_ascii=False, + indent=2, + ) + + return json.dumps( + { + "created": True, + "id": meta_id, + "name": list_obj.get("name", name), + "type": list_obj.get("taskListType"), + "shared_to_all": list_obj.get("sharedToAll") == "true", + }, + ensure_ascii=False, + indent=2, + ) + + +# --------------------------------------------------------------------------- +# Tool: delete_list +# --------------------------------------------------------------------------- + + +@mcp.tool() +def delete_list(list_id: str) -> str: + """Permanently delete a task list and all its tasks. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + This action cannot be undone. All tasks inside the list are also deleted. + Only lists with ``rights.canDelete="true"`` (user-created lists) can be + deleted. System lists are protected and this tool will refuse to delete them. + + Args: + list_id: List metaId from get_lists + (e.g. ``"taskList/23431854_29759623"``). Must be a custom list + (``rights.canDelete="true"``). + + Returns: + JSON success indicator or an error message. + """ + # Verify + delete in a single session to minimise round-trips. + try: + email, password = get_credentials() + except RuntimeError as exc: + return f"Error: {exc}" + + list_obj: dict[str, Any] | None = None + try: + with FamilyWallClient() as client: + client.login(email, password) + + # Fetch lists and verify the target can be deleted. + raw = client.call("taskgettasklists", {}) + 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_delete: str | None = (list_obj.get("rights") or {}).get("canDelete") + if can_delete != "true": + client.logout() + return json.dumps( + { + "error": "System lists cannot be deleted.", + "id": list_id, + "name": list_obj.get("name"), + "hint": "Only user-created lists (rights.canDelete=true) can be deleted.", + }, + ensure_ascii=False, + indent=2, + ) + + # Verified — delete in the same session. + # taskdeletelist uses 'id' (same pattern as metadelete / taskcategorydelete). + client.call("taskdeletelist", {"id": list_id}) + client.logout() + except FamilyWallError as exc: + return f"Error: Family Wall API error: {exc}" + except Exception as exc: + return f"Error: Connection error: {exc}" + + return json.dumps( + {"deleted": True, "id": list_id, "name": list_obj.get("name")}, + ensure_ascii=False, + indent=2, + ) + + # --------------------------------------------------------------------------- # Tool: like_post # ---------------------------------------------------------------------------