feat: category assignment in create_task / update_task (v0.4.10)
Verified via FW_DEBUG=1 + systematic param-name probing that the correct parameter is `taskCategoryId` with value = full metaId from get_categories (e.g. taskCategory/23431854_200). Numeric systemCategoryId alone causes API error; full metaId is accepted and stored. Changes: - create_task: add optional category_id parameter → sent as taskCategoryId - update_task: add optional category_id parameter → sent as taskCategoryId; guard now accepts category_id-only updates - get_tasks: expose category_id field in returned task objects - get_categories: update docstring (param name now known) - SPEC.md: document verified taskCategoryId param + clarify categories[] vs taskCategoryId field distinction - scripts/find_category_param.py: discovery script used to find param name Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,21 +23,21 @@ eingebunden.
|
||||
|
||||
## Aktueller Stand
|
||||
|
||||
### Implementierte Tools (v0.4.8)
|
||||
### Implementierte Tools (v0.4.10)
|
||||
|
||||
| Kategorie | Tools |
|
||||
|---|---|
|
||||
| Kreise | `get_circles`, `get_members` |
|
||||
| Listen | `get_lists` |
|
||||
| Tasks (Lesen) | `get_tasks`, `get_categories` |
|
||||
| Tasks (Lesen) | `get_tasks` (inkl. `category_id`), `get_categories` |
|
||||
| Wall | `get_activities`, `like_post` |
|
||||
| Tasks (Schreiben) | `create_task`, `update_task`, `toggle_task`, `delete_task` |
|
||||
| Tasks (Schreiben) | `create_task` (inkl. `category_id`), `update_task` (inkl. `category_id`), `toggle_task`, `delete_task` |
|
||||
|
||||
|
||||
## Roadmap
|
||||
|
||||
- v0.x: Erweiterter Lese- + Schreibzugriff ← aktuell
|
||||
- Offen: Unlike (`like_post(like=False)`), Task-Kategorie-Zuweisung beim Erstellen/Aktualisieren
|
||||
- Offen: Unlike (`like_post(like=False)`)
|
||||
|
||||
|
||||
## Referenzprojekt
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
MCP server for [Family Wall](https://www.familywall.com) -- read and manage your family's circles, lists, and tasks directly from Claude.
|
||||
|
||||
## Features (v0.4.9)
|
||||
## Features (v0.4.10)
|
||||
|
||||
### Read
|
||||
|
||||
- `get_circles` -- list all family circles
|
||||
- `get_members` -- list members of a circle (or all circles)
|
||||
- `get_lists` -- list all task lists (optionally filtered by circle)
|
||||
- `get_tasks` -- list tasks in a specific list
|
||||
- `get_tasks` -- list tasks in a specific list (includes `category_id` field)
|
||||
- `get_categories` -- list categories available for a list (locale-filtered, default: German)
|
||||
- `get_activities` -- list recent wall activities (author resolved to display name)
|
||||
|
||||
### Write (with confirmation prompt)
|
||||
|
||||
- `create_task` -- create a new task in a list
|
||||
- `update_task` -- update the text/description of an existing task
|
||||
- `create_task` -- create a new task in a list (supports `category_id` for shopping lists)
|
||||
- `update_task` -- update text, description, and/or category of an existing task
|
||||
- `toggle_task` -- mark a task complete or reopen it
|
||||
- `delete_task` -- permanently delete a task
|
||||
- `like_post` -- like or unlike a wall post/activity
|
||||
|
||||
@@ -158,19 +158,34 @@ a02.r.r.updatedCreated[] → tasksync (Tasks)
|
||||
.description → optionale Beschreibung
|
||||
.taskListId → Zugehörigkeit zur Liste
|
||||
.complete → "true" / "false" (String, nicht Boolean!)
|
||||
.categories[] → zugewiesene Kategorien des Tasks
|
||||
.system → "true" wenn System-Kategorie, "false" wenn custom
|
||||
.name → Kategoriename (z.B. "SYS-CAT-TODOS", "Beverages")
|
||||
.categories[] → Listen-Level-Systemkategorie (z.B. SYS-CAT-SHOPPINGLIST);
|
||||
NICHT die spezifische Task-Kategorie — immer identisch
|
||||
für alle Tasks einer Liste
|
||||
.system → "true" (immer System-Kategorie)
|
||||
.name → Listen-Systemkategorien (z.B. "SYS-CAT-SHOPPINGLIST", "SYS-CAT-TODOS")
|
||||
.taskCategoryId → spezifische Task-Kategorie (verifiziert): metaId-Format
|
||||
(z.B. "taskCategory/23431854_200"), null wenn nicht gesetzt
|
||||
.assignee / .assigneeIds → zugewiesene Mitglieder
|
||||
.reminder → Erinnerungsdatum (ISO 8601, optional)
|
||||
.recurrency → Wiederholungsregel (optional)
|
||||
.sortingIndex → Anzeigereihenfolge
|
||||
```
|
||||
|
||||
**Kategorie-Zuweisung bei taskcreate2 / taskupdate2:** Parameter-Name noch unbekannt.
|
||||
Getestete Varianten (`taskCategoryId`, `categoryId`, `category`, `categoryMetaId`) werden
|
||||
serverseitig ignoriert — die Task bekommt stets die Default-Systemkategorie der Liste.
|
||||
Service Worker in der Web-App verhindert Inspektion des echten Requests. Noch zu verifizieren.
|
||||
**Kategorie-Zuweisung bei taskcreate2 / taskupdate2 (verifiziert):**
|
||||
|
||||
| Parameter | Pflicht | Wert |
|
||||
|---|---|---|
|
||||
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
|
||||
|
||||
Hinweise:
|
||||
- Wert muss das vollständige metaId-Format `taskCategory/<familyId>_<systemCategoryId>` sein.
|
||||
Nur der numerische `systemCategoryId`-Teil (z.B. `200`) führt zu API-Fehler
|
||||
`"cannot find task category id=200"`.
|
||||
- Das `categories[]`-Feld in der Response zeigt immer `SYS-CAT-SHOPPINGLIST`
|
||||
(Listen-Level-Systemkategorie, unabhängig vom gesetzten `taskCategoryId`).
|
||||
Die tatsächliche Task-Kategorie ist im Feld `taskCategoryId` der Task gespeichert.
|
||||
- Nur für Einkaufslisten (`taskListType=SHOPPING_LIST`) relevant;
|
||||
TODO-Listen haben keine Kategorien.
|
||||
|
||||
## Systembezeichnungen für Listen-Namen
|
||||
|
||||
@@ -244,6 +259,7 @@ Content-Type: application/x-www-form-urlencoded
|
||||
| `taskListId` | ja | Listen-ID aus `get_lists` (z.B. `taskList/123_456`) |
|
||||
| `text` | ja | Aufgabentitel |
|
||||
| `description` | nein | Optionale Beschreibung |
|
||||
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
|
||||
|
||||
**Response-Struktur (verifiziert):**
|
||||
```
|
||||
@@ -263,8 +279,9 @@ Content-Type: application/x-www-form-urlencoded
|
||||
| Parameter | Pflicht | Wert |
|
||||
|---|---|---|
|
||||
| `metaId` | ja | Task-ID aus `get_tasks` |
|
||||
| `text` | nein | Neuer Titel (mindestens `text` oder `description` erforderlich) |
|
||||
| `text` | nein | Neuer Titel (mindestens eines der optionalen Felder erforderlich) |
|
||||
| `description` | nein | Neue Beschreibung |
|
||||
| `taskCategoryId` | nein | Kategorie-MetaId aus `get_categories` (z.B. `taskCategory/23431854_200`) |
|
||||
|
||||
Hinweis: `taskListId` ist **nicht** erforderlich (verifiziert – Update ohne `taskListId` funktioniert).
|
||||
|
||||
@@ -408,4 +425,5 @@ AND `moodStarShortcut: false` AND `moodMap: {}`.
|
||||
- ~~`metadelete`: korrekter Parameter-Name + Response-Struktur~~ → **`id`**, Response `"true"` (verifiziert)
|
||||
- ~~`wallmood`: Parameter-Name `wallId`~~ → **`wall_message_id`** (verifiziert via API-Fehlermeldung)
|
||||
- ~~`wallmood`: `moodType`-Werte, Toggle vs. explizit, Response-Struktur~~ → verifiziert: idempotentes SET mit `"STAR"`, kein Toggle (siehe oben)
|
||||
- `wallmood` Unlike: Mechanismus unbekannt — Service Worker verhindert Browser-Inspektion; alle getesteten Ansätze fehlgeschlagen (siehe oben)
|
||||
- `wallmood` Unlike: Mechanismus unbekannt — Service Worker verhindert Browser-Inspektion; alle getesteten Ansätze fehlgeschlagen (siehe oben)
|
||||
- ~~`taskcreate2` / `taskupdate2`: Kategorie-Paramter-Name~~ → **`taskCategoryId`**, Wert = vollständige metaId (verifiziert)
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mcp-familywall"
|
||||
version = "0.4.9"
|
||||
version = "0.4.10"
|
||||
description = "MCP server for Family Wall — read your family's lists and tasks via Claude"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Discover the category assignment parameter name for taskcreate2 / taskupdate2.
|
||||
|
||||
Usage:
|
||||
FW_DEBUG=1 python scripts/find_category_param.py
|
||||
|
||||
The script:
|
||||
1. Fetches the first available shopping-list category (German locale).
|
||||
2. For each candidate parameter name, creates a test task with that parameter set,
|
||||
checks whether the category appears in the response, then deletes the task.
|
||||
3. Reports which parameter name (if any) caused the category to be applied.
|
||||
|
||||
Prerequisites: credentials stored in OS keyring or FW_EMAIL / FW_PASSWORD set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Allow running from repo root without installing the package.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from mcp_familywall.auth import get_credentials
|
||||
from mcp_familywall.fw_client import FamilyWallClient, FamilyWallError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Shopping list to use for the test (adjust if needed).
|
||||
TEST_LIST_ID = "taskList/23431854_29740941"
|
||||
|
||||
# Category to assign — will be resolved from the first taskcategorysync entry
|
||||
# for this list in German locale; override here if desired.
|
||||
FORCED_CATEGORY_META_ID: str | None = None
|
||||
|
||||
# Parameter-name candidates not yet tested (already ruled out are commented out):
|
||||
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
|
||||
# Numeric variants of already-tried names
|
||||
{"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]:
|
||||
"""Return (meta_id, system_category_id_str, name) for the first German category."""
|
||||
data = client.call(
|
||||
"accgetallfamily",
|
||||
{"a01call": "taskcategorysync", "a02call": "tasksync"},
|
||||
)
|
||||
cats = data["a01"]["r"]["r"]["updatedCreated"]
|
||||
|
||||
# Also resolve list type for filtering.
|
||||
list_type: str | None = None
|
||||
try:
|
||||
list_data = client.call("taskgettasklists", {})
|
||||
for lst in list_data.get("a00", {}).get("r", {}).get("r", []) or []:
|
||||
if lst.get("metaId") == list_id:
|
||||
list_type = lst.get("taskListType")
|
||||
break
|
||||
except FamilyWallError:
|
||||
pass
|
||||
|
||||
for cat in cats:
|
||||
if cat.get("locale") != "de":
|
||||
continue
|
||||
if list_type and cat.get("taskListType") != list_type:
|
||||
continue
|
||||
meta_id: str = cat["metaId"]
|
||||
sys_id: str = str(cat.get("systemCategoryId", ""))
|
||||
name: str = cat.get("name", "")
|
||||
print(f" Using category: metaId={meta_id!r}, systemCategoryId={sys_id!r}, name={name!r}")
|
||||
return meta_id, sys_id, name
|
||||
|
||||
raise RuntimeError("No German category found for list; adjust TEST_LIST_ID.")
|
||||
|
||||
|
||||
def _get_task_categories(task_obj: dict) -> list[str]:
|
||||
"""Extract category names from a task API object."""
|
||||
cats = task_obj.get("categories") or []
|
||||
return [c.get("name", "") for c in cats if isinstance(c, dict)]
|
||||
|
||||
|
||||
def _delete_task(client: FamilyWallClient, task_id: str) -> None:
|
||||
try:
|
||||
client.call("metadelete", {"id": task_id})
|
||||
except FamilyWallError as exc:
|
||||
print(f" [warn] Could not delete task {task_id}: {exc}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
email, password = get_credentials()
|
||||
|
||||
with FamilyWallClient() as client:
|
||||
client.login(email, password)
|
||||
|
||||
print(f"\nResolving category for list {TEST_LIST_ID!r} …")
|
||||
if FORCED_CATEGORY_META_ID:
|
||||
# Can't resolve name/numeric from meta_id easily; just use the meta_id.
|
||||
cat_meta = FORCED_CATEGORY_META_ID
|
||||
cat_numeric = cat_meta.split("_")[-1] if "_" in cat_meta else cat_meta
|
||||
cat_name = cat_meta
|
||||
else:
|
||||
cat_meta, cat_numeric, cat_name = _resolve_category(client, TEST_LIST_ID)
|
||||
|
||||
value_map = {
|
||||
"meta_id": cat_meta,
|
||||
"numeric": cat_numeric,
|
||||
"name": cat_name,
|
||||
}
|
||||
|
||||
print(f"\nTesting {len(CANDIDATES)} candidate parameter names …\n")
|
||||
results: list[tuple[str, str, bool]] = []
|
||||
|
||||
seen: set[tuple[str, str]] = set() # avoid duplicate tests
|
||||
|
||||
for cand in CANDIDATES:
|
||||
key: str = cand["key"] # type: ignore[assignment]
|
||||
fmt: str = cand["fmt"] # type: ignore[assignment]
|
||||
value: str = value_map[fmt]
|
||||
pair = (key, value)
|
||||
if pair in seen:
|
||||
continue
|
||||
seen.add(pair)
|
||||
|
||||
params: dict = {
|
||||
"taskListId": TEST_LIST_ID,
|
||||
"text": f"[TEST] cat-param-probe {key}={fmt}",
|
||||
key: value,
|
||||
}
|
||||
|
||||
print(f" Testing {key!r} = {value!r} …", end=" ", flush=True)
|
||||
try:
|
||||
data = client.call("taskcreate2", params)
|
||||
except FamilyWallError as exc:
|
||||
print(f"API ERROR: {exc}")
|
||||
results.append((key, value, False))
|
||||
continue
|
||||
|
||||
task_obj = data.get("a00", {}).get("r", {}).get("r", {})
|
||||
task_id: str = task_obj.get("metaId", "")
|
||||
applied_cats = _get_task_categories(task_obj)
|
||||
|
||||
# A non-default category was applied when the name matches our target.
|
||||
success = any(cat_name in c or c == cat_name for c in applied_cats)
|
||||
|
||||
if success:
|
||||
print(f"SUCCESS -> categories={applied_cats}")
|
||||
else:
|
||||
print(f"no effect -> categories={applied_cats}")
|
||||
|
||||
results.append((key, value, success))
|
||||
|
||||
if task_id:
|
||||
_delete_task(client, task_id)
|
||||
|
||||
client.logout()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------
|
||||
print("\n" + "=" * 60)
|
||||
print("RESULTS")
|
||||
print("=" * 60)
|
||||
hits = [(k, v) for k, v, ok in results if ok]
|
||||
misses = [(k, v) for k, v, ok in results if not ok]
|
||||
|
||||
if hits:
|
||||
print(f"\nWORKING parameter(s) found ({len(hits)}):")
|
||||
for k, v in hits:
|
||||
print(f" {k!r} = {v!r}")
|
||||
else:
|
||||
print("\nNo working parameter found.")
|
||||
|
||||
print(f"\nDid NOT work ({len(misses)}):")
|
||||
for k, v in misses:
|
||||
print(f" {k!r} = {v!r}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -325,6 +325,7 @@ def get_tasks(list_id: str, only_open: bool = True):
|
||||
"text": task.get("text"),
|
||||
"description": task.get("description"),
|
||||
"completed": completed,
|
||||
"category_id": task.get("taskCategoryId"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -344,8 +345,8 @@ def get_categories(list_id: str, locale: str = "de") -> str:
|
||||
lists return an empty list. Categories are filtered by locale so only
|
||||
the language-appropriate names are returned (default: German).
|
||||
|
||||
Note: the parameter name for assigning a category when creating or
|
||||
updating a task is not yet verified via FW_DEBUG=1. See SPEC.md.
|
||||
Use the returned ``id`` values as the ``category_id`` parameter in
|
||||
``create_task`` and ``update_task``.
|
||||
|
||||
Args:
|
||||
list_id: List ID from get_lists (e.g. ``taskList/23431854_29740942``).
|
||||
@@ -517,7 +518,12 @@ def _authenticated_call(endpoint: str, params: dict[str, Any]) -> dict[str, Any]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_task(list_id: str, text: str, description: str | None = None) -> str:
|
||||
def create_task(
|
||||
list_id: str,
|
||||
text: str,
|
||||
description: str | None = None,
|
||||
category_id: str | None = None,
|
||||
) -> str:
|
||||
"""Create a new task in the given list.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
@@ -526,6 +532,9 @@ def create_task(list_id: str, text: str, description: str | None = None) -> str:
|
||||
list_id: Target list ID from get_lists (e.g. ``taskList/123_456``).
|
||||
text: Task title / main text.
|
||||
description: Optional longer description.
|
||||
category_id: Optional category metaId from get_categories
|
||||
(e.g. ``taskCategory/23431854_200``). Only meaningful for
|
||||
shopping lists; ignored for TODO lists.
|
||||
|
||||
Returns:
|
||||
JSON with the new task's metaId on success, or an error message.
|
||||
@@ -533,6 +542,8 @@ def create_task(list_id: str, text: str, description: str | None = None) -> str:
|
||||
params: dict[str, Any] = {"taskListId": list_id, "text": text}
|
||||
if description:
|
||||
params["description"] = description
|
||||
if category_id:
|
||||
params["taskCategoryId"] = category_id
|
||||
|
||||
try:
|
||||
data = _authenticated_call("taskcreate2", params)
|
||||
@@ -559,28 +570,38 @@ def create_task(list_id: str, text: str, description: str | None = None) -> str:
|
||||
|
||||
|
||||
@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.
|
||||
def update_task(
|
||||
task_id: str,
|
||||
text: str | None = None,
|
||||
description: str | None = None,
|
||||
category_id: str | None = None,
|
||||
) -> str:
|
||||
"""Update the text, description, and/or category of an existing task.
|
||||
|
||||
IMPORTANT: Ask the user for confirmation before calling this tool.
|
||||
At least one of *text* or *description* must be provided.
|
||||
At least one of *text*, *description*, or *category_id* 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).
|
||||
category_id: New category metaId from get_categories
|
||||
(e.g. ``taskCategory/23431854_200``). Only meaningful for
|
||||
shopping lists; ignored for TODO lists.
|
||||
|
||||
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."
|
||||
if text is None and description is None and category_id is None:
|
||||
return "Error: At least one of 'text', 'description', or 'category_id' 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
|
||||
if category_id is not None:
|
||||
params["taskCategoryId"] = category_id
|
||||
|
||||
try:
|
||||
_authenticated_call("taskupdate2", params)
|
||||
|
||||
Reference in New Issue
Block a user