Skip to main content
Glama
README.md
# todo-mcp

An MCP server whose store is a `TODO.md` you can read, edit and diff by hand. Writes are
byte-range splices, so the file stays yours: hand-authored tables, tab indentation and any
prose outside a task are never re-serialised.

Speaks stdio for MCP clients and Streamable HTTP for everything else.

## Credit

Based on [CalamityAdam/mcp-todo](https://github.com/CalamityAdam/mcp-todo), which supplied
the original scaffold: the `createTodoMcpServer` factory shape, the Express Streamable HTTP
wrapper and the session handling.

Almost nothing else survives. That version kept todos as numbered records in a JSON blob at
`~/.mcp-todos.json` with three tools over `{ id, title, done }`. This one replaces the store
with a markdown document, swaps numeric ids for slugs, and grows the tool surface to seven
with statuses, areas, reference breadcrumbs, dated log notes, full-text query and
duplicate detection. The two projects no longer share an implementation.

Upstream ships no `LICENSE` file; its `package.json` declares ISC, which is what this repo
carries forward.

## Install

Run it straight from GitHub, no clone:

```bash
npx github:adrianhardy/todo-mcp
```

After pushing any changes, use `npx --ignore-existing
github:adrianhardy/todo-mcp` to pick them up.

For regular use, install once and forget about it:

```bash
npm i -g github:adrianhardy/todo-mcp
todo-mcp
```

Either route builds from source on install via the `prepare` script, so `dist/` is never
committed. Node 20 or newer.

## Usage

`todo-mcp` starts the **HTTP** server by default, because that is the useful thing to do
when a person runs it in a terminal. Set `MCP_STDIO=1` to speak stdio instead, which is
what an MCP client spawning it as a subprocess wants.

### With an MCP client

```json
{
  "mcpServers": {
    "todo": {
      "command": "npx",
      "args": ["-y", "github:adrianhardy/todo-mcp"],
      "env": { "MCP_STDIO": "1" }
    }
  }
}
```

Installed globally, that becomes `"command": "todo-mcp"` with the same `env` block.

**The working directory decides which file you get.** `TODO_FILE` resolves against the
process's cwd and defaults to `TODO.md`, so a client launched in a project edits that
project's todo list. Set `TODO_FILE` to an absolute path if you want one shared list
regardless of where the server starts.

### Over HTTP

```bash
PORT=8080 TODO_MCP_TOKEN=$(openssl rand -hex 32) todo-mcp
```

- `POST /mcp` - JSON-RPC requests
- `GET /mcp` - SSE stream for server notifications
- `DELETE /mcp` - end the session

Setting `TODO_MCP_TOKEN` requires `Authorization: Bearer <token>` on all three. Leaving it
unset disables authentication, which is fine on localhost and nowhere else.

### Configuration

| variable | default | meaning |
|---|---|---|
| `TODO_FILE` | `TODO.md` | store path, resolved against cwd |
| `MCP_STDIO` | unset | `1` selects stdio instead of HTTP |
| `PORT` | `3000` | HTTP port |
| `TODO_MCP_TOKEN` | unset | bearer token; unset means no auth |

A `.env` file is read if present. See `.env.example`.

## Storage

`TODO.md` is the store, not a JSON blob. The file is the record: readable, editable by
hand, and diffable in git.

A task is a `## ` section. Fields the server owns live in a comment block directly under
the heading; everything below it is prose the human owns.

```markdown
## Feature Idea version two: the new widget which tracks things

<!-- todo
id: feature-idea-version-two
area: inventory
status: next
refs: [./src/do_stuff.ts, ClassName.Method, OtherClassName]
created: 2026-08-19
updated: 2026-08-22
-->

**Next step:** close the ledger. ClassName.Method uses 0.25 and it needs 17.2%.

**Already known:** ...

### Log

- 2026-08-22 Slab_Wall_1x3 not started; parade places 24 of those to every 6 of the 3x3.
```

Ids are slugs, not numbers, so they survive reordering and deletion. File order is
priority order, which is why there is no priority field.

Writes are **byte-range splices**: a mutation rewrites only the span it owns. Hand-authored
tables, tab indentation and any prose outside a task section are never re-serialised, so
they cannot be reflowed or lost. Handlers are serialised through a lock, because two
interleaved read-modify-write cycles would splice against offsets that no longer describe
the file.

## Design

- **list is abbreviated** `list_todos` returns a one-line index and never task bodies. 
  `get_todo` returns one whole section. `q` or `ref` is the expected path; listing 
   everything is the exception.
- **Capture takes one field.** Only `title` is required, and new tasks default to status
  `captured`. A tool that demands an area and a next step at the moment something is noticed
  does not get used, and the file only earns its keep if things get written down as they are
  found. Triage moves `captured` to `open`/`next`/`parked`/`someday` later.

## Status values

| status | meaning |
|---|---|
| `captured` | raw, untriaged. The default for a new task. Hidden from unfiltered lists |
| `open` | real work, understood |
| `next` | up now |
| `parked` | deliberately deferred; the body says why |
| `someday` | aspirational |
| `done` | finished. Stays in the file for the record. Hidden from unfiltered lists |

## Tools

- `list_todos` - abbreviated index; filters `area`, `status`, `ref`, `q`, `limit`
- `get_todo` - full markdown of one task, body included
- `add_todo` - capture a task; only `title` required. Reports possible duplicates
- `update_todo` - change any field; only what is passed is rewritten
- `append_note` - add a dated bullet to a task's log
- `set_status` - move a task through triage
- `remove_todo` - delete a task and its prose. Prefer `set_status done`

## Resources

- `todos://list` - one-line index of open tasks

## Architecture

- `src/todo.ts` - the markdown store: parsing, byte-range patching, query, dedup
- `src/server.ts` - MCP tool surface. Pure factory, no side effects on import
- `src/http.ts` - Streamable HTTP transport, auth and session map
- `src/cli.ts` - the `todo-mcp` binary; picks a transport and starts it