"""Milestone CRUD MCP tools — thin wrappers over services/milestones.py. Mirrors existing fable-mcp milestone tool contracts: list/create/update. The existing surface has no fable_get_milestone or fable_delete_milestone — kept that way for parity. Sentinels: - title="" / description="" / status="" → "leave unchanged" on update - order_index=-1 → "leave unchanged" on update (0 is a valid order_index) - status="active" default on create """ from __future__ import annotations 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. Returns id, title, description, status (active/done), order_index, and task counts. """ uid = current_user_id() rows = await milestones_svc.get_project_milestone_summary(uid, project_id) return {"milestones": rows} async def fable_create_milestone( project_id: int, title: str, description: str = "", status: str = "active", ) -> dict: """Create a milestone within a Fable project. Args: project_id: The project this milestone belongs to (required). title: Milestone name (required). description: Optional description of what this milestone covers. status: active (default) or done. """ uid = current_user_id() milestone = await milestones_svc.create_milestone( uid, project_id=project_id, title=title, description=description or None, status=status, ) return milestone.to_dict() 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. Only explicitly provided fields are changed. Args: project_id: Project the milestone belongs to (preserved for API parity; ownership scoping is enforced by user_id at the service layer). milestone_id: ID of the milestone to update. title: New title, or omit to leave unchanged. description: New description, or omit to leave unchanged. status: New status — active or done. order_index: New display position (0-based). Use -1 to leave unchanged. """ uid = current_user_id() fields: dict = {} if title: fields["title"] = title if description: fields["description"] = description if status: fields["status"] = status if order_index >= 0: fields["order_index"] = order_index milestone = await milestones_svc.update_milestone(uid, milestone_id, **fields) if milestone is None: raise ValueError(f"milestone {milestone_id} not found") return milestone.to_dict() def register(mcp) -> None: for fn in ( fable_list_milestones, fable_create_milestone, fable_update_milestone, ): mcp.tool(name=fn.__name__)(fn)