diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 142a7b1..0a08400 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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)" ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 044726a..693536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/_` + ## [1.2.0] – 2026-04-17 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 2f36d96..9b3be70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/README.md b/README.md index f8ee7ec..7cf5b08 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SPEC.md b/SPEC.md index 841bdbb..6c9be35 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index e77e0c0..bb0db35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index 91770b1..c2788b1 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -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 # ---------------------------------------------------------------------------