Files
FabledScribe/src/fabledassistant/mcp/tools/milestones.py
T

109 lines
3.6 KiB
Python

"""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
from fabledassistant.services import trash as trash_svc
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.
"""
uid = current_user_id()
rows = await milestones_svc.get_project_milestone_summary(uid, project_id)
return {"milestones": rows}
async def create_milestone(
project_id: int,
title: str,
description: str = "",
status: str = "active",
) -> dict:
"""Create a milestone within a Scribe 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 update_milestone(
project_id: int,
milestone_id: int,
title: str = "",
description: str = "",
status: str = "",
order_index: int = -1,
) -> dict:
"""Update a Scribe 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()
async def delete_milestone(milestone_id: int) -> dict:
"""Move a milestone to the trash (recoverable). Its tasks go with it as one batch.
Restore via restore(batch_id)."""
uid = current_user_id()
batch = await trash_svc.delete(uid, "milestone", milestone_id)
if batch is None:
raise ValueError(f"milestone {milestone_id} not found")
return {"deleted_batch_id": batch,
"message": f"Milestone {milestone_id} + its tasks moved to trash. Restore with restore('{batch}')."}
def register(mcp) -> None:
for fn in (
list_milestones,
create_milestone,
update_milestone,
delete_milestone,
):
mcp.tool(name=fn.__name__)(fn)