# ticktick-sdk: A TickTick MCP Server & Full Python SDK ![PyPI - Version](https://img.shields.io/pypi/v/ticktick-sdk?color=green) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/ticktick-sdk?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=ORANGE&left_text=downloads)](https://pepy.tech/projects/ticktick-sdk) A comprehensive async Python SDK for [TickTick](https://ticktick.com) with [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server support. Includes full support for [Dida365 (滴答清单)](https://dida365.com) as well. **Use TickTick programmatically from Python, or let AI assistants manage your tasks.** ## Table of Contents - [Features](#features) - [Why This Library?](#why-this-library) - [Installation](#installation) - [MCP Server Setup & Usage](#mcp-server-setup--usage) - [Step 1: Register Your App](#step-1-register-your-app) - [Step 2: Get OAuth2 Access Token](#step-2-get-oauth2-access-token) - [Step 3: Configure Your AI Assistant](#step-3-configure-your-ai-assistant) - [CLI Reference](#cli-reference) - [Example Conversations](#example-conversations) - [Available MCP Tools](#available-mcp-tools-43-total) - [Python Library Setup & Usage](#python-library-setup--usage) - [Setup](#setup) - [Quick Start](#quick-start) - [Tasks](#tasks) - [Projects & Folders](#projects--folders) - [Tags](#tags) - [Habits](#habits) - [Focus/Pomodoro](#focuspomodoro) - [User & Statistics](#user--statistics) - [Error Handling](#error-handling) - [Architecture](#architecture) - [API Reference](#api-reference) - [TickTick API Quirks](#important-ticktick-api-quirks) - [Environment Variables](#environment-variables) - [Running Tests](#running-tests) - [Troubleshooting](#troubleshooting) - [Contributing](#contributing) --- ## Features ### MCP Server - **43 Tools**: Streamlined coverage of TickTick functionality - **Batch Operations**: All mutations accept lists (1-100 items) for bulk operations - **AI-Ready**: Works with Claude, GPT, and other MCP-compatible assistants - **Dual Output**: Markdown for humans, JSON for machines ### Python Library - **Full Async Support**: Built on `httpx` for high-performance async operations - **Batch Operations**: Create, update, delete, complete up to 100 tasks in a single call - **Complete Task Management**: Create, read, update, delete, complete, move, pin tasks - **Kanban Boards**: Full column management (create, update, delete, move tasks between columns) - **Project Organization**: Projects, folders, kanban boards - **Tag System**: Hierarchical tags with colors - **Habit Tracking**: Full CRUD for habits with batch check-ins, streaks, and goals - **Focus/Pomodoro**: Access focus session data and statistics - **User Analytics**: Productivity scores, levels, completion rates ### Developer Experience - **Type-Safe**: Full Pydantic v2 validation with comprehensive type hints - **Well-Tested**: 300+ tests covering both mock and live API interactions - **Documented**: Extensive docstrings and examples --- ## Why This Library? ### The Two-API Problem TickTick has **two different APIs**: | API | Type | What We Use It For | |-----|------|-------------------| | **V1 (OAuth2)** | Official, documented | Project with all tasks, basic operations | | **V2 (Session)** | Unofficial, reverse-engineered | Tags, folders, habits, focus, subtasks, and more | The official V1 API is limited. Most of TickTick's power features (tags, habits, focus tracking) are only available through the undocumented V2 web API. **This library combines both**, routing each operation to the appropriate API automatically. ### Compared to Other Libraries Based on analysis of the actual source code of available TickTick Python libraries: | Feature | ticktick-sdk | [pyticktick](https://github.com/sebpretzer/pyticktick) | [ticktick-py](https://github.com/lazeroffmichael/ticktick-py) | [tickthon](https://github.com/anggelomos/tickthon) | [ticktick-python](https://github.com/glasslion/ticktick-python) | |---------|:------------:|:----------:|:-----------:|:--------:|:---------------:| | **I/O Model** | Async | Async | Sync | Sync | Sync | | **Type System** | Pydantic V2 | Pydantic V2 | Dicts | attrs | addict | | **MCP Server** | **Yes** | No | No | No | No | | **Habits** | **Full CRUD** | No | Basic | Basic | No | | **Focus/Pomo** | Yes | Yes | Yes | Yes | No | | **Unified V1+V2** | **Smart Routing** | Separate | Both | V2 only | V2 only | | **Subtasks** | Advanced | Batch | Yes | Basic | Basic | | **Tags** | Full (merge/rename) | Yes | Yes | Yes | No | **Key Differentiators:** - **MCP Server**: Only ticktick-sdk provides AI assistant integration via Model Context Protocol - **Unified API Routing**: Automatically routes operations to V1 or V2 based on feature requirements - **Full Habit CRUD**: Complete habit management including check-ins, streaks, archive/unarchive - **Async-First**: Built on `httpx` for high-performance async operations --- ## Installation ```bash pip install ticktick-sdk ``` **Requirements:** - Python 3.11+ - TickTick account (free or Pro) --- ## MCP Server Setup & Usage Use TickTick with AI assistants like Claude through the Model Context Protocol. ### Step 1: Register Your App 1. Go to the [TickTick Developer Portal](https://developer.ticktick.com/manage) 2. Click **"Create App"** 3. Fill in: - **App Name**: e.g., "My TickTick MCP" - **Redirect URI**: `http://127.0.0.1:8080/callback` 4. Save your **Client ID** and **Client Secret** ### Step 2: Get OAuth2 Access Token Run the auth command with your credentials: ```bash TICKTICK_CLIENT_ID=your_client_id \ TICKTICK_CLIENT_SECRET=your_client_secret \ ticktick-sdk auth ``` This will: 1. **Open your browser** to TickTick's authorization page 2. **Authorize the app** - Click "Authorize" to grant access 3. **Return to terminal** - After authorizing, you'll see output like this: ``` ============================================================ SUCCESS! Here is your access token: ============================================================ a]234abc-5678-90de-f012-34567890abcd ============================================================ NEXT STEPS: For Claude Code users: Run (replace YOUR_* placeholders): claude mcp add ticktick \ -e TICKTICK_CLIENT_ID=YOUR_CLIENT_ID \ ... ``` 4. **Copy this token** - You'll need it in the next step > **Note**: Sometimes the browser shows an "invalid credentials" error page. Just refresh the page and it should work. > **SSH/Headless Users**: Add `--manual` flag for a text-based flow that doesn't require a browser. ### Step 3: Configure Your AI Assistant #### Claude Code (Recommended) ```bash claude mcp add ticktick \ -e TICKTICK_CLIENT_ID=your_client_id \ -e TICKTICK_CLIENT_SECRET=your_client_secret \ -e TICKTICK_ACCESS_TOKEN=your_access_token \ -e TICKTICK_USERNAME=your_email \ -e TICKTICK_PASSWORD=your_password \ -- ticktick-sdk ``` > **Note**: For `TICKTICK_ACCESS_TOKEN`, paste the token you copied from Step 2. Verify it's working: ```bash claude mcp list # See all configured servers /mcp # Within Claude Code, check server status ``` #### Claude Desktop Add to your Claude Desktop config: **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` ```json { "mcpServers": { "ticktick": { "command": "ticktick-sdk", "env": { "TICKTICK_CLIENT_ID": "your_client_id", "TICKTICK_CLIENT_SECRET": "your_client_secret", "TICKTICK_ACCESS_TOKEN": "your_access_token", "TICKTICK_USERNAME": "your_email", "TICKTICK_PASSWORD": "your_password" } } } } ``` #### Other MCP-Compatible Tools This server works with any tool that supports the Model Context Protocol, which includes most modern AI assistants and IDEs. The configuration is similar - you just need to provide the command (`ticktick-sdk`) and the environment variables shown above. ### CLI Reference The `ticktick-sdk` command provides several subcommands: | Command | Description | |---------|-------------| | `ticktick-sdk` | Start the MCP server (default) | | `ticktick-sdk server` | Start the MCP server (explicit) | | `ticktick-sdk server --host HOST` | Use specific API host (`ticktick.com` or `dida365.com`) | | `ticktick-sdk server --enabledModules MODULES` | Enable only specific tool modules (comma-separated) | | `ticktick-sdk server --enabledTools TOOLS` | Enable only specific tools (comma-separated) | | `ticktick-sdk auth` | Get OAuth2 access token (opens browser) | | `ticktick-sdk auth --manual` | Get OAuth2 access token (SSH-friendly) | | `ticktick-sdk --version` | Show version information | | `ticktick-sdk --help` | Show help message | **Tool Filtering** (reduces context window usage for AI assistants): ```bash # Enable only task and project tools ticktick-sdk server --enabledModules tasks,projects # Enable specific tools only ticktick-sdk server --enabledTools ticktick_create_tasks,ticktick_list_tasks # Available modules: tasks, projects, folders, columns, tags, habits, user, focus ``` ### Example Conversations Once configured, you can ask Claude things like: - "What tasks do I have due today?" - "Create a task to call John tomorrow at 2pm" - "Show me my high priority tasks" - "Mark the grocery shopping task as complete" - "What's my current streak for the Exercise habit?" - "Check in my meditation habit for today" - "Create a new habit to drink 8 glasses of water daily" ### Available MCP Tools (43 Total) All mutation tools accept lists for batch operations (1-100 items). #### Task Tools (Batch-Capable) | Tool | Description | |------|-------------| | `ticktick_create_tasks` | Create 1-50 tasks with titles, dates, tags, etc. | | `ticktick_get_task` | Get task details by ID | | `ticktick_list_tasks` | List tasks (active/completed/abandoned/deleted via status filter) | | `ticktick_update_tasks` | Update 1-100 tasks (includes column assignment) | | `ticktick_complete_tasks` | Complete 1-100 tasks | | `ticktick_delete_tasks` | Delete 1-100 tasks (moves to trash) | | `ticktick_move_tasks` | Move 1-50 tasks between projects | | `ticktick_set_task_parents` | Set parent-child relationships for 1-50 tasks | | `ticktick_unparent_tasks` | Remove parent relationships from 1-50 tasks | | `ticktick_search_tasks` | Search tasks by text | | `ticktick_pin_tasks` | Pin or unpin 1-100 tasks | #### Project Tools | Tool | Description | |------|-------------| | `ticktick_list_projects` | List all projects | | `ticktick_get_project` | Get project details with tasks | | `ticktick_create_project` | Create a new project | | `ticktick_update_project` | Update project properties | | `ticktick_delete_project` | Delete a project | #### Folder Tools | Tool | Description | |------|-------------| | `ticktick_list_folders` | List all folders | | `ticktick_create_folder` | Create a folder | | `ticktick_rename_folder` | Rename a folder | | `ticktick_delete_folder` | Delete a folder | #### Kanban Column Tools | Tool | Description | |------|-------------| | `ticktick_list_columns` | List columns for a kanban project | | `ticktick_create_column` | Create a kanban column | | `ticktick_update_column` | Update column name or order | | `ticktick_delete_column` | Delete a kanban column | #### Tag Tools | Tool | Description | |------|-------------| | `ticktick_list_tags` | List all tags | | `ticktick_create_tag` | Create a tag with color | | `ticktick_update_tag` | Update tag properties (includes rename via label) | | `ticktick_delete_tag` | Delete a tag | | `ticktick_merge_tags` | Merge two tags | #### Habit Tools (Batch-Capable) | Tool | Description | |------|-------------| | `ticktick_habits` | List all habits | | `ticktick_habit` | Get habit details | | `ticktick_habit_sections` | List sections (morning/afternoon/night) | | `ticktick_create_habit` | Create a new habit | | `ticktick_update_habit` | Update habit properties (includes archive/unarchive) | | `ticktick_delete_habit` | Delete a habit | | `ticktick_checkin_habits` | Check in 1-50 habits (supports backdating) | | `ticktick_habit_checkins` | Get check-in history | #### User & Analytics Tools | Tool | Description | |------|-------------| | `ticktick_get_profile` | Get user profile | | `ticktick_get_status` | Get account status | | `ticktick_get_statistics` | Get productivity stats | | `ticktick_get_preferences` | Get user preferences | | `ticktick_focus_heatmap` | Get focus heatmap data | | `ticktick_focus_by_tag` | Get focus time by tag | --- ## Python Library Setup & Usage Use TickTick programmatically in your Python applications. ### Setup #### Step 1: Register Your App Same as MCP setup - go to the [TickTick Developer Portal](https://developer.ticktick.com/manage) and create an app. #### Step 2: Create Your .env File Create a `.env` file in your project directory: ```bash # V1 API (OAuth2) TICKTICK_CLIENT_ID=your_client_id_here TICKTICK_CLIENT_SECRET=your_client_secret_here TICKTICK_REDIRECT_URI=http://127.0.0.1:8080/callback TICKTICK_ACCESS_TOKEN= # Will be filled in Step 3 # V2 API (Session) TICKTICK_USERNAME=your_ticktick_email@example.com TICKTICK_PASSWORD=your_ticktick_password # Optional TICKTICK_TIMEOUT=30 ``` #### Step 3: Get OAuth2 Access Token ```bash # Source your .env file first, or export the variables ticktick-sdk auth ``` Copy the access token to your `.env` file. #### Step 4: Verify Setup ```python import asyncio from ticktick_sdk import TickTickClient async def test(): async with TickTickClient.from_settings() as client: profile = await client.get_profile() print(f'Connected as: {profile.display_name}') asyncio.run(test()) ``` ### Quick Start ```python import asyncio from ticktick_sdk import TickTickClient async def main(): async with TickTickClient.from_settings() as client: # Create a task task = await client.create_task( title="Learn ticktick-sdk", tags=["python", "productivity"], ) print(f"Created: {task.title} (ID: {task.id})") # List all tasks tasks = await client.get_all_tasks() print(f"You have {len(tasks)} active tasks") # Complete the task await client.complete_task(task.id, task.project_id) print("Task completed!") asyncio.run(main()) ``` ### Tasks #### Creating Tasks ```python from datetime import datetime, timedelta from ticktick_sdk import TickTickClient async with TickTickClient.from_settings() as client: # Simple task task = await client.create_task(title="Buy groceries") # Task with due date and priority task = await client.create_task( title="Submit report", due_date=datetime.now() + timedelta(days=1), priority="high", # none, low, medium, high ) # Task with tags and content task = await client.create_task( title="Review PR #123", content="Check for:\n- Code style\n- Tests\n- Documentation", tags=["work", "code-review"], ) # Recurring task (MUST include start_date!) task = await client.create_task( title="Daily standup", start_date=datetime(2025, 1, 20, 9, 0), recurrence="RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR", ) # Task with reminder task = await client.create_task( title="Meeting with team", due_date=datetime(2025, 1, 20, 14, 0), reminders=["TRIGGER:-PT15M"], # 15 minutes before ) # All-day task task = await client.create_task( title="Project deadline", due_date=datetime(2025, 1, 31), all_day=True, ) ``` #### Managing Tasks ```python async with TickTickClient.from_settings() as client: # Get a specific task task = await client.get_task(task_id="...") # Update a task task.title = "Updated title" task.priority = 5 # high priority await client.update_task(task) # Complete a task await client.complete_task(task_id="...", project_id="...") # Delete a task (moves to trash) await client.delete_task(task_id="...", project_id="...") # Move task to another project await client.move_task( task_id="...", from_project_id="...", to_project_id="...", ) ``` #### Subtasks ```python async with TickTickClient.from_settings() as client: # Create parent task parent = await client.create_task(title="Main task") # Create child task child = await client.create_task(title="Subtask") # Make it a subtask (parent_id in create is ignored by API) await client.make_subtask( task_id=child.id, parent_id=parent.id, project_id=child.project_id, ) # Remove parent relationship await client.unparent_subtask( task_id=child.id, project_id=child.project_id, ) ``` #### Querying Tasks ```python async with TickTickClient.from_settings() as client: # All active tasks all_tasks = await client.get_all_tasks() # Tasks due today today = await client.get_today_tasks() # Overdue tasks overdue = await client.get_overdue_tasks() # Tasks by tag work_tasks = await client.get_tasks_by_tag("work") # Tasks by priority urgent = await client.get_tasks_by_priority("high") # Search tasks results = await client.search_tasks("meeting") # Recently completed completed = await client.get_completed_tasks(days=7, limit=50) # Abandoned tasks ("won't do") abandoned = await client.get_abandoned_tasks(days=30) # Deleted tasks (in trash) deleted = await client.get_deleted_tasks(limit=50) ``` ### Projects & Folders #### Projects ```python async with TickTickClient.from_settings() as client: # List all projects projects = await client.get_all_projects() for project in projects: print(f"{project.name} ({project.id})") # Get project with all its tasks project_data = await client.get_project_tasks(project_id="...") print(f"Project: {project_data.project.name}") print(f"Tasks: {len(project_data.tasks)}") # Create a project project = await client.create_project( name="Q1 Goals", color="#4A90D9", view_mode="kanban", # list, kanban, timeline ) # Update a project await client.update_project( project_id=project.id, name="Q1 Goals 2025", color="#FF5500", ) # Delete a project await client.delete_project(project_id="...") ``` #### Folders (Project Groups) ```python async with TickTickClient.from_settings() as client: # List all folders folders = await client.get_all_folders() # Create a folder folder = await client.create_folder(name="Work Projects") # Create project in folder project = await client.create_project( name="Client A", folder_id=folder.id, ) # Rename a folder await client.rename_folder(folder_id=folder.id, name="Work") # Delete a folder await client.delete_folder(folder_id="...") ``` ### Tags Tags in TickTick support hierarchy (parent-child relationships) and custom colors. ```python async with TickTickClient.from_settings() as client: # List all tags tags = await client.get_all_tags() for tag in tags: print(f"{tag.label} ({tag.name}) - {tag.color}") # Create a tag tag = await client.create_tag( name="urgent", color="#FF0000", ) # Create nested tag child_tag = await client.create_tag( name="critical", parent="urgent", # Parent tag name ) # Rename a tag await client.rename_tag(old_name="urgent", new_name="priority") # Update tag color or parent await client.update_tag( name="priority", color="#FF5500", ) # Merge tags (move all tasks from source to target) await client.merge_tags(source="old-tag", target="new-tag") # Delete a tag await client.delete_tag(name="obsolete") ``` ### Habits TickTick habits are recurring activities you want to track daily. #### Habit Types | Type | Description | Example | |------|-------------|---------| | `Boolean` | Simple yes/no | "Did you exercise today?" | | `Real` | Numeric counter | "How many pages did you read?" | #### Creating and Managing Habits ```python async with TickTickClient.from_settings() as client: # List all habits habits = await client.get_all_habits() # Boolean habit (yes/no) exercise = await client.create_habit( name="Exercise", color="#4A90D9", reminders=["07:00", "19:00"], target_days=30, encouragement="Stay strong!", ) # Numeric habit reading = await client.create_habit( name="Read", habit_type="Real", goal=30, # 30 pages per day step=5, # +5 button increment unit="Pages", ) # Check in a habit (today) habit = await client.checkin_habit("habit_id") print(f"Streak: {habit.current_streak} days!") # Check in for a past date (backdate) from datetime import date habit = await client.checkin_habit("habit_id", checkin_date=date(2025, 12, 15)) # Archive/unarchive await client.archive_habit("habit_id") await client.unarchive_habit("habit_id") ``` #### Habit Repeat Rules (RRULE Format) | Schedule | RRULE | |----------|-------| | Daily (every day) | `RRULE:FREQ=WEEKLY;BYDAY=SU,MO,TU,WE,TH,FR,SA` | | Weekdays only | `RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR` | | Weekends only | `RRULE:FREQ=WEEKLY;BYDAY=SA,SU` | | X times per week | `RRULE:FREQ=WEEKLY;TT_TIMES=5` | | Specific days | `RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR` | ### Focus/Pomodoro ```python from datetime import date, timedelta async with TickTickClient.from_settings() as client: # Focus heatmap (like GitHub contribution graph) heatmap = await client.get_focus_heatmap( start_date=date.today() - timedelta(days=90), end_date=date.today(), ) # Focus time by tag by_tag = await client.get_focus_by_tag(days=30) for tag, seconds in sorted(by_tag.items(), key=lambda x: -x[1]): hours = seconds / 3600 print(f" {tag}: {hours:.1f} hours") ``` ### User & Statistics ```python async with TickTickClient.from_settings() as client: # User profile profile = await client.get_profile() print(f"Username: {profile.username}") # Account status status = await client.get_status() print(f"Pro User: {status.is_pro}") print(f"Inbox ID: {status.inbox_id}") # Productivity statistics stats = await client.get_statistics() print(f"Level: {stats.level}") print(f"Score: {stats.score}") print(f"Tasks completed today: {stats.today_completed}") ``` ### Error Handling ```python from ticktick_sdk import ( TickTickClient, TickTickError, TickTickNotFoundError, TickTickAuthenticationError, TickTickRateLimitError, TickTickValidationError, ) async with TickTickClient.from_settings() as client: try: task = await client.get_task("nonexistent-id") except TickTickNotFoundError as e: print(f"Task not found: {e}") except TickTickAuthenticationError: print("Authentication failed - check credentials") except TickTickRateLimitError: print("Rate limited - wait and retry") except TickTickValidationError as e: print(f"Invalid input: {e}") except TickTickError as e: print(f"TickTick error: {e}") ``` --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Your Application │ │ (or MCP Server for AI Assistants) │ └─────────────────────────┬───────────────────────────────────┘ │ ┌─────────────────────────▼───────────────────────────────────┐ │ TickTickClient │ │ High-level, user-friendly async API │ │ (tasks, projects, tags, habits, focus, user methods) │ └─────────────────────────┬───────────────────────────────────┘ │ ┌─────────────────────────▼───────────────────────────────────┐ │ UnifiedTickTickAPI │ │ Routes calls to V1 or V2, converts responses │ │ to unified Pydantic models │ └─────────────────────────┬───────────────────────────────────┘ │ ┌──────────────┴──────────────┐ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────┐ │ V1 API │ │ V2 API │ │ (OAuth2) │ │ (Session) │ │ │ │ │ │ • Official API │ │ • Unofficial API │ │ • Project with tasks │ │ • Tags, folders │ │ • Limited features │ │ • Habits, focus │ │ │ │ • Full subtasks │ └──────────────────────┘ └──────────────────────┘ ``` ### Key Design Decisions 1. **V2-First**: Most operations use V2 API (more features), falling back to V1 only when needed 2. **Unified Models**: Single set of Pydantic models regardless of which API provides the data 3. **Async Throughout**: All I/O operations are async for performance 4. **Type Safety**: Full type hints and Pydantic validation --- ## API Reference ### Models | Model | Description | |-------|-------------| | `Task` | Task with title, dates, priority, tags, subtasks, recurrence, etc. | | `Project` | Project/list container for tasks | | `ProjectGroup` | Folder for organizing projects | | `ProjectData` | Project with its tasks (from get_project_tasks) | | `Column` | Kanban column for organizing tasks in boards | | `Tag` | Tag with name, label, color, and optional parent | | `Habit` | Recurring habit with type, goals, streaks, and check-ins | | `HabitSection` | Time-of-day grouping (morning/afternoon/night) | | `HabitCheckin` | Individual habit check-in record | | `HabitPreferences` | User habit settings | | `User` | User profile information | | `UserStatus` | Account status (Pro, inbox ID, etc.) | | `UserStatistics` | Productivity statistics (level, score, counts) | | `ChecklistItem` | Subtask/checklist item within a task | ### Enums | Enum | Values | |------|--------| | `TaskStatus` | `ABANDONED (-1)`, `ACTIVE (0)`, `COMPLETED (2)` | | `TaskPriority` | `NONE (0)`, `LOW (1)`, `MEDIUM (3)`, `HIGH (5)` | | `TaskKind` | `TEXT`, `NOTE`, `CHECKLIST` | | `ProjectKind` | `TASK`, `NOTE` | | `ViewMode` | `LIST`, `KANBAN`, `TIMELINE` | ### Exceptions | Exception | Description | |-----------|-------------| | `TickTickError` | Base exception for all errors | | `TickTickAuthenticationError` | Authentication failed | | `TickTickNotFoundError` | Resource not found | | `TickTickValidationError` | Invalid input data | | `TickTickRateLimitError` | Rate limit exceeded | | `TickTickConfigurationError` | Missing configuration | | `TickTickForbiddenError` | Access denied | | `TickTickServerError` | Server-side error | --- ## Important: TickTick API Quirks TickTick's API has several unique behaviors you should know about: ### 1. Recurrence Requires start_date **If you create a recurring task without a start_date, TickTick silently ignores the recurrence rule.** ```python # WRONG - recurrence will be ignored! task = await client.create_task( title="Daily standup", recurrence="RRULE:FREQ=DAILY", ) # CORRECT task = await client.create_task( title="Daily standup", start_date=datetime(2025, 1, 20, 9, 0), recurrence="RRULE:FREQ=DAILY", ) ``` ### 2. Subtasks Require Separate Call Setting `parent_id` during task creation is **ignored** by the API: ```python # Create the child task first child = await client.create_task(title="Subtask") # Then make it a subtask await client.make_subtask( task_id=child.id, parent_id="parent_task_id", project_id=child.project_id, ) ``` ### 3. Soft Delete Deleting tasks moves them to trash (`deleted=1`) rather than permanently removing them. ### 4. Date Clearing To clear a task's `due_date`, you must also clear `start_date`: ```python task.due_date = None task.start_date = None await client.update_task(task) ``` ### 5. Tag Order Not Preserved The API does not preserve tag order - tags may be returned in any order. ### 6. Inbox is Special The inbox is a special project that cannot be deleted. Get its ID via `await client.get_status()`. --- ## Environment Variables | Variable | Required | Description | |----------|:--------:|-------------| | `TICKTICK_CLIENT_ID` | Yes | OAuth2 client ID from developer portal | | `TICKTICK_CLIENT_SECRET` | Yes | OAuth2 client secret | | `TICKTICK_ACCESS_TOKEN` | Yes | OAuth2 access token (from auth command) | | `TICKTICK_USERNAME` | Yes | Your TickTick email | | `TICKTICK_PASSWORD` | Yes | Your TickTick password | | `TICKTICK_REDIRECT_URI` | No | OAuth2 redirect URI (default: `http://127.0.0.1:8080/callback`) | | `TICKTICK_HOST` | No | API host: `ticktick.com` (default) or `dida365.com` (Chinese) | | `TICKTICK_TIMEOUT` | No | Request timeout in seconds (default: `30`) | | `TICKTICK_DEVICE_ID` | No | Device ID for V2 API (auto-generated) | --- ## Running Tests ```bash # Install dev dependencies pip install -e ".[dev]" # All tests (mock mode - no API calls) pytest # With verbose output pytest -v # Live tests (requires credentials) pytest --live # With coverage pytest --cov=ticktick_sdk --cov-report=term-missing ``` ### Test Markers | Marker | Description | |--------|-------------| | `unit` | Unit tests (fast, isolated) | | `tasks` | Task-related tests | | `projects` | Project-related tests | | `tags` | Tag-related tests | | `habits` | Habit-related tests | | `focus` | Focus/Pomodoro tests | | `pinning` | Task pinning tests | | `columns` | Kanban column tests | | `mock_only` | Tests that only work with mocks | | `live_only` | Tests that only run with `--live` | --- ## Troubleshooting ### "Token exchange failed" - Verify your Client ID and Client Secret are correct - Ensure the Redirect URI matches exactly (including trailing slashes) - Check that you're using the correct TickTick developer portal ### "Authentication failed" - Check your TickTick username (email) and password - Try logging into ticktick.com to verify credentials ### "V2 initialization failed" - Your password may contain special characters - try changing it - Check for 2FA/MFA (not currently supported) ### "Rate limit exceeded" - Wait 30-60 seconds before retrying - Reduce the frequency of API calls --- ## Contributing Contributions are welcome! Please: 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Write tests for new functionality 4. Ensure all tests pass (`pytest`) 5. Run type checking (`mypy src/`) 6. Submit a pull request ### Development Setup ```bash git clone https://github.com/dev-mirzabicer/ticktick-sdk.git cd ticktick-sdk python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` --- ## License MIT License - see [LICENSE](LICENSE) for details. --- ## Acknowledgments - [TickTick](https://ticktick.com) for the excellent task management app - [Model Context Protocol](https://modelcontextprotocol.io/) for the AI integration standard - [FastMCP](https://github.com/jlowin/fastmcp) for the MCP framework - [Pydantic](https://docs.pydantic.dev/) for data validation - [httpx](https://www.python-httpx.org/) for async HTTP