From ffb8b062c86dff4e02c4854f6eebbae1db6c440e Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 15 Apr 2026 17:04:35 +0200 Subject: [PATCH] feat: get_categories tool, author resolution in get_activities, update CLAUDE.md (v0.4.8) - get_categories(list_id): new tool filtering taskcategorysync by sortingIndexByTaskList, returns {id, name, emoji} ordered by sort index - get_activities: author IDs now resolved to display names (firstName) via famlistfamily members; raw author_id preserved as separate field; member lookup is non-fatal (falls back to raw ID on error) - CLAUDE.md: tools table updated to reflect all implemented tools (v0.4.8) - SPEC.md: full accgetallfamily response structure documented including categories[] on tasks and category parameter investigation findings - Category assignment in create/update task: all tested parameter names ignored by API; still to verify (Service Worker blocks web inspection) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 12 +++--- README.md | 5 ++- SPEC.md | 40 +++++++++++++----- pyproject.toml | 2 +- src/mcp_familywall/server.py | 78 +++++++++++++++++++++++++++++++++++- 5 files changed, 118 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1b87250..51e3863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,19 +23,21 @@ eingebunden. ## Aktueller Stand -### Implementierte Tools (v1.0) +### Implementierte Tools (v0.4.8) | Kategorie | Tools | |---|---| -| Kreise | `get_circles` | +| Kreise | `get_circles`, `get_members` | | Listen | `get_lists` | -| Tasks | `get_tasks` | +| Tasks (Lesen) | `get_tasks`, `get_categories` | +| Wall | `get_activities`, `like_post` | +| Tasks (Schreiben) | `create_task`, `update_task`, `toggle_task`, `delete_task` | ## Roadmap -- v1.0: Lesezugriff (Kreise + Listen + Tasks) ← aktuell -- v2.0: Schreibzugriff (Tasks anlegen, abhaken, löschen) +- v0.x: Erweiterter Lese- + Schreibzugriff ← aktuell +- Offen: Unlike (`like_post(like=False)`), Task-Kategorie-Zuweisung beim Erstellen/Aktualisieren ## Referenzprojekt diff --git a/README.md b/README.md index 9e00dcf..bfc0080 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.7) +## Features (v0.4.8) ### Read @@ -10,7 +10,8 @@ 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 -- `get_activities` -- list recent wall activities +- `get_categories` -- list categories available for a list +- `get_activities` -- list recent wall activities (author resolved to display name) ### Write (with confirmation prompt) diff --git a/SPEC.md b/SPEC.md index 2a82dbf..0ed79f8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -133,18 +133,38 @@ Hinweis: `a03call=tasklistsync` ist **kein gültiger Endpoint** — API antworte **Response-Struktur (verifiziert):** ``` -a00 → famlistfamily-Daten (Kreise) – Nebeneffekt, nicht verwendet -a01.r.r.updatedCreated[] → taskcategorysync (Einkaufskategorien/Abteilungen) - .sortingIndexByTaskList → dict, Keys = Listen-IDs (z.B. "taskList/23431854_29740942") - → Quelle der Listen-IDs (Namen/Zähler noch unbekannt) -a02.r.r.updatedCreated[] → tasksync (Tasks) - .metaId → eindeutige Task-ID - .text → Aufgabentext - .description → optionale Beschreibung - .taskListId → Zugehörigkeit zur Liste (= Listen-ID aus sortingIndexByTaskList) - .complete → "true" / "false" (String, nicht Boolean!) +a00 → famlistfamily-Daten (Kreise inkl. members[]) – Nebeneffekt +a01.r.r.updatedCreated[] → taskcategorysync (Kategorien/Abteilungen pro Liste) + .metaId → Kategorie-ID (Format taskCategory/_) + .name → Kategoriename (sprachabhängig, z.B. "Beverages") + .emoji → Emoji-Symbol der Kategorie + .systemCategoryId → numerische System-ID (sprach-unabhängig) + .taskListType → Listentyp (z.B. "SHOPPING_LIST", "TODO") + .sortingIndexByTaskList → dict: Listen-ID → Sortierposition + Keys = Listen-IDs der zugeordneten Listen + .locale → Sprache des Namens (z.B. "de", "en", "ru") + .hiddenByTaskList → Liste von Listen-IDs, in denen die Kat. versteckt ist +a02.r.r.updatedCreated[] → tasksync (Tasks) + .metaId → eindeutige Task-ID + .taskId → identisch zu metaId (zweiter Alias) + .text → Aufgabentext + .description → optionale Beschreibung + .taskListId → Zugehörigkeit zur Liste + .complete → "true" / "false" (String, nicht Boolean!) + .categories[] → zugewiesene Kategorien des Tasks + .system → "true" wenn System-Kategorie, "false" wenn custom + .name → Kategoriename (z.B. "SYS-CAT-TODOS", "Beverages") + .assignee / .assigneeIds → zugewiesene Mitglieder + .reminder → Erinnerungsdatum (ISO 8601, optional) + .recurrency → Wiederholungsregel (optional) + .sortingIndex → Anzeigereihenfolge ``` +**Kategorie-Zuweisung bei taskcreate2 / taskupdate2:** Parameter-Name noch unbekannt. +Getestete Varianten (`taskCategoryId`, `categoryId`, `category`, `categoryMetaId`) werden +serverseitig ignoriert — die Task bekommt stets die Default-Systemkategorie der Liste. +Service Worker in der Web-App verhindert Inspektion des echten Requests. Noch zu verifizieren. + ## Systembezeichnungen für Listen-Namen Bekannte Systembezeichnungen werden deutsch übersetzt: diff --git a/pyproject.toml b/pyproject.toml index 94dd6ce..b4b13e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.4.7" +version = "0.4.8" 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 fdb3ec2..35c997e 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -331,6 +331,67 @@ def get_tasks(list_id: str, only_open: bool = True): return json.dumps(result, ensure_ascii=False, indent=2) +# --------------------------------------------------------------------------- +# Tool: get_categories +# --------------------------------------------------------------------------- + + +@mcp.tool() +def get_categories(list_id: str) -> str: + """Return the task categories available for a list as JSON. + + Categories are loaded from taskcategorysync (part of accgetallfamily) and + filtered to those assigned to the given list via sortingIndexByTaskList. + Shopping lists have predefined system categories (aisles); TODO lists + typically only carry a single default system category. + + Note: the parameter name for assigning a category when creating or + updating a task is not yet verified via FW_DEBUG=1. See SPEC.md. + + Args: + list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``). + + Returns: + JSON list of {id, name, emoji} objects ordered by sortingIndex. + """ + try: + data = _accgetallfamily() + except RuntimeError as exc: + return f"Error: {exc}" + + try: + raw_cats = data["a01"]["r"]["r"]["updatedCreated"] + if not isinstance(raw_cats, list): + raise TypeError("a01.r.r.updatedCreated is not a list") + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected taskcategorysync response structure", "raw": data.get("a01")}, + ensure_ascii=False, + indent=2, + ) + + # Collect categories assigned to this list, ordered by their sorting index. + matched: list[tuple[int, dict[str, Any]]] = [] + seen: set[str] = set() + for cat in raw_cats: + sorting = cat.get("sortingIndexByTaskList") or {} + if list_id not in sorting: + continue + cat_id: str = cat.get("metaId", "") + if cat_id in seen: + continue + seen.add(cat_id) + sort_val = int(sorting.get(list_id, cat.get("initialSortingIndex", 0)) or 0) + matched.append((sort_val, cat)) + + matched.sort(key=lambda t: t[0]) + result = [ + {"id": cat.get("metaId"), "name": cat.get("name"), "emoji": cat.get("emoji")} + for _, cat in matched + ] + return json.dumps(result, ensure_ascii=False, indent=2) + + # --------------------------------------------------------------------------- # Tool: get_activities # --------------------------------------------------------------------------- @@ -344,6 +405,19 @@ def get_activities(limit: int = 20): except RuntimeError as exc: return f"Error: {exc}" + # Load member data to resolve author IDs to display names. + # Non-fatal: fall back to raw account IDs when member lookup fails. + author_map: dict[str, str] = {} + try: + for circle in _famlistfamily(): + for member in circle.get("members") or []: + acc_id: str = member.get("accountId", "") + display = member.get("firstName") or member.get("name") or acc_id + if acc_id: + author_map[acc_id] = display + except RuntimeError: + pass + try: with FamilyWallClient() as client: client.login(email, password) @@ -375,13 +449,15 @@ def get_activities(limit: int = 20): result = [] for item in raw_activities: + raw_author: str = item.get("accountId", "") result.append( { "id": item.get("metaId"), "type": item.get("refType"), "text": item.get("text"), "date": item.get("creationDate"), - "author": item.get("accountId"), + "author": author_map.get(raw_author, raw_author), + "author_id": raw_author, } )