feat: create_category + delete_category tools (v0.4.11)

Verified via systematic FW_DEBUG=1 probing:
- taskcategoryput: requires 'name'; optional 'emoji' (Unicode or string code)
  accepted as-is. 'listId' param has no per-list effect — categories are
  family-wide.
- taskcategorydelete: uses 'id' param (not 'metaId'), returns r='true'.

Changes:
- create_category(list_id, name, icon=None): creates custom category via
  taskcategoryput; icon maps to 'emoji' API param
- delete_category(category_id): safety check via accgetallfamily looks up
  rights.canDelete='true'; system categories (rights.canDelete=null) are
  refused with a clear error
- get_categories: now exposes 'custom' bool field (rights.canDelete='true')
  so callers can identify deletable categories
- SPEC.md: document taskcategoryput + taskcategorydelete params, responses,
  error formats, and system-category protection behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 07:43:54 +02:00
parent a76dc0fd51
commit 5698196c43
5 changed files with 205 additions and 8 deletions
+3 -2
View File
@@ -23,15 +23,16 @@ eingebunden.
## Aktueller Stand
### Implementierte Tools (v0.4.10)
### Implementierte Tools (v0.4.11)
| Kategorie | Tools |
|---|---|
| Kreise | `get_circles`, `get_members` |
| Listen | `get_lists` |
| Tasks (Lesen) | `get_tasks` (inkl. `category_id`), `get_categories` |
| Tasks (Lesen) | `get_tasks` (inkl. `category_id`), `get_categories` (inkl. `custom`-Flag) |
| Wall | `get_activities`, `like_post` |
| Tasks (Schreiben) | `create_task` (inkl. `category_id`), `update_task` (inkl. `category_id`), `toggle_task`, `delete_task` |
| Kategorien (Schreiben) | `create_category` (inkl. `icon`), `delete_category` (System-Kategorien geschützt) |
## Roadmap
+5 -3
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.
## Features (v0.4.10)
## Features (v0.4.11)
### Read
@@ -10,7 +10,7 @@ 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 (includes `category_id` field)
- `get_categories` -- list categories available for a list (locale-filtered, default: German)
- `get_categories` -- list categories for a list (locale-filtered; `custom` flag marks user-created ones)
- `get_activities` -- list recent wall activities (author resolved to display name)
### Write (with confirmation prompt)
@@ -19,7 +19,9 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your
- `update_task` -- update text, description, and/or category of an existing task
- `toggle_task` -- mark a task complete or reopen it
- `delete_task` -- permanently delete a task
- `like_post` -- like or unlike a wall post/activity
- `create_category` -- create a custom category for a shopping list (with optional icon)
- `delete_category` -- delete a custom category (system categories are protected)
- `like_post` -- like a wall post/activity
## Requirements
+61
View File
@@ -331,6 +331,65 @@ mit `{"a00": {"un": {"un": {"message": "missing value in: id"}}}}` auf Top-Level
→ wird vom fw_client fälschlich als Erfolg interpretiert. Daher ist der korrekte Parameter-Name
kritisch.
### `taskcategoryput` Kategorie erstellen
POST https://api.familywall.com/api/taskcategoryput
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert via FW_DEBUG=1):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `name` | ja | Kategorie-Name (beliebiger String) |
| `emoji` | nein | Icon: Unicode-Emoji-Zeichen (z.B. `🌿`) oder beliebiger String-Code (z.B. `"FOOD"`) — wird as-is gespeichert |
Hinweise:
- Die neue Kategorie wird **allen** Listen der Familie zugeordnet — es gibt keine per-Liste-Einschränkung.
- Benutzerdefinierte Kategorien haben `systemCategoryId=null` und `rights.canDelete='true'`.
- System-Kategorien haben `rights.canDelete=null` — API erlaubt Löschen, aber `delete_category` Tool verweigert es.
**Response-Struktur (verifiziert):**
```
a00.r.r → vollständiges Kategorie-Objekt
.metaId → neue Kategorie-ID (z.B. "taskCategory/23431854_4956637")
.name → Kategorie-Name
.taskListType → "SHOPPING_LIST" (automatisch gesetzt)
.familyId → Familien-ID
.accountId → Account-ID des Erstellers
.rights.canDelete → "true" (custom Kategorien)
.rights.canUpdate → "true" (custom Kategorien)
.emoji → gespeicherter Icon-Wert (falls übergeben)
```
**Fehlerverhalten:** Ohne `name`-Parameter:
```json
{"a00": {"un": {"un": {"FiZClassId": "502", "message": "cat without a name ..."}}}}
```
### `taskcategorydelete` Kategorie löschen
POST https://api.familywall.com/api/taskcategorydelete
Content-Type: application/x-www-form-urlencoded
**Body-Parameter (verifiziert via FW_DEBUG=1):**
| Parameter | Pflicht | Wert |
|---|---|---|
| `id` | ja | Kategorie-MetaId aus `get_categories` (**WICHTIG: `id`, nicht `metaId`!**) |
**Achtung:** Falscher Parameter-Name `metaId` führt zu:
```json
{"a00": {"un": {"un": {"FiZClassId": "502", "message": "In request, missing value in : id"}}}}
```
**Response-Struktur (verifiziert):**
```
a00.r.r → "true" (String)
```
**Wichtig System-Kategorien:** Die API erlaubt technisch das Löschen von System-Kategorien
(`taskCategory/<familyId>_200` etc.), entfernt sie aber nur aus der Familie — nicht global.
Das `delete_category`-MCP-Tool verweigert dies (Schutz via `rights.canDelete`-Check).
Erkennung: custom Kategorien haben `rights.canDelete='true'`; System-Kategorien haben `rights.canDelete=null`.
### `wallmood` Wall-Post liken
POST https://api.familywall.com/api/wallmood
Content-Type: application/x-www-form-urlencoded
@@ -427,3 +486,5 @@ AND `moodStarShortcut: false` AND `moodMap: {}`.
- ~~`wallmood`: `moodType`-Werte, Toggle vs. explizit, Response-Struktur~~ → verifiziert: idempotentes SET mit `"STAR"`, kein Toggle (siehe oben)
- `wallmood` Unlike: Mechanismus unbekannt — Service Worker verhindert Browser-Inspektion; alle getesteten Ansätze fehlgeschlagen (siehe oben)
- ~~`taskcreate2` / `taskupdate2`: Kategorie-Paramter-Name~~ → **`taskCategoryId`**, Wert = vollständige metaId (verifiziert)
- ~~`taskcategoryput`: Body-Parameter, Response-Struktur~~ → `name` (Pflicht), `emoji` (optional), Response = neues Kategorie-Objekt (verifiziert)
- ~~`taskcategorydelete`: Body-Parameter~~**`id`** (nicht `metaId`!), Response = `"true"` (verifiziert)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mcp-familywall"
version = "0.4.10"
version = "0.4.11"
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
readme = "README.md"
requires-python = ">=3.12"
+134 -1
View File
@@ -402,12 +402,145 @@ def get_categories(list_id: str, locale: str = "de") -> str:
matched.sort(key=lambda t: t[0])
result = [
{"id": cat.get("metaId"), "name": cat.get("name"), "emoji": cat.get("emoji")}
{
"id": cat.get("metaId"),
"name": cat.get("name"),
"emoji": cat.get("emoji"),
# custom=True means the category was created by the family and can be
# deleted with delete_category. System categories have no canDelete right.
"custom": cat.get("rights", {}).get("canDelete") == "true",
}
for _, cat in matched
]
return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: create_category
# ---------------------------------------------------------------------------
@mcp.tool()
def create_category(list_id: str, name: str, icon: str | None = None) -> str:
"""Create a new custom category for a shopping list.
IMPORTANT: Ask the user for confirmation before calling this tool.
Custom categories appear alongside system categories when assigning
categories to tasks via ``create_task`` or ``update_task``. They can
later be removed with ``delete_category``.
Note: although ``list_id`` is accepted for context, the Family Wall API
assigns new categories to all lists in the family — there is no
per-list restriction.
Args:
list_id: Target list ID from get_lists (e.g. ``taskList/123_456``).
Only SHOPPING_LIST lists have categories; the parameter is
accepted for user context but does not restrict category scope.
name: Display name for the new category (e.g. ``"Bio-Produkte"``).
icon: Optional icon for the category. Pass a Unicode emoji character
(e.g. ``"🌿"``) or any short string identifier. When omitted the
category has no icon.
Returns:
JSON with the new category's ``id`` and ``name`` on success, or an
error message.
"""
params: dict[str, Any] = {"name": name}
if icon:
params["emoji"] = icon
try:
data = _authenticated_call("taskcategoryput", params)
except RuntimeError as exc:
return f"Error: {exc}"
try:
cat_obj = data["a00"]["r"]["r"]
meta_id: str = cat_obj["metaId"]
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected taskcategoryput response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
return json.dumps(
{"created": True, "id": meta_id, "name": cat_obj.get("name", name)},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: delete_category
# ---------------------------------------------------------------------------
@mcp.tool()
def delete_category(category_id: str) -> str:
"""Permanently delete a custom category.
IMPORTANT: Ask the user for confirmation before calling this tool.
Only custom (user-created) categories can be deleted. System categories
supplied by Family Wall (identified by ``custom=false`` in
``get_categories`` output) are protected and this tool will refuse to
delete them.
Args:
category_id: Category metaId from get_categories
(e.g. ``taskCategory/23431854_4956637``). Must be a custom
category (``custom=true`` in get_categories output).
Returns:
JSON success indicator or an error message.
"""
# Safety check: look up the category and verify it is custom (canDelete=true).
# This prevents accidental deletion of shared system categories.
try:
data = _accgetallfamily()
except RuntimeError as exc:
return f"Error: {exc}"
try:
raw_cats: list[dict[str, Any]] = data["a01"]["r"]["r"]["updatedCreated"]
except (KeyError, TypeError):
raw_cats = []
cat_obj: dict[str, Any] | None = next(
(c for c in raw_cats if c.get("metaId") == category_id), None
)
if cat_obj is None:
return f"Error: Category '{category_id}' not found."
can_delete: str | None = cat_obj.get("rights", {}).get("canDelete")
if can_delete != "true":
return json.dumps(
{
"error": "System categories cannot be deleted.",
"id": category_id,
"name": cat_obj.get("name"),
"hint": "Only custom categories (custom=true in get_categories) can be deleted.",
},
ensure_ascii=False,
indent=2,
)
try:
_authenticated_call("taskcategorydelete", {"id": category_id})
except RuntimeError as exc:
return f"Error: {exc}"
return json.dumps(
{"deleted": True, "id": category_id, "name": cat_obj.get("name")},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: get_activities
# ---------------------------------------------------------------------------