donetick-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@donetick-mcpwhat chores are due today?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
donetick-mcp
A stdio MCP server that lets an AI assistant read and manage a self-hosted Donetick instance: chores, recurrence, assignment, completions, activity history, circle members, and projects.
What this is
donetick-mcp runs as a local process, speaks the Model Context Protocol over stdio, and calls your Donetick instance's HTTP API on your behalf. It targets MCP protocol revision 2026-07-28 using the v2 @modelcontextprotocol/server package, and its serveStdio transport defaults to legacy: 'serve', so a client still speaking the older 2025-era initialize handshake works without any extra configuration.
The server exposes twenty tools: five that read, eight that write, and seven that act on a chore's lifecycle. Deleting is the only one that asks the user to confirm before it proceeds. Every tool carries MCP annotations, so a client can tell list_chores from delete_chore before calling either: the five readers are readOnlyHint, and edit_chore, delete_chore, reassign_chore, complete_chore, skip_chore, undo_chore and reject_chore are destructiveHint, with openWorldHint false throughout since every tool talks to one known instance.
Related MCP server: ticktick-mcp-server
Requirements
Bun (version pinned in
.bun-version)A running Donetick instance you can reach over HTTP or HTTPS
A Donetick API token
Docker, for
bun run verify:liveonly. The server itself does not need it.
Setup
Install dependencies:
bun installCopy the example environment file and fill it in:
cp .env.example .envGet an API token from Donetick: open the Donetick web UI, go to Settings, then Access Token, then Generate New Token. Paste it into
DONETICK_TOKEN. This token is not your password; you can revoke it from the same screen.Set
DONETICK_URLto your instance's origin, with no trailing path, for examplehttps://donetick.example.com. Plainhttpis allowed for LAN addresses.Run the server directly to confirm it starts and can reach Donetick:
bun src/index.tsA successful connection is logged to stderr as
donetick-mcp connected to <url>, N chores visible. All diagnostic output goes to stderr; stdout is reserved for the JSON-RPC transport.Bun reads
.envfrom the working directory, not from the script's directory, so this works from the repo root and not from elsewhere. An MCP client launches the process with its own working directory, which is why the configuration below passes the variables inline instead.
Configuration
Variable | Required | Default | Purpose |
| yes | none | Donetick instance origin. Must be an http or https URL with no path, query string, fragment, or credentials. |
| yes | none | API token, sent as the |
| no | the system's local IANA zone | Used when a chore carries no timezone of its own. |
| no |
| How long the chore list, and only the chore list, is cached in memory. |
| no |
| How long circle members and projects are cached in memory, governing how stale a point standing can be. |
| no |
| Per-request timeout in milliseconds. |
See src/config.ts for the exact validation rules; invalid values fail at startup with a specific message rather than a stack trace.
Using it with Claude Code
Add an entry to your MCP configuration, using an absolute path to src/index.ts:
{
"mcpServers": {
"donetick": {
"command": "bun",
"args": ["/absolute/path/to/donetick/src/index.ts"],
"env": {
"DONETICK_URL": "https://donetick.example.com",
"DONETICK_TOKEN": "your-api-token"
}
}
}
}Tools
Reading
Tool | What it does |
| List chores with filters for scope (all, overdue, due today, due this week, due within N days, unscheduled, archived), project, priority, label, assignee, status, a name search, sorting, and a result limit. |
| Fetch one chore in full, by id or by name, including subtasks and last-completion detail. |
| Recent chore completions across the circle, defaulting to the last 7 days and capped at 90. Days are calendar days in your timezone, the same unit |
| Circle members with their roles and point totals. |
| Projects used to group chores. |
Writing
Tool | What it does |
| Create a chore, with recurrence, due date, assignees, priority, points, subtasks, and notification settings. |
| Change any subset of a chore's fields. Everything not passed is preserved. |
| Permanently remove a chore and its history, after confirming with the user. Works on archived chores too. |
| Move a chore's due date, or clear it. |
| Change who a chore is assigned to. |
| Set or clear a chore's priority. |
| Take a chore out of active lists while keeping its history. |
| Put an archived chore back. |
Acting
Tool | What it does |
| Mark a chore done, optionally backdated or on someone else's behalf. Reports a chore that needs approval as pending rather than done. |
| Skip this occurrence and move to the next. Refuses a chore with a running or paused timer, which Donetick answers 200 to and then does nothing about, and a chore with a completion awaiting sign-off, which it would discard along with that person's points. |
| Reverse your own completion. Fails on any instance whose clock is behind UTC: see Known limitations. |
| Approve a completion that is waiting on sign-off. |
| Reject one. |
| Remind whoever the chore is assigned to. |
| Tick or untick one subtask. |
Donetick's data model
Two conventions the tool descriptions above rely on without re-explaining.
Priority is stored as 0..4, and this server treats it as inverted: P1 is the most urgent, P4 is the least, and 0 means no priority is set. Donetick's Go source encodes no ordering anywhere; nothing sorts, compares, or filters priority as a scale, only as a plain integer. The P1-most-urgent convention rests on the naming itself and on Donetick's own project README, which lists its five levels in that order, not on anything the wire enforces. This server always accepts and returns the label, never the bare number, so the convention only decides behavior in one place: list_chores's sort: priority, which ranks P1 first and unset last.
Chore status has four values: no status, in progress, paused, and pending approval (3). A completion recorded at status 3 is a submission waiting on a circle admin's sign-off, not a finished chore. complete_chore reports one as pending_approval rather than completed, skip_chore refuses to touch one so it cannot discard someone else's pending points, and approve_chore / reject_chore are what act on it.
Which Donetick API this uses, and why
This server calls Donetick's internal /api/v1 routes rather than the documented /eapi/v1 external API. The external API's create endpoint accepts a five-field ChoreLiteReq body and hardcodes frequencyType to once, so it has no way to create a recurring chore. The internal routes accept the same secretkey API token through Donetick's MultiAuthMiddleware, so no additional credential is needed.
The tradeoff is not that /api/v1 has no documentation: a swaggo spec ships in docs/swagger.json, with basePath set to /api/v1, and its Swagger UI is mounted by default on a self-hosted instance. The tradeoff is that the spec is stale and wrong in ways that would have produced a server built against the wrong contract. It omits includeSubtasks, syncVersion, actionOptions and draftId entirely; it marks isPrivate required when Donetick accepts its absence; and it states a 0..5 bound on PUT /chores's priority field while missing the 0..4 bound Donetick actually enforces on PUT /chores/:id/priority. It carries no behavioral semantics at all, so nothing in it says a completion window is measured in hours or that a concurrency token has to be sent back verbatim. Following it would have gotten the merge base, the concurrency envelope, and the priority contract wrong, which is why every wire fact here comes from the Go source and from measuring a pinned container instead. Donetick is also on a beta version line where these routes can change release to release. This is mitigated by keeping every path this server calls in one place, src/endpoints.ts, and by a verification script that checks all of them against a Donetick container pinned to a known version.
All 20 tools verified against Donetick v0.1.76 (commit d4eca08), on MCP protocol revision
2026-07-28. Check your own instance with curl -s https://your-host/health, which
returns the version without needing a token. Note that unmatched paths return the
frontend HTML with a 200, so a wrong DONETICK_URL fails by returning a web page
rather than an error. The startup probe checks the response shape for this reason.
Checking the API contract
This server targets Donetick's internal API, whose committed swagger spec is stale and carries no behavioral semantics, so the unit tests prove the code is self-consistent, not that Donetick still behaves as it was read. One command covers the rest:
bun run verify:liveIt needs Docker and nothing else. It starts a Donetick container pinned to the tag
in compose.verify.yaml, signs a throwaway user up over plain HTTP, mints that
user's API token, and exercises 35 contract facts against it. A clean run reports
36 passed: the last is a cleanup assertion, not a contract fact. Scratch chores and
things carry a run-scoped name prefix and are deleted in a finally, so a mid-run
failure leaves nothing behind. It exits non-zero if any check fails, and distinguishes
a warning (something changed but nothing is broken) from a failure.
It never talks to a running instance, and reads no credentials from the
environment: DONETICK_URL and DONETICK_TOKEN in a populated .env cannot point
it at a real account. It does read DONETICK_TZ, this server's own documented
variable, to set the throwaway container's timezone, so a .env that sets it does
change which container comes up, and can flip which direction of the undo_chore
check below actually runs. Checking a newer Donetick means pointing it at a newer
container:
DONETICK_IMAGE_TAG=v0.1.77 bun run verify:liveThe container holds its database in its own writable layer, so nothing persists
and nothing is written into the working tree. bun run verify:up brings it up and
bootstraps it without running any check, which is worth doing once if several runs
follow; bun run verify:down destroys it. verify:live reuses a container that is
already up rather than replacing it, and does not tear one down when it finishes, so
run verify:down when you are done with it. CI runs the type check, the unit suite,
and verify:live on every push and pull request.
The container's timezone is America/New_York rather than UTC on purpose. One
check asserts that undo_chore fails if and only if the server stores timestamps
behind UTC; on a UTC container it would pass for the opposite reason and stop
guarding the diagnosis the tool reports. The other half of that conditional is
measurable rather than assumed:
DONETICK_TZ=UTC bun run verify:liveMeasured on 2026-08-08 against v0.1.76, undo succeeds there and created_at comes
back as ...Z, which is what makes the offset the cause rather than a correlate.
DONETICK_IMAGE_TAG and DONETICK_TZ are both part of the container's recorded
identity, so changing either replaces a running container instead of reusing it.
Known limitations
delete_chore needs a client that supports elicitation. It asks the user to
confirm through the protocol's multi-round-trip flow rather than trusting an input
flag. Before it elicits, this server checks whether the connected client declared
that capability; when it has not, delete_chore returns a plain error result
saying nothing was deleted and naming archive_chore, instead of leaving the call
to fail as a raw protocol error. A client that declares the capability but has its
user decline the prompt gets the same plain "nothing was deleted" result. Every
other tool works regardless. Archiving takes a chore out of active lists while
keeping its history, and asks for no confirmation, but it is available only to the
chore's own creator, the same restriction delete has (see below).
These were verified against a live Donetick instance, not assumed from its source.
A chore whose recurrence is
triggercannot be edited here. Donetick drops a chore's Thing association on every edit and restores it only when the request names the Thing by id, trigger state and condition.GET /chores/:idreturns exactly those three fields on itsthingChore, so this server could rebuild that request; it refuses instead because doing so needs an extra read on every whole-chore write and still could not survive a Thing owned by a different circle member, not because the data is unreachable.edit_choreand every other tool that rewrites a whole chore refuse outright when the stored recurrence istrigger. They do not catch the wider case: a chore with an ordinary recurrence, likedaily, that also has a Thing attached is undetectable here, because Donetick's chore-list query never returns that association, onlyGET /chores/:iddoes, and this server's whole-chore writes read the list. Editing such a chore through this server severs its Thing silently.A completion window requires a due date, and so does an adaptive chore. Donetick reads the due date without checking whether it is there, so a chore with a completion window and no due date can never be completed, and an adaptive one with no due date can never be skipped. A window of
0is not "off": it means no early completion is allowed, refusing a completion strictly before the due instant and allowing one at or after it, the strictest point on the scale rather than a broken one. There is still no way to express "no window at all" with a number, so omit the field oncreate_chorefor no window;edit_choretakescompletion_window: nullto remove an existing one. A rolling chore needs no due date; Donetick schedules the first occurrence from the first completion.Only reminder offsets produce a notification.
notify'sdue_date,completion,predueandnaggingflags are stored and never read, sonotifywithoutreminderssends nothing. Offsets are written to the wire negated, because Donetick adds the value to the due date rather than subtracting it; a positive one would schedule an overdue nag instead of a reminder. At most five per chore.undo_chorefails on any instance whose timezone is behind UTC. Donetick answers "no recent action found" immediately after both a completion and a skip, well inside its own five-minute window. Its handler accepts either action, so the refusal is not skip-specific. The cause is a string comparison:created_atis written in the server's own UTC offset while the cutoff is built in UTC, and SQLite compares the two as text, so on a server behind UTC the stored value always sorts earlier and nothing is ever recent enough. An instance running at UTC or ahead of it is unaffected.verify:liveasserts the conditional directly, failing if undo starts working without the offset changing, or stops working when it has not; its container runs atAmerica/New_Yorkso that the check is exercised in the failing direction.A time of day applies to three recurrence types only. Donetick's scheduler reads
frequency.timeforinterval,days_of_the_weekandday_of_the_month, and ignores it for the other eight. For an hourly interval, the clock resets to that time in UTC before the interval is added, so the chore does not actually run every N hours: when the stored UTC hour plus N is under 24 it freezes on the same instant every completion; when it is 24 or more it instead advances by whole days, silently swapping the requested cadence for a daily or multi-day one. Either way the chore never runs at the configured interval, so this server refuses an hourly interval combined with a time of day at build time, along with a time of day on any of the other eight types; set the hour throughdue_dateinstead, which every type honors.Labels cannot be set or changed through this server, and it cannot list every label in the circle.
/api/v1/labelsrequires JWT session auth an API token cannot provide, so this server can never enumerate a label that exists but is attached to nothing. That auth boundary is not what blocks editing, though: attaching a label to a chore travels through the chore body, which this server's token can already write.create_choreandedit_choresimply expose no labels field to set one through: create always sends an empty list, and edit carries a chore's existing labels through unchanged. Labels already attached to a chore are readable and filterable throughlist_choresandget_chore, and their ids ride on every list row.Removing a chore is creator-only, in every form Donetick offers. Delete, archive and unarchive all compare the chore's
CreatedByfield directly and never check edit permission, so a circle admin can rename, reschedule, reassign, prioritize and complete a chore they did not create, but cannot delete it, archive it, or bring it back from archive. This server cannot warn before trying: nothing in the API it calls returns which member its own token belongs to.The chore list and chore detail views are not supersets of each other.
GET /chores/returns rows that alone carryassignStrategy,assignees,frequency,frequencyMetadata,isRolling,isPrivate,labelsV2,notification,notificationMetadata,points, andrequireApproval.GET /chores/:id/detailsalone carrieslastCompletedDate,lastCompletedBy,totalCompletedCount,notes,duration,startTime, andtimerUpdatedAt.get_chorefetches both and merges them, so last-completion data and every list-only field are present either way.totalCompletedCount,notes,duration,startTimeandtimerUpdatedAtare fetched too but not returned by any tool yet; they sit on the same detail objectget_chorealready holds, so surfacing them is a projection change, not another request.list_choresreads the row alone and cannot report last-completion data at all: rather than anullthat would read as "never completed" for a chore finished minutes ago, it omitslast_completed_atandlast_completed_byand carrieslast_completed_unknownin their place.Activity history rows carry no chore name, only a
choreId.list_activityjoins each row against the current chore list to recover a name. Deleting a chore in Donetick also deletes its history, and the history query filters out rows whose chore is gone, so a genuinely deleted chore's completions never reach this join. The(deleted)label this server prints when the join misses is reachable a different way: a chore completed within the chore list's cache window (10 seconds by default) can appear in the history response before the cached list picks it up, and gets the same label though the chore still exists. Widen or disableDONETICK_CACHE_TTL_MSif this label ever shows up on a chore you know is not deleted.The concurrency token is the row's own stamp, never a clock reading. The endpoints that take an
updatedAtcompare it against the stored row and refuse anything older, andPUT /:id/assigneewrites the value it receives back into the row, so a machine running ahead of the server would stamp a chore with a version its own skew invented.concurrencyTokeninsrc/chore-request.tssends the stored string verbatim, because it carries nanosecond precision aDateround trip truncates downward.Recurring chores drift by an hour across a daylight-saving boundary, for every type except
days_of_the_weekandyearly. Donetick reschedules by adding elapsed time in UTC rather than by the chore's local calendar day, sodaily,weekly,monthly, and anintervalmeasured in days, weeks or months, all drift. A 9am daily chore becomes an 8am one from the first completion after the autumn change.days_of_the_weekholds its time of day across the transition, because it is the one type Donetick reschedules in the chore's own timezone rather than in UTC;yearlydoes not drift because the same date a year later is almost always in the same DST state. This server reports that drift accurately; it does not cause it.A monthly or months-interval chore whose due day exceeds the length of a later month is normalised forward, and holds the new day from then on. Donetick adds a month with Go's date arithmetic, which overflows a short month into the next one: the 31st of August becomes the 1st of October, the 31st of January becomes the 3rd of March. Where it lands depends on how far the day overflows, so the result is the 1st, 2nd or 3rd, not always the 1st, and once it lands there it stays. The break can be several completions away rather than immediate: a chore due the 30th runs cleanly for six months before February catches it, and a 29th survives a leap year's February untouched. This applies to
monthlyand to anintervalmeasured in months.day_of_the_monthclamps to the last day of the month instead of overflowing, and is the right choice for a fixed calendar day.
AI Disclosure
Built with Claude Code. I directed and reviewed the architecture and data flow. I've skimmed the code to confirm that my rules and direction was properly enforced.
Donetick's /api/v1 has a swagger spec, but it is stale and carries no behavioral semantics, so the wire contracts were worked out by a fleet of Claude Code agents from the Go source and by measuring a pinned container. bun run verify:live is used for verifying the contracts are still accurate.
Expect that there could be edge cases that are handled wrong. There is a chance for data loss. For example, Donetick has no partial update so edit_chore rewrites the whole chore. Any field this server gets wrong it overwrites. There are guards and tests for that, but covering every permutation and possibility is not realistic.
Please keep backups. I auto snapshot the LXC my Donetick runs in just in case I need to revert.
Development
See CLAUDE.md for the operating rules this codebase follows and the full command list.
bun run typecheck # tsc --noEmit
bun test --isolate # the unit suite: no network, no wall clock, no sleeps
bun run verify:live # the wire contract, against a disposable containerLicense
Apache-2.0. See LICENSE.
This project is not affiliated with or endorsed by Donetick.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceMCP server for Donetick chores management. Provides 27 tools to control Donetick instance via Claude or other MCP clients.272MIT
- AlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server that connects AI assistants to your TickTick tasks, enabling task management through natural language.81MIT
- AlicenseAqualityDmaintenanceMCP server for Donetick chores management, enabling AI assistants to list, create, complete, update, and delete chores with full API integration and rate limiting.209MIT
- Alicense-qualityBmaintenanceAn MCP server that connects AI assistants to TickTick, enabling project and task management through natural language, including reading projects, finding tasks, creating tasks, and completing work.2MIT
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.
MCP server for Argo RPG Platform — connects AI assistants to campaign data via OAuth2
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/jaredglaser/donetick-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server