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.
|
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
|
### Read
|
||||||
|
|
||||||
- `get_circles` -- list all family circles
|
- `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_lists` -- list all task lists (optionally filtered by circle)
|
||||||
- `get_tasks` -- list tasks in a specific list
|
- `get_tasks` -- list tasks in a specific list
|
||||||
- `get_activities` -- list recent wall activities
|
- `get_activities` -- list recent wall activities
|
||||||
|
|||||||
@@ -83,8 +83,29 @@ Content-Type: application/x-www-form-urlencoded
|
|||||||
**Response-Struktur (verifiziert):**
|
**Response-Struktur (verifiziert):**
|
||||||
```
|
```
|
||||||
a00.r.r[] → Kreise
|
a00.r.r[] → Kreise
|
||||||
.metaId → eindeutige Kreis-ID
|
.metaId → eindeutige Kreis-ID (Format family/<id>)
|
||||||
.name → Kreisname
|
.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
|
### `taskgettasklists` – Listen abrufen
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcp-familywall"
|
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"
|
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+100
-11
@@ -121,32 +121,121 @@ def _extract_tasks(data: dict[str, Any]) -> list[dict[str, Any]]:
|
|||||||
def get_circles():
|
def get_circles():
|
||||||
"""Return all Family Wall circles as JSON list of {id, name}."""
|
"""Return all Family Wall circles as JSON list of {id, name}."""
|
||||||
try:
|
try:
|
||||||
email, password = get_credentials()
|
raw_circles = _famlistfamily()
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
return f"Error: {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:
|
try:
|
||||||
with FamilyWallClient() as client:
|
with FamilyWallClient() as client:
|
||||||
client.login(email, password)
|
client.login(email, password)
|
||||||
data = client.call("famlistfamily")
|
data = client.call("famlistfamily")
|
||||||
client.logout()
|
client.logout()
|
||||||
except FamilyWallError as exc:
|
except FamilyWallError as exc:
|
||||||
return f"Error: {exc}"
|
raise RuntimeError(f"Family Wall API error: {exc}") from exc
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Connection error: {exc}"
|
raise RuntimeError(f"Connection error: {exc}") from exc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_circles = data["a00"]["r"]["r"]
|
raw = data["a00"]["r"]["r"]
|
||||||
if not isinstance(raw_circles, list):
|
if not isinstance(raw, list):
|
||||||
raise TypeError("a00.r.r is not a list")
|
raise TypeError("a00.r.r is not a list")
|
||||||
except (KeyError, TypeError):
|
return raw
|
||||||
return json.dumps(
|
except (KeyError, TypeError) as exc:
|
||||||
{"warning": "Unexpected famlistfamily response structure", "raw": data},
|
raise RuntimeError(f"Unexpected famlistfamily response structure: {exc}") from exc
|
||||||
ensure_ascii=False,
|
|
||||||
indent=2,
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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)
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user