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:
@@ -10,6 +10,7 @@ from mcp.server.fastmcp import FastMCP
|
||||
|
||||
from mcp_familywall.auth import get_credentials
|
||||
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.recipes import (
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user