diff --git a/SPEC.md b/SPEC.md index 73a1b3c..cc4d0eb 100644 --- a/SPEC.md +++ b/SPEC.md @@ -97,26 +97,23 @@ Content-Type: application/x-www-form-urlencoded |---|---| | `a01call` | `"taskcategorysync"` | | `a02call` | `"tasksync"` | -| `a03call` | `"tasklistsync"` | -Hinweis: `partnerScope`, `a03id`, `withStateBean` werden weggelassen. +Hinweis: `a03call=tasklistsync` ist **kein gültiger Endpoint** — API antwortet mit +"The call tasklistsync is not registered". Nicht verwenden. +`partnerScope`, `a03id`, `withStateBean` werden weggelassen. **Response-Struktur (verifiziert):** ``` -a00 → famlistfamily-Daten (Kreise) – Nebeneffekt, nicht verwendet -a01.r.r[] → taskcategorysync (Einkaufskategorien/Abteilungen) -a02.r.r.updatedCreated[] → tasksync (Tasks) - .metaId → eindeutige Task-ID - .text → Aufgabentext - .description → optionale Beschreibung - .taskListId → Zugehörigkeit zur Liste (= metaId der Liste) - .complete → "true" / "false" (String, nicht Boolean!) -a03.r.r.updatedCreated[] → tasklistsync (Listen) - .metaId → eindeutige Listen-ID - .name → Name (ggf. Systembezeichnung, s.u.) - .taskListType → Typ der Liste - .remainingTaskNumber → offene Einträge (String) - .totalTaskNumber → Gesamteinträge (String) +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!) ``` ## Systembezeichnungen für Listen-Namen @@ -142,7 +139,9 @@ offener Punkte (z.B. `type`-Parameter beim Login, Kreis-Felder in Response). - ~~Exakter Wert für `type`-Parameter beim Login~~ → nicht senden (verifiziert per JS-Analyse) - ~~Response-Struktur von `famlistfamily` (Kreise)~~ → a00.r.r[], metaId + name (verifiziert) -- ~~Ob `a03call=tasklistsync` benötigt wird~~ → ja, liefert Listen unter a03.r.r.updatedCreated[] (verifiziert) -- Kreis-Zuordnung in `accgetallfamily`-Response → noch offen (Feld in Listen-Objekten unbekannt) +- ~~Ob `a03call=tasklistsync` benötigt wird~~ → **nein**, kein gültiger Endpoint (verifiziert) +- Listen-IDs aus `a01.r.r.updatedCreated[].sortingIndexByTaskList`-Keys (verifiziert) +- Listen-Namen und Zähler (remainingTaskNumber, totalTaskNumber) → noch unbekannt +- Kreis-Zuordnung in `accgetallfamily`-Response → noch offen - ~~Ob `partnerScope` / `withStateBean` benötigt werden~~ → nein (verifiziert) - Session-Lebensdauer (irrelevant da kein Caching) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 01e4126..bf750e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.2.1" +version = "0.2.2" 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/__init__.py b/src/mcp_familywall/__init__.py index 3ced358..b5fdc75 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index c0d107d..833dfce 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -37,11 +37,7 @@ def _accgetallfamily() -> dict[str, Any]: client.login(email, password) data = client.call( "accgetallfamily", - { - "a01call": "taskcategorysync", - "a02call": "tasksync", - "a03call": "tasklistsync", - }, + {"a01call": "taskcategorysync", "a02call": "tasksync"}, ) client.logout() return data @@ -54,22 +50,46 @@ def _accgetallfamily() -> dict[str, Any]: def _extract_lists(data: dict[str, Any]) -> list[dict[str, Any]]: """Extract task lists from an accgetallfamily response. - Lists live under a03.r.r.updatedCreated[] (tasklistsync). + List IDs are derived from the sortingIndexByTaskList keys present in each + taskcategorysync entry (a01.r.r.updatedCreated[]). Each key is a unique + list ID of the form ``taskList/``. Names and counters are not yet + available from this path and are left as None. Args: data: Raw response body from accgetallfamily. Returns: - List of raw task-list dicts (may be empty). + Deduplicated list of dicts with keys id, name, type, open, total. """ try: - items = data["a03"]["r"]["r"]["updatedCreated"] - if isinstance(items, list): - logger.debug("Lists found under a03.r.r.updatedCreated (%d items)", len(items)) - return items # type: ignore[return-value] + categories = data["a01"]["r"]["r"]["updatedCreated"] + if not isinstance(categories, list): + return [] except (KeyError, TypeError): - pass - return [] + return [] + + seen: set[str] = set() + result: list[dict[str, Any]] = [] + for cat in categories: + sorting = cat.get("sortingIndexByTaskList") + if not isinstance(sorting, dict): + continue + for list_id in sorting: + if list_id in seen: + continue + seen.add(list_id) + result.append( + { + "id": list_id, + "name": list_id, # real name unknown — TODO once field identified + "type": "UNKNOWN", + "open": None, + "total": None, + } + ) + + logger.debug("Extracted %d unique list IDs from sortingIndexByTaskList", len(result)) + return result def _extract_tasks(data: dict[str, Any]) -> list[dict[str, Any]]: