Metadata-Version: 2.1
Name: mcp-server-things
Version: 1.12.1
Summary: Model Context Protocol server for Things 3 task management integration
Author-email: Eric Bowman <ebowman@boboco.ie>
Project-URL: Homepage, https://github.com/ebowman/mcp-server-things
Project-URL: Bug Reports, https://github.com/ebowman/mcp-server-things/issues
Project-URL: Source, https://github.com/ebowman/mcp-server-things
Keywords: mcp,things3,applescript,task-management,productivity
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Scheduling
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastmcp<4,>=3.4.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: pydantic-settings>=2.0.0
Requires-Dist: asyncio-mqtt>=0.11.0
Requires-Dist: things.py>=1.0.0
Requires-Dist: dateparser>=1.1.0
Requires-Dist: python-dateutil>=2.8.0
Requires-Dist: cryptography<49; sys_platform == "darwin" and platform_machine == "x86_64"
Provides-Extra: config
Requires-Dist: python-dotenv>=0.19.0; extra == "config"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
Requires-Dist: pytest-benchmark>=4.0.0; extra == "dev"
Requires-Dist: pytest-timeout>=2.1.0; extra == "dev"
Requires-Dist: freezegun>=1.2.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: isort>=5.0.0; extra == "dev"
Requires-Dist: flake8>=6.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
Requires-Dist: python-dotenv>=0.19.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=5.0.0; extra == "docs"
Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == "docs"
Requires-Dist: myst-parser>=1.0.0; extra == "docs"
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
Requires-Dist: coverage>=7.0.0; extra == "test"

