Skip to main content
Glama
jaredglaser

donetick-mcp

by jaredglaser

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:live only. The server itself does not need it.

Setup

  1. Install dependencies:

    bun install
  2. Copy the example environment file and fill it in:

    cp .env.example .env
  3. Get 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.

  4. Set DONETICK_URL to your instance's origin, with no trailing path, for example https://donetick.example.com. Plain http is allowed for LAN addresses.

  5. Run the server directly to confirm it starts and can reach Donetick:

    bun src/index.ts

    A 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 .env from 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

DONETICK_URL

yes

none

Donetick instance origin. Must be an http or https URL with no path, query string, fragment, or credentials.

DONETICK_TOKEN

yes

none

API token, sent as the secretkey header on every request.

DONETICK_TZ

no

the system's local IANA zone

Used when a chore carries no timezone of its own.

DONETICK_CACHE_TTL_MS

no

10000

How long the chore list, and only the chore list, is cached in memory. 0 disables caching.

DONETICK_MEMBER_CACHE_TTL_MS

no

300000

How long circle members and projects are cached in memory, governing how stale a point standing can be. 0 disables caching. Independent of DONETICK_CACHE_TTL_MS.

DONETICK_TIMEOUT_MS

no

15000

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

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.

get_chore

Fetch one chore in full, by id or by name, including subtasks and last-completion detail.

list_activity

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 list_chores uses. Returns {activity, total, truncated}, capped by limit (default 200, max 1000), the same shape and pattern list_chores uses for its own {chores, total, truncated}.

list_members

Circle members with their roles and point totals.

list_projects

Projects used to group chores.

Writing

Tool

What it does

create_chore

Create a chore, with recurrence, due date, assignees, priority, points, subtasks, and notification settings.

edit_chore

Change any subset of a chore's fields. Everything not passed is preserved.

delete_chore

Permanently remove a chore and its history, after confirming with the user. Works on archived chores too.

reschedule_chore

Move a chore's due date, or clear it.

reassign_chore

Change who a chore is assigned to.

set_priority

Set or clear a chore's priority.

archive_chore

Take a chore out of active lists while keeping its history.

unarchive_chore

Put an archived chore back.

Acting

Tool

What it does

complete_chore

Mark a chore done, optionally backdated or on someone else's behalf. Reports a chore that needs approval as pending rather than done.

skip_chore

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.

undo_chore

Reverse your own completion. Fails on any instance whose clock is behind UTC: see Known limitations.

approve_chore

Approve a completion that is waiting on sign-off.

reject_chore

Reject one.

nudge_chore

Remind whoever the chore is assigned to.

set_subtask_completed

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:live

It 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:live

The 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:live

Measured 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 trigger cannot 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/:id returns exactly those three fields on its thingChore, 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_chore and every other tool that rewrites a whole chore refuse outright when the stored recurrence is trigger. They do not catch the wider case: a chore with an ordinary recurrence, like daily, that also has a Thing attached is undetectable here, because Donetick's chore-list query never returns that association, only GET /chores/:id does, 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 0 is 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 on create_chore for no window; edit_chore takes completion_window: null to 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's due_date, completion, predue and nagging flags are stored and never read, so notify without reminders sends 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_chore fails 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_at is 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:live asserts the conditional directly, failing if undo starts working without the offset changing, or stops working when it has not; its container runs at America/New_York so that the check is exercised in the failing direction.

  • A time of day applies to three recurrence types only. Donetick's scheduler reads frequency.time for interval, days_of_the_week and day_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 through due_date instead, which every type honors.

  • Labels cannot be set or changed through this server, and it cannot list every label in the circle. /api/v1/labels requires 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_chore and edit_chore simply 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 through list_chores and get_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 CreatedBy field 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 carry assignStrategy, assignees, frequency, frequencyMetadata, isRolling, isPrivate, labelsV2, notification, notificationMetadata, points, and requireApproval. GET /chores/:id/details alone carries lastCompletedDate, lastCompletedBy, totalCompletedCount, notes, duration, startTime, and timerUpdatedAt. get_chore fetches both and merges them, so last-completion data and every list-only field are present either way. totalCompletedCount, notes, duration, startTime and timerUpdatedAt are fetched too but not returned by any tool yet; they sit on the same detail object get_chore already holds, so surfacing them is a projection change, not another request. list_chores reads the row alone and cannot report last-completion data at all: rather than a null that would read as "never completed" for a chore finished minutes ago, it omits last_completed_at and last_completed_by and carries last_completed_unknown in their place.

  • Activity history rows carry no chore name, only a choreId. list_activity joins 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 disable DONETICK_CACHE_TTL_MS if 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 updatedAt compare it against the stored row and refuse anything older, and PUT /:id/assignee writes 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. concurrencyToken in src/chore-request.ts sends the stored string verbatim, because it carries nanosecond precision a Date round trip truncates downward.

  • Recurring chores drift by an hour across a daylight-saving boundary, for every type except days_of_the_week and yearly. Donetick reschedules by adding elapsed time in UTC rather than by the chore's local calendar day, so daily, weekly, monthly, and an interval measured 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_week holds 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; yearly does 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 monthly and to an interval measured in months. day_of_the_month clamps 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 container

License

Apache-2.0. See LICENSE.

This project is not affiliated with or endorsed by Donetick.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

–Maintainers
–Response time
–Release cycle
–Releases (12mo)
Commit activity

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

View all related MCP servers

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

View all MCP Connectors

Latest Blog Posts

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