feat(lists): add update_list tool (v0.7.3)

Implements taskupdatelist endpoint (verified via FW_DEBUG=1):
- Parameter 'metaId' (not 'id') identifies the list
- Partial update: only provided fields (name/color/emoji) are changed
- Reads rights.canUpdate before calling the endpoint (single session)
- System lists (canUpdate != 'true') are rejected with a clear error
- Scope derived from list metaId for secondary-circle support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 20:12:28 +02:00
parent dc21416a61
commit d144a77662
6 changed files with 169 additions and 8 deletions
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.7.2"
__version__ = "0.7.3"
+126
View File
@@ -1140,6 +1140,132 @@ def delete_list(list_id: str) -> str:
)
# ---------------------------------------------------------------------------
# Tool: update_list
# ---------------------------------------------------------------------------
@mcp.tool()
def update_list(
list_id: str,
name: str | None = None,
color: str | None = None,
emoji: str | None = None,
) -> str:
"""Update a task list's name, color, or emoji.
IMPORTANT: Ask the user for confirmation before calling this tool.
Performs a partial update — only the fields you provide are changed.
The current values for any omitted fields are preserved on the server
(the server keeps them; no need to fetch and re-send them).
Only user-created lists with ``rights.canUpdate="true"`` can be updated.
System lists are protected and this tool will refuse to update them.
Args:
list_id: List metaId from get_lists
(e.g. ``"taskList/23431854_29759623"``).
name: New display name (omit to keep existing).
color: New background colour as a hex string (e.g. ``"#E53935"``).
Omit to keep existing.
emoji: New Unicode emoji icon (e.g. ``"🧪"``).
Omit to keep existing.
Returns:
JSON with the updated list object on success, or an error message.
"""
if name is None and color is None and emoji is None:
return "Error: At least one of 'name', 'color', or 'emoji' must be provided."
try:
email, password = get_credentials()
except RuntimeError as exc:
return f"Error: {exc}"
# Derive the owning circle from the list metaId (same as delete_list).
circle_scope = _circle_id_from_list_id(list_id)
list_obj: dict[str, Any] | None = None
try:
with FamilyWallClient() as client:
client.login(email, password)
# Fetch list to verify rights.canUpdate before updating.
get_params: dict[str, Any] = {}
if circle_scope:
get_params["scope"] = circle_scope
raw = client.call("taskgettasklists", get_params)
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_update: str | None = (list_obj.get("rights") or {}).get("canUpdate")
if can_update != "true":
client.logout()
return json.dumps(
{
"error": "System lists cannot be updated.",
"id": list_id,
"name": list_obj.get("name"),
"hint": "Only user-created lists (rights.canUpdate=true) can be updated.",
},
ensure_ascii=False,
indent=2,
)
# Build update params — only include provided fields.
# Verified: taskupdatelist uses 'metaId' (not 'id') as the list identifier.
upd_params: dict[str, Any] = {"metaId": list_id}
if name is not None:
upd_params["name"] = name
if color is not None:
upd_params["color"] = color
if emoji is not None:
upd_params["emoji"] = emoji
resp = client.call("taskupdatelist", upd_params)
client.logout()
except FamilyWallError as exc:
return f"Error: Family Wall API error: {exc}"
except Exception as exc:
return f"Error: Connection error: {exc}"
try:
updated_obj: dict[str, Any] = resp["a00"]["r"]["r"]
if not isinstance(updated_obj, dict) or "metaId" not in updated_obj:
raise TypeError("unexpected shape")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected taskupdatelist response structure", "raw": resp},
ensure_ascii=False,
indent=2,
)
raw_emoji: str = updated_obj.get("emoji", "")
return json.dumps(
{
"updated": True,
"id": updated_obj.get("metaId"),
"name": translate_name(updated_obj.get("name", "")),
"type": updated_obj.get("taskListType"),
"emoji": raw_emoji if raw_emoji else None,
"color": updated_obj.get("color") or None,
"circle_id": updated_obj.get("familyId"),
},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: create_circle
# ---------------------------------------------------------------------------