# Things 3 MCP Server

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![macOS](https://img.shields.io/badge/macOS-12+-green.svg)](https://www.apple.com/macos/)

A Model Context Protocol (MCP) server that connects Claude and other AI assistants to Things 3 for natural language task management.

## Why this server?

Writes go through **AppleScript**, not the Things URL scheme, which is what enables `delete_todo`, `move_record`/`bulk_move_records`, `remove_tags`, real IDs returned synchronously, and no Things auth token — the trade-off is a one-time macOS Automation permission prompt on first write. Operationally it also ships built-in `doctor` diagnostics, `config --write` client setup, context-optimized response modes for large databases, and 1300+ unit tests.

[hald/things-mcp](https://github.com/hald/things-mcp) is a solid, lighter URL-scheme-based alternative — several of its ideas (Someday-project filtering, tag usage reporting, `.mcpb` packaging) are adopted here too. See [docs/COMPARISON.md](docs/COMPARISON.md) for the detailed matrix.

## Prerequisites

- macOS 12+
- Things 3 installed and opened at least once
- [uv](https://docs.astral.sh/uv/) (`brew install uv`)
- macOS will ask for Automation permission for Things 3 on the first write — that's expected (AppleScript is what enables delete/move operations other servers lack).

## Install

### Claude Desktop

**Option A: One-click `.mcpb`**

Download the latest `.mcpb` file from the [releases page](https://github.com/ebowman/mcp-server-things/releases) and double-click it to install into Claude Desktop.

The bundle launches the server via `uvx`, so [uv](https://docs.astral.sh/uv/) must be installed and on `PATH` (`brew install uv`). The generated config pins uv's managed Python (`--python-preference only-managed`) so a stray Intel/Rosetta Python on your PATH can't break the install; first launch may download a managed CPython.

**Option B: `config` CLI**

```bash
mcp-server-things config --client claude-desktop --write
```

Safely adds/updates the `things` entry in your Claude Desktop config (`--force` overwrites an existing, different entry instead of refusing).

**Option C: Manual JSON**

```json
{
  "mcpServers": {
    "things": {
      "command": "uvx",
      "args": ["--python-preference", "only-managed", "--python", "3.12", "mcp-server-things"]
    }
  }
}
```

### Claude Code

```bash
claude mcp add-json things '{"command":"uvx","args":["--python-preference","only-managed","--python","3.12","mcp-server-things"]}'
claude mcp add-json things '{"command":"uvx","args":["--python-preference","only-managed","--python","3.12","mcp-server-things"]}' -s user
```

`mcp-server-things config --client claude-code` prints these exact commands.

### Any MCP client

```json
{
  "command": "uvx",
  "args": ["--python-preference", "only-managed", "--python", "3.12", "mcp-server-things"]
}
```

## Verify

Run `mcp-server-things doctor` (or `uvx mcp-server-things doctor`) to confirm Things 3, permissions, and the database are all reachable.
Then ask your client "What's in my Things inbox?".

> **`uvx mcp-server-things` fails with `Building cryptography==...` / maturin / Rust errors?**
> Your default Python is an x86_64 (Intel/Rosetta) build — `cryptography` no longer ships
> macOS x86_64 wheels. Fix: run with an arm64 interpreter, e.g. `uvx -p 3.12 mcp-server-things`
> (Homebrew or uv-managed Python), or `uv python install 3.12` first. `mcp-server-things doctor`
> warns about this.

<details>
<summary>Advanced: pip, virtualenv, from source, existing installs</summary>

Upgrading from an existing install? See [docs/UPGRADING.md](docs/UPGRADING.md).

### Option 1: From PyPI

1. Create and activate a virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate  # On macOS/Linux
```

2. Install the package:
```bash
pip install mcp-server-things
```

### Option 2: From Source (Development)

1. Clone the repository:
```bash
git clone https://github.com/ebowman/mcp-server-things.git
cd mcp-server-things
```

2. Create and activate a virtual environment:
```bash
python3 -m venv venv
source venv/bin/activate  # On macOS/Linux
```

3. Install dependencies:
```bash
pip install -r requirements.txt
```

4. Install in development mode:
```bash
pip install -e .
```

### Claude Desktop Configuration

#### `config` CLI

`mcp-server-things config --client <claude-desktop|claude-code|generic> [--via uvx|current-python] [--write] [--force]`
prints the MCP client configuration for the requested client (or, for
`claude-desktop --write`, safely merges it into
`~/Library/Application Support/Claude/claude_desktop_config.json`, backing up
the previous file first and refusing to clobber an existing, different
`things` entry unless `--force` is also passed). Run
`mcp-server-things config --client claude-desktop --write` or
`mcp-server-things config --client claude-code` instead of hand-editing JSON
or memorizing the `claude mcp add-json` syntax.

**Shortcut for venv/pip installs:** `mcp-server-things config --client claude-desktop --via current-python` targets the currently-running interpreter (`sys.executable -m things_mcp`) instead of the default `uvx mcp-server-things`.

#### For PyPI Installation

Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "things": {
      "command": "/path/to/your/venv/bin/python",
      "args": ["-m", "things_mcp"],
      "env": {
        "THINGS_MCP_LOG_LEVEL": "INFO",
        "THINGS_MCP_APPLESCRIPT_TIMEOUT": "30"
      }
    }
  }
}
```

#### For Source Installation

Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "things": {
      "command": "/path/to/mcp-server-things/venv/bin/python",
      "args": ["-m", "things_mcp"],
      "env": {
        "PYTHONPATH": "/path/to/mcp-server-things/src",
        "THINGS_MCP_LOG_LEVEL": "INFO",
        "THINGS_MCP_APPLESCRIPT_TIMEOUT": "30"
      }
    }
  }
}
```

**Notes:**
- **PyPI**: Replace `/path/to/your/venv/bin/python` with your virtual environment's Python path
- **Source**: Replace `/path/to/mcp-server-things` with your actual installation path and include the `PYTHONPATH`
- Use the full path to the Python executable in your virtual environment
- See Configuration section below for environment variable options

</details>

![Demo showing Claude creating tasks in Things 3](demo.gif)
*Creating tasks with natural language through Claude*

## 📚 Documentation

- **[User Examples](docs/USER_EXAMPLES.md)** - Rich examples of how to use Things 3 with AI assistants
- **[Architecture Overview](docs/ARCHITECTURE.md)** - Technical design and implementation details
- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common issues and solutions
- **[macOS Permissions](docs/MACOS_PERMISSIONS.md)** - The TCC dialogs you'll see, why they recur, and headless setup

## Features

### Core Todo Operations
- **Create**: Add todos with full metadata (tags, deadlines, projects, notes)
- **Read**: Get todos by ID, project, or built-in lists (Today, Inbox, Upcoming, etc.)
- **Update**: Modify existing todos with partial updates
- **Delete**: Remove todos safely
- **Search**: Find todos by title, notes, or advanced filters

### Project & Area Management
- Get all projects and areas with optional task inclusion
- Create new projects with initial todos
- Update project metadata and status
- Create and rename areas, including tags (`add_area`, `update_area`)
- Organize todos within project hierarchies

### Built-in List Access
- **Inbox**: Capture new items
- **Today**: Items scheduled for today
- **Upcoming**: Future scheduled items
- **Anytime**: Items without specific dates
- **Someday**: Items for future consideration
- **Logbook**: Completed items history
- **Trash**: Deleted items

### Advanced Features
- **Tag Management**: Full tag support with AI creation control, plus usage reporting (`get_tag_usage`) for weekly-review cleanup
- **Date-Range Queries**: Get todos due/activating within specific timeframes
- **URL Schemes**: Native Things 3 URL scheme integration
- **Health Monitoring**: System health checks and queue status monitoring
- **Error Handling**: Robust error handling with configurable retries
- **Logging**: Structured logging with configurable levels
- **Concurrency Support**: Multi-client safe operation with operation queuing
- **Input Validation**: Configurable limits for titles, notes, and tags
- **Structured Output**: Every read tool returns both human-readable text and machine-readable `structured_content` (via FastMCP 3.x) with a consistent `{items, count, total, mode, limit, offset}` shape (`{item: {...}}` for single-item lookups like `get_todo_by_id`), so clients can consume results programmatically without re-parsing text

## Requirements

- **macOS**: This server requires macOS (tested on macOS 12+)
- **Things 3**: Things 3 must be installed and accessible
- **Python**: Python 3.10 or higher
- **Permissions**: AppleScript permissions for Things 3 access

## Quick Start

Once installed, Claude (or other MCP clients) can automatically discover and use all available tools. No additional setup required.

## Configuration

The server uses environment variables for configuration, settable via system environment variables or a `.env` file (auto-loaded from the current directory, or pointed to with `--env-file`). The env vars that matter most:

| Variable | Default | Description |
|----------|---------|-------------|
| `THINGS_MCP_LOG_LEVEL` | `INFO` | Logging level: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| `THINGS_MCP_AI_CAN_CREATE_TAGS` | `false` | Whether the AI can create new tags (`false` = existing tags only) |
| `THINGS_MCP_APPLESCRIPT_TIMEOUT` | `30.0` | AppleScript execution timeout in seconds (1-300) |
| `THINGS_MCP_TRANSPORT` | `stdio` | Transport to use: `stdio` or `http` |
| `THINGS_MCP_PORT` | `8000` | Port to bind to when `THINGS_MCP_TRANSPORT=http` |

See [`.env.example`](.env.example) for the full list of options, including validation limits, retry counts, the auth-token file, and `THINGS_MCP_HOST`.

### Things URL-scheme auth token

`add_checklist_items`, `prepend_checklist_items`, and `replace_checklist_items`
(and any other tool built on `things:///update`, e.g. moving a to-do under a
heading) require a Things URL-scheme auth token. Without one configured,
these tools return `success: false` with an actionable error instead of
silently doing nothing - Things itself rejects un-authenticated `update`
requests, but `open -g` still exits 0, so the failure has to be caught before
the URL is ever opened. `things:///add`-based tools (`add_todo`,
`add_project`, including todo creation with a checklist) do **not** need a
token.

To configure one:

1. In Things 3: Settings > General > Enable Things URLs > Manage.
2. Save the token to one of (checked in this order, first match wins):
   - `.things-auth` in the project root
   - `things-auth.txt` in the project root
   - `~/.things-auth` in your home directory
3. No restart required - the token is loaded at startup, then reloaded from
   disk automatically the next time an auth-gated tool is called while no
   token is currently loaded, so a token file added or fixed after the
   server starts is picked up on its next use. Once a token is
   successfully loaded it stays loaded for the life of the process.

An empty or whitespace-only token file is treated the same as a missing one
(the loader falls through to the next candidate path). `health_check` and
`get_server_capabilities` report the current `auth_token_configured` state,
and an `AUTH_TOKEN_NOT_CONFIGURED` error includes a `checked_paths` field
showing which candidate paths were checked and why each was rejected
(`missing` / `empty` / `unreadable`) - never the token value itself.

### HTTP Transport

By default the server speaks MCP over stdio. It can optionally run an HTTP
transport instead, which is the reliable fix when a client's stdio subprocess
lacks Automation (TCC) access to Things 3: run the server from a Terminal that
has been granted access, then point the client at the HTTP URL instead of
launching it as a subprocess (see Troubleshooting for details).

| Variable | Default | Description |
|----------|---------|-------------|
| `THINGS_MCP_TRANSPORT` | `stdio` | Transport to use: `stdio` or `http` |
| `THINGS_MCP_HOST` | `127.0.0.1` | Host to bind to when `THINGS_MCP_TRANSPORT=http` |
| `THINGS_MCP_PORT` | `8000` | Port to bind to when `THINGS_MCP_TRANSPORT=http` |

```bash
THINGS_MCP_TRANSPORT=http THINGS_MCP_PORT=8000 uvx mcp-server-things
```

Then add it to Claude Code as an HTTP server:

```bash
claude mcp add --transport http things http://127.0.0.1:8000/mcp
```

`--transport`, `--host`, and `--port` CLI flags are also available and take
precedence over the environment variables above.

### Command Line Options

The server supports several command-line options:

```bash
# Start with debug logging
python -m things_mcp --debug

# Use a custom .env file
python -m things_mcp --env-file ~/my-config.env

# Check system health
python -m things_mcp --health-check

# Test AppleScript connectivity
python -m things_mcp --test-applescript

# Show version
python -m things_mcp --version

# Customize timeout and retry settings
python -m things_mcp --timeout 60 --retry-count 5   # 5 retries after the first attempt (default 2)

# Run with HTTP transport instead of stdio
python -m things_mcp --transport http --host 127.0.0.1 --port 8000
```

### Claude Desktop Environment Variables

You can set environment variables directly in your Claude Desktop configuration:

```json
{
  "mcpServers": {
    "things": {
      "env": {
        "THINGS_MCP_LOG_LEVEL": "DEBUG",
        "THINGS_MCP_AI_CAN_CREATE_TAGS": "true",
        "THINGS_MCP_APPLESCRIPT_TIMEOUT": "60"
      }
    }
  }
}
```

## Available MCP Tools

### Todo Management
- `get_todos(project_uuid?, include_items?)` - List todos
- `add_todo(title, ...)` - Create new todo
- `update_todo(id, ..., heading?, list_id?, list_title?)` - Update existing todo; when the write reports success, the response includes the target `todo_id` and a post-write readback. `readback_succeeded` means only that the read completed; it does not claim requested field values were applied. When true, `item` is what the database showed immediately after the write; for URL-scheme-routed fields (heading moves, `when='evening'`, `when='YYYY-MM-DD@HH:MM'`) it may still reflect pre-write state, and a mismatch is not evidence the write failed - do not retry the write, poll `get_todo_by_id` with a short backoff instead. If readback fails, `success` remains true, `readback_succeeded` is false, and `readback_error` plus a warning make clear that the write must not be retried automatically. `heading` moves it under a heading (requires the Things URL-scheme auth token), `list_id`/`list_title` move it to a different project or area
- `bulk_update_todos(todo_ids, ...)` - Update multiple todos in one operation
- `get_todo_by_id(todo_id)` - Get specific todo. A to-do/heading that is itself untrashed but whose containing project is trashed reports `trashed: true` plus `trashedViaParent: true` (Things marks only the trashed container, not its descendants); direct trash keeps `trashed: true` alone.
- `delete_todo(todo_id)` - Delete a to-do or a project (auto-detects the id type; headings/areas/tags cannot be deleted via any public Things 3 API)

### Project Management
- `get_projects(include_items?)` - List projects
- `add_project(title, ..., todos?)` - Create new project; a `##`-prefixed line in `todos` creates a real heading (via the Things URL scheme's `json` action), with subsequent lines nesting under it
- `update_project(id, ...)` - Update existing project. Completing a project (`completed="true"`) cascades to its child to-dos (Things marks them completed too), but reopening a project (`completed="false"`) does **not** cascade back - child to-dos already completed stay completed. This is upstream Things behavior, not a bug here; reopen specific to-dos explicitly (e.g. `bulk_update_todos`) if needed.
- `get_project_headings(project_id, mode?)` - Read a project's heading structure (title, order, open-todo count per heading), in Things' own order. Read-only: headings can only be created at project-creation time (`add_project`'s `##` lines) and cannot be renamed/deleted via any public Things 3 API.

### Area Management
- `get_areas(include_items?)` - List areas
- `add_area(title, tags?)` - Create new area
- `update_area(id, title?, tags?)` - Update existing area

### List Access

`get_today`, `get_upcoming`, `get_anytime`, `get_someday`, and `get_trash` never return headings, and exclude projects by default - pass `include_projects=true` to also include projects that belong to that list (e.g. a project due today), matching the Things app's list views. `get_inbox` has no `include_projects` flag since the Inbox can never contain projects.

- `get_inbox(mode?, limit?, offset?)` - Get Inbox todos
- `get_today(mode?, limit?, offset?, include_projects?)` - Get Today's todos
- `get_upcoming(mode?, limit?, offset?, days?, include_projects?)` - Get upcoming todos (with optional days filter)
- `get_anytime(mode?, limit?, offset?, include_projects?)` - Get Anytime todos. Responses over the ~80KB budget are truncated (`truncated: true`); page with `offset` / the returned `next_offset` to reach every item (also on get_inbox/get_today/get_upcoming/get_someday)
- `get_someday(mode?, limit?, offset?, include_project_tasks?, include_projects?)` - Get Someday todos. By default only returns items whose own start state is Someday; pass `include_project_tasks=true` to also include tasks that live inside Someday projects (marked `inheritedSomeday: true`). Today/Anytime/Upcoming always exclude tasks that belong to a Someday project, regardless of this flag.
- `get_logbook(limit?, period?, offset?, include_canceled?)` - Get completed todos; includes canceled todos by default (`include_canceled=true`), matching the Things app's own Logbook view
- `get_trash(include_projects?)` - Get trashed todos

### Date-Range Queries
- `get_due_in_days(days, include_overdue?, mode?, limit?)` - Get todos due within specified days; includes already-overdue todos by default (`include_overdue=true`)
- `get_activating_in_days(days, mode?, limit?)` - Get todos activating within days

### Search & Tags
- `search_todos(query, status?, offset?)` - Basic search; matches incomplete todos by default (`status='incomplete'`), pass `status=None` to search all statuses
- `search_advanced(...)` - Advanced search with filters; searches all statuses by default when no `status` filter is given
- `get_tags(include_items?)` - List tags
- `get_tag_usage(only_unused?, mode?)` - Per-tag open/total/area usage counts, sorted by usage, for cleanup. Caveats: tags sharing an identical title are merged into one row (uuid picks the last match), and area-only tags are counted via `area_count`/`total_count` but never affect `open_count`.
- `create_tag(name)` - Create a new tag
- `get_tagged_items(tag)` - Get items with specific tag
- `add_tags(todo_id, tags)` - Add tags to a todo
- `remove_tags(todo_id, tags)` - Remove tags from a todo
- `get_recent(period, status?, type?)` - Get recently created items; returns all statuses and both to-dos and projects by default (headings are never included unless `type='heading'` is passed explicitly)

### Bulk Operations
- `move_record(todo_id, destination_list)` - Move one todo; uses the same `todo_id`/`readback_succeeded`/`item` receipt as `update_todo`. The readback is what the database showed immediately after the write, not a claim that the requested destination was applied (it may lag; poll `get_todo_by_id` rather than retrying); failed readback preserves the reported write success and adds an explicit no-auto-retry warning
- `bulk_move_records(todo_ids, destination, max_concurrent?)` - Move multiple records

### System & Utilities
- `health_check()` - Check server and Things 3 status
- `queue_status()` - Check operation queue status and statistics
- `get_server_capabilities()` - Get server features and configuration
- `get_usage_recommendations()` - Get usage tips and best practices
- `context_stats()` - Get context-aware response statistics


## Troubleshooting

### Install fails on an x86_64 Python on macOS

`cryptography` 49+ publishes only arm64 macOS wheels, so an x86_64 Python (Intel Mac, or a Rosetta/Anaconda x86 Python on Apple Silicon) would fall back to a failing Rust source build. The server pins `cryptography<49` automatically on x86_64 macOS, so installs use the last wheel-backed release. On Apple Silicon an arm64 Python is still recommended:
`uvx --python cpython-3.12-macos-aarch64 --managed-python mcp-server-things`.

### Scoped database access on macOS

The optional scoped reader lets the connector read Things without granting
Full Disk Access to the connector host or Python interpreter. This mode
requires a source checkout; the helper is not included in the Python wheel.
Build the signed, sandboxed helper, then select the exact
`Things Database.thingsdatabase` bundle in its picker:

```bash
THINGS_MCP_SCOPED_BUILD_DIR="$HOME/Applications" sh native/build.sh
open -n -W "$HOME/Applications/ThingsReadHelper.app" --args select
export THINGS_MCP_SCOPED_HELPER_APP="$HOME/Applications/ThingsReadHelper.app"
PYTHONPATH=src python -m things_mcp doctor
```

The picker starts in the Things Group Container. Open the `ThingsData-*`
folder and choose `Things Database.thingsdatabase` as one package. Set the
same `THINGS_MCP_SCOPED_HELPER_APP` value in the MCP server's environment.
Use a local build directory; some File Provider folders alter app bundle
metadata and invalidate its signature. If the helper reports a stale or
revoked bookmark, select the bundle again.

macOS confines the helper to the selected bundle. It opens only `main.sqlite`
read-only; a SQLite authorizer permits the task, project, area, tag, and
checklist tables used by read tools and denies writes, schema inspection, and
the `TMSettings` table that contains the Things URL auth token. The saved
grant belongs to the helper app: any process running as the same macOS user
can launch it and request those allowed reads. Data returned by read tools
still reaches the MCP server. Writes use Things' separate AppleScript or URL
scheme permissions. The database read row in
`doctor` verifies this path even if its separate Full Disk Access row reports
that the broader permission is off. To revoke the saved grant, run
`open -n -W "$THINGS_MCP_SCOPED_HELPER_APP" --args clear`; remove any matching
macOS Privacy & Security grant if shown.

Run `mcp-server-things doctor` (`--json` for machine-readable output, or
`python -m things_mcp doctor` from source) first - it's a read-only
diagnostic covering Things 3 installation/running state, macOS Automation
permission, database readability (Full Disk Access/TCC), the interpreter
Claude Desktop actually launches, `uv`/`uvx` availability, the auth token,
and environment info. The most common failure is TCC blocking reads on
every Claude Desktop restart even when writes work; see
[docs/MACOS_PERMISSIONS.md](docs/MACOS_PERMISSIONS.md) for the fix, and read
its ["Risks of granting Full Disk Access to a Python
interpreter"](docs/MACOS_PERMISSIONS.md#risks-of-granting-full-disk-access-to-a-python-interpreter)
section before granting it.

### Checklist tools return "Things URL-scheme auth token not configured"

`add_checklist_items`, `prepend_checklist_items`, and `replace_checklist_items`
need a Things URL-scheme auth token (see "Things URL-scheme auth token" under
Configuration above). `mcp-server-things doctor` reports this as a WARN on
the "Auth token file" row, naming the affected tools, until a token file is
in place.

### Common Issues

#### Permission Denied Errors
```bash
# Grant AppleScript permissions to your terminal/IDE
# System Preferences > Security & Privacy > Privacy > Automation
# Enable access for your terminal application to control Things 3
```

#### Things 3 Not Found
```bash
# Verify Things 3 is installed and running
python -m things_mcp.main --health-check

# Check if Things 3 is in Applications folder
ls /Applications/ | grep -i things
```

#### Connection Timeouts
```bash
# Increase timeout value via environment variable
export THINGS_MCP_APPLESCRIPT_TIMEOUT=60

# Or in your .env file
THINGS_MCP_APPLESCRIPT_TIMEOUT=60
```

### Debug Mode

```bash
# Enable debug logging
python -m things_mcp.main --debug

# Check logs
tail -f things_mcp.log
```

### Health Diagnostics

```bash
# Comprehensive health check
python -m things_mcp.main --health-check

# Test specific components
python -m things_mcp.main --test-applescript
```

### Boot diagnostics

If the server appears to hang before an MCP client can connect (especially on
a cold start), the process writes timestamped boot-phase markers to stderr:

```
things-mcp boot: 2026-07-20T09:00:00.000+00:00 +0.001s process-start
things-mcp boot: 2026-07-20T09:00:00.010+00:00 +0.011s watchdog-armed (25.0s)
things-mcp boot: 2026-07-20T09:00:00.050+00:00 +0.051s things-import-start
things-mcp boot: 2026-07-20T09:00:00.120+00:00 +0.121s things-import-done
```

A one-shot startup watchdog also runs in the background: if boot doesn't
complete the MCP handshake within the deadline, it dumps every thread's stack
to stderr (`Timeout (0:00:25)!` followed by a traceback for each thread). On a
healthy, long-running server this fires at most once, if the deadline is reached
while running, and is harmless - it's stderr-only and does not affect the MCP
stdio protocol (which only uses stdout). The watchdog is cancelled when the
server stops or a non-server command (`doctor`, `--health-check`, ...) finishes.

Relevant environment variables:

```bash
# Startup watchdog deadline in seconds. 0 (or any value <= 0) disables it.
THINGS_MCP_BOOT_WATCHDOG_SECS=25

# Timeout for lazily importing the third-party `things` package, in seconds.
# 0 (or any value <= 0) makes the import unbounded (blocking).
THINGS_MCP_THINGS_IMPORT_TIMEOUT_SECS=10
```

To diagnose a cold-start hang from a client's debug log: find the last
`things-mcp boot:` marker line - the phase named there is where boot stalled.
If a watchdog stack dump follows, its traceback shows exactly where each
thread was blocked at that moment.

## Performance

- **Startup Time**: Less than 2 seconds
- **Response Time**: Less than 500ms for most operations
- **Memory Usage**: 15MB baseline, 50MB under concurrent load
- **Concurrent Requests**: Serialized write operations to prevent conflicts
- **Throughput**: Multiple operations per second depending on complexity
- **Queue Processing**: Less than 50ms latency for operation enqueuing

## Security

- No network access required (local AppleScript only)
- No data stored outside of Things 3
- Minimal system permissions needed
- Secure AppleScript execution with timeouts
- Input validation on all parameters

## Contributing

Contributions are welcome! Please follow these guidelines:

- Set up a virtual environment and install dependencies
- Follow existing code style and patterns
- Add tests for new features
- Submit pull requests with clear descriptions

**Testing:** `pytest tests/unit` runs entirely against mocked AppleScript/`things.py`
calls and needs no Things 3 install. `pytest tests/integration` is mostly mock-based
too; its `real_things_tools`/`cleanup_test_todos` fixtures, plus the local fixtures in
`test_bulk_operations_comprehensive.py`, `test_search_comprehensive.py`, and
`test_search_performance.py` (like the opt-in `tests/live` smoke suite) all require a
real, running Things 3 and are gated behind `THINGS_MCP_LIVE_TESTS=1` - without it they
skip with a clear reason rather than writing to your live database. Run the live smoke
suite with `make test-live` (or
`THINGS_MCP_LIVE_TESTS=1 pytest tests/live -q`); it creates and cleans up its own
throwaway project and never touches pre-existing data. `tests/regression` is a second,
opt-in live suite (same `THINGS_MCP_LIVE_TESTS=1` gate) that drives the real MCP tool
boundary end-to-end - run it with `make test-regression`. See
[docs/TESTING.md](docs/TESTING.md) for the full testing policy.

## Documentation

- [Troubleshooting Guide](docs/TROUBLESHOOTING.md) - Common issues and solutions
- [Development Roadmap](docs/ROADMAP.md) - Implementation status and missing features

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Support

- **Issues**: [GitHub Issues](https://github.com/ebowman/mcp-server-things/issues)
- **Discussions**: [GitHub Discussions](https://github.com/ebowman/mcp-server-things/discussions)
- **Email**: ebowman@boboco.ie

---

Built for the Things 3 and MCP community.
