"""Fable MCP server — exposes Fable Assistant as MCP tools via stdio transport.""" from __future__ import annotations from mcp.server.fastmcp import FastMCP from dotenv import load_dotenv from fable_mcp.client import FableClient from fable_mcp.tools import notes, tasks, projects, milestones, search, chat load_dotenv() mcp = FastMCP("fable") # --------------------------------------------------------------------------- # Notes # --------------------------------------------------------------------------- @mcp.tool() async def fable_list_notes( limit: int = 20, offset: int = 0, tag: str = "", search_text: str = "", ) -> dict: """List notes stored in Fable. Optionally filter by tag or search text.""" async with FableClient() as client: return await notes.list_notes( client, limit=limit, offset=offset, tag=tag or None, search=search_text or None, ) @mcp.tool() async def fable_get_note(note_id: int) -> dict: """Fetch the full content of a single Fable note by its ID.""" async with FableClient() as client: return await notes.get_note(client, note_id=note_id) @mcp.tool() async def fable_create_note( title: str, body: str = "", tags: list[str] | None = None, project_id: int = 0, ) -> dict: """Create a new note in Fable.""" async with FableClient() as client: return await notes.create_note( client, title=title, body=body, tags=tags, project_id=project_id or None, ) @mcp.tool() async def fable_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 provided fields are changed.""" async with FableClient() as client: return await notes.update_note( client, note_id=note_id, title=title or None, body=body or None, tags=tags, project_id=project_id or None, ) @mcp.tool() async def fable_delete_note(note_id: int) -> str: """Delete a Fable note by ID.""" async with FableClient() as client: await notes.delete_note(client, note_id=note_id) return f"Note {note_id} deleted." # --------------------------------------------------------------------------- # Tasks # --------------------------------------------------------------------------- @mcp.tool() async def fable_list_tasks( limit: int = 20, offset: int = 0, status: str = "", project_id: int = 0, ) -> dict: """List tasks in Fable. Filter by status (todo/in_progress/done) or project.""" async with FableClient() as client: return await tasks.list_tasks( client, limit=limit, offset=offset, status=status or None, project_id=project_id or None, ) @mcp.tool() async def fable_get_task(task_id: int) -> dict: """Fetch a single Fable task by ID, including parent task title.""" async with FableClient() as client: return await tasks.get_task(client, task_id=task_id) @mcp.tool() async def fable_create_task( title: str, body: str = "", status: str = "todo", priority: str = "", project_id: int = 0, milestone_id: int = 0, parent_id: int = 0, tags: list[str] | None = None, ) -> dict: """Create a new task in Fable.""" async with FableClient() as client: return await tasks.create_task( client, title=title, body=body, status=status, priority=priority or None, project_id=project_id or None, milestone_id=milestone_id or None, parent_id=parent_id or None, tags=tags, ) @mcp.tool() async def fable_update_task( task_id: int, title: str = "", body: str = "", status: str = "", priority: str = "", project_id: int = 0, milestone_id: int = 0, ) -> dict: """Update an existing Fable task. Only provided fields are changed.""" async with FableClient() as client: return await tasks.update_task( client, task_id=task_id, title=title or None, body=body or None, status=status or None, priority=priority or None, project_id=project_id or None, milestone_id=milestone_id or None, ) @mcp.tool() async def fable_add_task_log(task_id: int, body: str) -> dict: """Append a progress log entry to a Fable task.""" async with FableClient() as client: return await tasks.add_task_log(client, task_id=task_id, body=body) # --------------------------------------------------------------------------- # Projects # --------------------------------------------------------------------------- @mcp.tool() async def fable_list_projects() -> dict: """List all Fable projects for the current user.""" async with FableClient() as client: return await projects.list_projects(client) @mcp.tool() async def fable_get_project(project_id: int) -> dict: """Fetch a Fable project by ID, including milestone summary.""" async with FableClient() as client: return await projects.get_project(client, project_id=project_id) @mcp.tool() async def fable_create_project( title: str, description: str = "", goal: str = "", status: str = "active", color: str = "", ) -> dict: """Create a new project in Fable.""" async with FableClient() as client: return await projects.create_project( client, title=title, description=description, goal=goal or None, status=status, color=color or None, ) @mcp.tool() async def fable_update_project( project_id: int, title: str = "", description: str = "", goal: str = "", status: str = "", color: str = "", ) -> dict: """Update an existing Fable project.""" async with FableClient() as client: return await projects.update_project( client, project_id=project_id, title=title or None, description=description or None, goal=goal or None, status=status or None, color=color or None, ) # --------------------------------------------------------------------------- # Milestones # --------------------------------------------------------------------------- @mcp.tool() async def fable_list_milestones(project_id: int) -> dict: """List milestones for a Fable project.""" async with FableClient() as client: return await milestones.list_milestones(client, project_id=project_id) @mcp.tool() async def fable_create_milestone( project_id: int, title: str, description: str = "", status: str = "active", ) -> dict: """Create a milestone within a Fable project.""" async with FableClient() as client: return await milestones.create_milestone( client, project_id=project_id, title=title, description=description, status=status, ) @mcp.tool() async def fable_update_milestone( project_id: int, milestone_id: int, title: str = "", description: str = "", status: str = "", order_index: int = -1, ) -> dict: """Update a Fable milestone.""" async with FableClient() as client: return await milestones.update_milestone( client, project_id=project_id, milestone_id=milestone_id, title=title or None, description=description or None, status=status or None, order_index=order_index if order_index >= 0 else None, ) # --------------------------------------------------------------------------- # Search # --------------------------------------------------------------------------- @mcp.tool() async def fable_search( q: str, content_type: str = "all", limit: int = 10, ) -> dict: """Semantic search over Fable notes and tasks. content_type: "note", "task", or "all" (default). Returns results ranked by similarity with id, title, body snippet, tags. """ async with FableClient() as client: return await search.search(client, q=q, content_type=content_type, limit=limit) # --------------------------------------------------------------------------- # Chat # --------------------------------------------------------------------------- @mcp.tool() async def fable_list_conversations(limit: int = 20, offset: int = 0) -> dict: """List MCP chat conversations stored in Fable.""" async with FableClient() as client: return await chat.list_conversations(client, limit=limit, offset=offset) @mcp.tool() async def fable_send_message( message: str, conversation_id: str = "", think: bool = False, ) -> dict: """Send a message to Fable's LLM and receive the full response. Fable handles tool use, RAG, and history internally. Pass conversation_id to continue an existing conversation. Returns conversation_id, response text, and any tool_call events. """ async with FableClient() as client: return await chat.send_message( client, message=message, conversation_id=conversation_id or None, think=think, ) # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- def main() -> None: # Validate env vars at startup — raises ValueError with a clear message if missing. FableClient() mcp.run(transport="stdio") if __name__ == "__main__": main()