From b15af18606631a0a3fa75db76480ad92ff6f9d0c Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 17 Apr 2026 06:40:09 +0200 Subject: [PATCH] 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=) - 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 --- CLAUDE.md | 8 +++-- SPEC.md | 6 +++- pyproject.toml | 2 +- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/server.py | 56 ++++++++++++++++++---------------- 5 files changed, 41 insertions(+), 33 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b87d1f9..6f91f90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden. ## Aktueller Stand -### Implementierte Tools (v0.8.0) +### Implementierte Tools (v0.8.2) | Kategorie | Tools | |---|---| @@ -51,8 +51,10 @@ und wird in Claude Desktop eingebunden. - v0.7.3: update_list (Umbenennen, emoji/color ändern) ✓ - v0.7.4: update_circle (Kreis umbenennen) ✓ - 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.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich) +- v0.8.0: Rezept-Kategorien (get_recipe_categories, create_recipe + category_ids, update_recipe + category_ids) ✓ +- 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) - v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren) diff --git a/SPEC.md b/SPEC.md index adf7c70..11dd983 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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-Verhalten:** +**Scope-Verhalten (Server):** - 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 +**Scope-Verhalten (MCP-Tool get_lists, v0.8.2+):** +- Ohne `scope`: Der MCP-Server ruft für **jeden Kreis** separate `taskgettasklists(scope=)`-Calls ab und merged die Ergebnisse +- Mit `scope=family/XXXX` oder `scope="Kreis Name"`: Nur dieser Kreis (wie bisher) + **Response-Struktur:** ``` a00.r.r[] → Liste aller Task-Listen des Kreises diff --git a/pyproject.toml b/pyproject.toml index 472e448..01cc8ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] 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" readme = "README.md" requires-python = ">=3.12" diff --git a/src/mcp_familywall/__init__.py b/src/mcp_familywall/__init__.py index 8088f75..deded32 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.8.1" +__version__ = "0.8.2" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index 0a5968c..1243723 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -286,7 +286,7 @@ def get_lists(scope: str | None = None) -> str: - 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. + When ``None`` (default) all lists from all circles are returned. Returns: 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. # 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 + api_scopes: list[str] = [] if scope: if scope.startswith("family/"): - api_scope = scope + api_scopes = [scope] else: # Treat as circle name — look up the metaId. try: @@ -321,41 +321,43 @@ def get_lists(scope: str | None = None) -> str: ensure_ascii=False, 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: with FamilyWallClient() as client: client.login(email, password) - params: dict[str, Any] = {} - if api_scope: - params["scope"] = api_scope - data = client.call("taskgettasklists", params) + all_lists: list[dict[str, Any]] = [] + for circle_scope in api_scopes: + params: dict[str, Any] = {"scope": circle_scope} + 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() except FamilyWallError as exc: return f"Error: {exc}" except Exception as 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 = [] - for item in raw_lists: + for item in all_lists: # 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", "")