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
|
## 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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
.name → Kategoriename (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
|
.taskListType → Listentyp (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
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user