fix(lists): circle scope support for get_lists, create_list, delete_list (v0.7.1)

- get_lists(scope): API scope parameter now used server-side; accepts circle
  metaId ("family/XXXX") or circle name; returns circle_id field per list
- create_list(circle_id): new optional param; passes as API scope param
- delete_list: derives circle from list metaId and passes scope for
  secondary-circle lists
- Added _circle_id_from_list_id() helper (taskList/FAMNUM_LISTNUM -> family/FAMNUM)
- SPEC.md: documented scope param for taskgettasklists, taskcreatelist, taskdeletelist
- Verified: familyId/circleId/id params ignored by API, only scope works

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 18:10:05 +02:00
parent 2bc03e2165
commit abb557e96b
6 changed files with 144 additions and 26 deletions
+13 -7
View File
@@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden.
## Aktueller Stand
### Implementierte Tools (v0.7.0)
### Implementierte Tools (v0.7.1)
| Kategorie | Tools |
|---|---|
@@ -45,8 +45,9 @@ und wird in Claude Desktop eingebunden.
- v0.5.3: Kategorie-Auto-Assign-Hinweis im create_task Docstring ✓ (nachgeliefert in v0.6.1)
- v0.6.0: Rezept-Box (get_recipes, get_recipe, create_recipe, delete_recipe) ✓
- v0.6.1: update_recipe + Bugfix Zeilenumbrüche in create_recipe ✓
- v0.7.0: create_circle + add_member_to_circle ✓ ← aktuell
- v0.7.1: mpadditemtolist (Zutaten → Einkaufsliste)
- v0.7.0: create_circle + add_member_to_circle ✓
- v0.7.1: get_lists scope fix + create_list circle_id + delete_list scope ✓ ← aktuell
- v0.7.2: mpadditemtolist (Zutaten → Einkaufsliste)
- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung
- v0.8.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
@@ -62,8 +63,12 @@ um unnötige HTTP-Roundtrips zu vermeiden. Credentials liegen im OS Keyring
### Kreise (Scopes)
Family Wall kennt mehrere Kreise (z.B. Familie, erweiterter Familienkreis).
`get_lists` unterstützt optionalen `scope`-Parameter zur Filterung.
Ohne `scope` werden alle Kreise zurückgegeben.
Der API-Parameter `scope=family/XXXX` schaltet den Server-Kontext um.
- `taskgettasklists` ohne scope → primärer Kreis; mit scope → angegebener Kreis
- `taskcreatelist` mit scope → neue Liste im angegebenen Kreis
- `taskdeletelist` mit scope → löscht Liste aus angegebenem Kreis
- `get_lists(scope="family/XXXX")` oder `get_lists(scope="Kreis-Name")` zur Filterung
- Die Listen-metaId kodiert den Kreis: `taskList/<FAMNUM>_<LISTNUM>``family/<FAMNUM>`
### Listen-Namen
Systembezeichnungen (z.B. `SYS-CAT-SHOPPINGLIST`) werden in deutsche
@@ -133,8 +138,9 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
| `wallmood` | `wall_message_id`, `moodType` | `"STAR"` für Like |
| `taskcategoryput` | `name`, `emoji` | |
| `taskcategorydelete` | `id` | metaId der Kategorie |
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji` | `taskListType`: `"SHOPPING_LIST"`/`"TODOS"` |
| `taskdeletelist` | `id` | metaId der Liste |
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji`, `scope` | `scope`: Kreis-metaId für nicht-primäre Kreise |
| `taskgettasklists` | `scope` | Kreis-metaId; ohne scope → primärer Kreis |
| `taskdeletelist` | `id`, `scope` | `scope`: Kreis-metaId für sekundäre Kreise |
| `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! |
| `mprecipeput` (Update) | zusätzlich `recipe.metaId` | Vorhandene ID → Update statt Create |
| `metasync` (Rezepte lesen) | `id="recipe"` | liefert `a00.r.r.updatedCreated[]` |
+3 -3
View File
@@ -2,13 +2,13 @@
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude.
## Features (v0.7.0)
## Features (v0.7.1)
### Read
- `get_circles` -- list all family circles
- `get_members` -- list members of a circle (or all circles)
- `get_lists` -- list all task lists (includes `emoji` and `color`; `null` when unset)
- `get_lists` -- list all task lists (includes `emoji`, `color`, `circle_id`; `null` when unset); optional `scope` parameter filters by circle metaId or circle name
- `get_tasks` -- list tasks in a specific list (includes `category_id`, `due_date`, `assignee_ids`)
- `get_categories` -- list categories for a list (locale-filtered; custom categories always included; `custom` flag marks user-created ones)
- `get_activities` -- list recent wall activities (author resolved to display name)
@@ -21,7 +21,7 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your
- `update_task` -- update text, description, category, due date, assignees, or move to a different list; supports `clear_due_date=True` to remove a due date
- `toggle_task` -- mark a task complete or reopen it
- `delete_task` -- permanently delete a task
- `create_list` -- create a new task list (SHOPPING_LIST or TODOS; optional `emoji` and `color`)
- `create_list` -- create a new task list (SHOPPING_LIST or TODOS; optional `emoji`, `color`, and `circle_id` to target a specific circle)
- `delete_list` -- permanently delete a list and all its tasks (system lists are protected)
- `create_category` -- create a custom category for a shopping list (with optional icon)
- `delete_category` -- delete a custom category (system categories are protected)
+29 -5
View File
@@ -292,6 +292,13 @@ POST https://api.familywall.com/api/taskcreatelist
| `sharedToAll` | nein | `"true"` / `"false"` (default: `"true"`) |
| `color` | nein | Hex-Farbwert z.B. `"#4784EC"` |
| `emoji` | nein | Unicode-Emoji z.B. `"🛒"` |
| `scope` | nein | Kreis-metaId z.B. `"family/23447378"` (ohne scope → primärer Kreis) |
**Scope-Verhalten:**
- Ohne `scope`: Liste wird im primären Kreis des Accounts erstellt
- Mit `scope=family/XXXX`: Liste wird im angegebenen Kreis erstellt
- Die `metaId` der neuen Liste kodiert den Kreis: `taskList/CIRCLENUM_LISTNUM`
- Parameter `familyId`, `circleId`, `family`, `id` → werden ignoriert, nur `scope` wirkt
**Response:**
```
@@ -302,6 +309,7 @@ a00.r.r → vollständiges Listen-Objekt
.sharedToAll → "true" / "false"
.emoji → Unicode-Emoji (fehlt wenn nicht gesetzt)
.color → Hex-Farbwert z.B. "#E53935" (fehlt wenn nicht gesetzt)
.familyId → Kreis-metaId des erstellten Liste
.rights.canDelete → "true" (user-created lists)
```
@@ -312,9 +320,10 @@ POST https://api.familywall.com/api/taskdeletelist
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `id` | Listen-metaId ⚠️ nicht `listId` oder `taskListId`! |
| Parameter | Pflicht | Wert |
|---|---|---|
| `id` | ja | Listen-metaId ⚠️ nicht `listId` oder `taskListId`! |
| `scope` | nein | Kreis-metaId (erforderlich für sekundäre Kreise) |
**Response:**
```
@@ -324,20 +333,32 @@ a00.r.r → "true" (String)
**Hinweis:** Löscht die Liste und alle enthaltenen Tasks unwiderruflich.
System-Listen (`rights.canDelete` fehlt oder `null`) sind nicht löschbar.
MCP-Server prüft dies vor dem Löschen via `taskgettasklists`.
Für Listen in sekundären Kreisen muss `scope=family/XXXX` mitgeschickt werden.
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
### `taskgettasklists` Listen abrufen
POST https://api.familywall.com/api/taskgettasklists
**Body-Parameter:** keine
**Body-Parameter:**
| Parameter | Pflicht | Wert |
|---|---|---|
| `scope` | nein | Kreis-metaId z.B. `"family/23447378"` (ohne scope → primärer Kreis) |
**Scope-Verhalten:**
- Ohne `scope`: Nur Listen des primären Kreises werden zurückgegeben
- Mit `scope=family/XXXX`: Nur Listen des angegebenen Kreises
- Es gibt keinen servereitigen Filter für mehrere Kreise gleichzeitig
- Andere Parameter (`familyId`, `circleId`, etc.) werden ignoriert
**Response-Struktur:**
```
a00.r.r[] → Liste aller Task-Listen
a00.r.r[] → Liste aller Task-Listen des Kreises
.metaId → Listen-ID (z.B. "taskList/23431854_29740942")
.name → Systembezeichnung oder Benutzer-Name
.taskListType → SHOPPING_LIST oder TODOS
.familyId → Kreis-metaId (z.B. "family/23431854")
.emoji → Unicode-Emoji oder "" (leerer String = kein Emoji)
.color → Hex-Farbwert z.B. "#E53935" (fehlt wenn nicht gesetzt)
.remainingTaskNumber → offene Tasks (String)
@@ -348,6 +369,9 @@ a00.r.r[] → Liste aller Task-Listen
.systemId → vorhanden nur bei Systemlisten (z.B. "-10", "-11")
```
**metaId-Encoding:** `taskList/<family_num>_<list_num>` — die `family_num` kodiert den Kreis.
Beispiel: `taskList/23431854_29740942` gehört zu `family/23431854`.
**Hinweis emoji/color:**
- `emoji`: Systemlisten liefern `""`, user-created Listen liefern den Emoji-String
oder `""` wenn kein Emoji gesetzt. Normalisierung: `""``null` im MCP-Server.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mcp-familywall"
version = "0.7.0"
version = "0.7.1"
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
readme = "README.md"
requires-python = ">=3.12"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.7.0"
__version__ = "0.7.1"
+97 -9
View File
@@ -141,6 +141,26 @@ def get_circles():
# ---------------------------------------------------------------------------
def _circle_id_from_list_id(list_id: str) -> str | None:
"""Derive the circle metaId from a list metaId.
The list metaId format is ``taskList/<family_num>_<list_num>``.
This function returns ``"family/<family_num>"``.
Args:
list_id: List metaId (e.g. ``"taskList/23431854_29759623"``).
Returns:
Circle metaId (e.g. ``"family/23431854"``), or ``None`` when the
format cannot be parsed.
"""
bare = list_id.removeprefix("taskList/")
parts = bare.split("_", 1)
if len(parts) == 2 and parts[0].isdigit():
return f"family/{parts[0]}"
return None
def _famlistfamily() -> list[dict[str, Any]]:
"""Login, call famlistfamily, logout and return the raw circle list.
@@ -252,17 +272,64 @@ def get_members(circle_id: str | None = None) -> str:
@mcp.tool()
def get_lists(scope: str | None = None):
"""Return task lists as JSON, optionally filtered by circle name (scope)."""
def get_lists(scope: str | None = None) -> str:
"""Return task lists as JSON, optionally filtered to a specific circle.
Each list object includes a ``circle_id`` field with the owning circle's
metaId (e.g. ``"family/23431854"``).
Args:
scope: Optional circle filter. Accepts either:
- A circle metaId (e.g. ``"family/23447378"``) — passed directly
to the API.
- A circle display name (e.g. ``"Test Kreis 2"``) — resolved to
the matching circle metaId via ``get_circles`` first.
When ``None`` (default) the primary circle's lists are returned.
Returns:
JSON list of list objects with keys id, name, type, open, total,
emoji, color, circle_id.
"""
try:
email, password = get_credentials()
except RuntimeError as exc:
return f"Error: {exc}"
# Build the scope param for taskgettasklists.
# When scope is provided as a circle name (not a metaId), we need to
# resolve it via famlistfamily first — done in the same session.
api_scope: str | None = None
if scope:
if scope.startswith("family/"):
api_scope = scope
else:
# Treat as circle name — look up the metaId.
try:
circles = _famlistfamily()
except RuntimeError as exc:
return f"Error: {exc}"
matched = next((c for c in circles if c.get("name") == scope), None)
if matched is None:
circle_names = [c.get("name") for c in circles]
return json.dumps(
{
"error": f"Circle not found: {scope!r}",
"available_circles": circle_names,
},
ensure_ascii=False,
indent=2,
)
api_scope = matched["metaId"]
try:
with FamilyWallClient() as client:
client.login(email, password)
data = client.call("taskgettasklists", {})
params: dict[str, Any] = {}
if api_scope:
params["scope"] = api_scope
data = client.call("taskgettasklists", params)
client.logout()
except FamilyWallError as exc:
return f"Error: {exc}"
@@ -281,7 +348,6 @@ def get_lists(scope: str | None = None):
pass
if raw_lists is None:
# Response structure not yet verified — return raw JSON for inspection.
return json.dumps(
{"warning": "Unexpected taskgettasklists response structure", "raw": data},
ensure_ascii=False,
@@ -290,7 +356,6 @@ def get_lists(scope: str | None = None):
result = []
for item in raw_lists:
# TODO: apply scope filtering once the circle field is identified.
# emoji: API returns "" when unset — normalise to None for a clean JSON null.
# color: API omits the key entirely when unset — .get() returns None directly.
raw_emoji: str = item.get("emoji", "")
@@ -303,6 +368,7 @@ def get_lists(scope: str | None = None):
"total": item.get("totalTaskNumber"),
"emoji": raw_emoji if raw_emoji else None,
"color": item.get("color") or None,
"circle_id": item.get("familyId"),
}
)
@@ -912,6 +978,7 @@ def create_list(
shared_to_all: bool = True,
color: str | None = None,
emoji: str | None = None,
circle_id: str | None = None,
) -> str:
"""Create a new task list in Family Wall.
@@ -924,9 +991,15 @@ def create_list(
circle members. When ``False`` it is private to the creator.
color: Optional background colour as a hex string (e.g. ``"#4784EC"``).
emoji: Optional Unicode emoji to use as the list icon (e.g. ``"🛒"``).
circle_id: Optional circle metaId to create the list in
(e.g. ``"family/23447378"``). When ``None`` (default) the list
is created in the primary circle. Use ``get_circles`` to
retrieve available circle IDs.
Returns:
JSON with the new list object on success, or an error message.
Includes ``circle_id`` field showing which circle the list was
created in.
"""
if list_type not in ("SHOPPING_LIST", "TODOS"):
return "Error: list_type must be 'SHOPPING_LIST' or 'TODOS'."
@@ -942,6 +1015,9 @@ def create_list(
params["color"] = color
if emoji:
params["emoji"] = emoji
if circle_id:
# The API uses the 'scope' parameter to specify the target circle.
params["scope"] = circle_id
try:
data = _authenticated_call("taskcreatelist", params)
@@ -968,6 +1044,7 @@ def create_list(
"shared_to_all": list_obj.get("sharedToAll") == "true",
"emoji": raw_emoji if raw_emoji else None,
"color": list_obj.get("color") or None,
"circle_id": list_obj.get("familyId") or circle_id,
},
ensure_ascii=False,
indent=2,
@@ -1003,13 +1080,21 @@ def delete_list(list_id: str) -> str:
except RuntimeError as exc:
return f"Error: {exc}"
# Derive the owning circle from the list metaId so that secondary-circle
# lists can be queried and deleted with the correct scope parameter.
# Format: taskList/<family_num>_<list_num> → family/<family_num>
circle_scope = _circle_id_from_list_id(list_id)
list_obj: dict[str, Any] | None = None
try:
with FamilyWallClient() as client:
client.login(email, password)
# Fetch lists and verify the target can be deleted.
raw = client.call("taskgettasklists", {})
# Fetch lists scoped to the correct circle and verify deletion rights.
get_params: dict[str, Any] = {}
if circle_scope:
get_params["scope"] = circle_scope
raw = client.call("taskgettasklists", get_params)
try:
raw_lists: list[dict[str, Any]] = raw["a00"]["r"]["r"]
if not isinstance(raw_lists, list):
@@ -1037,8 +1122,11 @@ def delete_list(list_id: str) -> str:
)
# Verified — delete in the same session.
# taskdeletelist uses 'id' (same pattern as metadelete / taskcategorydelete).
client.call("taskdeletelist", {"id": list_id})
# For secondary circles the 'scope' parameter is required.
del_params: dict[str, Any] = {"id": list_id}
if circle_scope:
del_params["scope"] = circle_scope
client.call("taskdeletelist", del_params)
client.logout()
except FamilyWallError as exc:
return f"Error: Family Wall API error: {exc}"