Skip to main content
Glama
README.md
# Things App MCP

**English** | [한국어](README.ko.md)

MCP server for [Things 3](https://culturedcode.com/things/) on macOS. It adds, reads, and updates to-dos through the Things URL scheme and JXA, and `npm run sync` copies the Today list into a Notion database.

The server talks to the client over stdio on your Mac.

## Requirements

- macOS
- [Things 3](https://culturedcode.com/things/)
- Node.js 18 or newer
- Things URLs turned on: **Things > Settings > General > Enable Things URLs**
- Automation permission for whichever app starts the server (Terminal, Claude, Cursor, or Codex). `osascript` has to be allowed to control Things.

## Install

```bash
git clone https://github.com/lucas-flatwhite/things-app-mcp.git
cd things-app-mcp
npm install
npm run build
```

You can also skip the clone and let the client run `npx -y things-app-mcp@latest`. The configs below do that.

## Connect a client

Adding and reading to-dos works without a token. Changing an existing item, or applying a reschedule, needs the Things auth token.

In Things: **Settings > General > Enable Things URLs > Manage**.

Set `THINGS_AUTH_TOKEN` in the client config. If you pass `authToken` on each call, that token lands in the client log. Replies strip the token, notes, and JSON `data`. The URL given to `open` still has the token for a moment. Things expects it there.

### Claude Desktop

`~/Library/Application Support/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "things": {
      "command": "npx",
      "args": ["-y", "things-app-mcp@latest"],
      "env": {
        "THINGS_AUTH_TOKEN": "your-token-here"
      }
    }
  }
}
```

### Cursor

`.cursor/mcp.json`

```json
{
  "mcpServers": {
    "things": {
      "command": "npx",
      "args": ["-y", "things-app-mcp@latest"],
      "env": {
        "THINGS_AUTH_TOKEN": "your-token-here"
      }
    }
  }
}
```

### Codex

`~/.codex/config.toml`

```toml
[mcp_servers.things]
command = "npx"
args = ["-y", "things-app-mcp@latest"]
startup_timeout_sec = 20
tool_timeout_sec = 120

[mcp_servers.things.env]
THINGS_AUTH_TOKEN = "your-token-here"
```

### Gemini CLI

```bash
export THINGS_AUTH_TOKEN="your-token-here"
gemini mcp add things npx -y things-app-mcp@latest
```

For a local build, use `node` and the absolute path to `dist/index.js` instead of `npx`.

## Tools

### Writing

| Tool | Description |
|------|-------------|
| `add-todo` | New to-do: title, notes, when, deadline, tags, checklist, project, or area. |
| `add-project` | New project. Put to-do titles in `todos`, one per line. |
| `update-todo` | Change a to-do by id, including completing or canceling it. Needs the token. |
| `update-project` | Change a project by id. Needs the token. |
| `show` | Open a list, project, area, tag, or to-do in Things. |
| `search` | Open Things search. |
| `add-json` | Nested projects, headings, to-dos, and checklist items. Updates need the token. |

When a write succeeds, macOS has opened the URL. Read the item again if you need to see that Things saved it. `add-todo` does not return the new id.

### Reading

| Tool | Description |
|------|-------------|
| `get-todos` | To-dos from one list, project, area, or tag. |
| `get-todo-by-id` | One to-do by id. |
| `get-projects` | Every project. |
| `get-project-by-id` | One project by id. |
| `get-areas` | Every area. |
| `get-tags` | Every tag. |
| `search-todos` | To-dos whose title or notes contain the query. |
| `get-recent-todos` | To-dos changed in the last N days. Default is 7. |

Call `get-todos` with one of `list`, `project`, `area`, or `tag`. If you leave them all out, you get Today.

Lists: `Inbox`, `Today`, `Anytime`, `Upcoming`, `Someday`, `Logbook`.

Results include titles and notes.

### Clearing Today

`reschedule-distant-todos` looks through Today for open items whose deadline is still far off, and moves the start date to a few days before that deadline.

No arguments means preview only, and only when a token is set. To apply:

```json
{
  "daysThreshold": 7,
  "bufferDays": 3,
  "dryRun": false,
  "confirm": true
}
```

- `daysThreshold` (default 7): a closer deadline stays in Today.
- `bufferDays` (default 3): new start date, this many days before the deadline.
- An item you put on today yourself (`activationDate` is today) stays where it is.
- `dryRun: false` without `confirm: true` returns an error and leaves Today unread.
- `confirm` is a tool argument. A client that reads `destructiveHint` may ask again before it runs.
- The update is one JSON batch.

`update-todo`, `update-project`, and updates through `add-json` do not take `confirm`. With the token set, those calls can edit or complete items.

## Examples

Add a to-do:

```json
{
  "title": "Buy groceries",
  "when": "today",
  "tags": "Errand"
}
```

Create a project and its to-dos:

```json
{
  "title": "Launch Website",
  "area": "Work",
  "todos": "Design mockups\nBuild frontend\nDeploy"
}
```

Read Today:

```json
{ "list": "Today" }
```

Read one project:

```json
{ "project": "Launch Website" }
```

Complete a to-do once you have its id:

```json
{
  "id": "the-todo-id",
  "completed": true
}
```

Preview a reschedule, then apply it:

```json
{ "dryRun": true }
```

```json
{ "dryRun": false, "confirm": true }
```

## Dates, lists, and JSON

Write tools follow the [Things URL scheme](https://culturedcode.com/things/support/articles/2803573/).

### When and deadline

| Format | Example |
|--------|---------|
| Named | `today`, `tomorrow`, `evening`, `anytime`, `someday` |
| Date | `2026-03-15` |
| Date and time | `2026-03-15@14:00` |
| Natural language | `next friday`, `in 3 days` (Things reads these in English) |

### `show` list ids

`inbox`, `today`, `anytime`, `upcoming`, `someday`, `logbook`, `tomorrow`, `deadlines`, `repeating`, `all-projects`, `logged-projects`

### `add-json` types

| Type | What it is |
|------|------------|
| `to-do` | A task: title, notes, when, deadline, tags, checklist items. |
| `project` | A project: title, notes, nested items. |
| `heading` | A section in a project. |
| `checklist-item` | One line in a to-do. |

To update, set `"operation": "update"` and `"id"`. That needs the auth token.

## Layout

```
src/index.ts            MCP tools
src/things-url.ts       things:/// URLs, and stripping secrets from replies
src/applescript.ts      JXA reads
src/reschedule-mode.ts  preview versus apply
src/notion-sync.ts      `npm run sync`, separate from the MCP server
src/notion-sync/        plan, Notion client, Things read and write
```

Writes build a `things:///` URL and hand it to `open`. Reads run JXA through `osascript`.

## Development

```bash
npm install
npm run build
npm run dev    # tsc --watch
npm start      # node dist/index.js
```

## Testing

See [TESTING.md](TESTING.md).

```bash
# No Things required.
node scripts/test-unit.js

# Sync plan, hashes, and the Notion client. No network and no Things.
npm run test:sync

# macOS and Things 3. Writes stay off.
npm run test:tools

THINGS_MCP_TEST_ALLOW_WRITES=1 npm run test:tools
```

One case in the unit script reads Today when Things is installed. Skip it with:

```bash
THINGS_UNIT_SKIP_LIVE=1 node scripts/test-unit.js
```

## Notion sync

`npm run sync` copies the Things **Today** list into Notion database `9ac2752989a646918bfa93ab900dfb8c`. That list is whatever Things is showing under Today: items for today, this evening, and overdue items still sitting there. Inbox, Upcoming, Anytime, and Someday are left as they are.

You start each run yourself.

### Setup

Put the Notion token in the macOS keychain. Every run reads it from there.

```bash
security add-generic-password -s things-notion-sync -a "$USER" -w
```

Set `THINGS_AUTH_TOKEN` when the plan changes a Things to-do. A run that only creates or edits Notion rows can go without it.

If a column is missing, the dry-run exits 2 and prints a database patch. This command adds the missing columns and then writes the plan:

```bash
npm run sync -- --apply --ensure-schema
```

`시작일` is the calendar formula. A missing column, or a column of some other type, prints a warning and the sync continues. Paste this into Notion:

```
if(test(prop("언제"), "^\d{4}-\d{2}-\d{2}"), parseDate(slice(prop("언제"), 0, 10)), parseDate(""))
```

### Run

```bash
npm run build
npm run sync                  # print the plan
npm run sync -- --apply       # write it
```

The table columns are `action`, `things_id`, `title`, `fields`, and `detail`. With nothing to do, the command prints `변경 없음`. The same text is appended to `~/Library/Logs/things-notion-sync.log`, with ids, actions, and field names. Tokens and note text are stripped first.

### Columns

| Column | What the sync does |
|--------|--------------------|
| 제목, 메모, 마감일, 태그 | Copied both ways while the to-do is in Today. |
| 언제 | Copied both ways for `오늘`, `오늘 저녁`, and a date on or before today. `받은함`, `언제든`, `언젠가`, and a later date stay on the Notion row. |
| 상태 | `열림`, `완료`, or `취소`, both ways, while the to-do is in Today. |
| 시작일 | Shown on a calendar. The sync skips this column. |
| 프로젝트, 프로젝트 id, 영역, 영역 id | Filled from the Things to-do. |
| things_id | Things id for the row. The next run finds the row with this. |
| 동기화 해시 | Field hashes from the last write. The next run compares against this. Edit the hash and a later `완료` can be overwritten by the old Things status. |
| 마지막 동기화 | When that write happened. |

A tag name Things has never seen is dropped. Checklists, headings, and repeat rules stay in Things. A repeat that is in Today is copied like any other to-do. If Things ignores a when or deadline change, that Things value is written back to the row.

### Marking a to-do done

On the Notion page, set `상태` to `완료`. Leave `things_id` and `동기화 해시` alone. Then run `npm run sync -- --apply` while the to-do is still in Today, and Things completes it. The same works the next day if it is still in Today when you run the command. `취소` cancels the to-do. `열림` opens it again.

After Things completes it, the to-do leaves Today and the row stays `완료`. The next run fills any other differing fields from Things. From then on, edits to that row stay in Notion.

A new Notion row with an empty `언제`, or with `오늘` or `오늘 저녁`, is added to Today when you apply.

### Deleting rows

Rows are matched by `things_id`. Delete an `열림` row while its to-do is still in Today, and the next run creates the row again. The to-do stays open.

Finished rows can stay in the database. A completed to-do has left Today, so that row is left alone. A row can also be deleted after its to-do moves to Inbox, Anytime, Upcoming, or Someday. It comes back if the to-do returns to Today.

## License

MIT

TDQS

A3.8/5.0

Scored across 15 tools

Disambiguation4/5

Most tools are clearly distinct by resource and action, such as add-project vs. update-project. However, some potential confusion exists: 'search' opens the search screen, while 'search-todos' performs a specific search; 'get-todos' retrieves by source, and 'get-recent-todos' gets recent ones, which might overlap in use cases. Descriptions help clarify, but minor ambiguity remains.

Naming Consistency5/5

Tool names follow a consistent verb_noun pattern throughout, with clear actions like 'add', 'get', 'update', and 'search' paired with specific nouns like 'project', 'todo', or 'areas'. All names use snake_case uniformly, making them predictable and easy to parse.

Tool Count5/5

With 15 tools, this server is well-scoped for managing tasks and projects in Things. It covers core CRUD operations for projects and to-dos, plus additional utilities like listing areas and tags, which fits the domain appropriately without being overwhelming or insufficient.

Completeness4/5

The tool set provides strong coverage for the task management domain, including creation, retrieval, and updates for projects and to-dos, plus listing areas and tags. Minor gaps exist: there's no explicit delete tool for projects or to-dos, and 'add-json' might overlap with other add tools, but agents can likely work around these with updates or existing methods.

Maintenance

ActivityMaintained
ResponsivenessNo issues