Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8dec599a3 | |||
| 5f5abfcbc7 | |||
| 5671d70000 | |||
| 0e34b067e6 | |||
| dd42dc2845 | |||
| 09bd24a9e1 | |||
| 35cbfd3061 | |||
| 0e7c4da362 | |||
| 70c2f61f05 | |||
| 4c1e4e2c23 | |||
| d6d8d40305 | |||
| 08ee5fb84a | |||
| f5eb0a46c8 | |||
| 3e021bf01a |
@@ -5,7 +5,8 @@
|
|||||||
"Bash(ruff check *)",
|
"Bash(ruff check *)",
|
||||||
"Bash(uv run *)",
|
"Bash(uv run *)",
|
||||||
"Bash(git add *)",
|
"Bash(git add *)",
|
||||||
"Bash(git commit -m ' *)"
|
"Bash(git commit -m ' *)",
|
||||||
|
"Bash(FW_DEBUG=1 uv run mcp-familywall check)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -42,11 +42,11 @@ htmlcov/
|
|||||||
# uv
|
# uv
|
||||||
uv.lock
|
uv.lock
|
||||||
|
|
||||||
# Debug and probe scripts (root-level only)
|
# Debug and probe scripts
|
||||||
/debug_*.py
|
debug_*.py
|
||||||
/probe_*.py
|
probe_*.py
|
||||||
/probe_*.txt
|
probe_*.txt
|
||||||
/probe_out.txt
|
probe_out.txt
|
||||||
/p*_err.txt
|
p*_err.txt
|
||||||
/test_*.py
|
test_*.py
|
||||||
reference/
|
reference/
|
||||||
|
|||||||
+208
@@ -5,6 +5,214 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Versioning Policy
|
||||||
|
|
||||||
|
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
|
||||||
|
- IMPORTANT confirmation notes in docstrings for write operations (`toggle_task`, `like_post`)
|
||||||
|
- `[project.urls]` section in `pyproject.toml` with Gitea repository link
|
||||||
|
- Language note in CLAUDE.md explaining German/English split
|
||||||
|
- `tests/README.md` documentation for running integration tests
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `.gitignore` patterns now recursive (test_*.py without leading slash)
|
||||||
|
- CLI help text updated (removed "(read-only)" qualifier)
|
||||||
|
- `clear_list()` now logs exception reasons for failed deletions
|
||||||
|
- `get_categories()` docstring clarifies custom vs. system category locale behavior
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Exception handling in `clear_list()` with detailed logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.0.0] – 2026-04-17
|
## [1.0.0] – 2026-04-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# mcp-familywall
|
# mcp-familywall
|
||||||
|
|
||||||
|
**Note:** This file is intentionally written in German (developer preference).
|
||||||
|
Code, docstrings, and commit messages are in English.
|
||||||
|
|
||||||
## Kontext
|
## Kontext
|
||||||
|
|
||||||
Dieses Projekt entwickelt `mcp-familywall` – einen MCP-Server für den
|
Dieses Projekt entwickelt `mcp-familywall` – einen MCP-Server für den
|
||||||
@@ -24,20 +27,21 @@ und wird in Claude Desktop eingebunden.
|
|||||||
|
|
||||||
## Aktueller Stand
|
## Aktueller Stand
|
||||||
|
|
||||||
### Version: **v1.0.0** ← aktuell
|
### Version: **v1.4.3** ← aktuell
|
||||||
|
|
||||||
### Implementierte Tools
|
### Implementierte Tools
|
||||||
|
|
||||||
| Kategorie | 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` |
|
| 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` |
|
| 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` |
|
| Essensplaner | `get_meal_plan`, `add_recipe_to_meal_plan`, `add_meal_to_meal_plan`, `add_meal_note`, `delete_meal_plan_entry` |
|
||||||
|
|
||||||
### Roadmap (Nächstes)
|
### 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)
|
### Historische Meilensteine (kompakt)
|
||||||
|
|
||||||
@@ -47,6 +51,8 @@ und wird in Claude Desktop eingebunden.
|
|||||||
- v0.9: Task-Wiederholungen + Erinnerungen (read-only)
|
- v0.9: Task-Wiederholungen + Erinnerungen (read-only)
|
||||||
- v0.10–v0.11: Essensplaner (read + write)
|
- v0.10–v0.11: Essensplaner (read + write)
|
||||||
- v1.0: Cleanup, Unified errors, Datumsvalidierung, Partial-Failure-Reporting (Details: CHANGELOG.md)
|
- 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
|
## Architektur-Entscheidungen
|
||||||
@@ -91,6 +97,9 @@ können den echten Request-Body in diesen Fällen nicht sehen.
|
|||||||
|
|
||||||
## Claude Code – Implementierungsregeln
|
## 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
|
- **Feature complete before next feature** – jedes Feature vollständig
|
||||||
implementieren, testen und verifizieren bevor das nächste beginnt
|
implementieren, testen und verifizieren bevor das nächste beginnt
|
||||||
- **Kein destruktives Probing** – keine Probe-Calls auf System-Kategorien,
|
- **Kein destruktives Probing** – keine Probe-Calls auf System-Kategorien,
|
||||||
@@ -129,9 +138,12 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
|||||||
| `taskcreate2` | `taskListId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee` | – |
|
| `taskcreate2` | `taskListId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee` | – |
|
||||||
| `taskupdate2` | `metaId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee`, `taskListId` | – |
|
| `taskupdate2` | `metaId`, `text`, `description`, `taskCategoryId`, `dueDate`, `assignee`, `taskListId` | – |
|
||||||
| `taskupdate2` | `dueDate` löschen | `$empty` |
|
| `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"` |
|
| `taskmark` | `taskId`, `complete` | `"true"`/`"false"` |
|
||||||
| `metadelete` | `id` | metaId des Tasks / Rezepts |
|
| `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` | – |
|
| `taskcategoryput` | `name`, `emoji` | – |
|
||||||
| `taskcategorydelete` | `id` | metaId der Kategorie |
|
| `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 |
|
| `taskcreatelist` | `name`, `taskListType`, `sharedToAll`, `color`, `emoji`, `scope` | `taskListType`: `SHOPPING_LIST`, `TODOS`, `OTHER`; `scope`: Kreis-metaId für nicht-primäre Kreise |
|
||||||
@@ -150,9 +162,8 @@ Fehler bei falschen Parametern kommen nicht immer auf Top-Level:
|
|||||||
### Self-Like-Restriction
|
### Self-Like-Restriction
|
||||||
Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts.
|
Eigene Posts können nicht geliked werden. API antwortet 200, macht aber nichts.
|
||||||
|
|
||||||
### Unlike nicht möglich
|
### Unlike
|
||||||
Service Worker verschlüsselt den Unlike-Request-Body.
|
Unlike via `remove.0=STAR` (Array-Dot-Notation). Verifiziert 2026-04-17 via Network-Interceptor.
|
||||||
Endpoint unbekannt. `like_post(like=False)` gibt Fehlermeldung zurück.
|
|
||||||
|
|
||||||
### mpstar / Rezept-Favorit
|
### mpstar / Rezept-Favorit
|
||||||
Service Worker fängt `mpstar` ab. `metamood` funktioniert nur auf
|
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.
|
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
|
### 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_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_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_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) |
|
| `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) |
|
| `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) |
|
| `delete_list` 🔒 | Permanently delete a list and all its tasks (system lists protected) |
|
||||||
| `create_category` 🔒 | Create a custom category (with optional icon) |
|
| `create_category` 🔒 | Create a custom category (with optional icon) |
|
||||||
| `delete_category` 🔒 | Delete a custom category (system categories protected) |
|
| `delete_category` 🔒 | Delete a custom category (system categories protected) |
|
||||||
| `create_task` 🔒 | Create a task (supports category, due date, assignees; use `"Äpfel (5x)"` for quantities) |
|
| `create_task` 🔒 | Create a task (category, due date, assignees, reminder; use `"Äpfel (5x)"` for quantities) |
|
||||||
| `update_task` 🔒 | Update text, category, due date, assignees, or move to a different list |
|
| `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 |
|
| `toggle_task` 🔒 | Mark a task complete or reopen it |
|
||||||
| `delete_task` 🔒 | Permanently delete a task |
|
| `delete_task` 🔒 | Permanently delete a task |
|
||||||
| `clear_list` 🔒 | Bulk-delete all tasks in a list (optional `only_open=True` keeps completed tasks) |
|
| `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
|
### Recipe Box
|
||||||
|
|
||||||
|
|||||||
@@ -201,9 +201,41 @@ POST https://api.familywall.com/api/taskupdate2
|
|||||||
| `dueDate` | nein | ISO 8601 oder `$empty` zum Löschen |
|
| `dueDate` | nein | ISO 8601 oder `$empty` zum Löschen |
|
||||||
| `assignee` | nein | Member-accountId (mehrfach sendbar), `""` zum Entfernen aller |
|
| `assignee` | nein | Member-accountId (mehrfach sendbar), `""` zum Entfernen aller |
|
||||||
| `taskListId` | nein | neue Listen-metaId (verschiebt Task) |
|
| `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.
|
**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:**
|
**Response:**
|
||||||
```
|
```
|
||||||
a00.r.r → vollständiges Task-Objekt
|
a00.r.r → vollständiges Task-Objekt
|
||||||
@@ -237,32 +269,86 @@ POST https://api.familywall.com/api/metadelete
|
|||||||
- Tasks: `task/<id>`
|
- Tasks: `task/<id>`
|
||||||
- Rezepte: `recipe/<id>`
|
- Rezepte: `recipe/<id>`
|
||||||
- Essensplan-Einträge: `dish/<id>` und `meal/<id>`
|
- Essensplan-Einträge: `dish/<id>` und `meal/<id>`
|
||||||
|
- Wall-Posts: `wall/<id>` (v1.3.1+)
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
```
|
```
|
||||||
a00.r.r → "true" (String)
|
a00.r.r → "true" (String)
|
||||||
```
|
```
|
||||||
|
|
||||||
### `wallmood` – Post liken
|
### `wallmood` – Post liken / unlike
|
||||||
POST https://api.familywall.com/api/wallmood
|
POST https://api.familywall.com/api/wallmood
|
||||||
|
|
||||||
**Body-Parameter:**
|
**Body-Parameter (Like):**
|
||||||
|
|
||||||
| Parameter | Wert |
|
| Parameter | Wert |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `wall_message_id` | Post-metaId ⚠️ nicht `wallId` oder `id`! |
|
| `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:**
|
**Bekannte Einschränkungen:**
|
||||||
- Unlike: Endpoint/Parameter unbekannt (Service Worker verschlüsselt Request-Body)
|
|
||||||
- Self-Like: API antwortet 200, macht aber serverseitig nichts
|
- Self-Like: API antwortet 200, macht aber serverseitig nichts
|
||||||
- `moodType="NONE"` und andere Werte haben keine Wirkung
|
|
||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
```
|
```
|
||||||
a00.r.r → Wall-Objekt mit moodMap, refAction: "MOOD_STAR"
|
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
|
### `taskcategoryput` – Kategorie erstellen/aktualisieren
|
||||||
POST https://api.familywall.com/api/taskcategoryput
|
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
|
## Offene Punkte
|
||||||
|
|
||||||
- Unlike-Endpoint (Service Worker blockiert Analyse)
|
|
||||||
- `mpstar` / `isFavorite` für Rezepte (Service Worker blockiert Analyse, siehe oben)
|
- `mpstar` / `isFavorite` für Rezepte (Service Worker blockiert Analyse, siehe oben)
|
||||||
- Erinnerungen (reminder) – nur Premium-Account
|
- Erinnerungen (reminder) – nur Premium-Account
|
||||||
- Wiederholungen (repeat) – nur Premium-Account
|
- Wiederholungen (repeat) – nur Premium-Account
|
||||||
|
|||||||
+4
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mcp-familywall"
|
name = "mcp-familywall"
|
||||||
version = "1.0.0"
|
version = "1.4.4"
|
||||||
description = "MCP server for Family Wall — manage your family's circles, lists, tasks, recipes, and meal plan via Claude"
|
description = "MCP server for Family Wall — manage your family's circles, lists, tasks, recipes, and meal plan via Claude"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
@@ -21,6 +21,9 @@ dependencies = [
|
|||||||
"rich>=13.0",
|
"rich>=13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://gitea.gecheckt.de/marcus/mcp-familywall"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=8.0",
|
"pytest>=8.0",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Prerequisites: credentials stored in OS keyring or FW_EMAIL / FW_PASSWORD set.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -54,9 +53,7 @@ CANDIDATES: list[dict[str, str | None]] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _resolve_category(
|
def _resolve_category(client: FamilyWallClient, list_id: str) -> tuple[str, str, str]:
|
||||||
client: FamilyWallClient, list_id: str
|
|
||||||
) -> tuple[str, str, str]:
|
|
||||||
"""Return (meta_id, system_category_id_str, name) for the first German category."""
|
"""Return (meta_id, system_category_id_str, name) for the first German category."""
|
||||||
data = client.call(
|
data = client.call(
|
||||||
"accgetallfamily",
|
"accgetallfamily",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from mcp_familywall import __version__
|
|||||||
@click.version_option(__version__, "-v", "--version", prog_name="mcp-familywall")
|
@click.version_option(__version__, "-v", "--version", prog_name="mcp-familywall")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def app(ctx: click.Context) -> None:
|
def app(ctx: click.Context) -> None:
|
||||||
"""mcp-familywall — MCP server for Family Wall (read-only)."""
|
"""mcp-familywall — MCP server for Family Wall."""
|
||||||
if ctx.invoked_subcommand is None:
|
if ctx.invoked_subcommand is None:
|
||||||
click.echo(ctx.get_help())
|
click.echo(ctx.get_help())
|
||||||
|
|
||||||
|
|||||||
+492
-46
@@ -44,9 +44,13 @@ def _validate_date(date: str) -> str | None:
|
|||||||
return 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.
|
"""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:
|
Raises:
|
||||||
RuntimeError: On credential or API errors.
|
RuntimeError: On credential or API errors.
|
||||||
"""
|
"""
|
||||||
@@ -58,10 +62,10 @@ def _accgetallfamily() -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
with FamilyWallClient() as client:
|
with FamilyWallClient() as client:
|
||||||
client.login(email, password)
|
client.login(email, password)
|
||||||
data = client.call(
|
params: dict[str, Any] = {"a01call": "taskcategorysync", "a02call": "tasksync"}
|
||||||
"accgetallfamily",
|
if scope:
|
||||||
{"a01call": "taskcategorysync", "a02call": "tasksync"},
|
params["scope"] = scope
|
||||||
)
|
data = client.call("accgetallfamily", params)
|
||||||
client.logout()
|
client.logout()
|
||||||
return data
|
return data
|
||||||
except FamilyWallError as exc:
|
except FamilyWallError as exc:
|
||||||
@@ -142,7 +146,10 @@ def _extract_tasks(data: dict[str, Any]) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def get_circles() -> str:
|
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:
|
try:
|
||||||
raw_circles = _famlistfamily()
|
raw_circles = _famlistfamily()
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
@@ -216,6 +223,9 @@ def _famlistfamily() -> list[dict[str, Any]]:
|
|||||||
def get_members(circle_id: str | None = None) -> str:
|
def get_members(circle_id: str | None = None) -> str:
|
||||||
"""Return Family Wall circle members as JSON, optionally filtered by circle.
|
"""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:
|
Args:
|
||||||
circle_id: Optional circle ID from get_circles (e.g. ``family/23431854``).
|
circle_id: Optional circle ID from get_circles (e.g. ``family/23431854``).
|
||||||
When omitted all members across all circles are returned.
|
When omitted all members across all circles are returned.
|
||||||
@@ -307,6 +317,9 @@ def get_lists(scope: str | None = None) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
JSON list of list objects with keys id, name, type, open, total,
|
JSON list of list objects with keys id, name, type, open, total,
|
||||||
emoji, color, circle_id.
|
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:
|
try:
|
||||||
email, password = get_credentials()
|
email, password = get_credentials()
|
||||||
@@ -402,9 +415,35 @@ def get_lists(scope: str | None = None) -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def get_tasks(list_id: str, only_open: bool = True) -> str:
|
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:
|
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:
|
except RuntimeError as exc:
|
||||||
return _err(str(exc))
|
return _err(str(exc))
|
||||||
|
|
||||||
@@ -510,12 +549,16 @@ def get_categories(list_id: str, locale: str = "de") -> str:
|
|||||||
"""Return the task categories available for a list as JSON.
|
"""Return the task categories available for a list as JSON.
|
||||||
|
|
||||||
Only shopping lists (taskListType=SHOPPING_LIST) have categories. TODO
|
Only shopping lists (taskListType=SHOPPING_LIST) have categories. TODO
|
||||||
lists return an empty list. Categories are filtered by locale so only
|
lists return an empty list. System categories are filtered by locale so only
|
||||||
the language-appropriate names are returned (default: German).
|
the language-appropriate names are returned (default: German). Custom categories
|
||||||
|
have no locale field and are always displayed regardless of the locale parameter.
|
||||||
|
|
||||||
Use the returned ``id`` values as the ``category_id`` parameter in
|
Use the returned ``id`` values as the ``category_id`` parameter in
|
||||||
``create_task`` and ``update_task``.
|
``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:
|
Args:
|
||||||
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
|
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
|
||||||
locale: BCP-47 language code for category names (default ``"de"``).
|
locale: BCP-47 language code for category names (default ``"de"``).
|
||||||
@@ -736,7 +779,21 @@ def delete_category(category_id: str) -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def get_activities(limit: int = 20) -> str:
|
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:
|
try:
|
||||||
email, password = get_credentials()
|
email, password = get_credentials()
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
@@ -806,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]:
|
def _authenticated_call(endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Login, call *endpoint* with *params*, logout, and return the response body.
|
"""Login, call *endpoint* with *params*, logout, and return the response body.
|
||||||
|
|
||||||
@@ -846,6 +956,8 @@ def create_task(
|
|||||||
category_id: str | None = None,
|
category_id: str | None = None,
|
||||||
due_date: str | None = None,
|
due_date: str | None = None,
|
||||||
assignee_ids: list[str] | None = None,
|
assignee_ids: list[str] | None = None,
|
||||||
|
reminder_unit: str | None = None,
|
||||||
|
reminder_value: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Create a new task in the given list.
|
"""Create a new task in the given list.
|
||||||
|
|
||||||
@@ -877,10 +989,20 @@ def create_task(
|
|||||||
due_date: Optional due date in ISO 8601 format (e.g. ``"2026-04-30T18:00:00"``).
|
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
|
assignee_ids: Optional list of member IDs from get_members to assign the task
|
||||||
(e.g. ``["23431898"]``). Empty list assigns to nobody.
|
(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:
|
Returns:
|
||||||
JSON with the new task's metaId on success, or an error message.
|
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}
|
params: dict[str, Any] = {"taskListId": list_id, "text": text}
|
||||||
if description:
|
if description:
|
||||||
params["description"] = description
|
params["description"] = description
|
||||||
@@ -890,6 +1012,7 @@ def create_task(
|
|||||||
params["dueDate"] = due_date
|
params["dueDate"] = due_date
|
||||||
if assignee_ids is not None:
|
if assignee_ids is not None:
|
||||||
params["assignee"] = assignee_ids if assignee_ids else ""
|
params["assignee"] = assignee_ids if assignee_ids else ""
|
||||||
|
params.update(reminder_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = _authenticated_call("taskcreate2", params)
|
data = _authenticated_call("taskcreate2", params)
|
||||||
@@ -925,6 +1048,13 @@ def update_task(
|
|||||||
clear_due_date: bool = False,
|
clear_due_date: bool = False,
|
||||||
assignee_ids: list[str] | None = None,
|
assignee_ids: list[str] | None = None,
|
||||||
list_id: 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:
|
) -> str:
|
||||||
"""Update an existing task's fields.
|
"""Update an existing task's fields.
|
||||||
|
|
||||||
@@ -947,12 +1077,39 @@ def update_task(
|
|||||||
list_id: Move the task to a different list by providing the target list ID
|
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
|
from get_lists (e.g. ``"taskList/23431854_29740942"``). Omit to keep in
|
||||||
current list.
|
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:
|
Returns:
|
||||||
JSON success indicator or an error message.
|
JSON success indicator or an error message.
|
||||||
"""
|
"""
|
||||||
if clear_due_date and due_date is not None:
|
if clear_due_date and due_date is not None:
|
||||||
return _err("'clear_due_date' and 'due_date' cannot be used together.")
|
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 (
|
if (
|
||||||
text is None
|
text is None
|
||||||
@@ -962,10 +1119,15 @@ def update_task(
|
|||||||
and not clear_due_date
|
and not clear_due_date
|
||||||
and assignee_ids is None
|
and assignee_ids is None
|
||||||
and list_id is None
|
and list_id is None
|
||||||
|
and recurrency is None
|
||||||
|
and not clear_recurrency
|
||||||
|
and not reminder_params
|
||||||
):
|
):
|
||||||
return _err(
|
return _err(
|
||||||
"At least one of 'text', 'description', 'category_id', 'due_date',"
|
"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}
|
params: dict[str, Any] = {"metaId": task_id}
|
||||||
@@ -985,6 +1147,19 @@ def update_task(
|
|||||||
if list_id is not None:
|
if list_id is not None:
|
||||||
params["taskListId"] = list_id
|
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:
|
try:
|
||||||
_authenticated_call("taskupdate2", params)
|
_authenticated_call("taskupdate2", params)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
@@ -1003,6 +1178,8 @@ def update_task(
|
|||||||
def toggle_task(task_id: str, complete: bool) -> str:
|
def toggle_task(task_id: str, complete: bool) -> str:
|
||||||
"""Mark a task as complete or incomplete.
|
"""Mark a task as complete or incomplete.
|
||||||
|
|
||||||
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: Task metaId from get_tasks.
|
task_id: Task metaId from get_tasks.
|
||||||
complete: ``True`` to mark done, ``False`` to reopen.
|
complete: ``True`` to mark done, ``False`` to reopen.
|
||||||
@@ -1061,7 +1238,10 @@ def delete_task(task_id: str) -> str:
|
|||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def clear_list(list_id: str, only_open: bool = False) -> str:
|
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.
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
|
|
||||||
@@ -1072,7 +1252,7 @@ def clear_list(list_id: str, only_open: bool = False) -> str:
|
|||||||
list_id: List metaId from get_lists
|
list_id: List metaId from get_lists
|
||||||
(e.g. ``"taskList/16282169_29775360"``).
|
(e.g. ``"taskList/16282169_29775360"``).
|
||||||
only_open: When ``True`` only incomplete tasks are deleted;
|
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:
|
Returns:
|
||||||
JSON with ``deleted_count`` and ``list_id`` on success,
|
JSON with ``deleted_count`` and ``list_id`` on success,
|
||||||
@@ -1110,8 +1290,9 @@ def clear_list(list_id: str, only_open: bool = False) -> str:
|
|||||||
try:
|
try:
|
||||||
client.call("metadelete", {"id": meta_id})
|
client.call("metadelete", {"id": meta_id})
|
||||||
deleted_ids.append(meta_id)
|
deleted_ids.append(meta_id)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
failed_ids.append(meta_id)
|
failed_ids.append(meta_id)
|
||||||
|
logger.warning(f"Failed to delete {meta_id}: {e}")
|
||||||
|
|
||||||
client.logout()
|
client.logout()
|
||||||
except FamilyWallError as exc:
|
except FamilyWallError as exc:
|
||||||
@@ -1779,40 +1960,35 @@ def delete_circle(circle_id: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def like_post(post_id: str, like: bool = True) -> str:
|
def like_post(post_id: str, like: bool = True, mood: str = "STAR") -> str:
|
||||||
"""Like a wall post/activity with a STAR mood.
|
"""Like or unlike a wall post/activity.
|
||||||
|
|
||||||
Note: Unlike (like=False) is not yet supported. The Family Wall API offers
|
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||||
no discoverable endpoint or parameter to remove a like. Passing like=False
|
|
||||||
returns an error without making any API call.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
post_id: Wall post ID from get_activities (e.g. ``wall/23431854_31119189``).
|
post_id: Post metaId from get_wall_posts or get_activities
|
||||||
like: Must be ``True``. ``False`` is reserved for future unlike support.
|
(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:
|
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
|
# Like: add=STAR, remove=$empty (idempotent set)
|
||||||
# wallmood with moodType="STAR" is an idempotent SET operation (not a toggle).
|
# Unlike: add=$empty, remove.0=STAR (array dot-notation, verified via Network Interceptor)
|
||||||
# Tested and ruled out: moodType variations ("NONE", "REMOVE", "DELETE", ""),
|
if like:
|
||||||
# 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] = {
|
params: dict[str, Any] = {
|
||||||
"wall_message_id": post_id,
|
"wall_message_id": post_id,
|
||||||
"moodType": "STAR",
|
"add": mood,
|
||||||
|
"remove": "$empty",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
params = {
|
||||||
|
"wall_message_id": post_id,
|
||||||
|
"add": "$empty",
|
||||||
|
"remove.0": mood,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1844,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}
|
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
|
if now_liked != like:
|
||||||
# caller can distinguish a successful like from a silent API rejection
|
if like:
|
||||||
# (e.g. rate limit, unsupported post type, or self-like restriction).
|
|
||||||
if not now_liked:
|
|
||||||
result["warning"] = (
|
result["warning"] = (
|
||||||
"Like may not have been applied. "
|
"Like may not have been applied. "
|
||||||
"Possible causes: rate limit, unsupported post type (e.g. FAMILY_CREATED), "
|
"Possible causes: rate limit, unsupported post type (e.g. FAMILY_CREATED), "
|
||||||
"or self-like restriction."
|
"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)
|
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
|
# Helper: fetch all raw recipes
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1908,6 +2346,10 @@ def _get_raw_recipes() -> list[dict[str, Any]]:
|
|||||||
def get_recipe_categories() -> str:
|
def get_recipe_categories() -> str:
|
||||||
"""Return all available recipe categories for the family.
|
"""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:
|
Returns:
|
||||||
JSON list of category IDs (e.g. ["category/23431854_2", ...]).
|
JSON list of category IDs (e.g. ["category/23431854_2", ...]).
|
||||||
Always includes the 5 free-tier standard categories if family ID is available.
|
Always includes the 5 free-tier standard categories if family ID is available.
|
||||||
@@ -2358,6 +2800,9 @@ def get_meal_plan(date_from: str, date_to: str) -> str:
|
|||||||
- ``can_update`` — whether the entry can be updated
|
- ``can_update`` — whether the entry can be updated
|
||||||
- ``can_delete`` — whether the entry can be deleted
|
- ``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.
|
Returns an error message string on failure.
|
||||||
"""
|
"""
|
||||||
if date_err := _validate_date(date_from):
|
if date_err := _validate_date(date_from):
|
||||||
@@ -2637,7 +3082,8 @@ def add_meal_note(
|
|||||||
|
|
||||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
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.
|
At least one of ``note`` or ``serves`` must be provided.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Tests
|
||||||
|
|
||||||
|
Integration tests for mcp-familywall.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Tests require valid Family Wall credentials to run against the live API.
|
||||||
|
Credentials are loaded from:
|
||||||
|
|
||||||
|
1. **OS Keyring** (recommended) — set via `mcp-familywall setup`
|
||||||
|
2. **Environment variables** — `FW_EMAIL` and `FW_PASSWORD`
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
For verbose output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
- Circles management (create, list, update, delete, members)
|
||||||
|
- Lists and tasks (CRUD operations)
|
||||||
|
- Categories (system and custom)
|
||||||
|
- Recipes and meal planning
|
||||||
|
- Activity feed and reactions
|
||||||
|
- Date validation and error handling
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tests create temporary circles, lists, and tasks for verification
|
||||||
|
- All test objects are cleaned up after execution
|
||||||
|
- Do not run tests against production data — use test credentials only
|
||||||
|
- Some tests are marked as skipped if premium features are unavailable
|
||||||
Reference in New Issue
Block a user