From 5698196c433230f9ad26d25d8ae8b7b1bc0e10e0 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Thu, 16 Apr 2026 07:43:54 +0200 Subject: [PATCH] feat: create_category + delete_category tools (v0.4.11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified via systematic FW_DEBUG=1 probing: - taskcategoryput: requires 'name'; optional 'emoji' (Unicode or string code) accepted as-is. 'listId' param has no per-list effect — categories are family-wide. - taskcategorydelete: uses 'id' param (not 'metaId'), returns r='true'. Changes: - create_category(list_id, name, icon=None): creates custom category via taskcategoryput; icon maps to 'emoji' API param - delete_category(category_id): safety check via accgetallfamily looks up rights.canDelete='true'; system categories (rights.canDelete=null) are refused with a clear error - get_categories: now exposes 'custom' bool field (rights.canDelete='true') so callers can identify deletable categories - SPEC.md: document taskcategoryput + taskcategorydelete params, responses, error formats, and system-category protection behaviour Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 5 +- README.md | 8 ++- SPEC.md | 63 +++++++++++++++- pyproject.toml | 2 +- src/mcp_familywall/server.py | 135 ++++++++++++++++++++++++++++++++++- 5 files changed, 205 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 17c1fec..bbc5e66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,15 +23,16 @@ eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.4.10) +### Implementierte Tools (v0.4.11) | Kategorie | Tools | |---|---| | Kreise | `get_circles`, `get_members` | | Listen | `get_lists` | -| Tasks (Lesen) | `get_tasks` (inkl. `category_id`), `get_categories` | +| Tasks (Lesen) | `get_tasks` (inkl. `category_id`), `get_categories` (inkl. `custom`-Flag) | | Wall | `get_activities`, `like_post` | | Tasks (Schreiben) | `create_task` (inkl. `category_id`), `update_task` (inkl. `category_id`), `toggle_task`, `delete_task` | +| Kategorien (Schreiben) | `create_category` (inkl. `icon`), `delete_category` (System-Kategorien geschützt) | ## Roadmap diff --git a/README.md b/README.md index 12581e0..0416904 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, and tasks directly from Claude. -## Features (v0.4.10) +## Features (v0.4.11) ### Read @@ -10,7 +10,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `get_members` -- list members of a circle (or all circles) - `get_lists` -- list all task lists (optionally filtered by circle) - `get_tasks` -- list tasks in a specific list (includes `category_id` field) -- `get_categories` -- list categories available for a list (locale-filtered, default: German) +- `get_categories` -- list categories for a list (locale-filtered; `custom` flag marks user-created ones) - `get_activities` -- list recent wall activities (author resolved to display name) ### Write (with confirmation prompt) @@ -19,7 +19,9 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your - `update_task` -- update text, description, and/or category of an existing task - `toggle_task` -- mark a task complete or reopen it - `delete_task` -- permanently delete a task -- `like_post` -- like or unlike a wall post/activity +- `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 ## Requirements diff --git a/SPEC.md b/SPEC.md index 1a718aa..2c88e67 100644 --- a/SPEC.md +++ b/SPEC.md @@ -331,6 +331,65 @@ mit `{"a00": {"un": {"un": {"message": "missing value in: id"}}}}` auf Top-Level → wird vom fw_client fälschlich als Erfolg interpretiert. Daher ist der korrekte Parameter-Name kritisch. +### `taskcategoryput` – Kategorie erstellen +POST https://api.familywall.com/api/taskcategoryput +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (verifiziert via FW_DEBUG=1):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `name` | ja | Kategorie-Name (beliebiger String) | +| `emoji` | nein | Icon: Unicode-Emoji-Zeichen (z.B. `🌿`) oder beliebiger String-Code (z.B. `"FOOD"`) — wird as-is gespeichert | + +Hinweise: +- Die neue Kategorie wird **allen** Listen der Familie zugeordnet — es gibt keine per-Liste-Einschränkung. +- Benutzerdefinierte Kategorien haben `systemCategoryId=null` und `rights.canDelete='true'`. +- System-Kategorien haben `rights.canDelete=null` — API erlaubt Löschen, aber `delete_category` Tool verweigert es. + +**Response-Struktur (verifiziert):** +``` +a00.r.r → vollständiges Kategorie-Objekt + .metaId → neue Kategorie-ID (z.B. "taskCategory/23431854_4956637") + .name → Kategorie-Name + .taskListType → "SHOPPING_LIST" (automatisch gesetzt) + .familyId → Familien-ID + .accountId → Account-ID des Erstellers + .rights.canDelete → "true" (custom Kategorien) + .rights.canUpdate → "true" (custom Kategorien) + .emoji → gespeicherter Icon-Wert (falls übergeben) +``` + +**Fehlerverhalten:** Ohne `name`-Parameter: +```json +{"a00": {"un": {"un": {"FiZClassId": "502", "message": "cat without a name ..."}}}} +``` + +### `taskcategorydelete` – Kategorie löschen +POST https://api.familywall.com/api/taskcategorydelete +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (verifiziert via FW_DEBUG=1):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `id` | ja | Kategorie-MetaId aus `get_categories` (**WICHTIG: `id`, nicht `metaId`!**) | + +**Achtung:** Falscher Parameter-Name `metaId` führt zu: +```json +{"a00": {"un": {"un": {"FiZClassId": "502", "message": "In request, missing value in : id"}}}} +``` + +**Response-Struktur (verifiziert):** +``` +a00.r.r → "true" (String) +``` + +**Wichtig – System-Kategorien:** Die API erlaubt technisch das Löschen von System-Kategorien +(`taskCategory/_200` etc.), entfernt sie aber nur aus der Familie — nicht global. +Das `delete_category`-MCP-Tool verweigert dies (Schutz via `rights.canDelete`-Check). +Erkennung: custom Kategorien haben `rights.canDelete='true'`; System-Kategorien haben `rights.canDelete=null`. + ### `wallmood` – Wall-Post liken POST https://api.familywall.com/api/wallmood Content-Type: application/x-www-form-urlencoded @@ -426,4 +485,6 @@ AND `moodStarShortcut: false` AND `moodMap: {}`. - ~~`wallmood`: Parameter-Name `wallId`~~ → **`wall_message_id`** (verifiziert via API-Fehlermeldung) - ~~`wallmood`: `moodType`-Werte, Toggle vs. explizit, Response-Struktur~~ → verifiziert: idempotentes SET mit `"STAR"`, kein Toggle (siehe oben) - `wallmood` Unlike: Mechanismus unbekannt — Service Worker verhindert Browser-Inspektion; alle getesteten Ansätze fehlgeschlagen (siehe oben) -- ~~`taskcreate2` / `taskupdate2`: Kategorie-Paramter-Name~~ → **`taskCategoryId`**, Wert = vollständige metaId (verifiziert) \ No newline at end of file +- ~~`taskcreate2` / `taskupdate2`: Kategorie-Paramter-Name~~ → **`taskCategoryId`**, Wert = vollständige metaId (verifiziert) +- ~~`taskcategoryput`: Body-Parameter, Response-Struktur~~ → `name` (Pflicht), `emoji` (optional), Response = neues Kategorie-Objekt (verifiziert) +- ~~`taskcategorydelete`: Body-Parameter~~ → **`id`** (nicht `metaId`!), Response = `"true"` (verifiziert) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e04cacc..0c6ceba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.4.10" +version = "0.4.11" 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/server.py b/src/mcp_familywall/server.py index 1b6b82e..3aec20f 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -402,12 +402,145 @@ def get_categories(list_id: str, locale: str = "de") -> str: matched.sort(key=lambda t: t[0]) result = [ - {"id": cat.get("metaId"), "name": cat.get("name"), "emoji": cat.get("emoji")} + { + "id": cat.get("metaId"), + "name": cat.get("name"), + "emoji": cat.get("emoji"), + # custom=True means the category was created by the family and can be + # deleted with delete_category. System categories have no canDelete right. + "custom": cat.get("rights", {}).get("canDelete") == "true", + } for _, cat in matched ] return json.dumps(result, ensure_ascii=False, indent=2) +# --------------------------------------------------------------------------- +# Tool: create_category +# --------------------------------------------------------------------------- + + +@mcp.tool() +def create_category(list_id: str, name: str, icon: str | None = None) -> str: + """Create a new custom category for a shopping list. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Custom categories appear alongside system categories when assigning + categories to tasks via ``create_task`` or ``update_task``. They can + later be removed with ``delete_category``. + + Note: although ``list_id`` is accepted for context, the Family Wall API + assigns new categories to all lists in the family — there is no + per-list restriction. + + Args: + list_id: Target list ID from get_lists (e.g. ``taskList/123_456``). + Only SHOPPING_LIST lists have categories; the parameter is + accepted for user context but does not restrict category scope. + name: Display name for the new category (e.g. ``"Bio-Produkte"``). + icon: Optional icon for the category. Pass a Unicode emoji character + (e.g. ``"🌿"``) or any short string identifier. When omitted the + category has no icon. + + Returns: + JSON with the new category's ``id`` and ``name`` on success, or an + error message. + """ + params: dict[str, Any] = {"name": name} + if icon: + params["emoji"] = icon + + try: + data = _authenticated_call("taskcategoryput", params) + except RuntimeError as exc: + return f"Error: {exc}" + + try: + cat_obj = data["a00"]["r"]["r"] + meta_id: str = cat_obj["metaId"] + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected taskcategoryput response structure", "raw": data}, + ensure_ascii=False, + indent=2, + ) + + return json.dumps( + {"created": True, "id": meta_id, "name": cat_obj.get("name", name)}, + ensure_ascii=False, + indent=2, + ) + + +# --------------------------------------------------------------------------- +# Tool: delete_category +# --------------------------------------------------------------------------- + + +@mcp.tool() +def delete_category(category_id: str) -> str: + """Permanently delete a custom category. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Only custom (user-created) categories can be deleted. System categories + supplied by Family Wall (identified by ``custom=false`` in + ``get_categories`` output) are protected and this tool will refuse to + delete them. + + Args: + category_id: Category metaId from get_categories + (e.g. ``taskCategory/23431854_4956637``). Must be a custom + category (``custom=true`` in get_categories output). + + Returns: + JSON success indicator or an error message. + """ + # Safety check: look up the category and verify it is custom (canDelete=true). + # This prevents accidental deletion of shared system categories. + try: + data = _accgetallfamily() + except RuntimeError as exc: + return f"Error: {exc}" + + try: + raw_cats: list[dict[str, Any]] = data["a01"]["r"]["r"]["updatedCreated"] + except (KeyError, TypeError): + raw_cats = [] + + cat_obj: dict[str, Any] | None = next( + (c for c in raw_cats if c.get("metaId") == category_id), None + ) + + if cat_obj is None: + return f"Error: Category '{category_id}' not found." + + can_delete: str | None = cat_obj.get("rights", {}).get("canDelete") + if can_delete != "true": + return json.dumps( + { + "error": "System categories cannot be deleted.", + "id": category_id, + "name": cat_obj.get("name"), + "hint": "Only custom categories (custom=true in get_categories) can be deleted.", + }, + ensure_ascii=False, + indent=2, + ) + + try: + _authenticated_call("taskcategorydelete", {"id": category_id}) + except RuntimeError as exc: + return f"Error: {exc}" + + return json.dumps( + {"deleted": True, "id": category_id, "name": cat_obj.get("name")}, + ensure_ascii=False, + indent=2, + ) + + # --------------------------------------------------------------------------- # Tool: get_activities # ---------------------------------------------------------------------------