feat(wall-posts): add wall post reading/writing with comments (v1.3.0)

- Add get_wall_posts: read recent wall posts with like/comment counts
- Add create_wall_post: publish new status posts to the wall
- Add add_comment: add comments to wall posts and activities
- like_post already supports both wall posts and activities (v1.2.0)
- Update README.md with new Wall & Activities section
- Update CLAUDE.md with v1.3.0 and tool reorganization
- Update CHANGELOG.md with v1.3.0 release notes
- Add wallpublish and walladdComment documentation to SPEC.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 23:19:37 +02:00
parent 70c2f61f05
commit 0e7c4da362
7 changed files with 289 additions and 6 deletions
+212
View File
@@ -1974,6 +1974,218 @@ def like_post(post_id: str, like: bool = True, mood: str = "STAR") -> str:
return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: get_wall_posts
# ---------------------------------------------------------------------------
@mcp.tool()
def get_wall_posts(limit: int = 20) -> str:
"""Return recent Family Wall posts as JSON.
Returns posts from the primary circle's wall, sorted by date descending.
Includes status posts, activity entries, comments, and like information.
Args:
limit: Maximum number of posts to return (default 20).
Returns:
JSON list of post objects with keys:
id, type, text, date, author, author_id,
liked_by_me, like_count, comment_count.
"""
try:
email, password = get_credentials()
except RuntimeError as exc:
return _err(str(exc))
author_map: dict[str, str] = {}
try:
for circle in _famlistfamily():
for member in circle.get("members") or []:
acc_id: str = member.get("accountId", "")
display = member.get("firstName") or member.get("name") or acc_id
if acc_id:
author_map[acc_id] = display
except RuntimeError:
pass
try:
with FamilyWallClient() as client:
client.login(email, password)
data = client.call("wallget", {"nb": str(limit)})
client.logout()
except FamilyWallError as exc:
return _err(str(exc))
except Exception as exc:
return _err(f"Connection error: {exc}")
raw_posts: list[dict[str, Any]] | None = None
try:
candidate = data["a00"]["r"]["r"]
if isinstance(candidate, list):
raw_posts = candidate
elif isinstance(candidate, dict) and isinstance(candidate.get("updatedCreated"), list):
raw_posts = candidate["updatedCreated"]
except (KeyError, TypeError):
pass
if raw_posts is None:
return json.dumps(
{"warning": "Unexpected wallget response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
result = []
for item in raw_posts:
raw_author: str = item.get("accountId", "")
mood_map: dict[str, Any] = item.get("moodMap") or {}
liked_by_me = (
item.get("moodStarShortcut") == "true"
or any("STAR" in moods for moods in mood_map.values())
)
like_count = sum(len(moods) for moods in mood_map.values() if isinstance(moods, list))
comments: list[dict[str, Any]] = item.get("comments") or []
comment_count = len(comments)
result.append(
{
"id": item.get("metaId"),
"type": item.get("refType"),
"text": item.get("text") or item.get("tagline"),
"date": item.get("creationDate"),
"author": author_map.get(raw_author, raw_author),
"author_id": raw_author,
"liked_by_me": liked_by_me,
"like_count": like_count,
"comment_count": comment_count,
}
)
return json.dumps(result, ensure_ascii=False, indent=2)
# ---------------------------------------------------------------------------
# Tool: create_wall_post
# ---------------------------------------------------------------------------
@mcp.tool()
def create_wall_post(text: str) -> str:
"""Create a new status post on the Family Wall.
IMPORTANT: Ask the user for confirmation before calling this tool.
Args:
text: Text content of the post.
Returns:
JSON with the new post's metaId on success, or an error message.
"""
try:
email, password = get_credentials()
except RuntimeError as exc:
return _err(str(exc))
try:
with FamilyWallClient() as client:
client.login(email, password)
data = client.call("wallpublish", {"tagline": text})
client.logout()
except FamilyWallError as exc:
return _err(str(exc))
except Exception as exc:
return _err(f"Connection error: {exc}")
try:
post = data["a00"]["r"]["r"]
if not isinstance(post, dict) or "metaId" not in post:
raise TypeError("unexpected shape")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected wallpublish response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
return json.dumps(
{
"id": post.get("metaId"),
"text": post.get("tagline") or post.get("text"),
"date": post.get("creationDate"),
},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Tool: add_comment
# ---------------------------------------------------------------------------
@mcp.tool()
def add_comment(post_id: str, comment: str) -> str:
"""Add a comment to a wall post or activity.
IMPORTANT: Ask the user for confirmation before calling this tool.
Args:
post_id: Post metaId from get_wall_posts
(e.g. ``"wall/16282169_31119189"``).
comment: Comment text.
Returns:
JSON success indicator or an error message.
"""
try:
email, password = get_credentials()
except RuntimeError as exc:
return _err(str(exc))
try:
with FamilyWallClient() as client:
client.login(email, password)
data = client.call(
"walladdComment",
{
"wall_message_id": post_id,
"comment": comment,
},
)
client.logout()
except FamilyWallError as exc:
return _err(str(exc))
except Exception as exc:
return _err(f"Connection error: {exc}")
try:
comment_obj = data["a00"]["r"]["r"]
if not isinstance(comment_obj, dict) or "metaId" not in comment_obj:
raise TypeError("unexpected shape")
except (KeyError, TypeError):
return json.dumps(
{"warning": "Unexpected walladdComment response structure", "raw": data},
ensure_ascii=False,
indent=2,
)
return json.dumps(
{
"id": comment_obj.get("metaId"),
"post_id": post_id,
"text": comment_obj.get("text") or comment_obj.get("comment"),
"date": comment_obj.get("creationDate"),
},
ensure_ascii=False,
indent=2,
)
# ---------------------------------------------------------------------------
# Helper: fetch all raw recipes
# ---------------------------------------------------------------------------