"""OrgMyLife MCP Server β€” provides tools to interact with the OrgMyLife API. Tools: - list_projects: Get all project tasks from the kanban board - update_project: Move a project task to a different status column - create_project: Create a new project task - list_tasks: Get open tasks (with optional filters) - list_calls: Get pending call items """ import os import httpx from mcp.server.fastmcp import FastMCP # Configuration from environment BASE_URL = os.environ.get("ORGMYLIFE_BASE_URL", "https://api.andreknie.de") API_SECRET = os.environ.get("ORGMYLIFE_API_SECRET", "") mcp = FastMCP("orgmylife-api", instructions="OrgMyLife project management API. Use these tools to read and update the projects board, task list, and call list.") def _headers(): """Build auth headers for the OrgMyLife API.""" return { "Authorization": f"Bearer {API_SECRET}", "X-API-Key": API_SECRET, "Content-Type": "application/json", } def _get(path: str, params: dict = None) -> dict: """Make an authenticated GET request to the OrgMyLife API.""" r = httpx.get(f"{BASE_URL}{path}", headers=_headers(), params=params, timeout=15) r.raise_for_status() return r.json() def _post(path: str, json_body: dict = None) -> dict: """Make an authenticated POST request to the OrgMyLife API.""" r = httpx.post(f"{BASE_URL}{path}", headers=_headers(), json=json_body, timeout=15) r.raise_for_status() return r.json() def _put(path: str, json_body: dict = None) -> dict: """Make an authenticated PUT request to the OrgMyLife API.""" r = httpx.put(f"{BASE_URL}{path}", headers=_headers(), json=json_body, timeout=15) r.raise_for_status() return r.json() def _delete(path: str) -> dict: """Make an authenticated DELETE request to the OrgMyLife API.""" r = httpx.delete(f"{BASE_URL}{path}", headers=_headers(), timeout=15) r.raise_for_status() return r.json() # --- Projects Board --- @mcp.tool() def list_projects() -> str: """Get all project tasks from the kanban board, grouped by status column.""" data = _get("/api/projects") tasks = data.get("tasks", []) if not tasks: return "No project tasks found." # Group by status columns = {} for t in tasks: status = t.get("status", "backlog") columns.setdefault(status, []).append(t) lines = [] status_order = ["backlog", "ready", "in_progress", "blocked", "review", "done"] status_labels = { "backlog": "πŸ“‹ Backlog", "ready": "🟒 Ready", "in_progress": "βš™οΈ In Progress", "blocked": "🚫 Blocked", "review": "πŸ‘ Review", "done": "βœ… Done", } for status in status_order: items = columns.get(status, []) lines.append(f"\n## {status_labels.get(status, status)} ({len(items)})") if items: for t in items: repo = f" [{t['repo']}]" if t.get("repo") else "" lines.append(f" - #{t['id']}: {t['title']}{repo}") else: lines.append(" (empty)") return "\n".join(lines) @mcp.tool() def update_project(task_id: int, status: str) -> str: """Move a project task to a different status column. Args: task_id: The project task ID (from list_projects) status: Target column β€” one of: backlog, ready, in_progress, blocked, review, done """ valid = ["backlog", "ready", "in_progress", "blocked", "review", "done"] if status not in valid: return f"Invalid status '{status}'. Must be one of: {', '.join(valid)}" result = _put(f"/api/projects/{task_id}", {"status": status}) return f"Moved project task #{task_id} to '{status}'. Result: {result.get('status', 'unknown')}" @mcp.tool() def create_project(title: str, repo: str = "") -> str: """Create a new project task on the kanban board (starts in Backlog). Args: title: Task title repo: Repository name (e.g., "OrgMyLife", "AI-Orchestrator") """ result = _post("/api/projects/create", {"title": title, "repo": repo}) if result.get("status") == "success": return f"Created project task: '{title}' (ID: {result.get('id')})" return f"Failed to create project task: {result}" # --- Tasks --- @mcp.tool() def list_tasks(filter_origin: str = "", filter_label: str = "", sort: str = "priority") -> str: """Get open tasks from the task list. Args: filter_origin: Filter by source β€” "manual", "nextcloud_todo", "dhive_email", "gmail", or "" for all filter_label: Filter by label name, or "" for all sort: Sort order β€” "priority", "due_date", "created", or "" for manual order """ params = {} if sort: params["sort"] = sort if filter_origin: params["filter_origin"] = filter_origin if filter_label: params["filter_label"] = filter_label data = _get("/api/tasks", params) tasks = data.get("tasks", []) total = data.get("total", len(tasks)) if not tasks: return "No open tasks." lines = [f"**{total} open tasks:**\n"] for t in tasks[:30]: # Limit to 30 to avoid huge output priority = t.get("priority", "") due = f" (due: {t['due_date']})" if t.get("due_date") else "" labels = f" [{t['labels']}]" if t.get("labels") else "" conflict = " ⚠️CONFLICT" if t.get("has_conflict") else "" lines.append(f" #{t['id']}: {t['title']} β€” {priority}{due}{labels}{conflict}") if total > 30: lines.append(f"\n ... and {total - 30} more") return "\n".join(lines) # --- Calls --- @mcp.tool() def list_calls() -> str: """Get all pending call items from the call list.""" data = _get("/api/calls") calls = data.get("calls", []) if not calls: return "No pending calls." lines = [f"**{len(calls)} pending calls:**\n"] for c in calls: phone = f" ({c['phone_number']})" if c.get("phone_number") else "" lines.append(f" #{c['id']}: {c['person']}{phone} β€” {c['reason']}") return "\n".join(lines) # --- Daily Digest --- @mcp.tool() def get_digest() -> str: """Get the daily digest summary (overdue, due today, top priorities, schedule).""" data = _get("/api/digest") lines = [f"**{data.get('greeting', 'Hello')}** β€” {data.get('date', '')}"] lines.append(f"Total open tasks: {data.get('total_open', '?')}\n") overdue = data.get("overdue", []) if overdue: lines.append(f"⚠️ **Overdue ({len(overdue)}):**") for t in overdue: lines.append(f" - {t['title']} ({t['days_overdue']}d overdue)") due_today = data.get("due_today", []) if due_today: lines.append(f"\nπŸ“… **Due Today ({len(due_today)}):**") for t in due_today: lines.append(f" - {t['title']}") top = data.get("top_tasks", []) if top: lines.append(f"\n🎯 **Top Priorities:**") for t in top: lines.append(f" - {t['title']} ({t['priority']})") events = data.get("events_today", []) if events: lines.append(f"\nπŸ“† **Today's Schedule:**") for e in events: lines.append(f" - {e['time']} {e['title']}") calls = data.get("pending_calls", []) if calls: lines.append(f"\nπŸ“ž **Calls to make ({len(calls)}):**") for c in calls: lines.append(f" - {c['person']} β€” {c['reason']}") return "\n".join(lines) if __name__ == "__main__": mcp.run()