feat(circles): create_circle + add_member_to_circle (v0.7.0)
- acccreatefamily endpoint creates a new circle (returns numeric ID) - accinvite endpoint invites new users by email (familyId, identifier, role, firstname) - fw_client now detects a00.ex errors (was only checking a00.un before) - New modules/circles.py with FamilyRoleTypeEnum constants - SPEC.md updated with acccreatefamily, accinvite, accupdatefamily docs - Note: circle deletion not supported by FW API (metadelete → "delete not supported") - Note: accinvite only works for new (non-existing) FW accounts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ und wird in Claude Desktop eingebunden.
|
|||||||
|
|
||||||
## Aktueller Stand
|
## Aktueller Stand
|
||||||
|
|
||||||
### Implementierte Tools (v0.6.1)
|
### Implementierte Tools (v0.7.0)
|
||||||
|
|
||||||
| Kategorie | Tools |
|
| Kategorie | Tools |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -34,6 +34,7 @@ und wird in Claude Desktop eingebunden.
|
|||||||
| Kategorien | `create_category`, `delete_category` |
|
| Kategorien | `create_category`, `delete_category` |
|
||||||
| Aktivitäten | `like_post` |
|
| Aktivitäten | `like_post` |
|
||||||
| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe` |
|
| Rezepte | `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe` |
|
||||||
|
| Kreise | `create_circle`, `add_member_to_circle` |
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
@@ -43,10 +44,11 @@ und wird in Claude Desktop eingebunden.
|
|||||||
- v0.5.2: Mengenkonvention im create_task Docstring ✓
|
- v0.5.2: Mengenkonvention im create_task Docstring ✓
|
||||||
- v0.5.3: Kategorie-Auto-Assign-Hinweis im create_task Docstring ✓ (nachgeliefert in v0.6.1)
|
- v0.5.3: Kategorie-Auto-Assign-Hinweis im create_task Docstring ✓ (nachgeliefert in v0.6.1)
|
||||||
- v0.6.0: Rezept-Box (get_recipes, get_recipe, create_recipe, delete_recipe) ✓
|
- v0.6.0: Rezept-Box (get_recipes, get_recipe, create_recipe, delete_recipe) ✓
|
||||||
- v0.6.1: update_recipe + Bugfix Zeilenumbrüche in create_recipe ✓ ← aktuell
|
- v0.6.1: update_recipe + Bugfix Zeilenumbrüche in create_recipe ✓
|
||||||
- v0.6.2: mpadditemtolist (Zutaten → Einkaufsliste)
|
- v0.7.0: create_circle + add_member_to_circle ✓ ← aktuell
|
||||||
|
- v0.7.1: mpadditemtolist (Zutaten → Einkaufsliste)
|
||||||
- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung
|
- v0.5.3: update_list (Umbenennen, emoji/color ändern), Sharing-Verwaltung
|
||||||
- v0.7.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
|
- v0.8.x: Erinnerungen + Wiederholungen (Premium-Account erforderlich)
|
||||||
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
|
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
|
||||||
|
|
||||||
|
|
||||||
@@ -136,6 +138,9 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
|||||||
| `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! |
|
| `mprecipeput` | `recipe.name`, `recipe.isRecipe="true"`, `recipe.description`, `recipe.ingredients`, `recipe.instructions`, `recipe.prepTime`, `recipe.cookTime`, `recipe.serves`, `recipe.url` | Alle mit `recipe.`-Prefix! |
|
||||||
| `mprecipeput` (Update) | zusätzlich `recipe.metaId` | Vorhandene ID → Update statt Create |
|
| `mprecipeput` (Update) | zusätzlich `recipe.metaId` | Vorhandene ID → Update statt Create |
|
||||||
| `metasync` (Rezepte lesen) | `id="recipe"` | liefert `a00.r.r.updatedCreated[]` |
|
| `metasync` (Rezepte lesen) | `id="recipe"` | liefert `a00.r.r.updatedCreated[]` |
|
||||||
|
| `acccreatefamily` | `name` | liefert numerische Kreis-ID als String in `a00.r.r` |
|
||||||
|
| `accinvite` | `familyId`, `identifier`, `role="Unknown"`, `firstname` | nur für neue FW-Accounts |
|
||||||
|
| `accupdatefamily` | `name` | aktualisiert immer den PRIMARY Kreis (ignoriert `id`/`familyId`) |
|
||||||
|
|
||||||
### Self-Like-Restriction
|
### Self-Like-Restriction
|
||||||
Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts.
|
Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude.
|
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, tasks, and recipes directly from Claude.
|
||||||
|
|
||||||
## Features (v0.6.1)
|
## Features (v0.7.0)
|
||||||
|
|
||||||
### Read
|
### Read
|
||||||
|
|
||||||
@@ -29,6 +29,8 @@ MCP server for [Family Wall](https://www.familywall.com) -- read and manage your
|
|||||||
- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url); use `\n` to separate ingredient lines
|
- `create_recipe` -- create a new recipe (name, description, ingredients, instructions, prep/cook time, serves, url); use `\n` to separate ingredient lines
|
||||||
- `update_recipe` -- update any field of an existing recipe (partial update — omitted fields unchanged)
|
- `update_recipe` -- update any field of an existing recipe (partial update — omitted fields unchanged)
|
||||||
- `delete_recipe` -- permanently delete a recipe (only own recipes)
|
- `delete_recipe` -- permanently delete a recipe (only own recipes)
|
||||||
|
- `create_circle` -- create a new Family Wall circle (group); note: circles cannot be deleted via API
|
||||||
|
- `add_member_to_circle` -- invite a person to a circle by e-mail (for new Family Wall users only; existing accounts require in-app invitation)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -491,11 +491,80 @@ a00.r.r → "true" (String)
|
|||||||
|
|
||||||
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
|
### `acccreatefamily` – Kreis erstellen
|
||||||
|
POST https://api.familywall.com/api/acccreatefamily
|
||||||
|
|
||||||
|
**Body-Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Pflicht | Wert |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | ja | Kreis-Name |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```
|
||||||
|
a00.r.r → numerische Kreis-ID als String (z.B. "23447369")
|
||||||
|
→ vollständige metaId: "family/" + id
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweise:**
|
||||||
|
- Der Server kapitalisiert den ersten Buchstaben des Namens.
|
||||||
|
- Kreise können über die API nicht gelöscht werden (`metadelete` → "delete not supported").
|
||||||
|
- Vollständige Kreisdaten (incl. Name) via `famlistfamily` im gleichen Session-Call abrufbar.
|
||||||
|
|
||||||
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
|
### `accinvite` – Mitglied einladen
|
||||||
|
POST https://api.familywall.com/api/accinvite
|
||||||
|
|
||||||
|
**Body-Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Pflicht | Wert |
|
||||||
|
|---|---|---|
|
||||||
|
| `familyId` | ja | Kreis-metaId (z.B. `"family/23447369"`) |
|
||||||
|
| `identifier` | ja | E-Mail-Adresse des Eingeladenen |
|
||||||
|
| `role` | ja | `"Unknown"` (einziger verifizierter Enum-Wert für `FamilyRoleTypeEnum`) |
|
||||||
|
| `firstname` | ja | Vorname des Eingeladenen |
|
||||||
|
|
||||||
|
**Einschränkung:** Der Endpoint funktioniert nur für Personen, die noch kein
|
||||||
|
Family Wall-Konto haben. Bei bestehenden Accounts antwortet der Server mit:
|
||||||
|
`"This api is now only used to create and invite an account with login only."`
|
||||||
|
|
||||||
|
**Fehler-Struktur** (HTTP 200, aber Fehler unter `a00.ex`):**
|
||||||
|
```json
|
||||||
|
{"a00": {"ex": {"ex": {"FiZClassId": "17", "message": "..."}}}}
|
||||||
|
```
|
||||||
|
→ `fw_client.py` prüft `a00.ex` und wirft `FamilyWallError`.
|
||||||
|
|
||||||
|
**Response (Erfolg):**
|
||||||
|
```
|
||||||
|
a00.r.r → Einladungsobjekt (Struktur unbekannt, da nicht testbar mit Test-Account)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bekannte Enum-Werte für `role`:**
|
||||||
|
- `"Unknown"` ✓ verifiziert
|
||||||
|
- `"Parent"`, `"Child"` → `FamilyRoleTypeEnum` decode error
|
||||||
|
|
||||||
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
|
### `accupdatefamily` – Kreis umbenennen
|
||||||
|
POST https://api.familywall.com/api/accupdatefamily
|
||||||
|
|
||||||
|
**Body-Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Pflicht | Wert |
|
||||||
|
|---|---|---|
|
||||||
|
| `name` | ja | Neuer Kreis-Name |
|
||||||
|
|
||||||
|
**Hinweis:** Aktualisiert immer den PRIMARY Kreis des Accounts (ignoriert `id`/`familyId` Parameter).
|
||||||
|
|
||||||
|
**Verifiziert am:** 2026-04-16 via FW_DEBUG=1
|
||||||
|
|
||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- Unlike-Endpoint (Service Worker blockiert Analyse)
|
- Unlike-Endpoint (Service Worker blockiert Analyse)
|
||||||
- Erinnerungen (reminder) – nur Premium-Account
|
- Erinnerungen (reminder) – nur Premium-Account
|
||||||
- Wiederholungen (repeat) – nur Premium-Account
|
- Wiederholungen (repeat) – nur Premium-Account
|
||||||
- Sortierung von Kategorien via API
|
- Sortierung von Kategorien via API
|
||||||
- update_recipe (Rezept aktualisieren) – Endpoint: mprecipeput mit metaId
|
|
||||||
- mpadditemtolist (Zutaten aus Rezept → Einkaufsliste)
|
- mpadditemtolist (Zutaten aus Rezept → Einkaufsliste)
|
||||||
|
- Einladung bestehender FamilyWall-Nutzer (accinvite nur für neue Accounts)
|
||||||
|
- Kreis-Delete-Endpoint (API: "delete not supported" für family-Objekte)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcp-familywall"
|
name = "mcp-familywall"
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
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"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.6.1"
|
__version__ = "0.7.0"
|
||||||
|
|||||||
@@ -185,12 +185,17 @@ class FamilyWallClient:
|
|||||||
msg = f"API error from {endpoint!r}: {error_data}"
|
msg = f"API error from {endpoint!r}: {error_data}"
|
||||||
raise FamilyWallError(msg, response_data=body)
|
raise FamilyWallError(msg, response_data=body)
|
||||||
|
|
||||||
# Some endpoints (e.g. taskupdate2) return per-call errors nested under
|
# Some endpoints return per-call errors nested under a00.un.un or a00.ex.ex
|
||||||
# a00.un.un instead of top-level — detect and surface them.
|
# instead of top-level — detect and surface them.
|
||||||
a00 = body.get("a00", {})
|
a00 = body.get("a00", {})
|
||||||
if isinstance(a00, dict) and "un" in a00:
|
if isinstance(a00, dict):
|
||||||
|
if "un" in a00:
|
||||||
nested = a00["un"]
|
nested = a00["un"]
|
||||||
msg = f"API error from {endpoint!r}: {nested}"
|
msg = f"API error from {endpoint!r}: {nested}"
|
||||||
raise FamilyWallError(msg, response_data=body)
|
raise FamilyWallError(msg, response_data=body)
|
||||||
|
if "ex" in a00:
|
||||||
|
nested = a00["ex"]
|
||||||
|
msg = f"API error from {endpoint!r}: {nested}"
|
||||||
|
raise FamilyWallError(msg, response_data=body)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Circle (family group) helpers for the Family Wall MCP server."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Valid family role types known to the Family Wall API (FamilyRoleTypeEnum).
|
||||||
|
# Only "Unknown" has been verified to be accepted by accinvite.
|
||||||
|
ROLE_UNKNOWN = "Unknown"
|
||||||
|
|
||||||
|
# Default role used when inviting new members.
|
||||||
|
DEFAULT_INVITE_ROLE = ROLE_UNKNOWN
|
||||||
@@ -10,6 +10,7 @@ from mcp.server.fastmcp import FastMCP
|
|||||||
|
|
||||||
from mcp_familywall.auth import get_credentials
|
from mcp_familywall.auth import get_credentials
|
||||||
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||||
|
from mcp_familywall.modules.circles import DEFAULT_INVITE_ROLE
|
||||||
from mcp_familywall.modules.lists import translate_name
|
from mcp_familywall.modules.lists import translate_name
|
||||||
from mcp_familywall.modules.recipes import (
|
from mcp_familywall.modules.recipes import (
|
||||||
build_create_params,
|
build_create_params,
|
||||||
@@ -1051,6 +1052,155 @@ def delete_list(list_id: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: create_circle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def create_circle(name: str) -> str:
|
||||||
|
"""Create a new Family Wall circle (group).
|
||||||
|
|
||||||
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
|
|
||||||
|
A circle is a group of people who share content on Family Wall (e.g. a
|
||||||
|
family, a group of friends). After creation the caller is automatically
|
||||||
|
added as the circle owner (SuperAdmin).
|
||||||
|
|
||||||
|
Note: Circle deletion is not supported by the Family Wall API. Test
|
||||||
|
circles must be deleted manually in the Family Wall app settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Display name for the new circle (e.g. ``"Van Elst"``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with ``{created, id, name}`` on success, or an error message.
|
||||||
|
The server may capitalise the first letter of the name.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
email, password = get_credentials()
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return f"Error: {exc}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with FamilyWallClient() as client:
|
||||||
|
client.login(email, password)
|
||||||
|
|
||||||
|
# acccreatefamily returns only the numeric family ID.
|
||||||
|
data = client.call("acccreatefamily", {"name": name})
|
||||||
|
try:
|
||||||
|
numeric_id: str = data["a00"]["r"]["r"]
|
||||||
|
if not isinstance(numeric_id, str) or not numeric_id.isdigit():
|
||||||
|
raise TypeError(f"expected numeric ID, got {numeric_id!r}")
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
client.logout()
|
||||||
|
return json.dumps(
|
||||||
|
{"warning": "Unexpected acccreatefamily response", "raw": data},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
circle_id = f"family/{numeric_id}"
|
||||||
|
|
||||||
|
# Read back the circle to obtain the server-stored (possibly
|
||||||
|
# capitalised) name in the same session.
|
||||||
|
canonical_name = name
|
||||||
|
try:
|
||||||
|
circles_data = client.call("famlistfamily")
|
||||||
|
raw_circles = circles_data.get("a00", {}).get("r", {}).get("r", []) or []
|
||||||
|
for c in raw_circles:
|
||||||
|
if c.get("metaId") == circle_id:
|
||||||
|
canonical_name = c.get("name", name)
|
||||||
|
break
|
||||||
|
except FamilyWallError:
|
||||||
|
pass # fallback: use caller-provided 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}"
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
{"created": True, "id": circle_id, "name": canonical_name},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: add_member_to_circle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def add_member_to_circle(
|
||||||
|
circle_id: str,
|
||||||
|
email: str,
|
||||||
|
firstname: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Invite a person to a Family Wall circle by e-mail.
|
||||||
|
|
||||||
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
|
|
||||||
|
Sends an invitation to the given e-mail address. The recipient will
|
||||||
|
receive an e-mail with a link to join the circle. If they already have a
|
||||||
|
Family Wall account they must accept the invitation via the app; if they
|
||||||
|
do not have an account yet one will be created during the acceptance flow.
|
||||||
|
|
||||||
|
Note: The Family Wall API ``accinvite`` endpoint is limited to inviting
|
||||||
|
people who do not yet have an active Family Wall account. Inviting an
|
||||||
|
existing account holder returns an error from the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
circle_id: Target circle metaId from ``get_circles``
|
||||||
|
(e.g. ``"family/23431854"``).
|
||||||
|
email: E-mail address of the person to invite.
|
||||||
|
firstname: First name of the invitee (used in the invitation e-mail).
|
||||||
|
When omitted the local part of the e-mail address is used as a
|
||||||
|
fallback (e.g. ``"john"`` from ``john.doe@example.com``).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON success indicator or an error message.
|
||||||
|
"""
|
||||||
|
if not email or "@" not in email:
|
||||||
|
return "Error: 'email' must be a valid e-mail address."
|
||||||
|
|
||||||
|
# Derive a sensible first-name fallback from the email local part.
|
||||||
|
if firstname is None:
|
||||||
|
local_part = email.split("@")[0]
|
||||||
|
firstname = local_part.split(".")[0].capitalize()
|
||||||
|
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
"familyId": circle_id,
|
||||||
|
"identifier": email,
|
||||||
|
"role": DEFAULT_INVITE_ROLE,
|
||||||
|
"firstname": firstname,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = _authenticated_call("accinvite", params)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return f"Error: {exc}"
|
||||||
|
|
||||||
|
# On success the server returns the invitation object under a00.r.r.
|
||||||
|
try:
|
||||||
|
result_obj = data.get("a00", {}).get("r", {}).get("r")
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
result_obj = None
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"invited": True,
|
||||||
|
"circle_id": circle_id,
|
||||||
|
"email": email,
|
||||||
|
"result": result_obj,
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tool: like_post
|
# Tool: like_post
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user