feat(lists): get_lists() without scope returns all circles (v0.8.2)

Implement v0.8.2: When get_lists() is called without a scope parameter,
it now fetches lists from ALL circles instead of only the primary circle.

Implementation:
- Login once, call famlistfamily to get all circle IDs
- For each circle, call taskgettasklists(scope=<circle_id>)
- Merge results and return all lists with circle_id field

All tests passed: test_multi circle creation, list creation in secondary
circle, get_lists() without scope returns lists from both circles.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 06:40:09 +02:00
parent 74a8b83fde
commit b15af18606
5 changed files with 41 additions and 33 deletions
+5 -3
View File
@@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden.
## Aktueller Stand ## Aktueller Stand
### Implementierte Tools (v0.8.0) ### Implementierte Tools (v0.8.2)
| Kategorie | Tools | | Kategorie | Tools |
|---|---| |---|---|
@@ -51,8 +51,10 @@ und wird in Claude Desktop eingebunden.
- v0.7.3: update_list (Umbenennen, emoji/color ändern) ✓ - v0.7.3: update_list (Umbenennen, emoji/color ändern) ✓
- v0.7.4: update_circle (Kreis umbenennen) ✓ - v0.7.4: update_circle (Kreis umbenennen) ✓
- v0.7.5: Primärkreis-Schutz in update_circle (isFirstFamily-Check) ✓ - v0.7.5: Primärkreis-Schutz in update_circle (isFirstFamily-Check) ✓
- v0.8.0: Rezept-Kategorien (get_recipe_categories, create_recipe + category_ids, update_recipe + category_ids) ✓ ← aktuell - v0.8.0: Rezept-Kategorien (get_recipe_categories, create_recipe + category_ids, update_recipe + category_ids) ✓
- v0.8.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) - v0.8.1: Bugfixes (recipe categories) ✓
- v0.8.2: get_lists() ohne scope → alle Kreise ✓ ← aktuell
- v0.9.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
- v0.8.x: mpadditemtolist (Zutaten → Einkaufsliste) - v0.8.x: mpadditemtolist (Zutaten → Einkaufsliste)
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
+5 -1
View File
@@ -379,12 +379,16 @@ POST https://api.familywall.com/api/taskgettasklists
|---|---|---| |---|---|---|
| `scope` | nein | Kreis-metaId z.B. `"family/23447378"` (ohne scope → primärer Kreis) | | `scope` | nein | Kreis-metaId z.B. `"family/23447378"` (ohne scope → primärer Kreis) |
**Scope-Verhalten:** **Scope-Verhalten (Server):**
- Ohne `scope`: Nur Listen des primären Kreises werden zurückgegeben - Ohne `scope`: Nur Listen des primären Kreises werden zurückgegeben
- Mit `scope=family/XXXX`: Nur Listen des angegebenen Kreises - Mit `scope=family/XXXX`: Nur Listen des angegebenen Kreises
- Es gibt keinen servereitigen Filter für mehrere Kreise gleichzeitig - Es gibt keinen servereitigen Filter für mehrere Kreise gleichzeitig
- Andere Parameter (`familyId`, `circleId`, etc.) werden ignoriert - Andere Parameter (`familyId`, `circleId`, etc.) werden ignoriert
**Scope-Verhalten (MCP-Tool get_lists, v0.8.2+):**
- Ohne `scope`: Der MCP-Server ruft für **jeden Kreis** separate `taskgettasklists(scope=<circle_id>)`-Calls ab und merged die Ergebnisse
- Mit `scope=family/XXXX` oder `scope="Kreis Name"`: Nur dieser Kreis (wie bisher)
**Response-Struktur:** **Response-Struktur:**
``` ```
a00.r.r[] → Liste aller Task-Listen des Kreises a00.r.r[] → Liste aller Task-Listen des Kreises
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mcp-familywall" name = "mcp-familywall"
version = "0.8.1" version = "0.8.2"
description = "MCP server for Family Wall — read your family's lists and tasks via Claude" description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.8.1" __version__ = "0.8.2"
+29 -27
View File
@@ -286,7 +286,7 @@ def get_lists(scope: str | None = None) -> str:
- A circle display name (e.g. ``"Test Kreis 2"``) — resolved to - A circle display name (e.g. ``"Test Kreis 2"``) — resolved to
the matching circle metaId via ``get_circles`` first. the matching circle metaId via ``get_circles`` first.
When ``None`` (default) the primary circle's lists are returned. When ``None`` (default) all lists from all circles are returned.
Returns: Returns:
JSON list of list objects with keys id, name, type, open, total, JSON list of list objects with keys id, name, type, open, total,
@@ -300,10 +300,10 @@ def get_lists(scope: str | None = None) -> str:
# Build the scope param for taskgettasklists. # Build the scope param for taskgettasklists.
# When scope is provided as a circle name (not a metaId), we need to # When scope is provided as a circle name (not a metaId), we need to
# resolve it via famlistfamily first — done in the same session. # resolve it via famlistfamily first — done in the same session.
api_scope: str | None = None api_scopes: list[str] = []
if scope: if scope:
if scope.startswith("family/"): if scope.startswith("family/"):
api_scope = scope api_scopes = [scope]
else: else:
# Treat as circle name — look up the metaId. # Treat as circle name — look up the metaId.
try: try:
@@ -321,41 +321,43 @@ def get_lists(scope: str | None = None) -> str:
ensure_ascii=False, ensure_ascii=False,
indent=2, indent=2,
) )
api_scope = matched["metaId"] api_scopes = [matched["metaId"]]
else:
# No scope filter: fetch all circles and iterate over them.
try:
circles = _famlistfamily()
api_scopes = [c["metaId"] for c in circles if "metaId" in c]
except RuntimeError as exc:
return f"Error: {exc}"
try: try:
with FamilyWallClient() as client: with FamilyWallClient() as client:
client.login(email, password) client.login(email, password)
params: dict[str, Any] = {} all_lists: list[dict[str, Any]] = []
if api_scope: for circle_scope in api_scopes:
params["scope"] = api_scope params: dict[str, Any] = {"scope": circle_scope}
data = client.call("taskgettasklists", params) data = client.call("taskgettasklists", params)
raw_lists: list[dict[str, Any]] | None = None
try:
candidate = data["a00"]["r"]["r"]
if isinstance(candidate, list):
raw_lists = candidate
elif isinstance(candidate, dict) and isinstance(candidate.get("updatedCreated"), list):
raw_lists = candidate["updatedCreated"]
except (KeyError, TypeError):
pass
if raw_lists is not None:
all_lists.extend(raw_lists)
client.logout() client.logout()
except FamilyWallError as exc: except FamilyWallError as exc:
return f"Error: {exc}" return f"Error: {exc}"
except Exception as exc: except Exception as exc:
return f"Connection error: {exc}" return f"Connection error: {exc}"
# Try known response patterns; fall back to raw JSON for verification.
raw_lists: list[dict[str, Any]] | None = None
try:
candidate = data["a00"]["r"]["r"]
if isinstance(candidate, list):
raw_lists = candidate
elif isinstance(candidate, dict) and isinstance(candidate.get("updatedCreated"), list):
raw_lists = candidate["updatedCreated"]
except (KeyError, TypeError):
pass
if raw_lists is None:
return json.dumps(
{"warning": "Unexpected taskgettasklists response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
result = [] result = []
for item in raw_lists: for item in all_lists:
# emoji: API returns "" when unset — normalise to None for a clean JSON null. # 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. # color: API omits the key entirely when unset — .get() returns None directly.
raw_emoji: str = item.get("emoji", "") raw_emoji: str = item.get("emoji", "")