Files
mcp-familywall/SPEC.md
T
marcus 0d8036fd4a fix: get_categories always includes custom categories (v0.4.12)
Custom categories (rights.canDelete=true) have no locale field set by
the API and were silently excluded by the locale filter. They now bypass
both the locale and taskListType filters so they always appear in
get_categories output regardless of the locale parameter.

Also: deleted 7 test categories (TEmojiApple, Obst & Gemüse (old),
TestKategorie, ProbeKat2, [TEST]emoji=apple, ProbeKat1, TDelMeta)
and restored 'Obst & Gemüse' (emoji 🍎) as a clean custom category.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 08:06:21 +02:00

493 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Family Wall API Spezifikation
Erarbeitet durch Browser-Traffic-Analyse (April 2026).
Es gibt keine offizielle API-Dokumentation.
## Base URL
https://api.familywall.com/api
## Authentifizierung
### Login
POST https://api.familywall.com/api/log2in
Content-Type: application/x-www-form-urlencoded
**Request-Parameter:**
| Parameter | Wert |
|---|---|
| `identifier` | E-Mail-Adresse |
| `password` | Passwort |
| `type` | nicht senden — wird als `undefined` ignoriert (verifiziert per JS-Analyse) |
| `clientId` | weglassen |
| `clientSecret` | weglassen |
| `generateAutologinToken` | weglassen |
| `countryCode` | weglassen |
**Response (Erfolg):**
```json
{ "a00": { "r": { "r": <SessionObject> }, "cn": "log2in" } }
```
`SessionObject` enthält u.a. `tokenCsrf` und `webApiUrl`.
`tokenCsrf` ist die Session-ID identisch zur `JSESSIONID` im Cookie.
**Response (Fehler):**
```json
{ "ex": { "ex": <ErrorObject> } }
{ "un": { "un": <ErrorObject> } }
```
Der Server setzt nach erfolgreichem Login ein Session-Cookie:
Set-Cookie: JSESSIONID=<session-id> (= tokenCsrf)
### Folgecalls (nach Login)
Alle API-Calls nach dem Login benötigen:
| | |
|---|---|
| **Cookie** | `JSESSIONID=<session-id>` |
| **Header** | `Tokencsrf: <session-id>` (identisch zur JSESSIONID) |
| **Content-Type** | `application/x-www-form-urlencoded` |
### Logout
POST https://api.familywall.com/api/log2out
Content-Type: application/x-www-form-urlencoded
Keine Parameter. Session wird serverseitig invalidiert.
### Session-Strategie
Kein Session-Caching. Jeder MCP-Tool-Call führt folgende Sequenz aus:
POST /api/log2in → Session-ID
POST /api/<endpoint> → Nutzdaten
POST /api/log2out → Session invalidieren
Credentials (E-Mail + Passwort) werden einmalig via `mcp-familywall setup`
im OS Keyring gespeichert (Keys: `email`, `password`). Kein Keyring-Eintrag
für `session_id`.
## Bekannte Endpoints
### `famlistfamily` Kreise abrufen
POST https://api.familywall.com/api/famlistfamily
Content-Type: application/x-www-form-urlencoded
**Body-Parameter:** keine (verifiziert)
**Response-Struktur (verifiziert):**
```
a00.r.r[] → Kreise
.metaId → eindeutige Kreis-ID (Format family/<id>)
.name → Kreisname
.family_id → numerische Kreis-ID
.members[] → Mitglieder des Kreises
.accountId → numerische Account-ID
.metaId → Mitglieds-ID (Format familymember/<accountId>_<familyId>)
.firstName → Vorname (Display-Name; bevorzugen gegenüber .name)
.name → E-Mail-Adresse (Family Wall Default wenn kein Anzeigename)
.role → Familienrolle (z.B. "Unknown", "Parent", "Child")
.right → Berechtigung (z.B. "SuperAdmin", "Admin", "Member")
.color → Profilfarbe als Hex-String (z.B. "#FF8086")
.medias[0].pictureUrl → Avatar-URL (generierter Default wenn pictureDefault=true)
.identifiers[] → Kontaktdaten
.type → Typ (z.B. "Email")
.value → Wert (z.B. E-Mail-Adresse)
.familyId → Zugehöriger Kreis (= metaId des Kreises)
.isloggedaccount → "true" wenn das der angemeldete Account ist
.joinDate → Beitrittsdatum (ISO 8601)
.lastLoginDate → Letzter Login (ISO 8601)
.locale → Spracheinstellung (z.B. "de_DE")
.timeZone → Zeitzone (z.B. "Europe/Berlin")
.invitations[] → Offene Einladungen (leer wenn keine)
.coverUri → Cover-Bild URL
```
### `taskgettasklists` Listen abrufen
POST https://api.familywall.com/api/taskgettasklists
Content-Type: application/x-www-form-urlencoded
**Body-Parameter:** keine
**Response-Struktur:** zu verifizieren beim ersten echten Call
### `accgetallfamily` Listen + Tasks abrufen
POST https://api.familywall.com/api/accgetallfamily
Content-Type: application/x-www-form-urlencoded
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `a01call` | `"taskcategorysync"` |
| `a02call` | `"tasksync"` |
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 inkl. members[]) Nebeneffekt
a01.r.r.updatedCreated[] → taskcategorysync (Kategorien/Abteilungen pro Liste)
.metaId → Kategorie-ID (Format taskCategory/<familyId>_<sysId>)
.name → Kategoriename (sprachabhängig, z.B. "Beverages")
.emoji → Emoji-Symbol der Kategorie
.systemCategoryId → numerische System-ID (sprach-unabhängig)
.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
**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
**Wichtig:** Custom-Kategorien (rights.canDelete='true') haben
kein locale-Feld gesetzt — sie werden sprachunabhängig
zurückgegeben und dürfen nie über locale gefiltert werden.
.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[] → Listen-Level-Systemkategorie (z.B. SYS-CAT-SHOPPINGLIST);
NICHT die spezifische Task-Kategorie — immer identisch
für alle Tasks einer Liste
.system → "true" (immer System-Kategorie)
.name → Listen-Systemkategorien (z.B. "SYS-CAT-SHOPPINGLIST", "SYS-CAT-TODOS")
.taskCategoryId → spezifische Task-Kategorie (verifiziert): metaId-Format
(z.B. "taskCategory/23431854_200"), null wenn nicht gesetzt
.assignee / .assigneeIds → zugewiesene Mitglieder
.reminder → Erinnerungsdatum (ISO 8601, optional)
.recurrency → Wiederholungsregel (optional)
.sortingIndex → Anzeigereihenfolge
```
**Kategorie-Zuweisung bei taskcreate2 / taskupdate2 (verifiziert):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
Hinweise:
- Wert muss das vollständige metaId-Format `taskCategory/<familyId>_<systemCategoryId>` sein.
Nur der numerische `systemCategoryId`-Teil (z.B. `200`) führt zu API-Fehler
`"cannot find task category id=200"`.
- Das `categories[]`-Feld in der Response zeigt immer `SYS-CAT-SHOPPINGLIST`
(Listen-Level-Systemkategorie, unabhängig vom gesetzten `taskCategoryId`).
Die tatsächliche Task-Kategorie ist im Feld `taskCategoryId` der Task gespeichert.
- Nur für Einkaufslisten (`taskListType=SHOPPING_LIST`) relevant;
TODO-Listen haben keine Kategorien.
## Systembezeichnungen für Listen-Namen
Bekannte Systembezeichnungen werden deutsch übersetzt:
| Systembezeichnung | Deutsch |
|---|---|
| `SYS-CAT-SHOPPINGLIST` | `Einkaufsliste` |
Unbekannte Bezeichnungen werden unverändert zurückgegeben.
Mapping-Tabelle bei Bedarf erweitern.
### `wallget` Aktivitäten (Wall) abrufen
POST https://api.familywall.com/api/wallget
Content-Type: application/x-www-form-urlencoded
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `nb` | Anzahl Einträge (z.B. `"20"`) |
| `date` | optional, für Paginierung (Datum des letzten Eintrags) |
| `accountId` | optional |
| `type` | optional |
| `nested` | optional |
| `masterNested` | optional |
| `sortBy` | optional |
**Response-Struktur (verifiziert):**
```
a00.r.r[]
.metaId → ID der Aktivität (= wallMessageId)
.refType → Aktivitätstyp (z.B. STATUS, FAMILY_CREATED)
.text → Text (optional, fehlt bei System-Events)
.creationDate → Datum (ISO 8601)
.accountId → Autor-ID
```
**Response-Struktur:** zu verifizieren beim ersten echten Call
### `wallactivityget` Einzelne Aktivität abrufen
POST https://api.familywall.com/api/wallactivityget
Content-Type: application/x-www-form-urlencoded
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `accountId` | optional |
| `masterNested` | optional |
**Response-Struktur:** zu verifizieren beim ersten echten Call
(Endpoint noch nicht implementiert — für spätere Versionen)
## Debug-Logging
Wenn die Umgebungsvariable `FW_DEBUG=1` gesetzt ist, loggt `fw_client.py`
vollständige Request-Bodies und Responses nach stderr. Dient zur Verifikation
offener Punkte (z.B. `type`-Parameter beim Login, Kreis-Felder in Response).
**Wichtig:** Keine Secrets in Debug-Ausgaben (Passwort maskieren).
### `taskcreate2` Task erstellen
POST https://api.familywall.com/api/taskcreate2
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `taskListId` | ja | Listen-ID aus `get_lists` (z.B. `taskList/123_456`) |
| `text` | ja | Aufgabentitel |
| `description` | nein | Optionale Beschreibung |
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
**Response-Struktur (verifiziert):**
```
a00.r.r → vollständiges Task-Objekt der neu erstellten Task
.metaId → eindeutige Task-ID (z.B. "task/23431854_726362809")
.taskListId → Listen-ID
.text → Titel
.complete → "false" (immer, direkt nach Erstellung)
```
### `taskupdate2` Task aktualisieren
POST https://api.familywall.com/api/taskupdate2
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `metaId` | ja | Task-ID aus `get_tasks` |
| `text` | nein | Neuer Titel (mindestens eines der optionalen Felder erforderlich) |
| `description` | nein | Neue Beschreibung |
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
Hinweis: `taskListId` ist **nicht** erforderlich (verifiziert Update ohne `taskListId` funktioniert).
**Response-Struktur:** kein spezifischer Rückgabewert Erfolg = kein `ex`/`un`-Key auf Top-Level.
### `taskmark` Task als erledigt/offen markieren
POST https://api.familywall.com/api/taskmark
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `taskId` | ja | Task-ID aus `get_tasks` (**WICHTIG: `taskId`, nicht `metaId`!**) |
| `complete` | ja | `"true"` oder `"false"` (String, nicht Boolean!) |
**Achtung:** Der Endpoint heißt intern `taskId`, nicht `metaId`.
Falsche Parameter (`metaId`, `id`, `taskMetaId`) werden serverseitig ignoriert
die API antwortet dann mit einem Fehler in `a00.un.un` (nicht Top-Level!),
der vom Standard-Error-Check im fw_client übersehen wird.
**Response-Struktur (verifiziert):**
```
a00.r.r → vollständiges Task-Objekt mit aktuellem Stand (inkl. lastAction: "MARK_COMPLETED")
```
### `metadelete` Objekt löschen
POST https://api.familywall.com/api/metadelete
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `id` | ja | Task-ID aus `get_tasks` (**WICHTIG: `id`, nicht `metaId`!**) |
Hinweis: `metadelete` ist ein generischer Lösch-Endpoint für beliebige Objekte (Tasks, etc.).
Entsprechend vorsichtig verwenden.
**Response-Struktur (verifiziert):**
```
a00.r.r → "true" (String)
```
**Fehlerverhalten:** Bei falschem Parameter-Namen (`metaId`, `taskId` etc.) antwortet die API
mit `{"a00": {"un": {"un": {"message": "missing value in: id"}}}}` auf Top-Level ohne `un`-Key
→ wird vom fw_client fälschlich als Erfolg interpretiert. Daher ist der korrekte Parameter-Name
kritisch.
### `taskcategoryput` Kategorie erstellen
POST https://api.familywall.com/api/taskcategoryput
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert via FW_DEBUG=1):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `name` | ja | Kategorie-Name (beliebiger String) |
| `emoji` | nein | Icon: Unicode-Emoji-Zeichen (z.B. `🌿`) oder beliebiger String-Code (z.B. `"FOOD"`) — wird as-is gespeichert |
Hinweise:
- Die neue Kategorie wird **allen** Listen der Familie zugeordnet — es gibt keine per-Liste-Einschränkung.
- Benutzerdefinierte Kategorien haben `systemCategoryId=null` und `rights.canDelete='true'`.
- System-Kategorien haben `rights.canDelete=null` — API erlaubt Löschen, aber `delete_category` Tool verweigert es.
**Response-Struktur (verifiziert):**
```
a00.r.r → vollständiges Kategorie-Objekt
.metaId → neue Kategorie-ID (z.B. "taskCategory/23431854_4956637")
.name → Kategorie-Name
.taskListType → "SHOPPING_LIST" (automatisch gesetzt)
.familyId → Familien-ID
.accountId → Account-ID des Erstellers
.rights.canDelete → "true" (custom Kategorien)
.rights.canUpdate → "true" (custom Kategorien)
.emoji → gespeicherter Icon-Wert (falls übergeben)
```
**Fehlerverhalten:** Ohne `name`-Parameter:
```json
{"a00": {"un": {"un": {"FiZClassId": "502", "message": "cat without a name ..."}}}}
```
### `taskcategorydelete` Kategorie löschen
POST https://api.familywall.com/api/taskcategorydelete
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert via FW_DEBUG=1):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `id` | ja | Kategorie-MetaId aus `get_categories` (**WICHTIG: `id`, nicht `metaId`!**) |
**Achtung:** Falscher Parameter-Name `metaId` führt zu:
```json
{"a00": {"un": {"un": {"FiZClassId": "502", "message": "In request, missing value in : id"}}}}
```
**Response-Struktur (verifiziert):**
```
a00.r.r → "true" (String)
```
**Wichtig System-Kategorien:** Die API erlaubt technisch das Löschen von System-Kategorien
(`taskCategory/<familyId>_200` etc.), entfernt sie aber nur aus der Familie — nicht global.
Das `delete_category`-MCP-Tool verweigert dies (Schutz via `rights.canDelete`-Check).
Erkennung: custom Kategorien haben `rights.canDelete='true'`; System-Kategorien haben `rights.canDelete=null`.
### `wallmood` Wall-Post liken
POST https://api.familywall.com/api/wallmood
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert via FW_DEBUG=1):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `wall_message_id` | ja | Post-ID aus `get_activities` (z.B. `wall/23431854_31119189`) |
| `moodType` | ja | `"STAR"` (einziger bekannter Wert — entspricht dem Like-Button in der App) |
**Verhalten (verifiziert):**
- `wallmood` mit `moodType: "STAR"` ist eine **idempotente SET-Operation** — kein Toggle!
- Mehrfaches Aufrufen mit denselben Parametern hinterlässt denselben Zustand
- `moodType: "LIKE"` wurde serverseitig als `"STAR"` gespeichert → korrekter Wert ist `"STAR"`
- Nur für Post-Typ `STATUS` wirksam; `FAMILY_CREATED` und vermutlich System-Posts ignorieren den Call
**Unlike nicht implementierbar (Stand: April 2026):**
Ausgiebig getestete Ansätze, die alle fehlschlugen:
| Ansatz | Ergebnis |
|---|---|
| `moodType: "NONE"` / `"REMOVE"` / `"DELETE"` / `""` | moodMap unverändert |
| `moodType` ganz weglassen | moodMap unverändert |
| `moodStarShortcut: "false"` als Parameter | moodMap unverändert |
| Alternative Endpoints: `wallmooddelete`, `wallmoodremove`, `wallmoodelete`, `wallunmood`, `wallcommentdelete`, `wallmoodstar`, `wallstar`, `wallreact`, `wallreactdelete` | alle: `"The call X is not registered"` (502) |
| `metadelete` auf Mood-Comment-ID | löscht den Comment, aber `moodStarShortcut` bleibt gesetzt |
Die Web-App nutzt einen Service Worker, der Requests abfängt — der echte Unlike-Payload
ist dadurch nicht per Browser-DevTools inspizierbar. Unlike bleibt bis zur weiteren
Analyse nicht unterstützt.
**Response-Struktur (verifiziert):**
```
a00.r.r → vollständiges Wall-Message-Objekt
.metaId → Post-ID (= wallMessageId)
.wallMessageId → Post-ID (identisch zu metaId)
.refType → Aktivitätstyp (z.B. "STATUS")
.refAction → Letzte Aktion (z.B. "MOOD_STAR")
.text → Post-Text
.postAccountId → Account-ID des Post-Autors
.accountId → Account-ID (identisch)
.moodMap → dict: accountId → ["STAR"] wenn geliked
.moodStarShortcut → "true" wenn Like-Shortcut aktiv
.comments[] → Liste von Kommentaren/Moods
.commentId → Kommentar-ID (Format wallComment/...)
.accountId → Account-ID des Kommentators
.mood → Mood-Typ (z.B. "STAR")
.text → Kommentartext (falls vorhanden)
.creationDate → Erstelldatum (ISO 8601)
.creationDate → Erstelldatum des Posts (ISO 8601)
.modifDate → Letztes Änderungsdatum (ISO 8601)
.familyId → Kreis-ID (Format family/...)
.rights.canUpdate → "true"/"false"
.rights.canDelete → "true"/"false"
```
**Like-Zustand bestimmen (zwei Indikatoren, beide auswerten):**
| Feld | Typ | Bedeutung |
|---|---|---|
| `moodStarShortcut` | `"true"` / `"false"` | Primär: direktes User-Like-Flag für den anfragenden Account |
| `moodMap[accountId]` | `["STAR"]` | Sekundär: accountId → Mood-Liste; enthält `"STAR"` wenn geliked |
Beide Indikatoren können den Like-Zustand korrekt abbilden — je nach API-internem
Speicherpfad ist nur einer gesetzt. Immer beide prüfen.
**Silent-Fail-Szenarien (API antwortet 200, aber Like wird nicht gesetzt):**
- **Self-Like-Restriction**: Eigener Post kann nicht geliked werden
(verifiziert: Account 23431898 kann Post `wall/23431854_31119189` nicht liken,
obwohl API regulär antwortet — `modifDate` bleibt eingefroren)
- **Unsupported Post-Typ**: `FAMILY_CREATED`-Posts ignorieren wallmood-Calls
- **Rate-Limit**: Nach vielen Calls kann die API Still-Fails zurückgeben
Erkennungsmerkmal für Silent-Fail: `modifDate` im Response identisch zum Vorherigen
AND `moodStarShortcut: false` AND `moodMap: {}`.
## Noch zu verifizieren
- ~~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~~ → **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)
- ~~`taskcreate2`: Response-Struktur~~ → `a00.r.r` = vollständiges Task-Objekt (verifiziert)
- ~~`taskupdate2`: ob `taskListId` Pflichtfeld ist~~ → **nein**, nicht erforderlich (verifiziert)
- ~~`taskmark`: korrekter Parameter-Name~~ → **`taskId`** (nicht `metaId`!) (verifiziert)
- ~~`metadelete`: korrekter Parameter-Name + Response-Struktur~~ → **`id`**, Response `"true"` (verifiziert)
- ~~`wallmood`: Parameter-Name `wallId`~~ → **`wall_message_id`** (verifiziert via API-Fehlermeldung)
- ~~`wallmood`: `moodType`-Werte, Toggle vs. explizit, Response-Struktur~~ → verifiziert: idempotentes SET mit `"STAR"`, kein Toggle (siehe oben)
- `wallmood` Unlike: Mechanismus unbekannt — Service Worker verhindert Browser-Inspektion; alle getesteten Ansätze fehlgeschlagen (siehe oben)
- ~~`taskcreate2` / `taskupdate2`: Kategorie-Paramter-Name~~ → **`taskCategoryId`**, Wert = vollständige metaId (verifiziert)
- ~~`taskcategoryput`: Body-Parameter, Response-Struktur~~ → `name` (Pflicht), `emoji` (optional), Response = neues Kategorie-Objekt (verifiziert)
- ~~`taskcategorydelete`: Body-Parameter~~**`id`** (nicht `metaId`!), Response = `"true"` (verifiziert)