Skip to main content
Glama
README.md
# things3-mcp-server

MCP server for **Things 3** on macOS. Read and write to-dos, lists, areas and
projects — so an agent can drive the same task workflows a human does in
Things (triage Today, log completed work, file new tasks, reschedule).

Built for the KI-OS workflow: it replaces the ad-hoc AppleScript snippets used
for `/review`, the purchase-decision framework and the loop↔Things sync with a
proper tool surface.

## How it works

All access goes through **AppleScript** (`osascript`), invoked with an
`on run argv` handler so user-provided values (task titles, notes, tags) are
passed as real arguments — never interpolated into the script, so there is no
injection risk. No external Python dependency beyond the MCP SDK; nothing reads
the Things SQLite database directly, which keeps writes safe and avoids schema
coupling.

Requires the Things 3 app to be installed and running. On first use macOS will
ask to grant **Automation** permission to control Things 3 — approve it for the
host process (Claude / your terminal).

## Tools

| Tool | Kind | Purpose |
|---|---|---|
| `list_todos(scope_type, scope_name, status)` | read | To-dos in a list / area / project |
| `get_today()` | read | Everything in the Today list |
| `get_inbox()` | read | Everything in the Inbox |
| `get_completed(days)` | read | Recently completed to-dos (Logbook) |
| `search_todos(query, include_completed)` | read | Find to-dos by name substring |
| `get_todo(todo_id)` | read | A single to-do by id |
| `list_areas()` | read | All areas |
| `list_projects(area)` | read | Projects (optionally within an area) |
| `list_tags()` | read | All tag names |
| `create_todo(title, notes, area, project, tags, when, deadline)` | write | Create a to-do |
| `complete_todo(query, todo_id, exact)` | write | Complete open to-do(s) by name or id |
| `cancel_todo(query, todo_id, exact)` | write | Cancel open to-do(s) — reversible, no delete |
| `update_todo(query, todo_id, tags, when, deadline, move_to_list, exact)` | write | Update the first matching open to-do |
| `add_tags(tags, query, todo_id, exact)` | write | Append tags without dropping existing ones |

Write tools accept either `todo_id` (exact, safest — e.g. an id from
`create_todo`) or a name `query` (`exact=True` for an exact match instead of a
substring).

### Keeping list responses small

All list-returning read tools (`list_todos`, `get_today`, `get_inbox`,
`get_completed`, `search_todos`) accept three shaping arguments:

| arg | effect |
|---|---|
| `tag="a,b"` | keep only to-dos carrying ANY of these tags (case-insensitive) |
| `include_notes=False` | drop the `notes` field |
| `limit=N` | keep at most N to-dos, applied **after** the tag filter |

Defaults are unchanged (`""`, `True`, `0`), so existing calls behave exactly as
before.

Why this exists — measured against a real Anytime list of 87 to-dos:

```
list_todos("list", "Anytime")                          55,952 chars
  + include_notes=False                                18,085 chars   (-68 %)
  + tag="48h-Liste,Spontankauf"                         4,047 chars   (-93 %)
```

Two to-dos were being looked for. Notes are the bulk of the payload, and they are
worth having — just not in an overview. Fetch them per to-do with `get_todo()`.

The tag filter is also a correctness matter, not only a size one: a purchase
decision is identified by its **tag**, not by its area, and a to-do filed in no
area at all is the normal case for something captured quickly. Filtering by area
silently misses those.

### Removing a deadline

`update_todo(deadline="none")` **removes** an existing deadline. An empty string
leaves it untouched — those are different operations, and the distinction matters:

Things shows a to-do in **Today** whenever its deadline is due or overdue,
*regardless of when the to-do is scheduled*. Rescheduling alone therefore does not
get a to-do out of Today — a stale deadline from months ago keeps pulling it back,
silently, and Today stops being a list of what is actually due today.

AppleScript has no `missing value` for this property (`due date` is typed `date`,
so assigning `missing value` fails with error -1700). The tool uses
`delete due date of t` instead, which does work — verified against the live app in
both directions: removing yields `missing value`, and setting an ISO date
afterwards still works.

Todo objects carry: `id`, `name`, `status`, `tags`, `due` (deadline, ISO),
`when` (scheduled/activation date, ISO), `project`, `area`, `completion` (ISO),
`notes`. Dates are `YYYY-MM-DD` strings; `when` accepts `today`, `tomorrow`,
`anytime`, `someday` or an ISO date.

## Install

```bash
cd ~/workspace/things3-mcp-server
python3 -m venv .venv
.venv/bin/pip install -e .
```

## Register with Claude Code (user scope)

```bash
claude mcp add things3 --scope user -- \
  ~/workspace/things3-mcp-server/.venv/bin/things3-mcp-server
```

## Design note

The MCP stays a **generic Things wrapper**. Domain semantics — e.g. the ADHS
48h purchase-decision rule (`48h-Liste` tag, +2 days, then `Spontankauf` +7) —
live in the KI-OS skills, not here. `update_todo(query, tags=..., move_to_list="Anytime")`
is exactly the primitive the 48h→Spontankauf transition needs.

## Scope / non-goals

- The interactive "Einkäufe ADHS" Shortcut is deliberately **not** wrapped
  (it needs UI dialogs and stays manual).
- No delete tool by design — `cancel_todo` (reversible) covers "drop this
  task" (e.g. a rejected purchase decision); hard deletion is intentionally
  left out.

## Maintainer

Schimmi — https://schimmilab.de
Issues und Pull Requests willkommen.

TDQS

A3.6/5.0

Scored across 14 tools

Disambiguation2/5

There are five ways to retrieve to-do lists (list_todos, get_today, get_inbox, get_completed, search_todos), and list_todos is vague about what 'one scope' means, creating unclear boundaries. The update/complete/cancel/add_tags tools also share the same todo_id-or-query targeting pattern, which could lead an agent to pick the wrong mutating operation without careful reading.

Naming Consistency4/5

Names uniformly use lowercase verb_noun patterns: list_*, get_*, search_*, create_*, update_*, complete_*, cancel_*, add_tags. The main deviation is that get_* mixes single-item lookup (get_todo) with list-returning operations (get_today, get_completed, get_inbox), so the verb prefix does not strictly predict the return shape, but the overall style is predictable.

Tool Count4/5

14 tools is reasonable for a Things 3 server and stays within the expected range, covering to-do read/write plus area/project/tag lookup. The count is slightly inflated by the many overlapping read/list routes for to-dos, but each still has a concrete intended use.

Completeness4/5

The to-do lifecycle is well covered: create, read/list/search, update, complete, cancel, and tag management are all present. The main gaps are a lack of direct deletion (mitigated by cancel_todo) and write support for projects/areas/tags, but these are reasonable for a to-do-focused server.

Maintenance

ActivityMaintained
ResponsivenessNo issues