feat(lists): implement create_list + delete_list (v0.5.0)

Adds two new MCP tools:
- create_list(name, list_type, shared_to_all, color, emoji)
  POST taskcreatelist; returns full list object incl. metaId
- delete_list(list_id) – with canDelete safety guard
  POST taskdeletelist; param 'id' (same pattern as metadelete)

Both endpoints verified via FW_DEBUG=1 on 2026-04-16.
SPEC.md and CLAUDE.md updated with verified parameter names
and response structures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 12:26:16 +02:00
parent e76d80ece3
commit 311f37d72b
3 changed files with 204 additions and 3 deletions
+151
View File
@@ -869,6 +869,157 @@ def delete_task(task_id: str) -> str:
return json.dumps({"deleted": True, "id": task_id}, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: create_list
# ---------------------------------------------------------------------------
@mcp.tool()
def create_list(
name: str,
list_type: str,
shared_to_all: bool = True,
color: str | None = None,
emoji: str | None = None,
) -> str:
"""Create a new task list in Family Wall.
IMPORTANT: Ask the user for confirmation before calling this tool.
Args:
name: Display name for the new list (max 200 characters).
list_type: List type — either ``"SHOPPING_LIST"`` or ``"TODOS"``.
shared_to_all: When ``True`` (default) the list is shared with all
circle members. When ``False`` it is private to the creator.
color: Optional background colour as a hex string (e.g. ``"#4784EC"``).
emoji: Optional Unicode emoji to use as the list icon (e.g. ``"🛒"``).
Returns:
JSON with the new list object on success, or an error message.
"""
if list_type not in ("SHOPPING_LIST", "TODOS"):
return "Error: list_type must be 'SHOPPING_LIST' or 'TODOS'."
if len(name) > 200:
return "Error: name must not exceed 200 characters."
params: dict[str, Any] = {
"name": name,
"taskListType": list_type,
"sharedToAll": "true" if shared_to_all else "false",
}
if color:
params["color"] = color
if emoji:
params["emoji"] = emoji
try:
data = _authenticated_call("taskcreatelist", params)
except RuntimeError as exc:
return f"Error: {exc}"
try:
list_obj = data["a00"]["r"]["r"]
meta_id: str = list_obj["metaId"]
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected taskcreatelist response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
return json.dumps(
{
"created": True,
"id": meta_id,
"name": list_obj.get("name", name),
"type": list_obj.get("taskListType"),
"shared_to_all": list_obj.get("sharedToAll") == "true",
},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: delete_list
# ---------------------------------------------------------------------------
@mcp.tool()
def delete_list(list_id: str) -> str:
"""Permanently delete a task list and all its tasks.
IMPORTANT: Ask the user for confirmation before calling this tool.
This action cannot be undone. All tasks inside the list are also deleted.
Only lists with ``rights.canDelete="true"`` (user-created lists) can be
deleted. System lists are protected and this tool will refuse to delete them.
Args:
list_id: List metaId from get_lists
(e.g. ``"taskList/23431854_29759623"``). Must be a custom list
(``rights.canDelete="true"``).
Returns:
JSON success indicator or an error message.
"""
# Verify + delete in a single session to minimise round-trips.
try:
email, password = get_credentials()
except RuntimeError as exc:
return f"Error: {exc}"
list_obj: dict[str, Any] | None = None
try:
with FamilyWallClient() as client:
client.login(email, password)
# Fetch lists and verify the target can be deleted.
raw = client.call("taskgettasklists", {})
try:
raw_lists: list[dict[str, Any]] = raw["a00"]["r"]["r"]
if not isinstance(raw_lists, list):
raw_lists = []
except (KeyError, TypeError):
raw_lists = []
list_obj = next(
(lst for lst in raw_lists if lst.get("metaId") == list_id), None
)
if list_obj is None:
client.logout()
return f"Error: List '{list_id}' not found."
can_delete: str | None = (list_obj.get("rights") or {}).get("canDelete")
if can_delete != "true":
client.logout()
return json.dumps(
{
"error": "System lists cannot be deleted.",
"id": list_id,
"name": list_obj.get("name"),
"hint": "Only user-created lists (rights.canDelete=true) can be deleted.",
},
ensure_ascii=False,
indent=2,
)
# Verified — delete in the same session.
# taskdeletelist uses 'id' (same pattern as metadelete / taskcategorydelete).
client.call("taskdeletelist", {"id": list_id})
client.logout()
except FamilyWallError as exc:
return f"Error: Family Wall API error: {exc}"
except Exception as exc:
return f"Error: Connection error: {exc}"
return json.dumps(
{"deleted": True, "id": list_id, "name": list_obj.get("name")},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: like_post
# ---------------------------------------------------------------------------