import json import uuid from datetime import datetime, timedelta, timezone, date as date_type from decimal import Decimal from mcp.server.fastmcp import FastMCP from sqlalchemy import select, and_ from app.core.database import AsyncSessionLocal from app.models.todos import TodoItem from app.models.notes import NoteItem from app.models.shopping import ShoppingList, ListItem, Product mcp = FastMCP("HomeHub", stateless_http=True) def _serialize(obj): if isinstance(obj, uuid.UUID): return str(obj) if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, date_type): return obj.isoformat() if isinstance(obj, Decimal): return float(obj) raise TypeError(f"Type non sérialisable : {type(obj)}") def _dumps(data) -> str: return json.dumps(data, default=_serialize) def _iso_week_label() -> str: now = datetime.now(tz=timezone.utc) iso = now.isocalendar() return f"S{iso[1]} {iso[0]}" # ── TODOS ────────────────────────────────────────────────────────────────────── @mcp.tool() async def get_todos( status: str = "pending", domain: str | None = None, priority: str | None = None, ) -> str: """Liste filtrée des tâches. status: pending/done/cancelled. priority: low/medium/high.""" async with AsyncSessionLocal() as session: conditions = [TodoItem.status == status] if domain: conditions.append(TodoItem.domains.contains([domain])) if priority: conditions.append(TodoItem.priority == priority) stmt = ( select(TodoItem) .where(and_(*conditions)) .order_by(TodoItem.due_date.asc().nulls_last(), TodoItem.created_at.desc()) .limit(100) ) result = await session.execute(stmt) items = result.scalars().all() return _dumps([{ "id": item.id, "title": item.title, "status": item.status, "priority": item.priority, "domains": item.domains, "tags": item.tags, "due_date": item.due_date, "postponed_count": item.postponed_count, } for item in items]) @mcp.tool() async def create_todo( title: str, due_date: str | None = None, priority: str | None = None, domain: str | None = None, ) -> str: """Crée une tâche. due_date en ISO 8601 (ex: 2025-06-01T10:00:00Z). priority: low/medium/high.""" async with AsyncSessionLocal() as session: parsed_due = None if due_date: try: parsed_due = datetime.fromisoformat(due_date.replace("Z", "+00:00")) except ValueError: return _dumps({"error": f"Format de date invalide : {due_date}"}) item = TodoItem( title=title, due_date=parsed_due, priority=priority or "medium", domains=[domain] if domain else [], ) session.add(item) await session.commit() await session.refresh(item) return _dumps({ "id": item.id, "title": item.title, "status": item.status, "priority": item.priority, "domains": item.domains, "due_date": item.due_date, }) @mcp.tool() async def update_todo( id: str, title: str | None = None, status: str | None = None, priority: str | None = None, ) -> str: """Modifie une tâche. status: pending/done/cancelled. priority: low/medium/high.""" async with AsyncSessionLocal() as session: try: todo_id = uuid.UUID(id) except ValueError: return _dumps({"error": f"UUID invalide : {id}"}) item = await session.get(TodoItem, todo_id) if not item: return _dumps({"error": f"Tâche introuvable : {id}"}) if title is not None: item.title = title if status is not None: item.status = status if priority is not None: item.priority = priority item.updated_at = datetime.now(timezone.utc) await session.commit() await session.refresh(item) return _dumps({ "id": item.id, "title": item.title, "status": item.status, "priority": item.priority, }) @mcp.tool() async def postpone_todo(id: str, days: int) -> str: """Reporte une tâche de N jours à partir de sa date d'échéance (ou maintenant si nulle).""" async with AsyncSessionLocal() as session: try: todo_id = uuid.UUID(id) except ValueError: return _dumps({"error": f"UUID invalide : {id}"}) item = await session.get(TodoItem, todo_id) if not item: return _dumps({"error": f"Tâche introuvable : {id}"}) now = datetime.now(timezone.utc) base = item.due_date if item.due_date else now item.due_date = base + timedelta(days=days) item.postponed_count += 1 item.updated_at = now await session.commit() await session.refresh(item) return _dumps({ "id": item.id, "title": item.title, "due_date": item.due_date, "postponed_count": item.postponed_count, }) @mcp.tool() async def delete_todo(id: str) -> str: """Supprime une tâche définitivement.""" async with AsyncSessionLocal() as session: try: todo_id = uuid.UUID(id) except ValueError: return _dumps({"error": f"UUID invalide : {id}"}) item = await session.get(TodoItem, todo_id) if not item: return _dumps({"error": f"Tâche introuvable : {id}"}) await session.delete(item) await session.commit() return _dumps({"deleted": id})