fix: get_categories locale filter and list-type filter (v0.4.9)

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 21:22:00 +02:00
parent ffb8b062c8
commit 9bc6a54783
4 changed files with 42 additions and 20 deletions
+29 -14
View File
@@ -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])