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:
@@ -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[]` |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
__version__ = "0.7.0"
|
||||
__version__ = "0.7.1"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user