TickTick任务管理MCP服务器

为MCP客户端提供TickTick任务与项目管理能力,支持创建、更新、完成和删除任务,并涵盖子任务、优先级、标签、提醒与重复设置,适用于AI助手进行个人或团队任务管理。

  • by nikitatsym
  • Python
  • 本地部署
  • 更新于
在 GitHub 查看 nikitatsym/ticktick-mcp

使用说明

TickTick MCP Server

MCP server for TickTick task manager. Create, update, complete, and delete tasks and projects with support for subtasks, priorities, tags, reminders, and recurrence — from Claude, MetaMCP, or any MCP client.

Features

  • Tasks, projects, and the Inbox
  • Risk-graded meta-tools (ticktick_read / ticktick_write / ticktick_delete) — agents pick a tool surface by side-effect kind
  • Pydantic param validation with extra='forbid' — unknown keys, missing required fields, and wrong types fail loudly
  • Per-op operation='schema' returning a full JSON Schema; operation='help' with substring search (params={'search':'X'})
  • Brief enforcement: writes require a <brief>summary</brief> tag (or a brief parameter that injects it) so list views stay scannable
  • Fail-fast on the API's silent-drop trap: isAllDay must be passed explicitly when changing reminders/startDate/dueDate
  • Liveness guard on writes - UpdateTask/CompleteTask/MoveTask refuse a task that is provably in TickTick's trash (writes to trashed tasks silently vanish); completed tasks cannot be verified and pass unchecked
  • Explicit timezone contract - any operation where a zone matters takes an explicit timeZone parameter; no env or system fallback
  • Zero-config install via uvx

Quick Start

Go to the setup page — it walks you through creating a TickTick app, authorizing, and generating the ready-to-paste MCP config.

Works with Claude Desktop, MetaMCP, Cursor, and any MCP client that supports stdio servers. For Claude Code global config on macOS: ~/.claude.json → "mcpServers".

Manual config

{
  "mcpServers": {
    "ticktick": {
      "command": "uvx",
      "args": ["--refresh", "--extra-index-url", "https://nikitatsym.github.io/ticktick-mcp/simple", "ticktick-mcp"],
      "env": {
        "TICKTICK_CLIENT_ID": "YOUR_CLIENT_ID",
        "TICKTICK_CLIENT_SECRET": "YOUR_CLIENT_SECRET",
        "TICKTICK_ACCESS_TOKEN": "YOUR_ACCESS_TOKEN"
      }
    }
  }
}

Requires uv.

HTTP

ticktick-mcp --http serves streamable HTTP at http://127.0.0.1:8000/mcp (--host, --port) instead of stdio, same environment variables. No authentication: put a gateway in front.

The package can also be imported: mcp, Settings, TickTickClient, and client_var (a ContextVar the host sets per request) let one process serve several instances.

Configuration

Variable Required Default Description
TICKTICK_CLIENT_ID Yes — OAuth client id from developer.ticktick.com.
TICKTICK_CLIENT_SECRET Yes — OAuth client secret.
TICKTICK_ACCESS_TOKEN Yes — Bearer token issued by the OAuth flow. The setup page generates this for you.
MCP_TICKTICK_BRIEF_MAX No 100 Max length of the <brief> tag. 0 disables enforcement entirely.

The server checks the token at startup with one authenticated request (GET /project) and refuses to start if it is missing or rejected, printing the failing request. A broken credential never waits for the first tool call.

Tool Groups

Operations are exposed through risk-graded meta-tools — one tool surface per scope, dispatched via operation + params.

Meta-tool Scope Operations
ticktick_read GET, safe / read-only GetToday, GetInbox, GetInboxId, ListProjects, GetProject, GetProjectWithData, GetTask
ticktick_write Create + update CreateTask, UpdateTask, MoveTask, CompleteTask, CreateProject, UpdateProject
ticktick_delete Destructive DeleteTask, DeleteProject

MoveTask moves a task between projects as copy+delete (the API has no move); the copy gets a new id, returned in the result.

GetToday takes a required timeZone - "today" is computed in that zone.

Each meta-tool takes operation (PascalCase op name, or help / schema) plus a params dict:

ticktick_read(operation="help")                                                  # list every op in this group
ticktick_read(operation="help", params={"search": "today"})                      # filter by substring; surfaces cross-group hits
ticktick_read(operation="schema", params={"op": "GetTask"})                      # full JSON Schema for one op
ticktick_read(operation="GetTask", params={"projectId": "p1", "taskId": "t1"})   # invoke

ticktick_write(operation="CreateTask", params={"title": "Buy milk", "brief": "Buy milk"})
ticktick_delete(operation="DeleteTask", params={"projectId": "p1", "taskId": "t1"})

Params are validated strictly via Pydantic: unknown keys, wrong types, and missing required fields fail the tool call with field-level detail and a pointer to operation='schema'. Every expected failure (bad params, unknown operation, TickTick API error, transport failure) comes back as a failed tool result carrying its cause, with credentials and query values redacted.

Important semantics

Brief enforcement

When MCP_TICKTICK_BRIEF_MAX > 0 (the default), every CreateTask / UpdateTask body must carry a <brief>one-line summary</brief> tag. Two equivalent ways to satisfy it:

// (a) pass the brief parameter — auto-injected into content
{"title": "Q1 plan", "brief": "Outline Q1 OKRs"}

// (b) include the tag in content yourself
{"title": "Q1 plan", "content": "<brief>Outline Q1 OKRs</brief>\nFull notes…"}

Set MCP_TICKTICK_BRIEF_MAX=0 to disable the requirement and the length cap.

UpdateTask with brief alone (no content) preserves the existing body — the wrapper fetches the current task, rewrites only the <brief> tag, and sends the rest back unchanged.

Timezone semantics

When a task date carries a time of day (YYYY-MM-DDTHH:MM[:SS]) the wrapper needs an IANA timezone to normalize it, and GetToday needs one to compute day boundaries. There is exactly one source: the timeZone parameter of the call. There is no environment or system fallback - a missing zone is an error, so every applied zone is an explicit decision by the caller. Date-only values (YYYY-MM-DD) are all-day and need no zone. Manual UTC offsets (+0300, Z) are rejected - pass local time plus timeZone.

isAllDay and reminder triggers

TickTick reminder triggers are iCal duration strings whose shape depends on whether the task is all-day or timed:

Task mode Trigger shape Example
All-day (isAllDay=true) positive offset from midnight TRIGGER:PT9H = 9am
Timed (isAllDay=false) negative offset from event TRIGGER:-PT5M = 5min before

UpdateTask requires isAllDay to be passed explicitly when changing reminders, startDate, or dueDate — the TickTick API silently drops isAllDay if you omit it on a partial update, which causes the mode (and therefore the reminder interpretation) to flip. The wrapper rejects the call with an error before it reaches the API.

CreateTask infers isAllDay from the date shape: date-only (YYYY-MM-DD) implies all-day; date+time implies timed. Pass isAllDay explicitly to override or when there's no date.

Development

python3 dev.py check   # lint (ruff, mypy, tackbox) + tests; the pre-commit and CI gate
python3 dev.py lint
python3 dev.py test
python3 dev.py hook    # install the tracked pre-commit hook

src/ is type-checked under mypy --strict. The two intentionally dynamic surfaces (server.py::_build_params_model, _coerce_call) use explicit typed escape hatches (Any, Callable[..., Any], cast, TypeAlias); no # type: ignore is allowed.

License

MIT — GitHub