feat: get_categories tool, author resolution in get_activities, update CLAUDE.md (v0.4.8)

- get_categories(list_id): new tool filtering taskcategorysync by
  sortingIndexByTaskList, returns {id, name, emoji} ordered by sort index
- get_activities: author IDs now resolved to display names (firstName) via
  famlistfamily members; raw author_id preserved as separate field; member
  lookup is non-fatal (falls back to raw ID on error)
- CLAUDE.md: tools table updated to reflect all implemented tools (v0.4.8)
- SPEC.md: full accgetallfamily response structure documented including
  categories[] on tasks and category parameter investigation findings
- Category assignment in create/update task: all tested parameter names
  ignored by API; still to verify (Service Worker blocks web inspection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 17:04:35 +02:00
parent 70b1f73079
commit ffb8b062c8
5 changed files with 118 additions and 19 deletions
+7 -5
View File
@@ -23,19 +23,21 @@ eingebunden.
## Aktueller Stand ## Aktueller Stand
### Implementierte Tools (v1.0) ### Implementierte Tools (v0.4.8)
| Kategorie | Tools | | Kategorie | Tools |
|---|---| |---|---|
| Kreise | `get_circles` | | Kreise | `get_circles`, `get_members` |
| Listen | `get_lists` | | Listen | `get_lists` |
| Tasks | `get_tasks` | | Tasks (Lesen) | `get_tasks`, `get_categories` |
| Wall | `get_activities`, `like_post` |
| Tasks (Schreiben) | `create_task`, `update_task`, `toggle_task`, `delete_task` |
## Roadmap ## Roadmap
- v1.0: Lesezugriff (Kreise + Listen + Tasks) ← aktuell - v0.x: Erweiterter Lese- + Schreibzugriff ← aktuell
- v2.0: Schreibzugriff (Tasks anlegen, abhaken, löschen) - Offen: Unlike (`like_post(like=False)`), Task-Kategorie-Zuweisung beim Erstellen/Aktualisieren
## Referenzprojekt ## Referenzprojekt
+3 -2
View File
@@ -2,7 +2,7 @@
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, and tasks directly from Claude. MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, and tasks directly from Claude.
## Features (v0.4.7) ## Features (v0.4.8)
### Read ### Read
@@ -10,7 +10,8 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your
- `get_members` -- list members of a circle (or all circles) - `get_members` -- list members of a circle (or all circles)
- `get_lists` -- list all task lists (optionally filtered by circle) - `get_lists` -- list all task lists (optionally filtered by circle)
- `get_tasks` -- list tasks in a specific list - `get_tasks` -- list tasks in a specific list
- `get_activities` -- list recent wall activities - `get_categories` -- list categories available for a list
- `get_activities` -- list recent wall activities (author resolved to display name)
### Write (with confirmation prompt) ### Write (with confirmation prompt)
+30 -10
View File
@@ -133,18 +133,38 @@ Hinweis: `a03call=tasklistsync` ist **kein gültiger Endpoint** — API antworte
**Response-Struktur (verifiziert):** **Response-Struktur (verifiziert):**
``` ```
a00 → famlistfamily-Daten (Kreise) Nebeneffekt, nicht verwendet a00 → famlistfamily-Daten (Kreise inkl. members[]) Nebeneffekt
a01.r.r.updatedCreated[] → taskcategorysync (Einkaufskategorien/Abteilungen) a01.r.r.updatedCreated[] → taskcategorysync (Kategorien/Abteilungen pro Liste)
.sortingIndexByTaskList → dict, Keys = Listen-IDs (z.B. "taskList/23431854_29740942") .metaId → Kategorie-ID (Format taskCategory/<familyId>_<sysId>)
Quelle der Listen-IDs (Namen/Zähler noch unbekannt) .nameKategoriename (sprachabhängig, z.B. "Beverages")
a02.r.r.updatedCreated[] → tasksync (Tasks) .emoji → Emoji-Symbol der Kategorie
.metaId → eindeutige Task-ID .systemCategoryId → numerische System-ID (sprach-unabhängig)
.text Aufgabentext .taskListTypeListentyp (z.B. "SHOPPING_LIST", "TODO")
.description → optionale Beschreibung .sortingIndexByTaskList → dict: Listen-ID → Sortierposition
.taskListId → Zugehörigkeit zur Liste (= Listen-ID aus sortingIndexByTaskList) Keys = Listen-IDs der zugeordneten Listen
.complete"true" / "false" (String, nicht Boolean!) .locale Sprache des Namens (z.B. "de", "en", "ru")
.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[] → zugewiesene Kategorien des Tasks
.system → "true" wenn System-Kategorie, "false" wenn custom
.name → Kategoriename (z.B. "SYS-CAT-TODOS", "Beverages")
.assignee / .assigneeIds → zugewiesene Mitglieder
.reminder → Erinnerungsdatum (ISO 8601, optional)
.recurrency → Wiederholungsregel (optional)
.sortingIndex → Anzeigereihenfolge
``` ```
**Kategorie-Zuweisung bei taskcreate2 / taskupdate2:** Parameter-Name noch unbekannt.
Getestete Varianten (`taskCategoryId`, `categoryId`, `category`, `categoryMetaId`) werden
serverseitig ignoriert — die Task bekommt stets die Default-Systemkategorie der Liste.
Service Worker in der Web-App verhindert Inspektion des echten Requests. Noch zu verifizieren.
## Systembezeichnungen für Listen-Namen ## Systembezeichnungen für Listen-Namen
Bekannte Systembezeichnungen werden deutsch übersetzt: Bekannte Systembezeichnungen werden deutsch übersetzt:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mcp-familywall" name = "mcp-familywall"
version = "0.4.7" version = "0.4.8"
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"
+77 -1
View File
@@ -331,6 +331,67 @@ def get_tasks(list_id: str, only_open: bool = True):
return json.dumps(result, ensure_ascii=False, indent=2) return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: get_categories
# ---------------------------------------------------------------------------
@mcp.tool()
def get_categories(list_id: str) -> str:
"""Return the task categories available for a list as JSON.
Categories are loaded from taskcategorysync (part of accgetallfamily) and
filtered to those assigned to the given list via sortingIndexByTaskList.
Shopping lists have predefined system categories (aisles); TODO lists
typically only carry a single default system category.
Note: the parameter name for assigning a category when creating or
updating a task is not yet verified via FW_DEBUG=1. See SPEC.md.
Args:
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
Returns:
JSON list of {id, name, emoji} objects ordered by sortingIndex.
"""
try:
data = _accgetallfamily()
except RuntimeError as exc:
return f"Error: {exc}"
try:
raw_cats = data["a01"]["r"]["r"]["updatedCreated"]
if not isinstance(raw_cats, list):
raise TypeError("a01.r.r.updatedCreated is not a list")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected taskcategorysync response structure", "raw": data.get("a01")},
ensure_ascii=False,
indent=2,
)
# Collect categories assigned to this list, ordered by their sorting index.
matched: list[tuple[int, dict[str, Any]]] = []
seen: set[str] = set()
for cat in raw_cats:
sorting = cat.get("sortingIndexByTaskList") or {}
if list_id not in sorting:
continue
cat_id: str = cat.get("metaId", "")
if cat_id in seen:
continue
seen.add(cat_id)
sort_val = int(sorting.get(list_id, cat.get("initialSortingIndex", 0)) or 0)
matched.append((sort_val, cat))
matched.sort(key=lambda t: t[0])
result = [
{"id": cat.get("metaId"), "name": cat.get("name"), "emoji": cat.get("emoji")}
for _, cat in matched
]
return json.dumps(result, ensure_ascii=False, indent=2)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Tool: get_activities # Tool: get_activities
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -344,6 +405,19 @@ def get_activities(limit: int = 20):
except RuntimeError as exc: except RuntimeError as exc:
return f"Error: {exc}" return f"Error: {exc}"
# Load member data to resolve author IDs to display names.
# Non-fatal: fall back to raw account IDs when member lookup fails.
author_map: dict[str, str] = {}
try:
for circle in _famlistfamily():
for member in circle.get("members") or []:
acc_id: str = member.get("accountId", "")
display = member.get("firstName") or member.get("name") or acc_id
if acc_id:
author_map[acc_id] = display
except RuntimeError:
pass
try: try:
with FamilyWallClient() as client: with FamilyWallClient() as client:
client.login(email, password) client.login(email, password)
@@ -375,13 +449,15 @@ def get_activities(limit: int = 20):
result = [] result = []
for item in raw_activities: for item in raw_activities:
raw_author: str = item.get("accountId", "")
result.append( result.append(
{ {
"id": item.get("metaId"), "id": item.get("metaId"),
"type": item.get("refType"), "type": item.get("refType"),
"text": item.get("text"), "text": item.get("text"),
"date": item.get("creationDate"), "date": item.get("creationDate"),
"author": item.get("accountId"), "author": author_map.get(raw_author, raw_author),
"author_id": raw_author,
} }
) )