From 5aff3ac9bfddbe30f8411269f84bcac574e113a6 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Wed, 15 Apr 2026 15:06:38 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Task=20CRUD=20tools=20=E2=80=93?= =?UTF-8?q?=20create,=20update,=20toggle,=20delete=20(v0.4.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements four new MCP write tools via taskcreate2, taskupdate2, taskmark, and metadelete endpoints. Confirmation prompts noted in docstrings for destructive/mutating operations. Body parameters documented in SPEC.md as pending verification via FW_DEBUG=1. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 14 ++- SPEC.md | 66 ++++++++++++- src/mcp_familywall/__init__.py | 2 +- src/mcp_familywall/server.py | 172 ++++++++++++++++++++++++++++++++- 4 files changed, 249 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b0ec4df..9604bb4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ # mcp-familywall -MCP server for [Family Wall](https://www.familywall.com) -- read your family's circles, lists, and tasks directly from Claude. +MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, and tasks directly from Claude. -## Features (v1.0 -- read-only) +## Features (v0.4.0) + +### Read - `get_circles` -- list all family circles - `get_lists` -- list all task lists (optionally filtered by circle) - `get_tasks` -- list tasks in a specific list +- `get_activities` -- list recent wall activities + +### Write (with confirmation prompt) + +- `create_task` -- create a new task in a list +- `update_task` -- update the text/description of an existing task +- `toggle_task` -- mark a task complete or reopen it +- `delete_task` -- permanently delete a task ## Requirements diff --git a/SPEC.md b/SPEC.md index 85c9fc3..d22e552 100644 --- a/SPEC.md +++ b/SPEC.md @@ -185,6 +185,66 @@ offener Punkte (z.B. `type`-Parameter beim Login, Kreis-Felder in Response). **Wichtig:** Keine Secrets in Debug-Ausgaben (Passwort maskieren). +### `taskcreate2` – Task erstellen +POST https://api.familywall.com/api/taskcreate2 +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (aus API-Pattern abgeleitet, zu verifizieren):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `taskListId` | ja | Listen-ID aus `get_lists` (z.B. `taskList/123_456`) | +| `text` | ja | Aufgabentitel | +| `description` | nein | Optionale Beschreibung | + +**Response-Struktur (zu verifizieren):** +``` +a00.r.r.metaId → metaId der neu erstellten Task +``` + +### `taskupdate2` – Task aktualisieren +POST https://api.familywall.com/api/taskupdate2 +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (aus API-Pattern abgeleitet, zu verifizieren):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `metaId` | ja | Task-ID aus `get_tasks` | +| `text` | nein | Neuer Titel (mindestens `text` oder `description` erforderlich) | +| `description` | nein | Neue Beschreibung | +| `taskListId` | unklar | Evtl. erforderlich – zu verifizieren | + +**Response-Struktur:** kein spezifischer Rückgabewert erwartet (Erfolg = kein `ex`/`un`-Key) + +### `taskmark` – Task als erledigt/offen markieren +POST https://api.familywall.com/api/taskmark +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (aus API-Pattern abgeleitet, zu verifizieren):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `metaId` | ja | Task-ID aus `get_tasks` | +| `complete` | ja | `"true"` oder `"false"` (String, nicht Boolean!) | + +**Response-Struktur:** kein spezifischer Rückgabewert erwartet (Erfolg = kein `ex`/`un`-Key) + +### `metadelete` – Objekt löschen (Task) +POST https://api.familywall.com/api/metadelete +Content-Type: application/x-www-form-urlencoded + +**Body-Parameter (aus API-Pattern abgeleitet, zu verifizieren):** + +| Parameter | Pflicht | Wert | +|---|---|---| +| `metaId` | ja | Task-ID aus `get_tasks` | + +Hinweis: `metadelete` ist ein generischer Lösch-Endpoint. Er löscht jedes Objekt +mit der angegebenen `metaId` – nicht nur Tasks. Entsprechend vorsichtig verwenden. + +**Response-Struktur:** kein spezifischer Rückgabewert erwartet (Erfolg = kein `ex`/`un`-Key) + ## Noch zu verifizieren - ~~Exakter Wert für `type`-Parameter beim Login~~ → nicht senden (verifiziert per JS-Analyse) @@ -194,4 +254,8 @@ offener Punkte (z.B. `type`-Parameter beim Login, Kreis-Felder in Response). - Listen-Namen und Zähler (remainingTaskNumber, totalTaskNumber) → noch unbekannt - Kreis-Zuordnung in `accgetallfamily`-Response → noch offen - ~~Ob `partnerScope` / `withStateBean` benötigt werden~~ → nein (verifiziert) -- Session-Lebensdauer (irrelevant da kein Caching) \ No newline at end of file +- Session-Lebensdauer (irrelevant da kein Caching) +- `taskcreate2`: genaue Response-Struktur (metaId-Pfad), ob weitere Pflichtfelder existieren +- `taskupdate2`: ob `taskListId` Pflichtfeld ist; Response-Struktur +- `taskmark`: Response-Struktur +- `metadelete`: Response-Struktur, welche Objekt-Typen unterstützt werden \ No newline at end of file diff --git a/src/mcp_familywall/__init__.py b/src/mcp_familywall/__init__.py index f9aa3e1..6a9beea 100644 --- a/src/mcp_familywall/__init__.py +++ b/src/mcp_familywall/__init__.py @@ -1 +1 @@ -__version__ = "0.3.2" +__version__ = "0.4.0" diff --git a/src/mcp_familywall/server.py b/src/mcp_familywall/server.py index b5a3456..12e4869 100644 --- a/src/mcp_familywall/server.py +++ b/src/mcp_familywall/server.py @@ -1,4 +1,4 @@ -"""MCP server for Family Wall — read-only tools for circles, lists and tasks.""" +"""MCP server for Family Wall — tools for circles, lists, tasks (read + write).""" from __future__ import annotations @@ -299,6 +299,176 @@ def get_activities(limit: int = 20): return json.dumps(result, ensure_ascii=False, indent=2) +# --------------------------------------------------------------------------- +# Helper: authenticated single call +# --------------------------------------------------------------------------- + + +def _authenticated_call(endpoint: str, params: dict[str, Any]) -> dict[str, Any]: + """Login, call *endpoint* with *params*, logout, and return the response body. + + Args: + endpoint: Family Wall API endpoint name. + params: Form parameters to send. + + Raises: + RuntimeError: On credential or API errors. + """ + try: + email, password = get_credentials() + except RuntimeError as exc: + raise RuntimeError(str(exc)) from exc + + try: + with FamilyWallClient() as client: + client.login(email, password) + data = client.call(endpoint, params) + client.logout() + return data + except FamilyWallError as exc: + raise RuntimeError(f"Family Wall API error: {exc}") from exc + except Exception as exc: + raise RuntimeError(f"Connection error: {exc}") from exc + + +# --------------------------------------------------------------------------- +# Tool: create_task +# --------------------------------------------------------------------------- + + +@mcp.tool() +def create_task(list_id: str, text: str, description: str | None = None) -> str: + """Create a new task in the given list. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Args: + list_id: Target list ID from get_lists (e.g. ``taskList/123_456``). + text: Task title / main text. + description: Optional longer description. + + Returns: + JSON with the new task's metaId on success, or an error message. + """ + params: dict[str, Any] = {"taskListId": list_id, "text": text} + if description: + params["description"] = description + + try: + data = _authenticated_call("taskcreate2", params) + except RuntimeError as exc: + return f"Error: {exc}" + + # Try to extract the new task's metaId from the response. + try: + meta_id = data["a00"]["r"]["r"]["metaId"] + except (KeyError, TypeError): + # Return raw response so the caller can inspect the actual structure. + return json.dumps( + {"warning": "Unexpected taskcreate2 response structure", "raw": data}, + ensure_ascii=False, + indent=2, + ) + + return json.dumps({"created": True, "id": meta_id}, ensure_ascii=False, indent=2) + + +# --------------------------------------------------------------------------- +# Tool: update_task +# --------------------------------------------------------------------------- + + +@mcp.tool() +def update_task(task_id: str, text: str | None = None, description: str | None = None) -> str: + """Update the text and/or description of an existing task. + + IMPORTANT: Ask the user for confirmation before calling this tool. + At least one of *text* or *description* must be provided. + + Args: + task_id: Task metaId from get_tasks. + text: New title text (omit to leave unchanged). + description: New description (omit to leave unchanged). + + Returns: + JSON success indicator or an error message. + """ + if text is None and description is None: + return "Error: At least one of 'text' or 'description' must be provided." + + params: dict[str, Any] = {"metaId": task_id} + if text is not None: + params["text"] = text + if description is not None: + params["description"] = description + + try: + _authenticated_call("taskupdate2", params) + except RuntimeError as exc: + return f"Error: {exc}" + + # A response without 'ex'/'un' keys is treated as success by fw_client. + return json.dumps({"updated": True, "id": task_id}, ensure_ascii=False, indent=2) + + +# --------------------------------------------------------------------------- +# Tool: toggle_task +# --------------------------------------------------------------------------- + + +@mcp.tool() +def toggle_task(task_id: str, complete: bool) -> str: + """Mark a task as complete or incomplete. + + Args: + task_id: Task metaId from get_tasks. + complete: ``True`` to mark done, ``False`` to reopen. + + Returns: + JSON success indicator or an error message. + """ + params: dict[str, Any] = { + "metaId": task_id, + "complete": "true" if complete else "false", + } + + try: + _authenticated_call("taskmark", params) + except RuntimeError as exc: + return f"Error: {exc}" + + return json.dumps( + {"toggled": True, "id": task_id, "complete": complete}, + ensure_ascii=False, + indent=2, + ) + + +# --------------------------------------------------------------------------- +# Tool: delete_task +# --------------------------------------------------------------------------- + + +@mcp.tool() +def delete_task(task_id: str) -> str: + """Permanently delete a task. This action cannot be undone. + + IMPORTANT: Ask the user for confirmation before calling this tool. + + Args: + task_id: Task metaId from get_tasks. + + Returns: + JSON success indicator or an error message. + """ + try: + _authenticated_call("metadelete", {"metaId": task_id}) + except RuntimeError as exc: + return f"Error: {exc}" + + return json.dumps({"deleted": True, "id": task_id}, ensure_ascii=False, indent=2) + + # --------------------------------------------------------------------------- # Factory # ---------------------------------------------------------------------------