Skip to main content
Glama

Todoist Python MCP Server

by Johnxjp
todoist_server.py8.64 kB
from dataclasses import dataclass from datetime import datetime import logging import os import sys from typing import Literal, Optional from mcp.server.fastmcp import FastMCP from todoist_api_python.api import TodoistAPI TODOIST_API_TOKEN = os.getenv("TODOIST_API_TOKEN") todoist_api = TodoistAPI(TODOIST_API_TOKEN) mcp = FastMCP("todoist-server", dependencies=["todoist_api_python"]) logger = logging.getLogger("todoist_server") @dataclass class Project: id: str name: str # Abstraction for Todoist project # https://developer.todoist.com/rest/v2/#get-all-projects @dataclass class TodoistProjectResponse: id: str name: str def date_difference(date1: str, date2: str) -> int: """Compare two dates in the format 'YYYY-MM-DD'""" date1 = datetime.strptime(date1, "%Y-%m-%d") date2 = datetime.strptime(date2, "%Y-%m-%d") return (date1 - date2).days @mcp.tool() def get_projects() -> list[Project]: """Get all todo projects. These are like folders for tasks in Todoist""" try: projects: TodoistProjectResponse = todoist_api.get_projects() return [Project(p.id, p.name) for p in projects] except Exception as e: return f"Error: Couldn't fetch projects {str(e)}" def get_project_id_by_name(project_name: str) -> str: """Search for a project by name and return its ID""" projects = get_projects() for project in projects: if project.name.lower() == project_name.lower(): return project.id return None @mcp.tool() def get_tasks( project_id: Optional[str] = None, project_name: Optional[str] = None, task_name: Optional[str] = None, labels: Optional[list[str]] = None, due_date: Optional[str] = None, is_overdue: Optional[bool] = None, priority: Optional[Literal[1, 2, 3, 4]] = None, limit: Optional[int] = None, ) -> list[str]: """ Fetch user's tasks. These can be filtered by project, labels, time, etc. If no filters are provided, all tasks are returned. Args: - project_id: The string ID of the project to fetch tasks from. Example '1234567890' - project_name: Name of the project to fetch tasks from. Example 'Work' or 'Inbox' - task_name: Filter tasks by name. Example 'Buy groceries' - labels: List of tags used to filter tasks. - priority: Filter tasks by priority level. 4 (urgent), 3 (high), 2 (normal), 1 (low) - due_date: Specific due date in YYYY-MM-DD format. Example '2021-12-31' - is_overdue: Filter tasks that are overdue. - limit: Maximum number of tasks to return. Default is all. """ tasks = todoist_api.get_tasks() # How to implement "did you mean this project?" feature? if project_name: project_id = get_project_id_by_name(project_name) if not project_id: raise ValueError(f"Project '{project_name}' not found") if project_id: project_id = project_id.strip('"') tasks = [t for t in tasks if t.project_id == project_id] if task_name: tasks = [t for t in tasks if task_name.lower() in t.content.lower()] if due_date: tasks = [t for t in tasks if t.due and t.due["date"] == due_date] if is_overdue is not None: now = datetime.today().strftime("%Y-%m-%d") tasks = [ t for t in tasks if t.due and (date_difference(now, t.due["date"]) < 0) == is_overdue ] if labels: for label in labels: tasks = [t for t in tasks if label.lower() in [l.lower() for l in t.labels]] if priority: tasks = [t for t in tasks if t.priority == priority] return [ { "id": t.id, "title": t.content, "priority": t.priority, "due": t.due["date"] if t.due else None, } for t in tasks ][:limit] @mcp.tool() def delete_task(task_id: str): """Delete a task by its ID""" try: task_id = task_id.strip('"') is_success = todoist_api.delete_task(task_id=task_id) if not is_success: raise Exception return "Task deleted successfully" except Exception as e: raise Exception(f"Couldn't delete task {str(e)}") @mcp.tool() def create_task( content: str, description: Optional[str] = None, project_id: Optional[str] = None, labels: Optional[list[str]] = None, priority: Optional[int] = None, due_date: Optional[str] = None, section_id: Optional[str] = None, ) -> str: """ Create a new task Args: - content [str]: Task content. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Help Center. - description [str]: A description for the task. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Help Center. - project_id [str]: The ID of the project to add the task. If none, adds to user's inbox by default. - labels [list[str]]: The task's labels (a list of names that may represent either personal or shared labels). - priority [int]: Task priority from 1 (normal) to 4 (urgent). - due_date [str]: Specific date in YYYY-MM-DD format relative to user’s timezone. - section_id [str]: The ID of the section to add the task to Returns: - task_id: str: """ try: data = {} if description: data["description"] = description if project_id: data["project_id"] = project_id if labels: if isinstance(labels, str): labels = [labels] data["labels"] = labels if priority: data["priority"] = priority if due_date: data["due_date"] = due_date if section_id: data["section_id"] = section_id task = todoist_api.add_task(content, **data) return task.id except Exception as e: raise Exception(f"Couldn't create task {str(e)}") @mcp.tool() def update_task( task_id: str, content: Optional[str] = None, description: Optional[str] = None, labels: Optional[list[str]] = None, priority: Optional[int] = None, due_date: Optional[str] = None, deadline_date: Optional[str] = None, ): """ Update an attribute of a task given its ID. Any attribute can be updated. Args: - task_id [str | int]: The ID of the task to update. Example '1234567890' or 1234567890 - content [str]: Task content. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Help Center. - description [str]: A description for the task. This value may contain markdown-formatted text and hyperlinks. Details on markdown support can be found in the Text Formatting article in the Help Center. - labels [list[str]]: The task's labels (a list of names that may represent either personal or shared labels). - priority [int]: Task priority from 1 (normal) to 4 (urgent). - due_date [str]: Specific date in YYYY-MM-DD format relative to user’s timezone. - deadline_date [str]: Specific date in YYYY-MM-DD format relative to user’s timezone. """ # Client sometimes struggle to convert int to str task_id = task_id.strip('"') try: data = {} if content: data["content"] = content if description: data["description"] = description if labels: if isinstance(labels, str): labels = [labels] data["labels"] = labels if priority: data["priority"] = priority if due_date: data["due_date"] = due_date if deadline_date: data["deadline_date"] = deadline_date is_success = todoist_api.update_task(task_id=task_id, **data) if not is_success: raise Exception return "Task updated successfully" except Exception as e: raise Exception(f"Couldn't update task {str(e)}") @mcp.tool() def complete_task(task_id: str) -> str: """Mark a task as done""" try: task_id = task_id.strip('"') is_success = todoist_api.close_task(task_id=task_id) if not is_success: raise Exception return "Task closed successfully" except Exception as e: raise Exception(f"Couldn't close task {str(e)}") def main(): """Entry point for the installed package""" print("...", file=sys.stderr) mcp.run(transport="stdio") if __name__ == "__main__": main()

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Johnxjp/todoist-mcp-python'

If you have feedback or need assistance with the MCP directory API, please join our Discord server