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
+2 -1
View File
@@ -5,7 +5,8 @@
"Bash(ruff check *)",
"Bash(uv run *)",
"Bash(git add *)",
"Bash(git commit -m ' *)"
"Bash(git commit -m ' *)",
"Bash(FW_DEBUG=1 uv run mcp-familywall check)"
]
}
}
+16
View File
@@ -10,6 +10,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
This project follows Semantic Versioning (SemVer).
Breaking changes (removed tools, changed parameters) increment the major version.
## [1.3.0] 2026-04-17
### Added
- **Wall post reading**: `get_wall_posts` — retrieve recent wall posts with author, text,
creation date, like count, liked-by-me flag, and comment count
- **Wall post writing**: `create_wall_post` — publish a new status post to the wall
- **Comments**: `add_comment` — add a comment to a wall post or activity
- All three new wall post tools require user confirmation before calling
### Notes
- `get_wall_posts` supersedes the activity-listing functionality of `get_activities`
(which continues to exist for backward compatibility)
- `like_post` already supports both wall posts and activities; verified to work
with metaIds from both `get_wall_posts` and `get_activities`
- Post IDs are in the format `wall/<familyid>_<postid>`
## [1.2.0] 2026-04-17
### Added
+4 -3
View File
@@ -27,20 +27,21 @@ und wird in Claude Desktop eingebunden.
## Aktueller Stand
### Version: **v1.2.0** ← aktuell
### Version: **v1.3.0** ← aktuell
### Implementierte Tools
| Kategorie | Tools |
|---|---|
| Wall & Aktivitäten | `get_wall_posts`, `create_wall_post`, `add_comment`, `like_post` |
| Kreise & Mitglieder | `get_circles`, `get_members`, `create_circle`, `update_circle`, `delete_circle`, `add_member_to_circle` |
| Listen & Tasks | `get_lists`, `get_tasks`, `get_categories`, `get_activities`, `create_list`, `update_list`, `delete_list`, `create_category`, `delete_category`, `create_task`, `update_task`, `toggle_task`, `delete_task`, `clear_list`, `like_post` |
| Listen & Tasks | `get_lists`, `get_tasks`, `get_categories`, `get_activities`, `create_list`, `update_list`, `delete_list`, `create_category`, `delete_category`, `create_task`, `update_task`, `toggle_task`, `delete_task`, `clear_list` |
| Rezeptbox | `get_recipe_categories`, `get_recipe_box`, `get_recipes`, `get_recipe`, `create_recipe`, `update_recipe`, `delete_recipe` |
| Essensplaner | `get_meal_plan`, `add_recipe_to_meal_plan`, `add_meal_to_meal_plan`, `add_meal_note`, `delete_meal_plan_entry` |
### Roadmap (Nächstes)
- v2.0: Schreibzugriff auf Wall-Posts (Erstellen, Kommentieren)
- v2.0: Weitere Wall-Post Features (Edits, Deletes, Emoji-Reactions beyond STAR)
### Historische Meilensteine (kompakt)
+10 -1
View File
@@ -2,7 +2,16 @@
MCP server for [Family Wall](https://www.familywall.com) — manage your family's circles, lists, tasks, recipes, and meal plan directly from Claude.
## Tools (v1.0.0)
## Tools (v1.3.0)
### Wall & Activities
| Tool | Description |
|---|---|
| `get_wall_posts` | Get recent wall posts (text, author, likes, comments) |
| `create_wall_post` 🔒 | Create a new status post on the wall |
| `add_comment` 🔒 | Add a comment to a post |
| `like_post` 🔒 | Like or unlike a wall post/activity |
### Circles & Members
+44
View File
@@ -304,6 +304,50 @@ a00.r.r → Wall-Objekt mit moodMap, refAction: "MOOD_STAR"
**Verifiziert am:** 2026-04-17 via Network-Interceptor (echter Request-Body)
### `wallpublish` Wall-Post veröffentlichen
POST https://api.familywall.com/api/wallpublish
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `tagline` | Post-Text |
**Response:**
```
a00.r.r → Wall-Post-Objekt
.metaId → neue Post-ID
.tagline → Post-Text
.creationDate → Timestamp (ISO 8601)
```
**Verifiziert am:** 2026-04-17 via Briefing und Integration
### `walladdComment` Kommentar hinzufügen
POST https://api.familywall.com/api/walladdComment
**Body-Parameter:**
| Parameter | Wert |
|---|---|
| `wall_message_id` | Post-metaId (z.B. `wall/23431854_31119189`) |
| `comment` | Kommentartext |
**Response:**
```
a00.r.r → Kommentar-Objekt
.metaId → neue Kommentar-ID
.text → Kommentartext
.creationDate → Timestamp (ISO 8601)
```
**Bekannte Einschränkungen:**
- `mood` und `clientOpId` sind optional und werden ignoriert
**Verifiziert am:** 2026-04-17 via Briefing und Integration
### `taskcategoryput` Kategorie erstellen/aktualisieren
POST https://api.familywall.com/api/taskcategoryput
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mcp-familywall"
version = "1.2.0"
version = "1.3.0"
description = "MCP server for Family Wall — manage your family's circles, lists, tasks, recipes, and meal plan via Claude"
readme = "README.md"
requires-python = ">=3.12"
+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
# ---------------------------------------------------------------------------