feat(circles): add update_circle tool (v0.7.4)

Implements accupdatefamily endpoint (verified via FW_DEBUG=1):
- Parameter 'scope' targets any circle (primary or secondary)
- Without scope: renames the primary circle (API default)
- Server always capitalises the first letter of the new name
- Verifies circle existence via famlistfamily in same session
- Response a00.r.r = full circle object with updated name

Also corrects SPEC.md: accupdatefamily with scope= works for any
circle, not just the primary (previous note was incomplete).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-16 21:33:07 +02:00
parent d144a77662
commit 02f9d62720
6 changed files with 120 additions and 10 deletions
+90
View File
@@ -1342,6 +1342,96 @@ def create_circle(name: str) -> str:
)
# ---------------------------------------------------------------------------
# Tool: update_circle
# ---------------------------------------------------------------------------
@mcp.tool()
def update_circle(circle_id: str, name: str) -> str:
"""Rename a Family Wall circle.
IMPORTANT: Ask the user for confirmation before calling this tool.
Only the circle name can be changed via the API. Note that the server
always capitalises the first letter of the new name.
Args:
circle_id: Circle metaId from ``get_circles``
(e.g. ``"family/23431854"``).
name: New display name for the circle.
Returns:
JSON with ``{updated, id, name}`` on success, or an error message.
"""
if not name or not name.strip():
return "Error: 'name' must not be empty."
try:
email, password = get_credentials()
except RuntimeError as exc:
return f"Error: {exc}"
try:
with FamilyWallClient() as client:
client.login(email, password)
# Verify the circle exists before attempting the update.
circles_data = client.call("famlistfamily")
try:
raw_circles: list[dict[str, Any]] = circles_data["a00"]["r"]["r"]
if not isinstance(raw_circles, list):
raise TypeError("a00.r.r is not a list")
except (KeyError, TypeError):
client.logout()
return "Error: Unexpected famlistfamily response structure."
target = next((c for c in raw_circles if c.get("metaId") == circle_id), None)
if target is None:
client.logout()
available = [c.get("metaId") for c in raw_circles]
return json.dumps(
{
"error": f"Circle not found: {circle_id!r}",
"available_circles": available,
},
ensure_ascii=False,
indent=2,
)
# Rename the circle.
# Verified: accupdatefamily with scope=<circle_metaId> targets any circle,
# both primary and secondary. The server capitalises the first letter.
resp = client.call("accupdatefamily", {"scope": circle_id, "name": name})
client.logout()
except FamilyWallError as exc:
return f"Error: Family Wall API error: {exc}"
except Exception as exc:
return f"Error: Connection error: {exc}"
# Response: a00.r.r = full circle object
try:
circle_obj: dict[str, Any] = resp["a00"]["r"]["r"]
if not isinstance(circle_obj, dict) or "metaId" not in circle_obj:
raise TypeError("unexpected shape")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected accupdatefamily response structure", "raw": resp},
ensure_ascii=False,
indent=2,
)
return json.dumps(
{
"updated": True,
"id": circle_obj.get("metaId"),
"name": circle_obj.get("name"),
},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: add_member_to_circle
# ---------------------------------------------------------------------------