Skip to main content
Glama
ticktick_mcp_server.py9.32 kB
#!/usr/bin/env python3 """ TickTick MCP Server Model Context Protocol server for TickTick task management. Provides tools for creating, viewing, and managing TickTick tasks through Claude Desktop. """ import os import json from datetime import datetime, timedelta from typing import Optional, Any from mcp.server.fastmcp import FastMCP from ticktick_rest_api import TickTickClient from dotenv import load_dotenv # Load environment variables load_dotenv() # Initialize FastMCP server mcp = FastMCP("TickTick") # Global client instance _client: Optional[TickTickClient] = None def get_client() -> TickTickClient: """Get or create TickTick client instance""" global _client if _client is None: client_id = os.getenv('TICKTICK_CLIENT_ID') client_secret = os.getenv('TICKTICK_CLIENT_SECRET') redirect_uri = os.getenv('TICKTICK_REDIRECT_URI', 'http://127.0.0.1:8080') if not client_id or not client_secret: raise ValueError( "Missing TickTick credentials. Please set:\n" " TICKTICK_CLIENT_ID\n" " TICKTICK_CLIENT_SECRET\n" "Get these from: https://developer.ticktick.com/manage" ) _client = TickTickClient(client_id, client_secret, redirect_uri) # Ensure we're authenticated if not _client.access_token: raise Exception( "Not authenticated. Please run: python ticktick_rest_api.py --auth\n" "This will open a browser to complete OAuth authorization." ) return _client @mcp.tool() def add_task( title: str, content: str = "", project: str = "", priority: int = 0, due_date: str = "", time_zone: str = "America/Los_Angeles" ) -> str: """ Create a new task in TickTick Args: title: Task title (required) content: Task description/notes project: Project name (searches for matching project) priority: Task priority (0=none, 1=low, 3=medium, 5=high) due_date: Due date in ISO format (YYYY-MM-DD) or "today"/"tomorrow" time_zone: IANA timezone name Returns: Success message with task details """ client = get_client() # Build task data task_data = {"title": title} if content: task_data["content"] = content if priority in [0, 1, 3, 5]: task_data["priority"] = priority # Handle project lookup if project: projects = client.get_projects() matching = [p for p in projects if project.lower() in p['name'].lower()] if matching: task_data["projectId"] = matching[0]['id'] # Handle due date if due_date: if due_date.lower() == "today": dt = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) elif due_date.lower() == "tomorrow": dt = datetime.now() + timedelta(days=1) dt = dt.replace(hour=9, minute=0, second=0, microsecond=0) else: # Parse ISO date dt = datetime.fromisoformat(due_date.replace('Z', '+00:00')) task_data["dueDate"] = dt.strftime("%Y-%m-%dT%H:%M:%S+0000") task_data["timeZone"] = time_zone # Create task task = client.create_task(**task_data) priority_labels = {0: "None", 1: "Low", 3: "Medium", 5: "High"} result = f"✓ Created task: {task['title']}\n" result += f" ID: {task['id']}\n" result += f" Priority: {priority_labels.get(priority, 'None')}" if 'projectId' in task_data: result += f"\n Project: {project}" return result @mcp.tool() def list_tasks(project: str = "", limit: int = 10) -> str: """ List tasks from TickTick Args: project: Filter by project name (optional) limit: Maximum number of tasks to return (default: 10) Returns: Formatted list of tasks """ client = get_client() tasks = client.get_tasks() projects = {p['id']: p['name'] for p in client.get_projects()} # Filter by project if specified if project: project_ids = [pid for pid, pname in projects.items() if project.lower() in pname.lower()] tasks = [t for t in tasks if t.get('projectId') in project_ids] # Sort by due date (tasks without due date go last) tasks.sort(key=lambda t: t.get('dueDate', 'ZZZ')) # Limit results tasks = tasks[:limit] if not tasks: return "No tasks found" result = f"Found {len(tasks)} task(s):\n\n" for task in tasks: status = "✓" if task.get('status') == 2 else "○" project_name = projects.get(task.get('projectId'), 'Inbox') result += f"{status} {task['title']}\n" result += f" Project: {project_name}\n" if task.get('dueDate'): result += f" Due: {task['dueDate']}\n" if task.get('priority'): priority_labels = {1: "Low", 3: "Medium", 5: "High"} result += f" Priority: {priority_labels.get(task['priority'], 'None')}\n" result += "\n" return result.strip() @mcp.tool() def complete_task(task_title: str) -> str: """ Mark a task as complete Args: task_title: Title of the task to complete (searches for matching task) Returns: Success message """ client = get_client() # Find matching task tasks = client.get_tasks() matching = [t for t in tasks if task_title.lower() in t['title'].lower()] if not matching: return f"No task found matching: {task_title}" if len(matching) > 1: result = f"Multiple tasks found matching '{task_title}':\n" for t in matching[:5]: result += f" - {t['title']}\n" return result + "\nPlease be more specific." task = matching[0] client.complete_task(task['id']) return f"✓ Completed task: {task['title']}" @mcp.tool() def list_projects() -> str: """ List all TickTick projects Returns: Formatted list of projects """ client = get_client() projects = client.get_projects() result = f"Found {len(projects)} project(s):\n\n" for project in projects: result += f"• {project['name']}\n" result += f" ID: {project['id']}\n" if 'color' in project: result += f" Color: {project['color']}\n" result += "\n" return result.strip() @mcp.tool() def create_project(name: str, color: str = "#3b82f6") -> str: """ Create a new TickTick project Args: name: Project name color: Hex color code (default: blue #3b82f6) Returns: Success message with project details """ client = get_client() project = client.create_project(name, color) return f"✓ Created project: {project['name']}\n ID: {project['id']}\n Color: {color}" @mcp.tool() def get_task_details(task_title: str) -> str: """ Get detailed information about a specific task Args: task_title: Title of the task (searches for matching task) Returns: Detailed task information """ client = get_client() # Find matching task tasks = client.get_tasks() matching = [t for t in tasks if task_title.lower() in t['title'].lower()] if not matching: return f"No task found matching: {task_title}" if len(matching) > 1: result = f"Multiple tasks found matching '{task_title}':\n" for t in matching[:5]: result += f" - {t['title']}\n" return result + "\nPlease be more specific." task = matching[0] projects = {p['id']: p['name'] for p in client.get_projects()} result = f"Task: {task['title']}\n" result += f"ID: {task['id']}\n" result += f"Status: {'Completed' if task.get('status') == 2 else 'Active'}\n" result += f"Project: {projects.get(task.get('projectId'), 'Inbox')}\n" if task.get('content'): result += f"\nDescription:\n{task['content']}\n" if task.get('priority'): priority_labels = {1: "Low", 3: "Medium", 5: "High"} result += f"Priority: {priority_labels.get(task['priority'], 'None')}\n" if task.get('dueDate'): result += f"Due Date: {task['dueDate']}\n" if task.get('repeat'): result += f"Recurring: {task['repeat']}\n" return result @mcp.resource("ticktick://status") def get_status() -> str: """Get current TickTick status overview""" client = get_client() projects = client.get_projects() tasks = client.get_tasks() # Count tasks by project by_project = {} completed = 0 active = 0 for task in tasks: pid = task.get('projectId', 'Inbox') by_project[pid] = by_project.get(pid, 0) + 1 if task.get('status') == 2: completed += 1 else: active += 1 result = "TickTick Status Overview\n" result += "=" * 40 + "\n\n" result += f"Total Tasks: {len(tasks)}\n" result += f" Active: {active}\n" result += f" Completed: {completed}\n\n" result += f"Projects ({len(projects)}):\n" for project in projects: count = by_project.get(project['id'], 0) result += f" • {project['name']}: {count} tasks\n" return result if __name__ == "__main__": # Run the MCP server mcp.run()

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/kbadinger/ticktickmcp'

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