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

An MCP server that lets Claude (claude.ai, the Claude mobile and desktop apps, Claude Code) read
and edit your [Things 3](https://culturedcode.com/things/) to-dos. It talks to Things Cloud
directly, the sync service the Things apps use, so it works with none of your devices switched on,
and changes reach them like any other edit.

It isn't affiliated with Cultured Code. Things Cloud has no public API: this relies on a protocol
learned from the apps' own traffic, which can change without notice. Writes copy the exact shapes
Things 3.24 sends, because Things trusts its sync history completely and a malformed write can
break sync on every device. Use it at your own risk.

## Tools

| Tool | What it does |
|---|---|
| `overview` | Today's date, Inbox/Today/Upcoming counts, areas with their projects, tags |
| `list_todos` | Inbox, Today, Upcoming, Anytime, Someday, Logbook or Trash, filtered by project, area or tag; the Logbook takes a date range |
| `search_todos` | Title and notes search |
| `get_todo` | Notes, checklist, dates; a project's to-dos under their headings |
| `create_todo` | With notes, schedule, deadline, reminder, tags, checklist, and optionally a repeat rule |
| `create_project`, `create_heading`, `create_tag`, `create_area` | |
| `update_todo` | Title, notes, schedule (`when` or `shift_days`), deadline, reminder, tags, project/heading/area |
| `update_todos` | The same change on up to 200 to-dos in one commit, all or nothing |
| `set_repeat` | Make a to-do repeat, change its rule, or stop it |
| `set_status` | Complete, cancel or reopen (a project's open to-dos follow) |
| `trash` | Move to the Trash or restore; nothing is ever deleted permanently |
| `add_checklist_items`, `update_checklist_item` | |
| `set_timezone` | Which day "today" is; Things uses each device's local date |

Repeat rules cover what Things' Repeat dialog offers: daily, weekly on chosen days, monthly (a
day, the last day, or "2nd Tue"), yearly, every N, after completion, until a date or N times,
deadlines with an early start, and a reminder.

## How Things Cloud works

- Each account has a **history key**. `GET /version/1/history/{key}/items?start-index=N` returns
  the history as a list of commits; `POST /version/1/history/{key}/commit?ancestor-index=N`
  appends one, and answers 409 if another device committed first. The password is only needed
  once, to look the key up (`GET /version/1/account/{email}` with `Authorization: Password …`).
- Pages are capped at about 58 KB. `current-item-index` is the head of the history, not the end
  of the page: the next page starts at `start-index + items.length`.
- A commit maps object ids to `{t, e, p}`: the action (0 create, 1 modify, 2 delete), the entity
  (`Task7`, `ChecklistItem3`, `Area3`, `Tag4`, `Tombstone2`) and the changed properties. The
  server replays the history into the current state, and keeps a snapshot on disk.
- Ids are 16 random bytes in Base58 with leading-zero padding. Things.app crashes on any id that
  doesn't decode to exactly 16 bytes, so every id is checked before a write.
- Calendar days are stored as midnight UTC of the user's local date.

[`src/model.ts`](src/model.ts) documents the task properties and
[`src/repeat.ts`](src/repeat.ts) the repeat rule encoding (days, weekdays and months there are
zero-based). Each repeat shape was captured by setting the rule up in Things' Repeat dialog and
reading back the commit it wrote.

## Safety rules

[`src/writes.ts`](src/writes.ts) only writes what was verified against Things for Mac:

- creates carry the same 34 properties Things sends;
- start state, start date and Today date always agree; projects and headings never go to the
  Inbox;
- each copy of a repeating to-do is an ordinary to-do. Templates only change the way the Repeat
  dialog changes them, plus renames, moves, reminders and trash. Devices create the copies
  themselves when their date arrives;
- nothing is ever deleted permanently. Making a to-do repeat moves the original to the Trash,
  where Things itself would delete it.

## Setup

You need Node 24 and a Things Cloud account. The scripts are for macOS, where secrets live in
the Keychain.

1. Look up your history key and save it in the Keychain (the password is sent once and never
   stored):

   ```sh
   npm install
   scripts/things-login.sh
   ```

2. Try it against your data without any server:

   ```sh
   node scripts/call.ts --list
   node scripts/call.ts list_todos '{"view":"today"}'
   ```

### Hosting it for Claude

The server speaks MCP over HTTP behind a small OAuth sign-in: approving a client takes a
passphrase only you have, and sign-ins are only handed back to Claude (claude.ai, claude.com) or
to localhost. With [flyctl](https://fly.io/docs/flyctl/install/) installed and logged in:

1. `cp fly.example.toml fly.toml`, pick an app name, and set `PUBLIC_URL` to where it will be
   served (`https://<app>.fly.dev`, or your own domain with `flyctl certs add`) and `THINGS_TZ` to
   your time zone. `fly.toml` is ignored by git.
2. `flyctl apps create <app>` and `flyctl volumes create things_data --region cdg --size 1`.
3. `scripts/setup-hosted.sh`: creates the passphrase and a token-signing secret in your Keychain
   and sends them, with your history key, to fly as secrets, without printing any of them.
4. `flyctl deploy --remote-only` (also after code changes).
5. On claude.ai: Settings → Connectors → Add custom connector, with your `PUBLIC_URL` followed by
   `/mcp`. When the sign-in page asks, paste the passphrase:
   `security find-generic-password -s things-mcp-passphrase -w | pbcopy`

To sign every client out, delete the Keychain item `things-mcp-auth-secret` and rerun step 3.

## Configuration

| Variable | Default | Purpose |
|---|---|---|
| `THINGS_HISTORY_KEY` | – | Things Cloud history key |
| `PUBLIC_URL` | `http://localhost:$PORT` | Where the server is reachable |
| `MCP_PASSPHRASE` | – | Approves clients on the sign-in page (at least 12 characters) |
| `AUTH_SECRET` | – | Signs sign-ins (at least 32 characters) |
| `THINGS_TZ` | `UTC` | Time zone that decides "today"; `set_timezone` overrides it |
| `OAUTH_REDIRECT_HOSTS` | `claude.ai,claude.com` | Hosts allowed to receive sign-ins (loopback always is) |
| `DATA_DIR` | – | Where to keep the history snapshot and the time zone setting |
| `PORT` | `8080` | HTTP port |

## Development

```sh
npm test                      # unit tests (ids, history replay, notes, repeat rules)
npm run check                 # type check
BASE=http://localhost:8080 PASSPHRASE=… node scripts/oauth-smoke.ts
```

`oauth-smoke.ts` walks a local server through the whole sign-in and an MCP call. The code runs
on Node's built-in TypeScript support, without a build step.

## Limits

- Things for Mac pulls changes lazily in the background (1–3 minutes in testing); opening the
  app syncs right away.
- Repeating to-dos' copies are created by your devices. Until one of them syncs on the day, a
  due copy shows in Today as pending.
- Not supported: renaming or deleting areas and tags, switching a repeat between "after
  completion" and "on a schedule" (stop it and create a new one), permanent deletion.
- claude.ai shows custom connectors with the favicon Google has for the parent domain, so the
  server's own icon only appears in clients that read it.

## Credits

Knowledge of the protocol builds on
[arthursoares/things-cloud-sdk](https://github.com/arthursoares/things-cloud-sdk) (and
[nicolai86/things-cloud-sdk](https://github.com/nicolai86/things-cloud-sdk) before it),
notably its notes on Base58 ids and crash-causing payloads, and on
[evanpurkhiser/things3-cloud](https://github.com/evanpurkhiser/things3-cloud). No code was copied.

## License

MIT, see [LICENSE](LICENSE). Things is a trademark of Cultured Code GmbH & Co. KG.

Maintenance

ActivityMaintained
ResponsivenessNo issues