ticktick_mcp_server.py•9.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()