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

Composite [MCP](https://modelcontextprotocol.io) server for [Things 3](https://culturedcode.com/things/) on macOS. Two problems, one package:

1. **Call-count bloat.** Agent workflows against Things burn 10+ round trips for a morning review: projects, areas, tags, today, inbox, logbook, per-project todos. things-turbo bundles them. `get_review_data` is one call. `get_dashboard` is one call.
2. **`sqlite3.OperationalError: unable to open database file`.** The Things database lives in a TCC-protected group container. macOS decides per process, with caching, whether reads are allowed, and the answer can flip mid-session. When it flips, every read in every affected session dies at once. things-turbo ships a self-healing read path that falls back to a TCC-free SQLite mirror and keeps working.

Under 700 lines of Python. Reads go through [things.py](https://github.com/thingsapi/things.py) against the Things SQLite database; writes go through the official [Things URL scheme](https://culturedcode.com/things/support/articles/2803573/), so Things itself does the writing.

## Who this is for

People driving Things 3 from Claude (or any MCP client) who want fewer round trips, batch writes, and reads that survive macOS app-data protection. For one-at-a-time CRUD, upstream [things-mcp](https://pypi.org/project/things-mcp/) may be enough; this package also ships a `things-mcp-resilient` entry point that runs upstream things-mcp with the self-healing read path patched in.

## Tools

Composite reads, each replacing several separate MCP calls:

| Tool | What it returns |
|---|---|
| `get_context` | Projects, areas, and tags in one call |
| `get_dashboard` | Today, inbox, upcoming, and overdue |
| `get_review_data` | Everything a weekly review needs: today, inbox, anytime/someday counts, projects with todo counts, logbook |
| `get_project_health` | Stalled-project detection: per-project last completion, next-action presence, and a health class (shipping / cruising / stalled / zombie / empty) |

Batch writes via the URL scheme:

| Tool | What it does |
|---|---|
| `batch_update` | Update many todos in one call (title, notes, when, deadline, tags, completed, canceled) |
| `bulk_complete` | Complete a list of todos |
| `bulk_reschedule` | Move a list of todos to the same date |
| `update_checklist` | Replace, append, or prepend checklist items, which the standard MCP tools can't do |

## Install

Requires macOS with Things 3, Python 3.12+, and [uv](https://docs.astral.sh/uv/).

Claude Code (`.mcp.json` or `claude mcp add`):

```json
{
  "mcpServers": {
    "things-turbo": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/BradleyAllanDavis/things-turbo", "things-turbo"],
      "env": {
        "THINGS_AUTH_TOKEN": "your-token-here"
      }
    }
  }
}
```

The auth token is needed for writes only. Get it in Things: Settings > General > Enable Things URLs > Manage. To keep the token out of plain config, set `THINGS_AUTH_TOKEN_CMD` to a command that prints it:

```json
"env": {
  "THINGS_AUTH_TOKEN_CMD": "op read 'op://Private/Things Auth Token/credential'"
}
```

## The self-healing read path

things-turbo tries the live Things database first. On a TCC denial it:

1. remembers the denial for 5 minutes,
2. kickstarts the launchd mirror agent named in `THINGS_MIRROR_AGENT`, if set,
3. reads the mirror at `THINGS_MIRROR_PATH` (default `~/.cache/things-mirror/main.sqlite`).

While reads are healthy, it also refreshes the mirror opportunistically (`VACUUM INTO` a temp file, atomic replace), so any process that still has access keeps the mirror warm for the ones that don't.

The mirror can be produced by anything that copies the Things database on a schedule. A launchd agent running `sqlite3 <things-db> "VACUUM INTO '<mirror>'"` under a shell with a one-time Full Disk Access grant works well; set `THINGS_MIRROR_AGENT` to its label and things-turbo will kickstart it on demand.

`THINGSDB` (the standard things.py override) always wins and never falls back.

## Configuration

| Variable | Purpose | Default |
|---|---|---|
| `THINGS_AUTH_TOKEN` | Things URL-scheme token, used for writes | unset |
| `THINGS_AUTH_TOKEN_CMD` | Shell command that prints the token | unset |
| `THINGS_MIRROR_PATH` | TCC-free mirror location | `~/.cache/things-mirror/main.sqlite` |
| `THINGS_MIRROR_AGENT` | launchd label to kickstart for a mirror refresh | unset |
| `THINGSDB` | Explicit database path (things.py standard); disables fallback | unset |

## License

MIT

TDQS

A3.9/5.0

Scored across 8 tools

Disambiguation4/5

The read tools (get_context, get_dashboard, get_review_data, get_project_health) are clearly distinct in their aggregation purposes. The write tools are also named by intent, though batch_update overlaps some with bulk_complete and bulk_reschedule since it can set completed and when, creating mild ambiguity.

Naming Consistency4/5

Reads consistently use get_ and batch operations use bulk_ or batch_/update_. The pattern is readable and predictable, though batch_update and update_checklist break the bulk_ convention for write operations.

Tool Count5/5

8 tools is a well-scoped size for a Things integration that provides both high-level read aggregation and targeted update operations. No tool feels redundant enough to remove, and the count is not overwhelming.

Completeness3/5

The server covers read aggregation, checklist updates, bulk completion, and rescheduling, but there are notable gaps: no tools for creating new todos, projects, areas, or tags, and no explicit delete/cancel operation beyond a canceled flag in batch_update. For a task-management server, lack of creation is a significant missing lifecycle operation.

Maintenance

ActivityStale
ResponsivenessNo issues