From fc4a1627b556f1a650dfc5322b698633140b4701 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 08:22:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(plan):=20MCP=20task=20tools=20=E2=80=94=20?= =?UTF-8?q?kind=20on=20create=5Ftask=20+=20list=5Ftasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fabledassistant/mcp/tools/tasks.py | 6 ++++ tests/test_mcp_tool_tasks_kind.py | 45 ++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/test_mcp_tool_tasks_kind.py diff --git a/src/fabledassistant/mcp/tools/tasks.py b/src/fabledassistant/mcp/tools/tasks.py index 1077a00..8c7bbfe 100644 --- a/src/fabledassistant/mcp/tools/tasks.py +++ b/src/fabledassistant/mcp/tools/tasks.py @@ -28,12 +28,14 @@ async def list_tasks( offset: int = 0, status: str = "", project_id: int = 0, + kind: str = "", ) -> dict: """List tasks in Scribe. Args: status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all. project_id: Filter to a specific project. Use 0 for no filter. + kind: Filter by task kind — 'work' or 'plan'. Omit (empty) for all kinds. Results are ordered by last-updated descending. """ @@ -43,6 +45,7 @@ async def list_tasks( is_task=True, status=status or None, project_id=project_id or None, + task_kind=kind or None, limit=max(1, min(limit, 100)), offset=max(0, offset), ) @@ -78,6 +81,7 @@ async def create_task( milestone_id: int = 0, parent_id: int = 0, tags: list[str] | None = None, + kind: str = "work", ) -> dict: """Create a new task in Scribe. @@ -90,6 +94,7 @@ async def create_task( milestone_id: Place within a project milestone (0 = no milestone). parent_id: Make this a sub-task of another task (0 = top-level). tags: List of plain-string tags without # prefix. + kind: 'work' (default) or 'plan'. Prefer the start_planning tool to create plans. """ uid = current_user_id() note = await notes_svc.create_note( @@ -102,6 +107,7 @@ async def create_task( milestone_id=milestone_id or None, parent_id=parent_id or None, tags=tags, + task_kind=kind, ) return note.to_dict() diff --git a/tests/test_mcp_tool_tasks_kind.py b/tests/test_mcp_tool_tasks_kind.py new file mode 100644 index 0000000..9d8bda0 --- /dev/null +++ b/tests/test_mcp_tool_tasks_kind.py @@ -0,0 +1,45 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from fabledassistant.mcp._context import _user_id_ctx + + +@pytest.fixture(autouse=True) +def _bind_user(): + token = _user_id_ctx.set(7) + yield + _user_id_ctx.reset(token) + + +def _fake_note(task_kind="work"): + n = MagicMock() + n.to_dict.return_value = {"id": 1, "title": "T", "task_kind": task_kind} + return n + + +@pytest.mark.asyncio +async def test_create_task_passes_kind(): + mock = AsyncMock(return_value=_fake_note(task_kind="plan")) + with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock): + from fabledassistant.mcp.tools.tasks import create_task + await create_task(title="P", kind="plan") + assert mock.call_args.kwargs["task_kind"] == "plan" + + +@pytest.mark.asyncio +async def test_list_tasks_passes_kind_filter(): + mock = AsyncMock(return_value=([], 0)) + with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock): + from fabledassistant.mcp.tools.tasks import list_tasks + await list_tasks(kind="plan") + assert mock.call_args.kwargs["task_kind"] == "plan" + + +@pytest.mark.asyncio +async def test_list_tasks_kind_empty_means_no_filter(): + mock = AsyncMock(return_value=([], 0)) + with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock): + from fabledassistant.mcp.tools.tasks import list_tasks + await list_tasks() + assert mock.call_args.kwargs["task_kind"] is None