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
+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
# ---------------------------------------------------------------------------