From 9bc6a54783625f3879e28dddf878ba16333a7ad3 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 15 Apr 2026 21:22:00 +0200 Subject: [PATCH] fix: get_categories locale filter and list-type filter (v0.4.9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: all 171 category locale variants were returned; now filtered by locale parameter (default "de") → 13 German categories for shopping lists. Bug 2: TODO lists returned 171 shopping categories because sortingIndexByTaskList contains ALL list IDs regardless of type. Fix: look up list's taskListType via taskgettasklists, then match against category's taskListType. TODO lists (type=TODOS) return empty list since all API categories are SHOPPING_LIST type. sortingIndexByTaskList is explicitly documented as unreliable for type filtering in both code comments and SPEC.md. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 ++-- SPEC.md | 13 ++++++++--- pyproject.toml | 2 +- src/mcp_familywall/server.py | 43 ++++++++++++++++++++++++------------ 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index bfc0080..712b9f2 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.8) +## Features (v0.4.9) ### 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 -- `get_categories` -- list categories available for a list +- `get_categories` -- list categories available for a list (locale-filtered, default: German) - `get_activities` -- list recent wall activities (author resolved to display name) ### Write (with confirmation prompt) diff --git a/SPEC.md b/SPEC.md index 0ed79f8..4d16980 100644 --- a/SPEC.md +++ b/SPEC.md @@ -139,10 +139,17 @@ a01.r.r.updatedCreated[] → taskcategorysync (Kategorien/Abteilung .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") + .taskListType → Listentyp der Kategorie (z.B. "SHOPPING_LIST") + **Wichtig:** Alle 171 Kategorien sind SHOPPING_LIST — + es gibt keine TODO-Kategorien in der API .sortingIndexByTaskList → dict: Listen-ID → Sortierposition - Keys = Listen-IDs der zugeordneten Listen - .locale → Sprache des Namens (z.B. "de", "en", "ru") + **Achtung:** enthält ALLE Listen-IDs unabhängig vom Typ + → NICHT für Typ-Filterung verwenden! + Stattdessen: taskListType der Kategorie mit + taskListType der Liste (aus taskgettasklists) vergleichen + .locale → Sprache des Namens (z.B. "de", "en", "ru", "fr", "es", + "it", "nl", "pt", "sv", "ko", "ja") + Jede Sprache = eigener Eintrag mit eigenem metaId/systemCategoryId .hiddenByTaskList → Liste von Listen-IDs, in denen die Kat. versteckt ist a02.r.r.updatedCreated[] → tasksync (Tasks) .metaId → eindeutige Task-ID diff --git a/pyproject.toml b/pyproject.toml index b4b13e8..0428bab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.4.8" +version = "0.4.9" 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 35c997e..e6ae6f1 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -337,23 +337,40 @@ def get_tasks(list_id: str, only_open: bool = True): @mcp.tool() -def get_categories(list_id: str) -> str: +def get_categories(list_id: str, locale: str = "de") -> 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. + Only shopping lists (taskListType=SHOPPING_LIST) have categories. TODO + lists return an empty list. Categories are filtered by locale so only + the language-appropriate names are returned (default: German). 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``). + locale: BCP-47 language code for category names (default ``"de"``). + Supported values seen in API: de, en, fr, es, it, nl, pt, sv, + ru, ko, ja. Returns: - JSON list of {id, name, emoji} objects ordered by sortingIndex. + JSON list of {id, name, emoji} objects ordered by sortingIndex, + or an empty list when the list type has no categories. """ + # Resolve the list's taskListType from taskgettasklists so we can match + # only the categories that belong to the same list type. Non-fatal: if + # the lookup fails we skip the type filter and return all locale matches. + list_type: str | None = None + try: + list_data = _authenticated_call("taskgettasklists", {}) + raw_lists = list_data.get("a00", {}).get("r", {}).get("r", []) or [] + for lst in raw_lists: + if lst.get("metaId") == list_id: + list_type = lst.get("taskListType") + break + except RuntimeError: + pass + try: data = _accgetallfamily() except RuntimeError as exc: @@ -370,18 +387,16 @@ def get_categories(list_id: str) -> str: indent=2, ) - # Collect categories assigned to this list, ordered by their sorting index. + # Filter by locale (exact match) and taskListType (when known). + # sortingIndexByTaskList is NOT used for filtering: all categories are + # assigned to all lists regardless of type, making it an unreliable signal. 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: + if cat.get("locale") != locale: continue - cat_id: str = cat.get("metaId", "") - if cat_id in seen: + if list_type is not None and cat.get("taskListType") != list_type: continue - seen.add(cat_id) - sort_val = int(sorting.get(list_id, cat.get("initialSortingIndex", 0)) or 0) + sort_val = int(cat.get("initialSortingIndex") or 0) matched.append((sort_val, cat)) matched.sort(key=lambda t: t[0])