Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8dec599a3 | |||
| 5f5abfcbc7 | |||
| 5671d70000 | |||
| 0e34b067e6 | |||
| dd42dc2845 | |||
| 09bd24a9e1 | |||
| 35cbfd3061 | |||
| 0e7c4da362 | |||
| 70c2f61f05 | |||
| 4c1e4e2c23 | |||
| d6d8d40305 | |||
| 08ee5fb84a | |||
| f5eb0a46c8 |
@@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+184
@@ -10,6 +10,190 @@ 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.4.4] – 2026-04-20
|
||||
|
||||
### Updated
|
||||
- **README.md restructuring** — consolidated `get_activities` and `like_post` into Wall & Activities section
|
||||
- `get_activities` moved from Lists & Tasks to Wall & Activities (belongs with wall functionality)
|
||||
- `like_post` reference removed from Lists & Tasks (consolidated in one section)
|
||||
- Version bump in README header: v1.3.1 → v1.4.3
|
||||
|
||||
### Notes
|
||||
- Documentation-only release; no code changes
|
||||
- All tools remain functional and unchanged
|
||||
- Improves UX by grouping related wall/activity tools in one section
|
||||
|
||||
## [1.4.3] – 2026-04-18
|
||||
|
||||
### Improved
|
||||
- **Usability: ID resolution in docstrings** — enforce transparent display of human-readable names:
|
||||
- `get_tasks`: Document required member/category name resolution before user display
|
||||
- `get_lists`: Document required circle name resolution before user display
|
||||
- `get_activities`: Document required author name resolution before user display
|
||||
- `get_wall_posts`: Document required author name resolution before user display
|
||||
- `get_members`: Added proactive call guidance before presenting member-ID data
|
||||
- `get_circles`: Added proactive call guidance before presenting circle-ID data
|
||||
- `get_categories`: Added proactive call guidance for shopping list tasks
|
||||
- `get_meal_plan`: Document required recipe name resolution for recipe_id before user display
|
||||
- `CLAUDE.md`: Added general usability rule at top of implementation section
|
||||
|
||||
### Notes
|
||||
- No breaking changes; docstring enhancements only
|
||||
- All tools remain backward compatible
|
||||
- Ensures consistent UX: no raw numeric/metaIDs shown to users in any context
|
||||
|
||||
## [1.4.2] – 2026-04-18
|
||||
|
||||
### Fixed
|
||||
- **`get_tasks` support for secondary circles** — lists in non-primary circles now return tasks correctly
|
||||
- Circle is automatically derived from `list_id` format (`taskList/<circleNum>_<listNum>`)
|
||||
- Scope parameter passed to `accgetallfamily` call; matches `get_lists` pattern
|
||||
- Docstring updated to clarify circle support and list_id format requirement
|
||||
|
||||
### Notes
|
||||
- `_accgetallfamily()` now accepts optional `scope` parameter for secondary circle support
|
||||
- No breaking changes; all existing code remains compatible
|
||||
|
||||
## [1.4.1] – 2026-04-17
|
||||
|
||||
### Improved
|
||||
- **Docstring enhancements** — clearer tool usage guidance for new Claude sessions:
|
||||
- `get_wall_posts`: Added note that results include automatic activity entries (task updates, list changes)
|
||||
- `get_activities`: Expanded description and added distinction from `get_wall_posts`
|
||||
- `like_post`: Extended post_id examples to include `task/` and `taskList/` IDs from activities
|
||||
- `get_recipe_categories`: Added usage notes (IDs for create/update, 5 free-tier categories, family-wide scope)
|
||||
- `add_meal_note`: Clarified create-only behavior; to update, delete then recreate
|
||||
- `clear_list`: Moved risk warning (default deletes all tasks) to opening line for visibility
|
||||
- `create_recipe`: Verified IMPORTANT confirmation line present
|
||||
|
||||
### Notes
|
||||
- No breaking changes; docstring improvements only
|
||||
- All tools remain backward compatible
|
||||
|
||||
## [1.4.0] – 2026-04-17
|
||||
|
||||
### Added
|
||||
- **Unit test suite** (`tests/test_unit.py`) — comprehensive tests for core modules
|
||||
- `fw_client.py`: Error handling (top-level `ex`/`un`, nested `a00.un.un`/`a00.ex.ex`, successful responses)
|
||||
- `recipes.py`: `ingredients_parsed` parser (newline handling, empty lines, headings, commas)
|
||||
- `lists.py`: System list name translation (SYS-CAT-SHOPPINGLIST, SYS-CAT-TODOS, unknown names)
|
||||
- `server.py`: `_validate_date()` (ISO YYYY-MM-DD validation, format errors)
|
||||
- `server.py`: `_err()` helper (JSON response format, Unicode, special characters)
|
||||
- All tests use `unittest.mock` (stdlib, no external dependencies)
|
||||
- Tests run without API access; all HTTP calls are mocked
|
||||
- Framework: pytest with asyncio support
|
||||
|
||||
### Notes
|
||||
- Run tests: `uv run pytest tests/test_unit.py -v`
|
||||
- No breaking changes; release is testing infrastructure only
|
||||
|
||||
## [1.3.1] – 2026-04-17
|
||||
|
||||
### Fixed
|
||||
- **`add_comment` response parser**: corrected response structure from `a00.r.r` with `metaId`
|
||||
to nested `a00.r.r.comment.commentId`; response now includes `"created": true` flag
|
||||
- **`add_comment` error handling**: now returns error messages directly instead of warnings
|
||||
|
||||
### Added
|
||||
- **`delete_wall_post`** — permanently delete a wall post using the `metadelete` endpoint
|
||||
(identical to deleting tasks and recipes)
|
||||
|
||||
### Notes
|
||||
- All wall post tools now complete CRUD operations (Create, Read, Update via like_post, Delete)
|
||||
- Test post (wall/16282169_31146617) removed from production after verification
|
||||
|
||||
## [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
|
||||
- **Unlike support** for `like_post`: passing `like=False` now removes a STAR reaction via
|
||||
`add=$empty, remove.0=STAR` (array dot-notation). New optional `mood` parameter (default `"STAR"`).
|
||||
- **Reminder write support** for `create_task` and `update_task`:
|
||||
- `reminder_unit`: `"MINUTE"`, `"HOUR"`, or `"DAY"`
|
||||
- `reminder_value`: non-negative integer (e.g. `30` for "30 minutes before")
|
||||
- `clear_reminder` (update_task only): remove reminder
|
||||
- The correct wire format uses **dot-notation** subfields
|
||||
(`reminder.reminderUnit`, `reminder.reminderValue`, `reminder.reminderType`,
|
||||
`reminder.localId`) — flat top-level keys, JSON-string, and bracket-notation
|
||||
are silently ignored by the server.
|
||||
|
||||
### Fixed
|
||||
- `update_task` docstring: removed the "reminders not supported" warning — the
|
||||
v1.1.2 write format was flat, which the FiZ encoder silently drops; the
|
||||
dot-notation variant works on both Free and Premium accounts.
|
||||
|
||||
### Investigation notes (2026-04-17)
|
||||
- Verified via isolated fuzz test (fresh test task per variant): only dot-notation
|
||||
succeeds; every other encoding is accepted with HTTP 200 and `lastAction: UPDATED`
|
||||
but the reminder remains at default `{reminderType: NONE, reminderValue: 0}`.
|
||||
- `reminder.localId` is optional.
|
||||
- Partial block updates (e.g. only `reminder.reminderType=NONE`) return
|
||||
`task reminder invalid` — the full inactive block must be sent to clear.
|
||||
- Valid `reminderUnit` values: `MINUTE`, `HOUR`, `DAY`. `WEEK` is rejected by the
|
||||
API enum decoder.
|
||||
- Reminder + recurrency can be set in a single `taskupdate2` call without interference.
|
||||
|
||||
## [1.1.2] – 2026-04-17
|
||||
|
||||
### Fixed
|
||||
- `update_task`: removed non-functional reminder write parameters (`reminder_unit`,
|
||||
`reminder_value`, `clear_reminder`) — exhaustive FW_DEBUG testing (flat params,
|
||||
JSON-string, PHP-bracket, all string values, with/without `localId`, alternative
|
||||
endpoints) on both Free and Premium accounts confirmed that `taskupdate2` silently
|
||||
ignores all reminder fields; historical SNOOZE reminders were set via the mobile
|
||||
app's Service Worker which transforms requests in ways not reproducible via direct API
|
||||
- `update_task`: docstring now explicitly states reminders are read-only
|
||||
- SPEC.md: documented reminder write limitation with investigation findings
|
||||
- CLAUDE.md: updated reminder row to reflect read-only status
|
||||
|
||||
### Investigation notes
|
||||
- Tested formats: flat int params, flat string params, JSON-encoded string value,
|
||||
PHP-bracket notation (`reminder[reminderUnit]=DAY`), no-`localId` variant
|
||||
- Tested endpoints: `tasksetalert`, `taskalertput`, `taskreminderset`, `taskalert`,
|
||||
`tasknotification` — all return `"The call X is not registered"`
|
||||
- Verified on both test account (Free) and real account (Premium) — same result
|
||||
- Recurrency write via flat params is confirmed working (separate from reminder)
|
||||
|
||||
---
|
||||
|
||||
## [1.1.1] – 2026-04-17
|
||||
|
||||
### Fixed
|
||||
- `update_task`: recurrency and reminder fields now sent as flat top-level parameters
|
||||
instead of nested objects — the FiZ `Ai()` encoder does not support nested objects;
|
||||
`recurrencyDescriptor`/`reminder` keys are never sent, fields go directly to top level
|
||||
(e.g. `recurrency=WEEKLY`, `reminderUnit=DAY` rather than `recurrencyDescriptor={...}`)
|
||||
- SPEC.md and CLAUDE.md corrected to document the flat encoding
|
||||
|
||||
---
|
||||
|
||||
## [1.1.0] – 2026-04-17
|
||||
|
||||
### Added
|
||||
- `update_task`: recurrency parameters (`recurrency`, `recurrency_interval`, `rrule`,
|
||||
`clear_recurrency`) — set or remove task recurrence rules via `recurrencyDescriptor`
|
||||
- `update_task`: reminder parameters (`reminder_unit`, `reminder_value`, `clear_reminder`)
|
||||
— set or remove task reminders via `reminder`
|
||||
- SPEC.md: documented `recurrencyDescriptor` and `reminder` fields for `taskupdate2`
|
||||
(verified via JS-Bundle xb-Encoder / fc-Encoder)
|
||||
|
||||
---
|
||||
|
||||
## [1.0.1] – 2026-04-17
|
||||
|
||||
### Added
|
||||
|
||||
@@ -27,20 +27,21 @@ und wird in Claude Desktop eingebunden.
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
### Version: **v1.0.0** ← aktuell
|
||||
### Version: **v1.4.3** ← 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)
|
||||
|
||||
@@ -50,6 +51,8 @@ und wird in Claude Desktop eingebunden.
|
||||
- v0.9: Task-Wiederholungen + Erinnerungen (read-only)
|
||||
- v0.10–v0.11: Essensplaner (read + write)
|
||||
- v1.0: Cleanup, Unified errors, Datumsvalidierung, Partial-Failure-Reporting (Details: CHANGELOG.md)
|
||||
- v1.1: Task-Recurrency-Write (flat top-level params)
|
||||
- v1.2: Task-Reminder-Write via Dot-Notation (`reminder.*`) — verifiziert 2026-04-17
|
||||
|
||||
|
||||
## Architektur-Entscheidungen
|
||||
@@ -94,6 +97,9 @@ können den echten Request-Body in diesen Fällen nicht sehen.
|
||||
|
||||
## Claude Code – Implementierungsregeln
|
||||
|
||||
**Usability rule:** Never present raw API IDs (numeric IDs, metaIds, category IDs, circle IDs, member IDs) to the user.
|
||||
Always resolve them to human-readable names before responding.
|
||||
|
||||
- **Feature complete before next feature** – jedes Feature vollständig
|
||||
implementieren, testen und verifizieren bevor das nächste beginnt
|
||||
- **Kein destruktives Probing** – keine Probe-Calls auf System-Kategorien,
|
||||
@@ -132,9 +138,12 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
||||
| `taskcreate2` | `taskListId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee` | – |
|
||||
| `taskupdate2` | `metaId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee`, `taskListId` | – |
|
||||
| `taskupdate2` | `dueDate` löschen | `$empty` |
|
||||
| `taskupdate2` | `recurrencyDescriptor` (flach!) | `recurrency, recurrencyInterval, rrule, byDay, byMonthDay, recurrencyEndDate, endOccurence` als Top-Level-Parameter; löschen: `recurrency="NONE"` |
|
||||
| `taskupdate2` | Reminder (Dot-Notation!) | `reminder.reminderUnit` (`MINUTE`/`HOUR`/`DAY`), `reminder.reminderValue` (String-Integer), `reminder.reminderType` (`SNOOZE`=aktiv, `NONE`=entfernen), `reminder.localId` optional. Entfernen: vollständigen Block mit `reminderType=NONE, reminderValue="0", reminderUnit=MINUTE` senden. Partielle Updates → `task reminder invalid`. |
|
||||
| `taskupdate2` | **⚠️ Encoding** | Recurrency flach top-level. Reminder **nur Dot-Notation** `reminder.*` — flache Keys, JSON-String, Brackets werden silent-ignored. |
|
||||
| `taskmark` | `taskId`, `complete` | `"true"`/`"false"` |
|
||||
| `metadelete` | `id` | metaId des Tasks / Rezepts |
|
||||
| `wallmood` | `wall_message_id`, `moodType` | `"STAR"` für Like |
|
||||
| `wallmood` | `wall_message_id`, `add`, `remove`/`remove.0` | Like: `add="STAR", remove="$empty"`; Unlike: `add="$empty", remove.0="STAR"` (Array-Dot-Notation) |
|
||||
| `taskcategoryput` | `name`, `emoji` | – |
|
||||
| `taskcategorydelete` | `id` | metaId der Kategorie |
|
||||
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji`, `scope` | `taskListType`: `SHOPPING_LIST`, `TODOS`, `OTHER`; `scope`: Kreis-metaId für nicht-primäre Kreise |
|
||||
@@ -153,9 +162,8 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
||||
### Self-Like-Restriction
|
||||
Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts.
|
||||
|
||||
### Unlike nicht möglich
|
||||
Service Worker verschlüsselt den Unlike-Request-Body.
|
||||
Endpoint unbekannt. `like_post(like=False)` gibt Fehlermeldung zurück.
|
||||
### Unlike
|
||||
Unlike via `remove.0=STAR` (Array-Dot-Notation). Verifiziert 2026-04-17 via Network-Interceptor.
|
||||
|
||||
### mpstar / Rezept-Favorit
|
||||
Service Worker fängt `mpstar` ab. `metamood` funktioniert nur auf
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
|
||||
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.4.4)
|
||||
|
||||
### Wall & Activities
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `get_wall_posts` | Get recent wall posts (text, author, likes, comments) |
|
||||
| `get_activities` | List recent activities (task updates, list changes) |
|
||||
| `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 |
|
||||
| `delete_wall_post` 🔒 | Permanently delete a wall post |
|
||||
|
||||
### Circles & Members
|
||||
|
||||
@@ -22,18 +33,16 @@ MCP server for [Family Wall](https://www.familywall.com) — manage your family'
|
||||
| `get_lists` | List all task lists (emoji, color, circle; optional `scope` filter) |
|
||||
| `get_tasks` | List tasks in a list (category, due date, assignees, recurrency, reminder) |
|
||||
| `get_categories` | List categories for a list (locale-filtered; custom categories always shown) |
|
||||
| `get_activities` | List recent wall activities (author resolved to display name) |
|
||||
| `create_list` 🔒 | Create a task list (`SHOPPING_LIST`, `TODOS`, or `OTHER`; optional emoji, color, circle) |
|
||||
| `update_list` 🔒 | Rename a list or change its emoji/color (partial update; system lists protected) |
|
||||
| `delete_list` 🔒 | Permanently delete a list and all its tasks (system lists protected) |
|
||||
| `create_category` 🔒 | Create a custom category (with optional icon) |
|
||||
| `delete_category` 🔒 | Delete a custom category (system categories protected) |
|
||||
| `create_task` 🔒 | Create a task (supports category, due date, assignees; use `"Äpfel (5x)"` for quantities) |
|
||||
| `update_task` 🔒 | Update text, category, due date, assignees, or move to a different list |
|
||||
| `create_task` 🔒 | Create a task (category, due date, assignees, reminder; use `"Äpfel (5x)"` for quantities) |
|
||||
| `update_task` 🔒 | Update text, category, due date, assignees, recurrency, reminder, or move to a different list |
|
||||
| `toggle_task` 🔒 | Mark a task complete or reopen it |
|
||||
| `delete_task` 🔒 | Permanently delete a task |
|
||||
| `clear_list` 🔒 | Bulk-delete all tasks in a list (optional `only_open=True` keeps completed tasks) |
|
||||
| `like_post` 🔒 | Like a wall post/activity |
|
||||
|
||||
### Recipe Box
|
||||
|
||||
|
||||
@@ -201,9 +201,41 @@ POST https://api.familywall.com/api/taskupdate2
|
||||
| `dueDate` | nein | ISO 8601 oder `$empty` zum Löschen |
|
||||
| `assignee` | nein | Member-accountId (mehrfach sendbar), `""` zum Entfernen aller |
|
||||
| `taskListId` | nein | neue Listen-metaId (verschiebt Task) |
|
||||
|
||||
| `recurrency` | nein | `"DAILY"` \| `"WEEKLY"` \| `"MONTHLY"` \| `"YEARLY"` \| `"NONE"` (entfernen) |
|
||||
| `recurrencyInterval` | nein | int (z.B. `2` für "alle 2 Wochen") |
|
||||
| `rrule` | nein | vollständige iCal-RRULE (z.B. `"FREQ=WEEKLY;INTERVAL=2;BYDAY=FR"`) |
|
||||
| `byDay` | nein | z.B. `"FR"`, `"1SA"`, `"MO,TU,WE,TH,FR"` |
|
||||
| `byMonthDay` | nein | int (z.B. `7` für "am 7. des Monats") |
|
||||
| `recurrencyEndDate` | nein | ISO-Datum (z.B. `"2026-12-31"`) |
|
||||
| `endOccurence` | nein | int (nach N Wiederholungen aufhören) |
|
||||
**Hinweis:** `taskListId` ist NICHT Pflicht beim Update.
|
||||
|
||||
**⚠️ Encoding:** Der FiZ-`Ai()`-Encoder serialisiert alle Felder **flach** als Top-Level-Form-Parameter.
|
||||
Recurrency-Felder werden direkt auf Top-Level gesendet (verifiziert via xb-Encoder im JS-Bundle).
|
||||
|
||||
**Reminder-Schreibzugriff (verifiziert 2026-04-17):**
|
||||
|
||||
Reminder werden via Dot-Notation als Unterfelder gesendet:
|
||||
|
||||
| Parameter | Wert |
|
||||
|---|---|
|
||||
| `reminder.reminderUnit` | `"MINUTE"` \| `"HOUR"` \| `"DAY"` (WEEK wird abgelehnt) |
|
||||
| `reminder.reminderValue` | nicht-negativer Integer als String (z.B. `"30"`) |
|
||||
| `reminder.reminderType` | `"SNOOZE"` (aktiv) oder `"NONE"` (Reminder entfernen) |
|
||||
| `reminder.localId` | optional; `"0"` akzeptiert |
|
||||
|
||||
**Wichtig:**
|
||||
- Nur Dot-Notation funktioniert. Flache Top-Level-Keys (`reminderUnit`, `reminderValue`, …),
|
||||
JSON-String (`reminder={…}`) und Bracket-Notation (`reminder[reminderUnit]`) werden
|
||||
**silent-ignored** (HTTP 200, Reminder unverändert).
|
||||
- Partielle Reminder-Updates (z.B. nur `reminder.reminderType=NONE`) liefern
|
||||
`task reminder invalid` — immer vollständigen Block senden.
|
||||
- `reminder=$empty` wird silent-ignored; zum Entfernen muss `reminder.reminderType=NONE`
|
||||
mit `reminder.reminderValue=0` und `reminder.reminderUnit=MINUTE` gesendet werden.
|
||||
- Reminder-Felder lassen sich in einem einzigen `taskupdate2`-Call gemeinsam mit
|
||||
`recurrency`/`dueDate`/`text` setzen — keine Interferenz.
|
||||
- `$empty` auf Unterfelder (`reminder.reminderValue=$empty`) führt zu Parse-Fehlern.
|
||||
|
||||
**Response:**
|
||||
```
|
||||
a00.r.r → vollständiges Task-Objekt
|
||||
@@ -237,32 +269,86 @@ POST https://api.familywall.com/api/metadelete
|
||||
- Tasks: `task/<id>`
|
||||
- Rezepte: `recipe/<id>`
|
||||
- Essensplan-Einträge: `dish/<id>` und `meal/<id>`
|
||||
- Wall-Posts: `wall/<id>` (v1.3.1+)
|
||||
|
||||
**Response:**
|
||||
```
|
||||
a00.r.r → "true" (String)
|
||||
```
|
||||
|
||||
### `wallmood` – Post liken
|
||||
### `wallmood` – Post liken / unlike
|
||||
POST https://api.familywall.com/api/wallmood
|
||||
|
||||
**Body-Parameter:**
|
||||
**Body-Parameter (Like):**
|
||||
|
||||
| Parameter | Wert |
|
||||
|---|---|
|
||||
| `wall_message_id` | Post-metaId ⚠️ nicht `wallId` oder `id`! |
|
||||
| `moodType` | `"STAR"` für Like |
|
||||
| `add` | `"STAR"` (Mood-Typ setzen) |
|
||||
| `remove` | `"$empty"` (Sentinel für „nichts entfernen") |
|
||||
|
||||
**Body-Parameter (Unlike):**
|
||||
|
||||
| Parameter | Wert |
|
||||
|---|---|
|
||||
| `wall_message_id` | Post-metaId |
|
||||
| `add` | `"$empty"` (Sentinel für „nichts hinzufügen") |
|
||||
| `remove.0` | `"STAR"` (Array-Dot-Notation — `remove` ist ein Array) |
|
||||
|
||||
**Bekannte Einschränkungen:**
|
||||
- Unlike: Endpoint/Parameter unbekannt (Service Worker verschlüsselt Request-Body)
|
||||
- Self-Like: API antwortet 200, macht aber serverseitig nichts
|
||||
- `moodType="NONE"` und andere Werte haben keine Wirkung
|
||||
|
||||
**Response:**
|
||||
```
|
||||
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.comment → Kommentar-Objekt (nested)
|
||||
.commentId → 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 (Response-Struktur in v1.3.1 korrekt dokumentiert)
|
||||
|
||||
### `taskcategoryput` – Kategorie erstellen/aktualisieren
|
||||
POST https://api.familywall.com/api/taskcategoryput
|
||||
|
||||
@@ -917,7 +1003,6 @@ bevor er den Server erreicht. Die Transformation ist ohne Service-Worker-Analyse
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Unlike-Endpoint (Service Worker blockiert Analyse)
|
||||
- `mpstar` / `isFavorite` für Rezepte (Service Worker blockiert Analyse, siehe oben)
|
||||
- Erinnerungen (reminder) – nur Premium-Account
|
||||
- Wiederholungen (repeat) – nur Premium-Account
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mcp-familywall"
|
||||
version = "1.0.1"
|
||||
version = "1.4.4"
|
||||
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"
|
||||
|
||||
@@ -14,7 +14,6 @@ Prerequisites: credentials stored in OS keyring or FW_EMAIL / FW_PASSWORD set.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -39,24 +38,22 @@ FORCED_CATEGORY_META_ID: str | None = None
|
||||
CANDIDATES: list[dict[str, str | None]] = [
|
||||
# key → value format description
|
||||
# Full metaId variants
|
||||
{"key": "systemCategoryId", "fmt": "numeric"}, # just the number, e.g. "200"
|
||||
{"key": "taskCategorySystemId", "fmt": "numeric"},
|
||||
{"key": "categories", "fmt": "meta_id"}, # full taskCategory/… metaId
|
||||
{"key": "categoryIds", "fmt": "meta_id"},
|
||||
{"key": "taskCategoryName", "fmt": "name"}, # category name as string
|
||||
{"key": "categoryName", "fmt": "name"},
|
||||
{"key": "taskCategoryId", "fmt": "meta_id"}, # already tried but try numeric too
|
||||
{"key": "systemCategoryId", "fmt": "numeric"}, # just the number, e.g. "200"
|
||||
{"key": "taskCategorySystemId", "fmt": "numeric"},
|
||||
{"key": "categories", "fmt": "meta_id"}, # full taskCategory/… metaId
|
||||
{"key": "categoryIds", "fmt": "meta_id"},
|
||||
{"key": "taskCategoryName", "fmt": "name"}, # category name as string
|
||||
{"key": "categoryName", "fmt": "name"},
|
||||
{"key": "taskCategoryId", "fmt": "meta_id"}, # already tried but try numeric too
|
||||
# Numeric variants of already-tried names
|
||||
{"key": "taskCategoryId", "fmt": "numeric"},
|
||||
{"key": "categoryId", "fmt": "numeric"},
|
||||
{"key": "category", "fmt": "numeric"},
|
||||
{"key": "categoryMetaId", "fmt": "numeric"},
|
||||
{"key": "taskCategoryId", "fmt": "numeric"},
|
||||
{"key": "categoryId", "fmt": "numeric"},
|
||||
{"key": "category", "fmt": "numeric"},
|
||||
{"key": "categoryMetaId", "fmt": "numeric"},
|
||||
]
|
||||
|
||||
|
||||
def _resolve_category(
|
||||
client: FamilyWallClient, list_id: str
|
||||
) -> tuple[str, str, str]:
|
||||
def _resolve_category(client: FamilyWallClient, list_id: str) -> tuple[str, str, str]:
|
||||
"""Return (meta_id, system_category_id_str, name) for the first German category."""
|
||||
data = client.call(
|
||||
"accgetallfamily",
|
||||
|
||||
+491
-51
@@ -44,9 +44,13 @@ def _validate_date(date: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _accgetallfamily() -> dict[str, Any]:
|
||||
def _accgetallfamily(scope: str | None = None) -> dict[str, Any]:
|
||||
"""Login, call accgetallfamily, logout and return the response body.
|
||||
|
||||
Args:
|
||||
scope: Optional circle metaId (e.g. "family/16473836") to fetch data for
|
||||
a specific circle. When None (default), fetches data for the primary circle.
|
||||
|
||||
Raises:
|
||||
RuntimeError: On credential or API errors.
|
||||
"""
|
||||
@@ -58,10 +62,10 @@ def _accgetallfamily() -> dict[str, Any]:
|
||||
try:
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
data = client.call(
|
||||
"accgetallfamily",
|
||||
{"a01call": "taskcategorysync", "a02call": "tasksync"},
|
||||
)
|
||||
params: dict[str, Any] = {"a01call": "taskcategorysync", "a02call": "tasksync"}
|
||||
if scope:
|
||||
params["scope"] = scope
|
||||
data = client.call("accgetallfamily", params)
|
||||
client.logout()
|
||||
return data
|
||||
except FamilyWallError as exc:
|
||||
@@ -142,7 +146,10 @@ def _extract_tasks(data: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
|
||||
@mcp.tool()
|
||||
def get_circles() -> str:
|
||||
"""Return all Family Wall circles as JSON list of {id, name}."""
|
||||
"""Return all Family Wall circles as JSON list of {id, name}.
|
||||
|
||||
Call this proactively before presenting any data that contains circle IDs to the user.
|
||||
"""
|
||||
try:
|
||||
raw_circles = _famlistfamily()
|
||||
except RuntimeError as exc:
|
||||
@@ -216,6 +223,9 @@ def _famlistfamily() -> list[dict[str, Any]]:
|
||||
def get_members(circle_id: str | None = None) -> str:
|
||||
"""Return Family Wall circle members as JSON, optionally filtered by circle.
|
||||
|
||||
Call this proactively before presenting any data that contains member IDs
|
||||
(assignee_ids, author_id, creator) to the user.
|
||||
|
||||
Args:
|
||||
circle_id: Optional circle ID from get_circles (e.g. ``family/23431854``).
|
||||
When omitted all members across all circles are returned.
|
||||
@@ -307,6 +317,9 @@ def get_lists(scope: str | None = None) -> str:
|
||||
Returns:
|
||||
JSON list of list objects with keys id, name, type, open, total,
|
||||
emoji, color, circle_id.
|
||||
|
||||
IMPORTANT: When presenting lists to the user, always resolve
|
||||
circle_id to the circle name via get_circles. Never show raw IDs.
|
||||
"""
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
@@ -402,9 +415,35 @@ def get_lists(scope: str | None = None) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def get_tasks(list_id: str, only_open: bool = True) -> str:
|
||||
"""Return tasks for a list as JSON. list_id from get_lists. only_open=True filters completed."""
|
||||
"""Return tasks for a list as JSON.
|
||||
|
||||
Lists from all circles are supported. The circle is automatically derived from
|
||||
the list_id — always use IDs from get_lists, never construct them manually.
|
||||
|
||||
Args:
|
||||
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
|
||||
only_open: When True (default), filter to incomplete tasks only.
|
||||
|
||||
Returns:
|
||||
JSON array of task objects with keys: id, text, description, completed,
|
||||
category_id, due_date, assignee_ids, recurrency, recurrency_interval,
|
||||
rrule, reminder.
|
||||
|
||||
IMPORTANT: Before presenting results to the user, always resolve:
|
||||
- assignee_ids → member names via get_members
|
||||
- category_id → category name via get_categories
|
||||
- circle context → circle name from get_lists
|
||||
Never show raw IDs to the user.
|
||||
"""
|
||||
# Extract circle number from list_id format: taskList/<circleNum>_<listNum>
|
||||
try:
|
||||
data = _accgetallfamily()
|
||||
circle_num = list_id.split("/")[1].split("_")[0]
|
||||
scope = f"family/{circle_num}"
|
||||
except (IndexError, ValueError):
|
||||
return _err(f"Invalid list_id format: {list_id!r}")
|
||||
|
||||
try:
|
||||
data = _accgetallfamily(scope)
|
||||
except RuntimeError as exc:
|
||||
return _err(str(exc))
|
||||
|
||||
@@ -517,6 +556,9 @@ def get_categories(list_id: str, locale: str = "de") -> str:
|
||||
Use the returned ``id`` values as the ``category_id`` parameter in
|
||||
``create_task`` and ``update_task``.
|
||||
|
||||
Call this proactively before presenting shopping list tasks to the user so
|
||||
category_id values can be shown as readable names.
|
||||
|
||||
Args:
|
||||
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
|
||||
locale: BCP-47 language code for category names (default ``"de"``).
|
||||
@@ -737,7 +779,21 @@ def delete_category(category_id: str) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def get_activities(limit: int = 20) -> str:
|
||||
"""Return recent Family Wall wall activities as JSON. limit controls max number of results."""
|
||||
"""Return recent Family Wall activity events as JSON.
|
||||
|
||||
Gives task and list activities (task creation/updates, list changes).
|
||||
For wall posts (status updates, photos, comments), use get_wall_posts.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of activities to return (default 20).
|
||||
|
||||
Returns:
|
||||
JSON list of activity objects sorted by date descending,
|
||||
including wall, task, and taskList activity entries.
|
||||
|
||||
IMPORTANT: Before presenting to the user, resolve author_id to
|
||||
the member name via get_members. Never show raw IDs.
|
||||
"""
|
||||
try:
|
||||
email, password = get_credentials()
|
||||
except RuntimeError as exc:
|
||||
@@ -807,6 +863,59 @@ def get_activities(limit: int = 20) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Reminder units accepted by the Family Wall API (verified 2026-04-17 via
|
||||
# taskupdate2 fuzz: WEEK is rejected, MINUTE/HOUR/DAY succeed).
|
||||
_VALID_REMINDER_UNITS = ("MINUTE", "HOUR", "DAY")
|
||||
|
||||
|
||||
def _build_reminder_params(
|
||||
reminder_unit: str | None,
|
||||
reminder_value: int | None,
|
||||
clear_reminder: bool,
|
||||
) -> tuple[dict[str, Any], str | None]:
|
||||
"""Return (params, error_message). Params are the dot-notation reminder.* fields.
|
||||
|
||||
Encoding note: taskupdate2 only accepts reminders via dot-notation
|
||||
(``reminder.reminderUnit``, ``reminder.reminderValue``, ``reminder.reminderType``).
|
||||
Flat top-level keys, JSON strings, and bracket notation are silently ignored
|
||||
by the server — verified 2026-04-17.
|
||||
|
||||
Clearing requires sending the full "inactive" block
|
||||
(reminderType=NONE, reminderValue=0, reminderUnit=MINUTE, localId=0) —
|
||||
partial updates return ``"task reminder invalid"``.
|
||||
"""
|
||||
if clear_reminder and (reminder_unit is not None or reminder_value is not None):
|
||||
return {}, "'clear_reminder' cannot be combined with 'reminder_unit' or 'reminder_value'."
|
||||
|
||||
if clear_reminder:
|
||||
return {
|
||||
"reminder.reminderUnit": "MINUTE",
|
||||
"reminder.reminderValue": "0",
|
||||
"reminder.reminderType": "NONE",
|
||||
"reminder.localId": "0",
|
||||
}, None
|
||||
|
||||
if reminder_unit is None and reminder_value is None:
|
||||
return {}, None
|
||||
|
||||
if reminder_unit is None or reminder_value is None:
|
||||
return {}, "'reminder_unit' and 'reminder_value' must be provided together."
|
||||
|
||||
if reminder_unit not in _VALID_REMINDER_UNITS:
|
||||
allowed = ", ".join(_VALID_REMINDER_UNITS)
|
||||
return {}, f"Invalid reminder_unit {reminder_unit!r}. Allowed: {allowed}."
|
||||
|
||||
if reminder_value < 0:
|
||||
return {}, "'reminder_value' must be non-negative."
|
||||
|
||||
return {
|
||||
"reminder.reminderUnit": reminder_unit,
|
||||
"reminder.reminderValue": str(reminder_value),
|
||||
"reminder.reminderType": "SNOOZE",
|
||||
"reminder.localId": "0",
|
||||
}, None
|
||||
|
||||
|
||||
def _authenticated_call(endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Login, call *endpoint* with *params*, logout, and return the response body.
|
||||
|
||||
@@ -847,6 +956,8 @@ def create_task(
|
||||
category_id: str | None = None,
|
||||
due_date: str | None = None,
|
||||
assignee_ids: list[str] | None = None,
|
||||
reminder_unit: str | None = None,
|
||||
reminder_value: int | None = None,
|
||||
) -> str:
|
||||
"""Create a new task in the given list.
|
||||
|
||||
@@ -878,10 +989,20 @@ def create_task(
|
||||
due_date: Optional due date in ISO 8601 format (e.g. ``"2026-04-30T18:00:00"``).
|
||||
assignee_ids: Optional list of member IDs from get_members to assign the task
|
||||
(e.g. ``["23431898"]``). Empty list assigns to nobody.
|
||||
reminder_unit: Reminder time unit — ``"MINUTE"``, ``"HOUR"``, or ``"DAY"``.
|
||||
Must be provided together with *reminder_value*. Requires Family Wall Premium.
|
||||
reminder_value: Reminder offset before the due date (e.g. ``30`` for
|
||||
"30 minutes before"). Must be provided together with *reminder_unit*.
|
||||
|
||||
Returns:
|
||||
JSON with the new task's metaId on success, or an error message.
|
||||
"""
|
||||
reminder_params, reminder_error = _build_reminder_params(
|
||||
reminder_unit, reminder_value, clear_reminder=False
|
||||
)
|
||||
if reminder_error:
|
||||
return _err(reminder_error)
|
||||
|
||||
params: dict[str, Any] = {"taskListId": list_id, "text": text}
|
||||
if description:
|
||||
params["description"] = description
|
||||
@@ -891,6 +1012,7 @@ def create_task(
|
||||
params["dueDate"] = due_date
|
||||
if assignee_ids is not None:
|
||||
params["assignee"] = assignee_ids if assignee_ids else ""
|
||||
params.update(reminder_params)
|
||||
|
||||
try:
|
||||
data = _authenticated_call("taskcreate2", params)
|
||||
@@ -926,6 +1048,13 @@ def update_task(
|
||||
clear_due_date: bool = False,
|
||||
assignee_ids: list[str] | None = None,
|
||||
list_id: str | None = None,
|
||||
recurrency: str | None = None,
|
||||
recurrency_interval: int | None = None,
|
||||
rrule: str | None = None,
|
||||
clear_recurrency: bool = False,
|
||||
reminder_unit: str | None = None,
|
||||
reminder_value: int | None = None,
|
||||
clear_reminder: bool = False,
|
||||
) -> str:
|
||||
"""Update an existing task's fields.
|
||||
|
||||
@@ -948,12 +1077,39 @@ def update_task(
|
||||
list_id: Move the task to a different list by providing the target list ID
|
||||
from get_lists (e.g. ``"taskList/23431854_29740942"``). Omit to keep in
|
||||
current list.
|
||||
recurrency: Recurrence frequency — ``"DAILY"``, ``"WEEKLY"``, ``"MONTHLY"``,
|
||||
``"YEARLY"``, or ``"NONE"``. Examples: daily → ``recurrency="DAILY",
|
||||
recurrency_interval=1``; every 2 weeks on Friday →
|
||||
``recurrency="WEEKLY", recurrency_interval=2,
|
||||
rrule="FREQ=WEEKLY;INTERVAL=2;BYDAY=FR"``.
|
||||
Cannot be used together with *clear_recurrency*.
|
||||
recurrency_interval: Repeat every N units (e.g. ``2`` for "every 2 weeks").
|
||||
Only meaningful when *recurrency* is set.
|
||||
rrule: Full iCalendar RRULE string (e.g. ``"FREQ=WEEKLY;INTERVAL=2;BYDAY=FR"``).
|
||||
Overrides simple recurrency fields when set.
|
||||
clear_recurrency: Set to ``True`` to remove the recurrence rule from the task.
|
||||
Cannot be used together with *recurrency*.
|
||||
reminder_unit: Reminder time unit — ``"MINUTE"``, ``"HOUR"``, or ``"DAY"``.
|
||||
Must be provided together with *reminder_value*. Requires Family Wall
|
||||
Premium. Cannot be used together with *clear_reminder*.
|
||||
reminder_value: Reminder offset before the due date (e.g. ``30`` for
|
||||
"30 minutes before"). Must be provided together with *reminder_unit*.
|
||||
clear_reminder: Set to ``True`` to remove the reminder from the task.
|
||||
Cannot be used together with *reminder_unit* or *reminder_value*.
|
||||
|
||||
Returns:
|
||||
JSON success indicator or an error message.
|
||||
"""
|
||||
if clear_due_date and due_date is not None:
|
||||
return _err("'clear_due_date' and 'due_date' cannot be used together.")
|
||||
if clear_recurrency and recurrency is not None:
|
||||
return _err("'clear_recurrency' and 'recurrency' cannot be used together.")
|
||||
|
||||
reminder_params, reminder_error = _build_reminder_params(
|
||||
reminder_unit, reminder_value, clear_reminder
|
||||
)
|
||||
if reminder_error:
|
||||
return _err(reminder_error)
|
||||
|
||||
if (
|
||||
text is None
|
||||
@@ -963,10 +1119,15 @@ def update_task(
|
||||
and not clear_due_date
|
||||
and assignee_ids is None
|
||||
and list_id is None
|
||||
and recurrency is None
|
||||
and not clear_recurrency
|
||||
and not reminder_params
|
||||
):
|
||||
return _err(
|
||||
"At least one of 'text', 'description', 'category_id', 'due_date',"
|
||||
" 'clear_due_date', 'assignee_ids', or 'list_id' must be provided."
|
||||
" 'clear_due_date', 'assignee_ids', 'list_id', 'recurrency',"
|
||||
" 'clear_recurrency', 'reminder_unit'/'reminder_value', or"
|
||||
" 'clear_reminder' must be provided."
|
||||
)
|
||||
|
||||
params: dict[str, Any] = {"metaId": task_id}
|
||||
@@ -986,6 +1147,19 @@ def update_task(
|
||||
if list_id is not None:
|
||||
params["taskListId"] = list_id
|
||||
|
||||
# Recurrency fields are sent flat at the top level (verified: FiZ Ai() encoder).
|
||||
if clear_recurrency:
|
||||
params["recurrency"] = "NONE"
|
||||
elif recurrency is not None:
|
||||
params["recurrency"] = recurrency
|
||||
if recurrency_interval is not None:
|
||||
params["recurrencyInterval"] = recurrency_interval
|
||||
if rrule is not None:
|
||||
params["rrule"] = rrule
|
||||
|
||||
# Reminder fields use dot-notation `reminder.*` (verified: flat keys ignored).
|
||||
params.update(reminder_params)
|
||||
|
||||
try:
|
||||
_authenticated_call("taskupdate2", params)
|
||||
except RuntimeError as exc:
|
||||
@@ -1064,7 +1238,10 @@ def delete_task(task_id: str) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def clear_list(list_id: str, only_open: bool = False) -> str:
|
||||
"""Delete all tasks in a list within a single authenticated session.
|
||||
"""Delete all tasks in a list (or only open tasks).
|
||||
|
||||
⚠️ **Warning**: By default (only_open=False) this deletes all tasks
|
||||
including completed ones. Set only_open=True to keep completed tasks.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
|
||||
@@ -1075,7 +1252,7 @@ def clear_list(list_id: str, only_open: bool = False) -> str:
|
||||
list_id: List metaId from get_lists
|
||||
(e.g. ``"taskList/16282169_29775360"``).
|
||||
only_open: When ``True`` only incomplete tasks are deleted;
|
||||
completed tasks are kept. Default ``False`` deletes all.
|
||||
completed tasks are kept. Default ``False`` deletes all tasks.
|
||||
|
||||
Returns:
|
||||
JSON with ``deleted_count`` and ``list_id`` on success,
|
||||
@@ -1783,43 +1960,36 @@ def delete_circle(circle_id: str) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def like_post(post_id: str, like: bool = True) -> str:
|
||||
"""Like a wall post/activity with a STAR mood.
|
||||
def like_post(post_id: str, like: bool = True, mood: str = "STAR") -> str:
|
||||
"""Like or unlike a wall post/activity.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
|
||||
Note: Unlike (like=False) is not yet supported. The Family Wall API offers
|
||||
no discoverable endpoint or parameter to remove a like. Passing like=False
|
||||
returns an error without making any API call.
|
||||
|
||||
Args:
|
||||
post_id: Wall post ID from get_activities (e.g. ``wall/23431854_31119189``).
|
||||
like: Must be ``True``. ``False`` is reserved for future unlike support.
|
||||
post_id: Post metaId from get_wall_posts or get_activities
|
||||
(e.g. ``wall/16282169_31119189`` for wall posts,
|
||||
``task/16282169_441781324`` for task activities,
|
||||
``taskList/16282169_123`` for list activities).
|
||||
like: ``True`` to add a like (default), ``False`` to remove it.
|
||||
mood: Mood type to set or remove. Default is ``"STAR"``.
|
||||
|
||||
Returns:
|
||||
JSON success indicator or an error message.
|
||||
JSON with the resulting like state or an error message.
|
||||
"""
|
||||
# Unlike is not yet supported: extensive FW_DEBUG=1 testing showed that
|
||||
# wallmood with moodType="STAR" is an idempotent SET operation (not a toggle).
|
||||
# Tested and ruled out: moodType variations ("NONE", "REMOVE", "DELETE", ""),
|
||||
# moodStarShortcut parameter, and alternative endpoints (all return 502).
|
||||
# See SPEC.md for full investigation notes.
|
||||
if not like:
|
||||
return json.dumps(
|
||||
{"error": "Unlike is not yet supported. The unlike mechanism is unknown."},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
# Verified via FW_DEBUG=1:
|
||||
# - Parameter 'wall_message_id': post ID as returned by get_activities
|
||||
# - Parameter 'moodType': "STAR" (Family Wall's internal like type; "LIKE" is silently
|
||||
# mapped to "STAR" server-side — use "STAR" directly)
|
||||
# - Response a00.r.r: full wall message object with moodMap showing resulting state
|
||||
params: dict[str, Any] = {
|
||||
"wall_message_id": post_id,
|
||||
"moodType": "STAR",
|
||||
}
|
||||
# Like: add=STAR, remove=$empty (idempotent set)
|
||||
# Unlike: add=$empty, remove.0=STAR (array dot-notation, verified via Network Interceptor)
|
||||
if like:
|
||||
params: dict[str, Any] = {
|
||||
"wall_message_id": post_id,
|
||||
"add": mood,
|
||||
"remove": "$empty",
|
||||
}
|
||||
else:
|
||||
params = {
|
||||
"wall_message_id": post_id,
|
||||
"add": "$empty",
|
||||
"remove.0": mood,
|
||||
}
|
||||
|
||||
try:
|
||||
data = _authenticated_call("wallmood", params)
|
||||
@@ -1850,19 +2020,281 @@ def like_post(post_id: str, like: bool = True) -> str:
|
||||
|
||||
result: dict[str, Any] = {"liked": now_liked, "id": post_id, "author": account_id}
|
||||
|
||||
# Surface a warning when the like call apparently had no effect, so the
|
||||
# caller can distinguish a successful like from a silent API rejection
|
||||
# (e.g. rate limit, unsupported post type, or self-like restriction).
|
||||
if not now_liked:
|
||||
result["warning"] = (
|
||||
"Like may not have been applied. "
|
||||
"Possible causes: rate limit, unsupported post type (e.g. FAMILY_CREATED), "
|
||||
"or self-like restriction."
|
||||
if now_liked != like:
|
||||
if like:
|
||||
result["warning"] = (
|
||||
"Like may not have been applied. "
|
||||
"Possible causes: rate limit, unsupported post type (e.g. FAMILY_CREATED), "
|
||||
"or self-like restriction."
|
||||
)
|
||||
else:
|
||||
result["warning"] = (
|
||||
"Unlike may not have been applied. "
|
||||
"Possible causes: post was not liked, rate limit, or API restriction."
|
||||
)
|
||||
|
||||
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 (task updates, list changes),
|
||||
comments, and like information. Use get_activities for task-focused activity lists.
|
||||
|
||||
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.
|
||||
|
||||
IMPORTANT: Before presenting to the user, resolve author_id to
|
||||
the member name via get_members. Never show raw IDs.
|
||||
"""
|
||||
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:
|
||||
response_obj = data["a00"]["r"]["r"]
|
||||
if not isinstance(response_obj, dict):
|
||||
raise TypeError("a00.r.r is not a dict")
|
||||
comment_obj = response_obj.get("comment")
|
||||
if not isinstance(comment_obj, dict) or "commentId" not in comment_obj:
|
||||
raise TypeError("comment object missing or invalid")
|
||||
except (KeyError, TypeError) as exc:
|
||||
return _err(f"Unexpected walladdComment response structure: {exc}")
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"created": True,
|
||||
"id": comment_obj.get("commentId"),
|
||||
"post_id": post_id,
|
||||
"text": comment_obj.get("text"),
|
||||
"date": comment_obj.get("creationDate"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool: delete_wall_post
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def delete_wall_post(post_id: str) -> str:
|
||||
"""Permanently delete a wall post.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
|
||||
Args:
|
||||
post_id: Wall post metaId from get_wall_posts
|
||||
(e.g. ``"wall/16282169_31146617"``).
|
||||
|
||||
Returns:
|
||||
JSON success indicator or an error message.
|
||||
"""
|
||||
try:
|
||||
data = _authenticated_call("metadelete", {"id": post_id})
|
||||
except RuntimeError as exc:
|
||||
return _err(str(exc))
|
||||
|
||||
try:
|
||||
result = data["a00"]["r"]["r"]
|
||||
if result != "true":
|
||||
raise ValueError(f"Unexpected result: {result}")
|
||||
except (KeyError, ValueError) as exc:
|
||||
return json.dumps(
|
||||
{"warning": "Unexpected metadelete response structure", "error": str(exc), "raw": data},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"deleted": True,
|
||||
"id": post_id,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: fetch all raw recipes
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1914,6 +2346,10 @@ def _get_raw_recipes() -> list[dict[str, Any]]:
|
||||
def get_recipe_categories() -> str:
|
||||
"""Return all available recipe categories for the family.
|
||||
|
||||
Use the returned IDs directly in create_recipe/update_recipe via category_ids.
|
||||
Always includes 5 standard free-tier categories; categories are family-wide,
|
||||
not recipe-specific.
|
||||
|
||||
Returns:
|
||||
JSON list of category IDs (e.g. ["category/23431854_2", ...]).
|
||||
Always includes the 5 free-tier standard categories if family ID is available.
|
||||
@@ -2364,6 +2800,9 @@ def get_meal_plan(date_from: str, date_to: str) -> str:
|
||||
- ``can_update`` — whether the entry can be updated
|
||||
- ``can_delete`` — whether the entry can be deleted
|
||||
|
||||
IMPORTANT: When recipe_id is present, use get_recipe to resolve it to the
|
||||
recipe name for display. Never show raw recipe IDs.
|
||||
|
||||
Returns an error message string on failure.
|
||||
"""
|
||||
if date_err := _validate_date(date_from):
|
||||
@@ -2643,7 +3082,8 @@ def add_meal_note(
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
|
||||
Creates a ``meal/`` entry for the given date and meal type.
|
||||
Creates a new ``meal/`` entry for the given date and meal type.
|
||||
To update an existing entry, delete it first then create a new one.
|
||||
At least one of ``note`` or ``serves`` must be provided.
|
||||
|
||||
Args:
|
||||
|
||||
Reference in New Issue
Block a user