feat: add get_members tool, refactor get_circles via _famlistfamily helper (v0.4.7)
famlistfamily response already contains members[] on each circle object. get_members(circle_id=None) extracts id, name, email, role, right, color, avatar, circle_id and circle_name. get_circles refactored to use the new _famlistfamily() helper, eliminating duplicated auth/call/logout logic. SPEC.md updated with full famlistfamily response structure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+101
-12
@@ -121,32 +121,121 @@ def _extract_tasks(data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
def get_circles():
|
||||
"""Return all Family Wall circles as JSON list of {id, name}."""
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
raw_circles = _famlistfamily()
|
||||
except RuntimeError as exc:
|
||||
return f"Error: {exc}"
|
||||
|
||||
result = [{"id": c.get("metaId"), "name": c.get("name")} for c in raw_circles]
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: famlistfamily raw circles
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _famlistfamily() -> list[dict[str, Any]]:
|
||||
"""Login, call famlistfamily, logout and return the raw circle list.
|
||||
|
||||
Raises:
|
||||
RuntimeError: On credential or API errors.
|
||||
"""
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
except RuntimeError as exc:
|
||||
raise RuntimeError(str(exc)) from exc
|
||||
|
||||
try:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
data = client.call("famlistfamily")
|
||||
client.logout()
|
||||
except FamilyWallError as exc:
|
||||
return f"Error: {exc}"
|
||||
raise RuntimeError(f"Family Wall API error: {exc}") from exc
|
||||
except Exception as exc:
|
||||
return f"Connection error: {exc}"
|
||||
raise RuntimeError(f"Connection error: {exc}") from exc
|
||||
|
||||
try:
|
||||
raw_circles = data["a00"]["r"]["r"]
|
||||
if not isinstance(raw_circles, list):
|
||||
raw = data["a00"]["r"]["r"]
|
||||
if not isinstance(raw, list):
|
||||
raise TypeError("a00.r.r is not a list")
|
||||
except (KeyError, TypeError):
|
||||
return json.dumps(
|
||||
{"warning": "Unexpected famlistfamily response structure", "raw": data},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
return raw
|
||||
except (KeyError, TypeError) as exc:
|
||||
raise RuntimeError(f"Unexpected famlistfamily response structure: {exc}") from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: get_members
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_members(circle_id: str | None = None) -> str:
|
||||
"""Return Family Wall circle members as JSON, optionally filtered by circle.
|
||||
|
||||
Args:
|
||||
circle_id: Optional circle ID from get_circles (e.g. ``family/23431854``).
|
||||
When omitted all members across all circles are returned.
|
||||
|
||||
Returns:
|
||||
JSON list of member objects with keys id, name, email, role, right,
|
||||
color, avatar, circle_id, circle_name.
|
||||
"""
|
||||
try:
|
||||
raw_circles = _famlistfamily()
|
||||
except RuntimeError as exc:
|
||||
return f"Error: {exc}"
|
||||
|
||||
result: list[dict[str, Any]] = []
|
||||
seen_ids: set[str] = set()
|
||||
|
||||
for circle in raw_circles:
|
||||
c_id: str = circle.get("metaId", "")
|
||||
c_name: str = circle.get("name", "")
|
||||
|
||||
if circle_id is not None and c_id != circle_id:
|
||||
continue
|
||||
|
||||
for member in circle.get("members") or []:
|
||||
account_id: str = member.get("accountId", "")
|
||||
|
||||
# Deduplicate members that appear in multiple circles when no
|
||||
# circle_id filter is active (same account can be in several circles).
|
||||
dedup_key = f"{c_id}:{account_id}"
|
||||
if dedup_key in seen_ids:
|
||||
continue
|
||||
seen_ids.add(dedup_key)
|
||||
|
||||
# Extract avatar URL from the first media entry (may be a generated
|
||||
# default avatar when pictureDefault is "true").
|
||||
medias = member.get("medias") or []
|
||||
avatar: str | None = medias[0].get("pictureUrl") if medias else None
|
||||
|
||||
# Prefer firstName as display name; fall back to the name field
|
||||
# (which Family Wall sets to the e-mail address by default).
|
||||
display_name: str = member.get("firstName") or member.get("name", "")
|
||||
|
||||
# Extract e-mail from identifiers list.
|
||||
email_value: str | None = None
|
||||
for ident in member.get("identifiers") or []:
|
||||
if ident.get("type") == "Email":
|
||||
email_value = ident.get("value")
|
||||
break
|
||||
|
||||
result.append(
|
||||
{
|
||||
"id": account_id,
|
||||
"name": display_name,
|
||||
"email": email_value,
|
||||
"role": member.get("role"),
|
||||
"right": member.get("right"),
|
||||
"color": member.get("color"),
|
||||
"avatar": avatar,
|
||||
"circle_id": c_id,
|
||||
"circle_name": c_name,
|
||||
}
|
||||
)
|
||||
|
||||
result = [{"id": c.get("metaId"), "name": c.get("name")} for c in raw_circles]
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user