mcp server
todocore
Generic CLI-first SQLite todo core (CLI + MCP + generic done-hook)
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
{
"mcpServers": {
"todocore": {
"command": "uvx",
"args": [
"todocore"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 28, 2026todocore
A generic, CLI-first todo core backed by SQLite — with a thin MCP server and a transport-agnostic done-hook. todocore is the OSS core extracted from a larger personal-assistant setup; it carries no consumer-specific logic (no CRM, no dashboards). Those live in consumers that build on top of it.
Features
- CLI-first. Every action is a
todosubcommand. Slugs as ids (hallo-welt), legacyTODO-NNNids still resolve. - SQLite backend. Single file, WAL mode, FTS5 full-text search. The schema is created and upgraded on first use through numbered migrations — point it at a fresh path and it just works.
- Parent/child nesting (one level deep), repeats, deadlines vs. scheduled dates, categories, duplicate detection.
- Intake checks. The tool knows what makes a good todo instead of leaving it
to documentation: a near-identical name is refused, similar open todos are
reported so you can enrich instead of duplicating, and category and convention
problems come back as hints.
--dry-runchecks without writing,--jsonreturns the findings as data. - Built-in operating guide.
todo guide(and thetodo://guideMCP resource) serve the conventions from inside the package, translated, so a client never has to guess them. - Numbered migrations. One owner for the schema; a fresh database and an upgraded one provably end up identical.
- Generic done-hook. On
done, todocore fires a best-effort event so an external system (a CRM, a webhook, your own code) can react. The core itself stays agnostic. - MCP server (optional extra) so MCP clients (Claude Desktop, Claude Code) can drive it.
- i18n. English by default; German status/priority labels via
TODO_LANG=de.
Install
# from PyPI-style install via pipx (recommended once published):
pipx install todocore
# from source:
pip install -e .
# with the MCP server:
pip install -e ".[mcp]"
This installs two equivalent console scripts: todo and todocore.
Quickstart
todo add "Write the README" --prio high --tag docs
todo list
todo today
todo done write-the-readme
todo search "readme"
todo guide # how to use this well: conventions + what it checks
todo add "Write the docs" --dry-run # check without writing
Database path
The DB path is resolved in this order:
TODO_DB_PATH— canonical, wins if set.~/.local/share/todocore/todos.db— default.
The parent directory is created on demand.
Intake checks
Creating a second todo for work that already exists is the expensive mistake, so the tool checks instead of trusting a convention document. One rule refuses the write, the rest are advisory. Findings go to stderr, so a script parsing stdout is unaffected.
| Finding | Severity | Meaning |
|---|---|---|
duplicate | blocks | Name almost identical to an open todo. --force overrides. |
similar_open | warns | Looks like existing open work, with its id. Enrich instead? |
similar_closed | note | Similar to something already done, so not a duplicate. |
category_near_dup | warns | New category very close to a known one. |
category_no_workday | warns | Category on no workday: never shows up in today. |
deadline_without_prio | warns | Real deadline, priority below high. |
deadline_without_scheduled | note | No date for actually doing it. |
checklist_vs_nesting | note | Several checklist items: would nesting fit better? |
todo add "Name" --dry-run # run the checks, write nothing (exit 1 = would block)
todo add "Name" --json # findings as data, including rendered text
todo add "Name" --force # create despite a duplicate (still reported)
The rules live in todocore/intake.py as pure functions over an IntakeContext.
They return findings carrying a message KEY plus params, never finished prose, so
the language is a property of the adapter (TODO_LANG) and the same finding
renders in any language. todo guide explains the reasoning behind each rule; it
ships inside the package (GUIDE.en.md, plus translations) and is served over MCP
as todo://guide, so tool behaviour and instructions cannot drift apart.
Schema and migrations
The schema has exactly one owner: todocore/migrations.py. Migration 1 is the
frozen baseline; every later change is a new numbered migration, and the applied
number is recorded in the schema_version table. Migrations run on every connect
and are idempotent.
Rules for changing the schema:
- Never edit the baseline, and never edit a migration that has shipped.
- Append the next number.
- Write it defensively (
add_column_if_missing), so a re-run is a no-op. - Consumers that guard on
schema_versionshould readtodocore.migrations.LATESTrather than hardcoding a number.
A fresh database and an upgraded one must end up with the same schema;
tests/test_migrations.py asserts it.
Done-hook
When a todo is completed, todocore fires a best-effort done event. Resolution
order (first match wins; a hook failure never rolls back the done):
| Env | Behaviour |
|---|---|
TODO_DONE_WEBHOOK=<url> | POST JSON {event, todo, external_id, external_system} to the URL. |
TODO_DONE_HOOK=module:func | Import module, call func(conn=, todo=, args=) in-process. |
| (neither set) | no-op. |
Link a todo to an external system with --external-id X (and optionally
--external-system Y). --crm X is a generic shorthand for
--external-id X --external-system crm. The --crm-days / --crm-followup
flags on done are passed through to the hook, which decides whether to use
them.
Recurring todos
A todo can repeat. Set an interval on add or update:
todo add "Weekly review" --repeat weekly --scheduled 2026-06-29
todo update weekly-review --repeat daily # change interval
todo update weekly-review --repeat "" # turn off (one-shot again)
Intervals are daily | weekly | monthly. The model is a treadmill, not a
calendar/cron: when you done a recurring todo, the next instance is created
automatically. Its date anchors on the original's scheduled (or deadline) and
moves forward one period; a recurring todo with no date produces a dateless copy.
Marking the same todo done twice does not duplicate the next instance.
Note: recurrence is reachable via the CLI (and any UI that wraps it). The optional
MCP server does not yet expose the repeat field.
MCP server
pip install -e ".[mcp]"
python -m todocore.todos_mcp
It is a thin layer: it only shells out to the todocore.todo CLI, never writing
to the DB directly. Default transport is stdio (Claude Desktop, Claude Code);
set TODO_MCP_TRANSPORT=streamable-http to run it as a server for remote
connectors (TODO_MCP_HOST / TODO_MCP_PORT, default 127.0.0.1:8766).
Auth and multi-user
todocore/auth.py adds owner-scoped API tokens on top of the owner-scoped
storage (see "Database path" and the owner-columned tables): a token
belongs to one owner, and every CLI call made through it only ever sees that
owner's todos. Mint one from the CLI:
python -m todocore.auth create-token --owner <owner> --label <label>
This prints the plaintext token once (prefixed tdo_); only its hash is
stored. In streamable-http mode, a client authenticates either with a
standard Authorization: Bearer <token> header, or — for connectors that
cannot set custom headers — by carrying the same token in the URL path:
| Path | Tool profile |
|---|---|
/mcp/t/<token>/mcp | full |
/mcp/lite/t/<token>/mcp | lite (a smaller tool set for token-constrained chat clients) |
Either form resolves to the token's owner exactly the same way. An
unresolvable token is a 401. TODO_MCP_ALLOW_ANONYMOUS_LOCAL (default 1)
controls whether a request with no token at all falls back to a local,
unauthenticated owner — set it to 0 to require a valid token on every
request.
Tests
pip install -e ".[dev]"
pytest tests/ -q
157 tests, no network, no external services; each test runs against its own temporary SQLite file.
Consumers
todocore is the engine; consumers build their UX and integrations on top of it:
a CLI alias, a web UI, a personal agent environment, a cron job. todocore itself
stays consumer-agnostic, that's the point: it knows nothing about what calls it,
beyond the extension points it exposes (TODO_DONE_HOOK and friends).
Status
Personal project, maintained as used. Issues welcome, no support promise.
Author
Jens Rusitschka — https://github.com/jensru/todocore
License
MIT.