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:
@@ -23,19 +23,21 @@ eingebunden.
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
### Implementierte Tools (v1.0)
|
||||
### Implementierte Tools (v0.4.8)
|
||||
|
||||
| Kategorie | Tools |
|
||||
|---|---|
|
||||
| Kreise | `get_circles` |
|
||||
| Kreise | `get_circles`, `get_members` |
|
||||
| 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
|
||||
|
||||
- v1.0: Lesezugriff (Kreise + Listen + Tasks) ← aktuell
|
||||
- v2.0: Schreibzugriff (Tasks anlegen, abhaken, löschen)
|
||||
- v0.x: Erweiterter Lese- + Schreibzugriff ← aktuell
|
||||
- Offen: Unlike (`like_post(like=False)`), Task-Kategorie-Zuweisung beim Erstellen/Aktualisieren
|
||||
|
||||
|
||||
## Referenzprojekt
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Features (v0.4.7)
|
||||
## Features (v0.4.8)
|
||||
|
||||
### 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_lists` -- list all task lists (optionally filtered by circle)
|
||||
- `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)
|
||||
|
||||
|
||||
@@ -133,18 +133,38 @@ Hinweis: `a03call=tasklistsync` ist **kein gültiger Endpoint** — API antworte
|
||||
|
||||
**Response-Struktur (verifiziert):**
|
||||
```
|
||||
a00 → famlistfamily-Daten (Kreise) – Nebeneffekt, nicht verwendet
|
||||
a01.r.r.updatedCreated[] → taskcategorysync (Einkaufskategorien/Abteilungen)
|
||||
.sortingIndexByTaskList → dict, Keys = Listen-IDs (z.B. "taskList/23431854_29740942")
|
||||
→ Quelle der Listen-IDs (Namen/Zähler noch unbekannt)
|
||||
a02.r.r.updatedCreated[] → tasksync (Tasks)
|
||||
.metaId → eindeutige Task-ID
|
||||
.text → Aufgabentext
|
||||
.description → optionale Beschreibung
|
||||
.taskListId → Zugehörigkeit zur Liste (= Listen-ID aus sortingIndexByTaskList)
|
||||
.complete → "true" / "false" (String, nicht Boolean!)
|
||||
a00 → famlistfamily-Daten (Kreise inkl. members[]) – Nebeneffekt
|
||||
a01.r.r.updatedCreated[] → taskcategorysync (Kategorien/Abteilungen pro Liste)
|
||||
.metaId → Kategorie-ID (Format taskCategory/<familyId>_<sysId>)
|
||||
.name → Kategoriename (sprachabhängig, z.B. "Beverages")
|
||||
.emoji → Emoji-Symbol der Kategorie
|
||||
.systemCategoryId → numerische System-ID (sprach-unabhängig)
|
||||
.taskListType → Listentyp (z.B. "SHOPPING_LIST", "TODO")
|
||||
.sortingIndexByTaskList → dict: Listen-ID → Sortierposition
|
||||
Keys = Listen-IDs der zugeordneten Listen
|
||||
.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
|
||||
|
||||
Bekannte Systembezeichnungen werden deutsch übersetzt:
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
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"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -331,6 +331,67 @@ def get_tasks(list_id: str, only_open: bool = True):
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -344,6 +405,19 @@ def get_activities(limit: int = 20):
|
||||
except RuntimeError as 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:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
@@ -375,13 +449,15 @@ def get_activities(limit: int = 20):
|
||||
|
||||
result = []
|
||||
for item in raw_activities:
|
||||
raw_author: str = item.get("accountId", "")
|
||||
result.append(
|
||||
{
|
||||
"id": item.get("metaId"),
|
||||
"type": item.get("refType"),
|
||||
"text": item.get("text"),
|
||||
"date": item.get("creationDate"),
|
||||
"author": item.get("accountId"),
|
||||
"author": author_map.get(raw_author, raw_author),
|
||||
"author_id": raw_author,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user