diff --git a/src/fabledassistant/mcp/_context.py b/src/fabledassistant/mcp/_context.py index cd5760b..d503800 100644 --- a/src/fabledassistant/mcp/_context.py +++ b/src/fabledassistant/mcp/_context.py @@ -1,7 +1,7 @@ """Per-request MCP context. The ASGI middleware (mcp/server.py) populates `_user_id_ctx` from -scope['fable_user_id'] before dispatching to FastMCP tool handlers. +scope['scribe_user_id'] before dispatching to FastMCP tool handlers. Tool functions then call `current_user_id()` to retrieve it. Tools must never fall back to a default user — `current_user_id()` raises @@ -9,7 +9,7 @@ if called outside a properly-authed MCP request. """ from contextvars import ContextVar -_user_id_ctx: ContextVar[int | None] = ContextVar("fable_mcp_user_id", default=None) +_user_id_ctx: ContextVar[int | None] = ContextVar("scribe_mcp_user_id", default=None) def current_user_id() -> int: diff --git a/src/fabledassistant/mcp/server.py b/src/fabledassistant/mcp/server.py index eb67b73..d7cde5c 100644 --- a/src/fabledassistant/mcp/server.py +++ b/src/fabledassistant/mcp/server.py @@ -5,15 +5,16 @@ from mcp.server.fastmcp import FastMCP from quart import Quart _INSTRUCTIONS = """ -Fabled Scribe is the user's self-hosted second-brain and project-management -data store. You (Claude) are the assistant. +Scribe is the user's self-hosted second-brain and project-management data +store. You (Claude) are the assistant. Hierarchy: Project -> Milestone -> Task/Note. - Notes and Tasks share a model; tasks are notes with is_task=True. -- Use fable_*_note tools for notes, fable_*_task tools for tasks. -- Typed entities (person, place, list) are notes with a type field plus - type-specific columns; use the dedicated tools for those. +- Use the *_note tools for notes, the *_task tools for tasks. Don't mix them. +- Typed entities (person, place, list) are notes with a non-default note_type + plus type-specific columns; use the dedicated *_person / *_place / *_list + tools rather than create_note. - Tags are plain strings (no `#` prefix). Empty list clears tags; omit to leave unchanged on updates. - For optional integer FKs (project_id, milestone_id, parent_id), use 0 to mean @@ -23,7 +24,7 @@ Hierarchy: Project -> Milestone -> Task/Note. def build_mcp_server() -> FastMCP: """Build the FastMCP instance with all tools registered.""" - mcp = FastMCP("fable", instructions=_INSTRUCTIONS.strip()) + mcp = FastMCP("scribe", instructions=_INSTRUCTIONS.strip()) from fabledassistant.mcp.tools import register_all register_all(mcp) return mcp @@ -34,7 +35,7 @@ def mount_mcp(app: Quart) -> None: A small ASGI middleware between Quart and the FastMCP sub-app validates the Bearer token against the api_keys table. Authenticated requests have their - user_id attached to the ASGI scope under "fable_user_id" for tool handlers + user_id attached to the ASGI scope under "scribe_user_id" for tool handlers to read in later phases. """ from fabledassistant.mcp.auth import resolve_bearer_to_user_id @@ -55,7 +56,7 @@ def mount_mcp(app: Quart) -> None: "status": 401, "headers": [ (b"content-type", b"application/json"), - (b"www-authenticate", b'Bearer realm="fable-mcp"'), + (b"www-authenticate", b'Bearer realm="scribe-mcp"'), ], }) await send({ @@ -63,7 +64,7 @@ def mount_mcp(app: Quart) -> None: "body": b'{"error":"unauthorized"}', }) return - scope["fable_user_id"] = user_id + scope["scribe_user_id"] = user_id from fabledassistant.mcp._context import _user_id_ctx token = _user_id_ctx.set(user_id) try: diff --git a/src/fabledassistant/mcp/tools/entities.py b/src/fabledassistant/mcp/tools/entities.py index e61ce62..382469d 100644 --- a/src/fabledassistant/mcp/tools/entities.py +++ b/src/fabledassistant/mcp/tools/entities.py @@ -2,7 +2,7 @@ These are notes with a non-'note' note_type and type-specific JSON metadata stored in the entity_meta column. Three tools per type — list, create, update. -For get and delete, use fable_get_note / fable_delete_note (typed entities +For get and delete, use get_note / delete_note (typed entities share the Note model). The wrappers translate typed-field kwargs into the entity_meta dict shape that @@ -72,7 +72,7 @@ _PERSON_FIELDS = ("relationship", "email", "phone", "birthday", "organization", "address") -async def fable_list_persons(q: str = "", tag: str = "", limit: int = 25) -> dict: +async def list_persons(q: str = "", tag: str = "", limit: int = 25) -> dict: """List people in the user's knowledge base. Args: @@ -83,7 +83,7 @@ async def fable_list_persons(q: str = "", tag: str = "", limit: int = 25) -> dic return await _list_by_type("person", q, tag, limit) -async def fable_create_person( +async def create_person( name: str, relationship: str = "", email: str = "", @@ -109,7 +109,7 @@ async def fable_create_person( return await _create_entity("person", name, meta, tags) -async def fable_update_person( +async def update_person( person_id: int, name: str = "", relationship: str = "", @@ -135,12 +135,12 @@ async def fable_update_person( # ─── Place ─────────────────────────────────────────────────────────────────── -async def fable_list_places(q: str = "", tag: str = "", limit: int = 25) -> dict: +async def list_places(q: str = "", tag: str = "", limit: int = 25) -> dict: """List places (cafes, offices, addresses) in the user's knowledge base.""" return await _list_by_type("place", q, tag, limit) -async def fable_create_place( +async def create_place( name: str, address: str = "", phone: str = "", @@ -163,7 +163,7 @@ async def fable_create_place( return await _create_entity("place", name, meta, tags) -async def fable_update_place( +async def update_place( place_id: int, name: str = "", address: str = "", @@ -183,12 +183,12 @@ async def fable_update_place( # ─── List ──────────────────────────────────────────────────────────────────── -async def fable_list_lists(q: str = "", tag: str = "", limit: int = 25) -> dict: +async def list_lists(q: str = "", tag: str = "", limit: int = 25) -> dict: """List checklists in the user's knowledge base.""" return await _list_by_type("list", q, tag, limit) -async def fable_create_list( +async def create_list( name: str, category: str = "", items: list[str] | None = None, @@ -210,7 +210,7 @@ async def fable_create_list( return await _create_entity("list", name, meta, tags) -async def fable_update_list( +async def update_list( list_id: int, name: str = "", category: str = "", @@ -238,8 +238,8 @@ async def fable_update_list( def register(mcp) -> None: for fn in ( - fable_list_persons, fable_create_person, fable_update_person, - fable_list_places, fable_create_place, fable_update_place, - fable_list_lists, fable_create_list, fable_update_list, + list_persons, create_person, update_person, + list_places, create_place, update_place, + list_lists, create_list, update_list, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/events.py b/src/fabledassistant/mcp/tools/events.py index 5f355d9..74d3ecb 100644 --- a/src/fabledassistant/mcp/tools/events.py +++ b/src/fabledassistant/mcp/tools/events.py @@ -34,7 +34,7 @@ def _event_dict(event) -> dict: return event if isinstance(event, dict) else event.to_dict() -async def fable_list_events(date_from: str, date_to: str) -> dict: +async def list_events(date_from: str, date_to: str) -> dict: """List events between date_from and date_to (YYYY-MM-DD, both inclusive at the day level — `date_to` is interpreted as end-of-that-day). @@ -47,7 +47,7 @@ async def fable_list_events(date_from: str, date_to: str) -> dict: return {"events": [_event_dict(e) for e in rows], "total": len(rows)} -async def fable_create_event( +async def create_event( title: str, start_date: str, start_time: str = "00:00", @@ -80,7 +80,7 @@ async def fable_create_event( return event.to_dict() -async def fable_get_event(event_id: int) -> dict: +async def get_event(event_id: int) -> dict: """Fetch a single event by ID.""" uid = current_user_id() event = await events_svc.get_event(uid, event_id) @@ -89,7 +89,7 @@ async def fable_get_event(event_id: int) -> dict: return event.to_dict() -async def fable_update_event( +async def update_event( event_id: int, title: str = "", start_date: str = "", @@ -128,7 +128,7 @@ async def fable_update_event( return event.to_dict() -async def fable_delete_event(event_id: int) -> str: +async def delete_event(event_id: int) -> str: """Permanently delete a calendar event by ID. Cannot be undone.""" uid = current_user_id() existing = await events_svc.get_event(uid, event_id) @@ -140,10 +140,10 @@ async def fable_delete_event(event_id: int) -> str: def register(mcp) -> None: for fn in ( - fable_list_events, - fable_create_event, - fable_get_event, - fable_update_event, - fable_delete_event, + list_events, + create_event, + get_event, + update_event, + delete_event, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/milestones.py b/src/fabledassistant/mcp/tools/milestones.py index a07fc83..c2b004e 100644 --- a/src/fabledassistant/mcp/tools/milestones.py +++ b/src/fabledassistant/mcp/tools/milestones.py @@ -15,8 +15,8 @@ from fabledassistant.mcp._context import current_user_id from fabledassistant.services import milestones as milestones_svc -async def fable_list_milestones(project_id: int) -> dict: - """List milestones for a Fable project, ordered by order_index. +async def list_milestones(project_id: int) -> dict: + """List milestones for a Scribe project, ordered by order_index. Returns id, title, description, status (active/done), order_index, and task counts. @@ -26,13 +26,13 @@ async def fable_list_milestones(project_id: int) -> dict: return {"milestones": rows} -async def fable_create_milestone( +async def create_milestone( project_id: int, title: str, description: str = "", status: str = "active", ) -> dict: - """Create a milestone within a Fable project. + """Create a milestone within a Scribe project. Args: project_id: The project this milestone belongs to (required). @@ -51,7 +51,7 @@ async def fable_create_milestone( return milestone.to_dict() -async def fable_update_milestone( +async def update_milestone( project_id: int, milestone_id: int, title: str = "", @@ -59,7 +59,7 @@ async def fable_update_milestone( status: str = "", order_index: int = -1, ) -> dict: - """Update a Fable milestone. Only explicitly provided fields are changed. + """Update a Scribe milestone. Only explicitly provided fields are changed. Args: project_id: Project the milestone belongs to (preserved for API parity; @@ -88,8 +88,8 @@ async def fable_update_milestone( def register(mcp) -> None: for fn in ( - fable_list_milestones, - fable_create_milestone, - fable_update_milestone, + list_milestones, + create_milestone, + update_milestone, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/notes.py b/src/fabledassistant/mcp/tools/notes.py index b6325ed..e9d04e9 100644 --- a/src/fabledassistant/mcp/tools/notes.py +++ b/src/fabledassistant/mcp/tools/notes.py @@ -17,18 +17,18 @@ from fabledassistant.mcp._context import current_user_id from fabledassistant.services import notes as notes_svc -async def fable_list_notes( +async def list_notes( limit: int = 20, offset: int = 0, tag: str = "", search_text: str = "", ) -> dict: - """List notes (non-task documents) stored in Fable. + """List notes (non-task documents) stored in Scribe. Optionally filter by a single tag (plain string, no # prefix) or a keyword search against title and body. Results are ordered by last-updated descending. - Use fable_search for semantic/meaning-based lookup instead of exact keyword search. + Use search for semantic/meaning-based lookup instead of exact keyword search. """ uid = current_user_id() rows, total = await notes_svc.list_notes( @@ -42,8 +42,8 @@ async def fable_list_notes( return {"notes": [n.to_dict() for n in rows], "total": total} -async def fable_get_note(note_id: int) -> dict: - """Fetch the full content of a single Fable note by its ID. +async def get_note(note_id: int) -> dict: + """Fetch the full content of a single Scribe note by its ID. Returns id, title, body (markdown), tags, project_id, created_at, updated_at. """ @@ -54,13 +54,13 @@ async def fable_get_note(note_id: int) -> dict: return note.to_dict() -async def fable_create_note( +async def create_note( title: str, body: str = "", tags: list[str] | None = None, project_id: int = 0, ) -> dict: - """Create a new note in Fable. + """Create a new note in Scribe. Args: title: Note title (required). @@ -81,14 +81,14 @@ async def fable_create_note( return note.to_dict() -async def fable_update_note( +async def update_note( note_id: int, title: str = "", body: str = "", tags: list[str] | None = None, project_id: int = 0, ) -> dict: - """Update an existing Fable note. Only explicitly provided fields are changed. + """Update an existing Scribe note. Only explicitly provided fields are changed. Args: note_id: ID of the note to update. @@ -113,8 +113,8 @@ async def fable_update_note( return note.to_dict() -async def fable_delete_note(note_id: int) -> str: - """Permanently delete a Fable note by ID. This cannot be undone.""" +async def delete_note(note_id: int) -> str: + """Permanently delete a Scribe note by ID. This cannot be undone.""" uid = current_user_id() deleted = await notes_svc.delete_note(uid, note_id) if not deleted: @@ -124,10 +124,10 @@ async def fable_delete_note(note_id: int) -> str: def register(mcp) -> None: for fn in ( - fable_list_notes, - fable_get_note, - fable_create_note, - fable_update_note, - fable_delete_note, + list_notes, + get_note, + create_note, + update_note, + delete_note, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/projects.py b/src/fabledassistant/mcp/tools/projects.py index 18ec62a..d99a677 100644 --- a/src/fabledassistant/mcp/tools/projects.py +++ b/src/fabledassistant/mcp/tools/projects.py @@ -21,8 +21,8 @@ from fabledassistant.services import milestones as milestones_svc from fabledassistant.services import projects as projects_svc -async def fable_list_projects() -> dict: - """List all Fable projects for the current user. +async def list_projects() -> dict: + """List all Scribe projects for the current user. Returns id, title, description, goal, status (active/archived), color, and a short auto-generated summary for each project. @@ -32,8 +32,8 @@ async def fable_list_projects() -> dict: return {"projects": [p.to_dict() for p in rows]} -async def fable_get_project(project_id: int) -> dict: - """Fetch a Fable project by ID, including its milestone summary. +async def get_project(project_id: int) -> dict: + """Fetch a Scribe project by ID, including its milestone summary. Returns full project fields plus a milestone_summary list with each milestone's id, title, status, and task counts. @@ -49,14 +49,14 @@ async def fable_get_project(project_id: int) -> dict: return data -async def fable_create_project( +async def create_project( title: str, description: str = "", goal: str = "", status: str = "active", color: str = "", ) -> dict: - """Create a new project in Fable. + """Create a new project in Scribe. Args: title: Project name (required). @@ -77,7 +77,7 @@ async def fable_create_project( return project.to_dict() -async def fable_update_project( +async def update_project( project_id: int, title: str = "", description: str = "", @@ -85,7 +85,7 @@ async def fable_update_project( status: str = "", color: str = "", ) -> dict: - """Update an existing Fable project. Only explicitly provided fields are changed. + """Update an existing Scribe project. Only explicitly provided fields are changed. Args: project_id: ID of the project to update. @@ -115,9 +115,9 @@ async def fable_update_project( def register(mcp) -> None: for fn in ( - fable_list_projects, - fable_get_project, - fable_create_project, - fable_update_project, + list_projects, + get_project, + create_project, + update_project, ): mcp.tool(name=fn.__name__)(fn) diff --git a/src/fabledassistant/mcp/tools/recent.py b/src/fabledassistant/mcp/tools/recent.py index fb2f5a7..2d80aaf 100644 --- a/src/fabledassistant/mcp/tools/recent.py +++ b/src/fabledassistant/mcp/tools/recent.py @@ -1,4 +1,4 @@ -"""fable_get_recent — cross-type recent-activity tool. +"""get_recent — cross-type recent-activity tool. Returns the most-recently-touched notes, tasks, projects, and events for the user, ordered by updated_at descending. Useful for Claude to bootstrap context @@ -20,7 +20,7 @@ from fabledassistant.models.note import Note from fabledassistant.models.project import Project -async def fable_get_recent(days: int = 7, limit: int = 25) -> dict: +async def get_recent(days: int = 7, limit: int = 25) -> dict: """Return recently-touched items across notes, tasks, projects, events. Args: @@ -78,4 +78,4 @@ async def fable_get_recent(days: int = 7, limit: int = 25) -> dict: def register(mcp) -> None: - mcp.tool(name="fable_get_recent")(fable_get_recent) + mcp.tool(name="get_recent")(get_recent) diff --git a/src/fabledassistant/mcp/tools/search.py b/src/fabledassistant/mcp/tools/search.py index 73a69f9..2f51488 100644 --- a/src/fabledassistant/mcp/tools/search.py +++ b/src/fabledassistant/mcp/tools/search.py @@ -1,4 +1,4 @@ -"""fable_search — semantic search across the user's notes and tasks. +"""search — semantic search across the user's notes and tasks. Mirrors the existing fable-mcp contract so Claude's prior usage pattern keeps working. Differences from fable-mcp: @@ -11,7 +11,7 @@ from fabledassistant.mcp._context import current_user_id from fabledassistant.services.embeddings import semantic_search_notes -async def fable_search(q: str, content_type: str = "all", limit: int = 10) -> dict: +async def search(q: str, content_type: str = "all", limit: int = 10) -> dict: """Semantic search over the user's notes and tasks. Args: @@ -46,4 +46,4 @@ async def fable_search(q: str, content_type: str = "all", limit: int = 10) -> di def register(mcp) -> None: - mcp.tool(name="fable_search")(fable_search) + mcp.tool(name="search")(search) diff --git a/src/fabledassistant/mcp/tools/tags.py b/src/fabledassistant/mcp/tools/tags.py index aa19ebe..40f5f9b 100644 --- a/src/fabledassistant/mcp/tools/tags.py +++ b/src/fabledassistant/mcp/tools/tags.py @@ -1,4 +1,4 @@ -"""fable_list_tags — return the user's tag vocabulary with usage counts. +"""list_tags — return the user's tag vocabulary with usage counts. Python-side aggregation rather than SQL UNNEST. Personal-scale (~thousands of notes) makes the perf cost negligible, and a one-pass dict counter is much @@ -25,7 +25,7 @@ def _aggregate_tag_counts(tag_lists) -> dict[str, int]: return counts -async def fable_list_tags(limit: int = 50) -> dict: +async def list_tags(limit: int = 50) -> dict: """Return the user's most-used tags with usage counts. Args: @@ -51,4 +51,4 @@ async def fable_list_tags(limit: int = 50) -> dict: def register(mcp) -> None: - mcp.tool(name="fable_list_tags")(fable_list_tags) + mcp.tool(name="list_tags")(list_tags) diff --git a/src/fabledassistant/mcp/tools/tasks.py b/src/fabledassistant/mcp/tools/tasks.py index 6a597c5..1077a00 100644 --- a/src/fabledassistant/mcp/tools/tasks.py +++ b/src/fabledassistant/mcp/tools/tasks.py @@ -3,9 +3,9 @@ Tasks are notes with a non-null `status` — same model, different filter. Wrappers call services/notes.py for CRUD with is_task=True and add the task-specific fields (status, priority, due_date, parent_id), plus -services/task_logs.py for fable_add_task_log. +services/task_logs.py for add_task_log. -There is no fable_delete_task — matches the existing fable-mcp surface. +There is no delete_task — matches the existing fable-mcp surface. Cancel by updating status to "cancelled". Sentinels (preserved from existing fable-mcp): @@ -23,13 +23,13 @@ from fabledassistant.services import notes as notes_svc from fabledassistant.services import task_logs as task_logs_svc -async def fable_list_tasks( +async def list_tasks( limit: int = 20, offset: int = 0, status: str = "", project_id: int = 0, ) -> dict: - """List tasks in Fable. + """List tasks in Scribe. Args: status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all. @@ -49,8 +49,8 @@ async def fable_list_tasks( return {"tasks": [n.to_dict() for n in rows], "total": total} -async def fable_get_task(task_id: int) -> dict: - """Fetch a single Fable task by ID. +async def get_task(task_id: int) -> dict: + """Fetch a single Scribe task by ID. Returns id, title, body, status, priority, tags, project_id, milestone_id, parent_id, parent_title, due_date, created_at, updated_at. @@ -69,7 +69,7 @@ async def fable_get_task(task_id: int) -> dict: return data -async def fable_create_task( +async def create_task( title: str, body: str = "", status: str = "todo", @@ -79,7 +79,7 @@ async def fable_create_task( parent_id: int = 0, tags: list[str] | None = None, ) -> dict: - """Create a new task in Fable. + """Create a new task in Scribe. Args: title: Task title (required). @@ -106,7 +106,7 @@ async def fable_create_task( return note.to_dict() -async def fable_update_task( +async def update_task( task_id: int, title: str = "", body: str = "", @@ -115,7 +115,7 @@ async def fable_update_task( project_id: int = 0, milestone_id: int = 0, ) -> dict: - """Update an existing Fable task. Only explicitly provided fields are changed. + """Update an existing Scribe task. Only explicitly provided fields are changed. Args: task_id: ID of the task to update. @@ -146,8 +146,8 @@ async def fable_update_task( return note.to_dict() -async def fable_add_task_log(task_id: int, content: str) -> dict: - """Append a timestamped progress log entry to a Fable task. +async def add_task_log(task_id: int, content: str) -> dict: + """Append a timestamped progress log entry to a Scribe task. Use this to record work sessions, decisions, or status updates over time without overwriting the task's main body. Each entry is stored separately @@ -163,10 +163,10 @@ async def fable_add_task_log(task_id: int, content: str) -> dict: def register(mcp) -> None: for fn in ( - fable_list_tasks, - fable_get_task, - fable_create_task, - fable_update_task, - fable_add_task_log, + list_tasks, + get_task, + create_task, + update_task, + add_task_log, ): mcp.tool(name=fn.__name__)(fn) diff --git a/tests/test_mcp_tool_entities.py b/tests/test_mcp_tool_entities.py index db0b5da..85fdf08 100644 --- a/tests/test_mcp_tool_entities.py +++ b/tests/test_mcp_tool_entities.py @@ -8,9 +8,9 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.entities import ( - fable_list_persons, fable_create_person, fable_update_person, - fable_create_place, fable_update_place, - fable_create_list, fable_update_list, + list_persons, create_person, update_person, + create_place, update_place, + create_list, update_list, ) @@ -39,7 +39,7 @@ def _fake_note(*, note_type="note", entity_meta=None, **overrides) -> MagicMock: async def test_list_persons_calls_knowledge_with_person_type(): mock = AsyncMock(return_value=([{"id": 1, "title": "alice"}], 1)) with patch("fabledassistant.mcp.tools.entities.knowledge_svc.query_knowledge", mock): - out = await fable_list_persons(tag="work") + out = await list_persons(tag="work") kwargs = mock.call_args.kwargs assert kwargs["note_type"] == "person" assert kwargs["tags"] == ["work"] @@ -56,7 +56,7 @@ async def test_create_person_only_includes_provided_fields_in_meta(): fake = _fake_note(note_type="person") mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock): - await fable_create_person(name="Alice", email="a@x.com") + await create_person(name="Alice", email="a@x.com") kwargs = mock.call_args.kwargs assert kwargs["title"] == "Alice" assert kwargs["note_type"] == "person" @@ -69,7 +69,7 @@ async def test_create_person_all_empty_meta_is_none(): fake = _fake_note(note_type="person") mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock): - await fable_create_person(name="Bob") + await create_person(name="Bob") assert mock.call_args.kwargs["entity_meta"] is None @@ -78,7 +78,7 @@ async def test_create_place_categories_into_meta(): fake = _fake_note(note_type="place") mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock): - await fable_create_place(name="Cafe X", address="123 Main", category="coffee") + await create_place(name="Cafe X", address="123 Main", category="coffee") meta = mock.call_args.kwargs["entity_meta"] assert meta == {"address": "123 Main", "category": "coffee"} @@ -88,7 +88,7 @@ async def test_create_list_translates_strings_to_unchecked_items(): fake = _fake_note(note_type="list") mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.entities.notes_svc.create_note", mock): - await fable_create_list(name="shopping", items=["milk", "bread"]) + await create_list(name="shopping", items=["milk", "bread"]) meta = mock.call_args.kwargs["entity_meta"] assert meta["list_items"] == [ {"text": "milk", "checked": False}, @@ -114,7 +114,7 @@ async def test_update_person_merges_meta_preserving_other_fields(): ), patch( "fabledassistant.mcp.tools.entities.notes_svc.update_note", update_mock, ): - await fable_update_person(person_id=5, email="new@x.com") + await update_person(person_id=5, email="new@x.com") new_meta = update_mock.call_args.kwargs["entity_meta"] assert new_meta == {"email": "new@x.com", "phone": "555-1234"} @@ -133,7 +133,7 @@ async def test_update_person_no_typed_fields_keeps_meta_unchanged(): "fabledassistant.mcp.tools.entities.notes_svc.update_note", AsyncMock(return_value=updated), ) as update_mock: - await fable_update_person(person_id=5, name="New Name") + await update_person(person_id=5, name="New Name") # entity_meta unchanged, but still passed (service gets the full new dict) assert update_mock.call_args.kwargs["entity_meta"] == {"email": "a@x.com"} assert update_mock.call_args.kwargs["title"] == "New Name" @@ -148,7 +148,7 @@ async def test_update_person_rejects_wrong_type(): AsyncMock(return_value=wrong_type), ): with pytest.raises(ValueError, match="person 5 not found"): - await fable_update_person(person_id=5, email="x@x.com") + await update_person(person_id=5, email="x@x.com") @pytest.mark.asyncio @@ -166,7 +166,7 @@ async def test_update_list_items_empty_list_clears_items(): "fabledassistant.mcp.tools.entities.notes_svc.update_note", AsyncMock(return_value=updated), ) as update_mock: - await fable_update_list(list_id=5, items=[]) + await update_list(list_id=5, items=[]) assert update_mock.call_args.kwargs["entity_meta"]["list_items"] == [] @@ -184,7 +184,7 @@ async def test_update_list_items_none_leaves_items_unchanged(): "fabledassistant.mcp.tools.entities.notes_svc.update_note", AsyncMock(return_value=updated), ) as update_mock: - await fable_update_list(list_id=5, name="renamed") + await update_list(list_id=5, name="renamed") # Existing items intact in the merged meta assert update_mock.call_args.kwargs["entity_meta"]["list_items"] == [ {"text": "keep", "checked": False}, diff --git a/tests/test_mcp_tool_events.py b/tests/test_mcp_tool_events.py index b459df7..1a570f8 100644 --- a/tests/test_mcp_tool_events.py +++ b/tests/test_mcp_tool_events.py @@ -6,8 +6,8 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.events import ( - fable_list_events, fable_create_event, fable_get_event, - fable_update_event, fable_delete_event, + list_events, create_event, get_event, + update_event, delete_event, ) @@ -36,7 +36,7 @@ async def test_list_events_passes_datetime_range(): {"id": 1, "title": "morning standup"}, ]) with patch("fabledassistant.mcp.tools.events.events_svc.list_events", mock): - out = await fable_list_events(date_from="2026-06-01", date_to="2026-06-08") + out = await list_events(date_from="2026-06-01", date_to="2026-06-08") args, _ = mock.call_args assert args[0] == 7 # user_id assert args[1] == datetime(2026, 6, 1) @@ -49,7 +49,7 @@ async def test_create_event_combines_date_and_time(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.create_event", mock): - await fable_create_event( + await create_event( title="standup", start_date="2026-06-01", start_time="09:30", duration_minutes=15, ) @@ -64,7 +64,7 @@ async def test_create_event_zero_duration_means_point_event(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.create_event", mock): - await fable_create_event(title="x", start_date="2026-06-01") + await create_event(title="x", start_date="2026-06-01") assert mock.call_args.kwargs["duration_minutes"] is None @@ -75,7 +75,7 @@ async def test_get_event_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="event 999 not found"): - await fable_get_event(event_id=999) + await get_event(event_id=999) @pytest.mark.asyncio @@ -83,7 +83,7 @@ async def test_update_event_only_sends_non_default_fields(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock): - await fable_update_event(event_id=1, title="new title") + await update_event(event_id=1, title="new title") args, kwargs = mock.call_args assert args == (7, 1) assert kwargs == {"title": "new title"} @@ -94,7 +94,7 @@ async def test_update_event_duration_minus_one_means_unchanged(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock): - await fable_update_event(event_id=1, duration_minutes=-1) + await update_event(event_id=1, duration_minutes=-1) assert "duration_minutes" not in mock.call_args.kwargs @@ -104,7 +104,7 @@ async def test_update_event_duration_zero_clears_to_point(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock): - await fable_update_event(event_id=1, duration_minutes=0) + await update_event(event_id=1, duration_minutes=0) assert mock.call_args.kwargs["duration_minutes"] is None @@ -113,12 +113,12 @@ async def test_update_event_requires_both_date_and_time_to_move(): e = _fake_event() mock = AsyncMock(return_value=e) with patch("fabledassistant.mcp.tools.events.events_svc.update_event", mock): - await fable_update_event(event_id=1, start_date="2026-06-02") + await update_event(event_id=1, start_date="2026-06-02") # Only start_date, no start_time → start_dt NOT in fields assert "start_dt" not in mock.call_args.kwargs mock.reset_mock() - await fable_update_event( + await update_event( event_id=1, start_date="2026-06-02", start_time="11:00", ) assert mock.call_args.kwargs["start_dt"] == datetime(2026, 6, 2, 11) @@ -131,7 +131,7 @@ async def test_update_event_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="event 999 not found"): - await fable_update_event(event_id=999, title="x") + await update_event(event_id=999, title="x") @pytest.mark.asyncio @@ -144,7 +144,7 @@ async def test_delete_event_checks_existence_then_returns_confirmation(): "fabledassistant.mcp.tools.events.events_svc.delete_event", AsyncMock(return_value=None), ): - result = await fable_delete_event(event_id=7) + result = await delete_event(event_id=7) assert result == "Event 7 deleted." @@ -155,4 +155,4 @@ async def test_delete_event_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="event 999 not found"): - await fable_delete_event(event_id=999) + await delete_event(event_id=999) diff --git a/tests/test_mcp_tool_milestones.py b/tests/test_mcp_tool_milestones.py index 7c2c1f0..581a81d 100644 --- a/tests/test_mcp_tool_milestones.py +++ b/tests/test_mcp_tool_milestones.py @@ -5,7 +5,7 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.milestones import ( - fable_list_milestones, fable_create_milestone, fable_update_milestone, + list_milestones, create_milestone, update_milestone, ) @@ -32,7 +32,7 @@ async def test_list_milestones_returns_dict_with_progress(): "fabledassistant.mcp.tools.milestones.milestones_svc.get_project_milestone_summary", AsyncMock(return_value=rows), ): - out = await fable_list_milestones(project_id=1) + out = await list_milestones(project_id=1) assert out["milestones"] == rows @@ -41,7 +41,7 @@ async def test_create_milestone_passes_through(): m = _fake_ms(id=5) mock = AsyncMock(return_value=m) with patch("fabledassistant.mcp.tools.milestones.milestones_svc.create_milestone", mock): - out = await fable_create_milestone(project_id=1, title="new", description="d") + out = await create_milestone(project_id=1, title="new", description="d") assert out["id"] == 5 assert mock.call_args.kwargs["project_id"] == 1 assert mock.call_args.kwargs["title"] == "new" @@ -53,7 +53,7 @@ async def test_create_milestone_empty_description_becomes_none(): m = _fake_ms() mock = AsyncMock(return_value=m) with patch("fabledassistant.mcp.tools.milestones.milestones_svc.create_milestone", mock): - await fable_create_milestone(project_id=1, title="t", description="") + await create_milestone(project_id=1, title="t", description="") assert mock.call_args.kwargs["description"] is None @@ -62,7 +62,7 @@ async def test_update_milestone_only_sends_non_default_fields(): m = _fake_ms() mock = AsyncMock(return_value=m) with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock): - await fable_update_milestone(project_id=1, milestone_id=5, status="done") + await update_milestone(project_id=1, milestone_id=5, status="done") args, kwargs = mock.call_args assert args == (7, 5) assert kwargs == {"status": "done"} @@ -74,7 +74,7 @@ async def test_update_milestone_order_index_negative_is_omitted(): m = _fake_ms() mock = AsyncMock(return_value=m) with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock): - await fable_update_milestone(project_id=1, milestone_id=5, order_index=-1) + await update_milestone(project_id=1, milestone_id=5, order_index=-1) assert "order_index" not in mock.call_args.kwargs @@ -84,7 +84,7 @@ async def test_update_milestone_order_index_zero_is_explicit(): m = _fake_ms() mock = AsyncMock(return_value=m) with patch("fabledassistant.mcp.tools.milestones.milestones_svc.update_milestone", mock): - await fable_update_milestone(project_id=1, milestone_id=5, order_index=0) + await update_milestone(project_id=1, milestone_id=5, order_index=0) assert mock.call_args.kwargs["order_index"] == 0 @@ -95,4 +95,4 @@ async def test_update_milestone_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="milestone 999 not found"): - await fable_update_milestone(project_id=1, milestone_id=999, title="x") + await update_milestone(project_id=1, milestone_id=999, title="x") diff --git a/tests/test_mcp_tool_notes.py b/tests/test_mcp_tool_notes.py index ea21286..c0010d8 100644 --- a/tests/test_mcp_tool_notes.py +++ b/tests/test_mcp_tool_notes.py @@ -5,8 +5,8 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.notes import ( - fable_list_notes, fable_get_note, fable_create_note, - fable_update_note, fable_delete_note, + list_notes, get_note, create_note, + update_note, delete_note, ) @@ -32,7 +32,7 @@ async def test_list_notes_repackages_tuple_into_dict(): "fabledassistant.mcp.tools.notes.notes_svc.list_notes", AsyncMock(return_value=(rows, 2)), ): - out = await fable_list_notes() + out = await list_notes() assert out["total"] == 2 assert len(out["notes"]) == 2 @@ -41,7 +41,7 @@ async def test_list_notes_repackages_tuple_into_dict(): async def test_list_notes_passes_is_task_false(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock): - await fable_list_notes() + await list_notes() assert mock.call_args.kwargs["is_task"] is False @@ -50,7 +50,7 @@ async def test_list_notes_tag_filter_maps_to_list(): """The single-tag string param maps to a one-element list at the service layer.""" mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock): - await fable_list_notes(tag="ops") + await list_notes(tag="ops") assert mock.call_args.kwargs["tags"] == ["ops"] @@ -58,7 +58,7 @@ async def test_list_notes_tag_filter_maps_to_list(): async def test_list_notes_empty_tag_means_no_filter(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock): - await fable_list_notes(tag="") + await list_notes(tag="") assert mock.call_args.kwargs["tags"] is None @@ -66,7 +66,7 @@ async def test_list_notes_empty_tag_means_no_filter(): async def test_list_notes_search_text_maps_to_q(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock): - await fable_list_notes(search_text="kafka") + await list_notes(search_text="kafka") assert mock.call_args.kwargs["q"] == "kafka" @@ -74,7 +74,7 @@ async def test_list_notes_search_text_maps_to_q(): async def test_list_notes_limit_clamped(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.notes.notes_svc.list_notes", mock): - await fable_list_notes(limit=9999) + await list_notes(limit=9999) assert mock.call_args.kwargs["limit"] == 100 @@ -85,7 +85,7 @@ async def test_get_note_returns_dict(): "fabledassistant.mcp.tools.notes.notes_svc.get_note", AsyncMock(return_value=fake), ): - out = await fable_get_note(note_id=5) + out = await get_note(note_id=5) assert out["id"] == 5 assert out["title"] == "found" @@ -97,7 +97,7 @@ async def test_get_note_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="note 999 not found"): - await fable_get_note(note_id=999) + await get_note(note_id=999) @pytest.mark.asyncio @@ -105,7 +105,7 @@ async def test_create_note_passes_through(): fake = _fake_note(id=10, title="new") mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.notes.notes_svc.create_note", mock): - out = await fable_create_note(title="new", body="x", tags=["a"], project_id=5) + out = await create_note(title="new", body="x", tags=["a"], project_id=5) assert out["id"] == 10 assert mock.call_args.kwargs["title"] == "new" assert mock.call_args.kwargs["project_id"] == 5 @@ -117,7 +117,7 @@ async def test_create_note_project_zero_becomes_none(): fake = _fake_note() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.notes.notes_svc.create_note", mock): - await fable_create_note(title="t", project_id=0) + await create_note(title="t", project_id=0) assert mock.call_args.kwargs["project_id"] is None @@ -128,7 +128,7 @@ async def test_update_note_only_sends_non_default_fields(): fake = _fake_note() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock): - await fable_update_note(note_id=1, title="new title") + await update_note(note_id=1, title="new title") # Service got user_id, note_id positional + only the title kwarg args, kwargs = mock.call_args assert args == (7, 1) @@ -141,7 +141,7 @@ async def test_update_note_empty_tags_clears_explicitly(): fake = _fake_note() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock): - await fable_update_note(note_id=1, tags=[]) + await update_note(note_id=1, tags=[]) assert mock.call_args.kwargs == {"tags": []} @@ -150,7 +150,7 @@ async def test_update_note_tags_none_means_omit(): fake = _fake_note() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.notes.notes_svc.update_note", mock): - await fable_update_note(note_id=1, tags=None) + await update_note(note_id=1, tags=None) assert "tags" not in mock.call_args.kwargs @@ -161,7 +161,7 @@ async def test_update_note_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="note 999 not found"): - await fable_update_note(note_id=999, title="x") + await update_note(note_id=999, title="x") @pytest.mark.asyncio @@ -170,7 +170,7 @@ async def test_delete_note_returns_confirmation_string(): "fabledassistant.mcp.tools.notes.notes_svc.delete_note", AsyncMock(return_value=True), ): - result = await fable_delete_note(note_id=7) + result = await delete_note(note_id=7) assert result == "Note 7 deleted." @@ -181,4 +181,4 @@ async def test_delete_note_raises_when_not_found(): AsyncMock(return_value=False), ): with pytest.raises(ValueError, match="note 999 not found"): - await fable_delete_note(note_id=999) + await delete_note(note_id=999) diff --git a/tests/test_mcp_tool_projects.py b/tests/test_mcp_tool_projects.py index 9cbad8d..936f928 100644 --- a/tests/test_mcp_tool_projects.py +++ b/tests/test_mcp_tool_projects.py @@ -5,8 +5,8 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.projects import ( - fable_list_projects, fable_get_project, fable_create_project, - fable_update_project, + list_projects, get_project, create_project, + update_project, ) @@ -33,7 +33,7 @@ async def test_list_projects_wraps_in_dict(): "fabledassistant.mcp.tools.projects.projects_svc.list_projects", AsyncMock(return_value=rows), ): - out = await fable_list_projects() + out = await list_projects() assert len(out["projects"]) == 2 @@ -48,7 +48,7 @@ async def test_get_project_enriches_with_milestone_summary(): "fabledassistant.mcp.tools.projects.milestones_svc.get_project_milestone_summary", AsyncMock(return_value=milestone_summary), ): - out = await fable_get_project(project_id=5) + out = await get_project(project_id=5) assert out["id"] == 5 assert out["milestone_summary"] == milestone_summary @@ -60,7 +60,7 @@ async def test_get_project_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="project 999 not found"): - await fable_get_project(project_id=999) + await get_project(project_id=999) @pytest.mark.asyncio @@ -68,7 +68,7 @@ async def test_create_project_passes_color_empty_as_none(): p = _fake_project() mock = AsyncMock(return_value=p) with patch("fabledassistant.mcp.tools.projects.projects_svc.create_project", mock): - await fable_create_project(title="P", color="") + await create_project(title="P", color="") assert mock.call_args.kwargs["color"] is None @@ -77,7 +77,7 @@ async def test_update_project_only_sends_non_default_fields(): p = _fake_project() mock = AsyncMock(return_value=p) with patch("fabledassistant.mcp.tools.projects.projects_svc.update_project", mock): - await fable_update_project(project_id=1, status="archived") + await update_project(project_id=1, status="archived") args, kwargs = mock.call_args assert args == (7, 1) assert kwargs == {"status": "archived"} @@ -90,4 +90,4 @@ async def test_update_project_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="project 999 not found"): - await fable_update_project(project_id=999, title="x") + await update_project(project_id=999, title="x") diff --git a/tests/test_mcp_tool_search.py b/tests/test_mcp_tool_search.py index 89424c7..b826506 100644 --- a/tests/test_mcp_tool_search.py +++ b/tests/test_mcp_tool_search.py @@ -1,4 +1,4 @@ -"""fable_search tool — proves the tool pattern (context + service call + dict shape). +"""search tool — proves the tool pattern (context + service call + dict shape). Service call is mocked; no DB needed.""" from unittest.mock import AsyncMock, MagicMock, patch @@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from fabledassistant.mcp._context import _user_id_ctx -from fabledassistant.mcp.tools.search import fable_search +from fabledassistant.mcp.tools.search import search @pytest.fixture(autouse=True) @@ -31,7 +31,7 @@ def _fake_note(*, id: int, title: str, body: str = "", @pytest.mark.asyncio async def test_fable_search_raises_without_context(): with pytest.raises(RuntimeError, match="no MCP user context"): - await fable_search(q="anything") + await search(q="anything") @pytest.mark.asyncio @@ -43,7 +43,7 @@ async def test_fable_search_returns_repackaged_results(): "fabledassistant.mcp.tools.search.semantic_search_notes", AsyncMock(return_value=[(0.93, fake)]), ): - out = await fable_search(q="kafka") + out = await search(q="kafka") assert out["total"] == 1 assert len(out["results"]) == 1 @@ -65,7 +65,7 @@ async def test_fable_search_body_is_truncated_to_240_chars(): "fabledassistant.mcp.tools.search.semantic_search_notes", AsyncMock(return_value=[(0.5, fake)]), ): - out = await fable_search(q="x") + out = await search(q="x") assert len(out["results"][0]["body"]) == 240 @@ -75,15 +75,15 @@ async def test_fable_search_content_type_filters_at_service_layer(): _user_id_ctx.set(7) mock_search = AsyncMock(return_value=[]) with patch("fabledassistant.mcp.tools.search.semantic_search_notes", mock_search): - await fable_search(q="x", content_type="task") + await search(q="x", content_type="task") assert mock_search.call_args.kwargs["is_task"] is True mock_search.reset_mock() - await fable_search(q="x", content_type="note") + await search(q="x", content_type="note") assert mock_search.call_args.kwargs["is_task"] is False mock_search.reset_mock() - await fable_search(q="x", content_type="all") + await search(q="x", content_type="all") assert mock_search.call_args.kwargs["is_task"] is None @@ -92,9 +92,9 @@ async def test_fable_search_limit_is_clamped(): _user_id_ctx.set(7) mock_search = AsyncMock(return_value=[]) with patch("fabledassistant.mcp.tools.search.semantic_search_notes", mock_search): - await fable_search(q="x", limit=999) + await search(q="x", limit=999) assert mock_search.call_args.kwargs["limit"] == 50 mock_search.reset_mock() - await fable_search(q="x", limit=0) + await search(q="x", limit=0) assert mock_search.call_args.kwargs["limit"] == 1 diff --git a/tests/test_mcp_tool_tags.py b/tests/test_mcp_tool_tags.py index 31fae43..f4ce0fd 100644 --- a/tests/test_mcp_tool_tags.py +++ b/tests/test_mcp_tool_tags.py @@ -1,4 +1,4 @@ -"""Tests for fable_list_tags. +"""Tests for list_tags. The aggregation helper is unit-tested directly; the full tool is exercised with a mocked async_session to keep tests DB-free. @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from fabledassistant.mcp._context import _user_id_ctx -from fabledassistant.mcp.tools.tags import fable_list_tags, _aggregate_tag_counts +from fabledassistant.mcp.tools.tags import list_tags, _aggregate_tag_counts @pytest.fixture(autouse=True) @@ -42,7 +42,7 @@ async def test_fable_list_tags_returns_sorted_by_count_desc(): mock_session.execute = AsyncMock(return_value=mock_result) mock_ctx = MagicMock(return_value=mock_session) with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx): - out = await fable_list_tags() + out = await list_tags() assert out["tags"][0] == {"tag": "a", "count": 3} assert out["tags"][1] == {"tag": "b", "count": 1} assert out["total"] == 2 @@ -59,5 +59,5 @@ async def test_fable_list_tags_clamps_limit(): mock_ctx = MagicMock(return_value=mock_session) with patch("fabledassistant.mcp.tools.tags.async_session", mock_ctx): # No exception — just exercises the clamping branch - out = await fable_list_tags(limit=99999) + out = await list_tags(limit=99999) assert out["total"] == 0 diff --git a/tests/test_mcp_tool_tasks.py b/tests/test_mcp_tool_tasks.py index 9fb4c15..aa2f5ec 100644 --- a/tests/test_mcp_tool_tasks.py +++ b/tests/test_mcp_tool_tasks.py @@ -6,8 +6,8 @@ import pytest from fabledassistant.mcp._context import _user_id_ctx from fabledassistant.mcp.tools.tasks import ( - fable_list_tasks, fable_get_task, fable_create_task, - fable_update_task, fable_add_task_log, + list_tasks, get_task, create_task, + update_task, add_task_log, ) @@ -37,7 +37,7 @@ async def test_list_tasks_passes_is_task_true_and_repackages(): rows = [_fake_task(id=1), _fake_task(id=2)] mock = AsyncMock(return_value=(rows, 2)) with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock): - out = await fable_list_tasks() + out = await list_tasks() assert out["total"] == 2 assert len(out["tasks"]) == 2 assert mock.call_args.kwargs["is_task"] is True @@ -47,7 +47,7 @@ async def test_list_tasks_passes_is_task_true_and_repackages(): async def test_list_tasks_status_filter(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock): - await fable_list_tasks(status="in_progress") + await list_tasks(status="in_progress") assert mock.call_args.kwargs["status"] == "in_progress" @@ -55,7 +55,7 @@ async def test_list_tasks_status_filter(): async def test_list_tasks_empty_status_means_no_filter(): mock = AsyncMock(return_value=([], 0)) with patch("fabledassistant.mcp.tools.tasks.notes_svc.list_notes", mock): - await fable_list_tasks(status="") + await list_tasks(status="") assert mock.call_args.kwargs["status"] is None @@ -66,20 +66,20 @@ async def test_get_task_with_no_parent_returns_null_parent_title(): "fabledassistant.mcp.tools.tasks.notes_svc.get_note", AsyncMock(return_value=fake), ): - out = await fable_get_task(task_id=5) + out = await get_task(task_id=5) assert out["parent_title"] is None assert out["title"] == "solo" @pytest.mark.asyncio async def test_get_task_enriches_with_parent_title(): - """When parent_id is set, fable_get_task fetches the parent and adds parent_title.""" + """When parent_id is set, get_task fetches the parent and adds parent_title.""" child = _fake_task(id=10, title="child", parent_id=5) parent = _fake_task(id=5, title="parent of 10", parent_id=None) # get_note is called twice: once for child, once for parent mock_get = AsyncMock(side_effect=[child, parent]) with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note", mock_get): - out = await fable_get_task(task_id=10) + out = await get_task(task_id=10) assert out["parent_title"] == "parent of 10" assert mock_get.await_count == 2 @@ -90,7 +90,7 @@ async def test_get_task_parent_missing_returns_null(): child = _fake_task(id=10, parent_id=5) mock_get = AsyncMock(side_effect=[child, None]) with patch("fabledassistant.mcp.tools.tasks.notes_svc.get_note", mock_get): - out = await fable_get_task(task_id=10) + out = await get_task(task_id=10) assert out["parent_title"] is None @@ -101,7 +101,7 @@ async def test_get_task_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="task 999 not found"): - await fable_get_task(task_id=999) + await get_task(task_id=999) @pytest.mark.asyncio @@ -109,7 +109,7 @@ async def test_create_task_passes_status(): fake = _fake_task() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock): - await fable_create_task(title="do x", status="todo") + await create_task(title="do x", status="todo") assert mock.call_args.kwargs["status"] == "todo" @@ -118,7 +118,7 @@ async def test_create_task_priority_empty_becomes_none(): fake = _fake_task() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock): - await fable_create_task(title="x", priority="") + await create_task(title="x", priority="") assert mock.call_args.kwargs["priority"] is None @@ -127,7 +127,7 @@ async def test_create_task_zero_id_sentinels_become_none(): fake = _fake_task() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.tasks.notes_svc.create_note", mock): - await fable_create_task(title="x", project_id=0, milestone_id=0, parent_id=0) + await create_task(title="x", project_id=0, milestone_id=0, parent_id=0) assert mock.call_args.kwargs["project_id"] is None assert mock.call_args.kwargs["milestone_id"] is None assert mock.call_args.kwargs["parent_id"] is None @@ -138,7 +138,7 @@ async def test_update_task_only_sends_non_default_fields(): fake = _fake_task() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock): - await fable_update_task(task_id=1, status="done") + await update_task(task_id=1, status="done") args, kwargs = mock.call_args assert args == (7, 1) assert kwargs == {"status": "done"} @@ -150,7 +150,7 @@ async def test_update_task_empty_priority_is_omitted(): fake = _fake_task() mock = AsyncMock(return_value=fake) with patch("fabledassistant.mcp.tools.tasks.notes_svc.update_note", mock): - await fable_update_task(task_id=1, status="done", priority="") + await update_task(task_id=1, status="done", priority="") assert "priority" not in mock.call_args.kwargs @@ -161,7 +161,7 @@ async def test_update_task_raises_when_not_found(): AsyncMock(return_value=None), ): with pytest.raises(ValueError, match="task 999 not found"): - await fable_update_task(task_id=999, status="done") + await update_task(task_id=999, status="done") @pytest.mark.asyncio @@ -177,7 +177,7 @@ async def test_add_task_log_returns_log_dict(): "fabledassistant.mcp.tools.tasks.task_logs_svc.create_log", AsyncMock(return_value=log), ): - out = await fable_add_task_log(task_id=1, content="checkpoint") + out = await add_task_log(task_id=1, content="checkpoint") assert out["id"] == 5 assert out["task_id"] == 1 assert out["content"] == "checkpoint" @@ -192,4 +192,4 @@ async def test_add_task_log_propagates_value_error_when_task_missing(): AsyncMock(side_effect=ValueError("Task 999 not found")), ): with pytest.raises(ValueError): - await fable_add_task_log(task_id=999, content="x") + await add_task_log(task_id=999, content="x")