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:
@@ -2,11 +2,12 @@
|
||||
|
||||
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, and tasks directly from Claude.
|
||||
|
||||
## Features (v0.4.6)
|
||||
## Features (v0.4.7)
|
||||
|
||||
### Read
|
||||
|
||||
- `get_circles` -- list all family circles
|
||||
- `get_members` -- list members of a circle (or all circles)
|
||||
- `get_lists` -- list all task lists (optionally filtered by circle)
|
||||
- `get_tasks` -- list tasks in a specific list
|
||||
- `get_activities` -- list recent wall activities
|
||||
|
||||
@@ -82,9 +82,30 @@ Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
**Response-Struktur (verifiziert):**
|
||||
```
|
||||
a00.r.r[] → Kreise
|
||||
.metaId → eindeutige Kreis-ID
|
||||
.name → Kreisname
|
||||
a00.r.r[] → Kreise
|
||||
.metaId → eindeutige Kreis-ID (Format family/<id>)
|
||||
.name → Kreisname
|
||||
.family_id → numerische Kreis-ID
|
||||
.members[] → Mitglieder des Kreises
|
||||
.accountId → numerische Account-ID
|
||||
.metaId → Mitglieds-ID (Format familymember/<accountId>_<familyId>)
|
||||
.firstName → Vorname (Display-Name; bevorzugen gegenüber .name)
|
||||
.name → E-Mail-Adresse (Family Wall Default wenn kein Anzeigename)
|
||||
.role → Familienrolle (z.B. "Unknown", "Parent", "Child")
|
||||
.right → Berechtigung (z.B. "SuperAdmin", "Admin", "Member")
|
||||
.color → Profilfarbe als Hex-String (z.B. "#FF8086")
|
||||
.medias[0].pictureUrl → Avatar-URL (generierter Default wenn pictureDefault=true)
|
||||
.identifiers[] → Kontaktdaten
|
||||
.type → Typ (z.B. "Email")
|
||||
.value → Wert (z.B. E-Mail-Adresse)
|
||||
.familyId → Zugehöriger Kreis (= metaId des Kreises)
|
||||
.isloggedaccount → "true" wenn das der angemeldete Account ist
|
||||
.joinDate → Beitrittsdatum (ISO 8601)
|
||||
.lastLoginDate → Letzter Login (ISO 8601)
|
||||
.locale → Spracheinstellung (z.B. "de_DE")
|
||||
.timeZone → Zeitzone (z.B. "Europe/Berlin")
|
||||
.invitations[] → Offene Einladungen (leer wenn keine)
|
||||
.coverUri → Cover-Bild URL
|
||||
```
|
||||
|
||||
### `taskgettasklists` – Listen abrufen
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mcp-familywall"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
+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