Skip to content
Conduit

mcp server

todocore

Generic CLI-first SQLite todo core (CLI + MCP + generic done-hook)

jensru/todocorecommunitylistedunreviewed

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`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "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, 2026

todocore

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 todo subcommand. Slugs as ids (hallo-welt), legacy TODO-NNN ids 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-run checks without writing, --json returns the findings as data.
  • Built-in operating guide. todo guide (and the todo://guide MCP 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:

  1. TODO_DB_PATH — canonical, wins if set.
  2. ~/.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.

FindingSeverityMeaning
duplicateblocksName almost identical to an open todo. --force overrides.
similar_openwarnsLooks like existing open work, with its id. Enrich instead?
similar_closednoteSimilar to something already done, so not a duplicate.
category_near_dupwarnsNew category very close to a known one.
category_no_workdaywarnsCategory on no workday: never shows up in today.
deadline_without_priowarnsReal deadline, priority below high.
deadline_without_schedulednoteNo date for actually doing it.
checklist_vs_nestingnoteSeveral 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:

  1. Never edit the baseline, and never edit a migration that has shipped.
  2. Append the next number.
  3. Write it defensively (add_column_if_missing), so a re-run is a no-op.
  4. Consumers that guard on schema_version should read todocore.migrations.LATEST rather 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):

EnvBehaviour
TODO_DONE_WEBHOOK=<url>POST JSON {event, todo, external_id, external_system} to the URL.
TODO_DONE_HOOK=module:funcImport 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:

PathTool profile
/mcp/t/<token>/mcpfull
/mcp/lite/t/<token>/mcplite (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.

↑↓ navigate openesc close