diff --git a/SPEC.md b/SPEC.md index 025423d..73a1b3c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -78,9 +78,14 @@ für `session_id`. POST https://api.familywall.com/api/famlistfamily Content-Type: application/x-www-form-urlencoded -**Body-Parameter:** keine (zu verifizieren beim ersten echten Call) +**Body-Parameter:** keine (verifiziert) -**Response-Struktur:** zu verifizieren beim ersten echten Call +**Response-Struktur (verifiziert):** +``` +a00.r.r[] → Kreise + .metaId → eindeutige Kreis-ID + .name → Kreisname +``` ### `accgetallfamily` – Listen + Tasks abrufen POST https://api.familywall.com/api/accgetallfamily @@ -92,25 +97,27 @@ Content-Type: application/x-www-form-urlencoded |---|---| | `a01call` | `"taskcategorysync"` | | `a02call` | `"tasksync"` | +| `a03call` | `"tasklistsync"` | -Hinweis: `partnerScope`, `a03call`, `a03id`, `withStateBean` werden -weggelassen. Falls der Server sie erfordert, beim ersten echten Call -nachbessern. +Hinweis: `partnerScope`, `a03id`, `withStateBean` werden weggelassen. -**Response-Struktur (relevant für v1.0):** -a00.r.r[] → Listen (taskcategorysync) -.metaId → eindeutige Listen-ID -.name → Name (ggf. Systembezeichnung, s.u.) -.taskListType → Typ der Liste -.remainingTaskNumber → offene Einträge (String) -.totalTaskNumber → Gesamteinträge (String) -. → zu verifizieren beim ersten echten Call -a02.r.r.updatedCreated[] → Tasks (tasksync) -.metaId → eindeutige Task-ID -.text → Aufgabentext -.description → optionale Beschreibung -.taskListId → Zugehörigkeit zur Liste (= metaId der Liste) -.complete → "true" / "false" (String, nicht Boolean!) +**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) +``` ## Systembezeichnungen für Listen-Namen @@ -134,7 +141,8 @@ offener Punkte (z.B. `type`-Parameter beim Login, Kreis-Felder in Response). ## Noch zu verifizieren - ~~Exakter Wert für `type`-Parameter beim Login~~ → nicht senden (verifiziert per JS-Analyse) -- Response-Struktur von `famlistfamily` (Kreise) -- Kreis-Zuordnung in `accgetallfamily`-Response -- Ob `partnerScope` / `withStateBean` benötigt werden +- ~~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 `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 91a2a8a..01e4126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-familywall" -version = "0.2.0" +version = "0.2.1" 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 d3ec452..3ced358 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index 5f05924..c0d107d 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -37,7 +37,11 @@ def _accgetallfamily() -> dict[str, Any]: client.login(email, password) data = client.call( "accgetallfamily", - {"a01call": "taskcategorysync", "a02call": "tasksync"}, + { + "a01call": "taskcategorysync", + "a02call": "tasksync", + "a03call": "tasklistsync", + }, ) client.logout() return data @@ -48,25 +52,23 @@ def _accgetallfamily() -> dict[str, Any]: def _extract_lists(data: dict[str, Any]) -> list[dict[str, Any]]: - """Extract the list of task categories from an accgetallfamily response. + """Extract task lists from an accgetallfamily response. - SPEC says a00.r.r[], but a02.r.r[] has also been observed. - Both paths are tried defensively; the first non-empty result wins. + Lists live under a03.r.r.updatedCreated[] (tasklistsync). Args: data: Raw response body from accgetallfamily. Returns: - List of raw list-category dicts (may be empty). + List of raw task-list dicts (may be empty). """ - for key in ("a00", "a02"): - try: - items = data[key]["r"]["r"] - if isinstance(items, list) and items: - logger.debug("Lists found under %s.r.r (%d items)", key, len(items)) - return items # type: ignore[return-value] - except (KeyError, TypeError): - continue + 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] + except (KeyError, TypeError): + pass return [] @@ -100,13 +102,10 @@ def get_circles() -> str: """Return all Family Wall circles (Kreise) the account belongs to. Each circle has an id and a name. - Because the response structure of the famlistfamily endpoint has not yet - been verified against a live API call, the raw response is returned as - JSON for the first verification pass. + Response structure verified: a00.r.r[] with metaId and name fields. Returns: - JSON string — either a list of {id, name} objects once the structure - is confirmed, or the raw API response for verification. + JSON string — list of {id, name} objects. """ try: email, password = get_credentials() @@ -123,9 +122,19 @@ def get_circles() -> str: except Exception as exc: return f"Connection error: {exc}" - # Response structure not yet verified — return raw JSON for inspection. - # TODO: once confirmed, extract and return [{id, name}, ...] list. - return json.dumps(data, ensure_ascii=False, indent=2) + try: + raw_circles = data["a00"]["r"]["r"] + if not isinstance(raw_circles, list): + raise TypeError("a00.r.r is not a list") + except (KeyError, TypeError): + return json.dumps( + {"warning": "Unexpected famlistfamily response structure", "raw": data}, + ensure_ascii=False, + indent=2, + ) + + result = [{"id": c.get("metaId"), "name": c.get("name")} for c in raw_circles] + return json.dumps(result, ensure_ascii=False, indent=2) # ---------------------------------------------------------------------------