Local Python MCP server for Things.app on macOS. The server executes Things actions through the official things:/// URL scheme.
- macOS
- Things.app installed
uv- Python 3.13+
uv syncUpdates in the Things URL scheme require an auth token.
export THINGS_AUTH_TOKEN="<your-things-auth-token>"You can also pass auth_token directly to update tools, or store a token in macOS keychain:
make call TOOL=things_auth_set_token ARGS_JSON='{"token":"YOUR_TOKEN"}'
make call TOOL=things_auth_get_status
make call TOOL=things_auth_clear_tokenToken resolution order for update operations:
- explicit
auth_tokenargument THINGS_AUTH_TOKENenvironment variable- keychain token (
things_auth_set_token)
These commands use the FastMCP CLI patterns from the FastMCP docs (inspect, run, list, call) and are wrapped in this repo's helper targets.
- Default server entrypoint (
python app/server.pyormcp.run()) uses FastMCP's standard stdio transport, which is the transport Codex MCP integrations expect. - Local development smoke tests in this repo use HTTP transport (
fastmcp run --transport http) sofastmcp list/callcan run from a second terminal. - No transport changes are required for Codex use as long as your Codex MCP config launches this server command over stdio.
make inspect
make run-http
make list-http
make call-health
make call TOOL=things_capabilities
make call TOOL=things_add_todo ARGS_JSON='{"title":"Example task","notes":"From FastMCP CLI"}'
make call TOOL=things_update_project ARGS_JSON='{"id":"PROJECT_ID","title":"New title","auth_token":"TOKEN"}'
make call TOOL=things_import_json ARGS_JSON='{"data":[{"type":"to-do","attributes":{"title":"From JSON"}}]}'
make test
make lint
make typecheck
make check
make smoke-http
make smoke-json
make smoke-read./scripts/local_fastmcp.sh inspect
./scripts/local_fastmcp.sh run-http
./scripts/local_fastmcp.sh list-http
./scripts/local_fastmcp.sh call-health
./scripts/local_fastmcp.sh call things_capabilities
./scripts/local_fastmcp.sh call things_add_todo '{"title":"Example task","notes":"From FastMCP CLI"}'
./scripts/local_fastmcp.sh call things_update_project '{"id":"PROJECT_ID","title":"New title","auth_token":"TOKEN"}'
./scripts/local_fastmcp.sh call things_import_json '{"data":[{"type":"to-do","attributes":{"title":"From JSON"}}]}'
./scripts/local_fastmcp.sh test
./scripts/local_fastmcp.sh smoke-http
./scripts/local_fastmcp.sh smoke-json
./scripts/local_fastmcp.sh smoke-readDefaults:
HOST=127.0.0.1PORT=8123MCP_URL=http://127.0.0.1:8123/mcpSERVER_SPEC=app/server.py
Override example:
make run-http PORT=9000
make list-http MCP_URL=http://127.0.0.1:9000/mcpGeneric tool calling:
TOOLselects any MCP tool exposed by this server.ARGS_JSONis optional JSON passed to--input-json.make callexpects a running server atMCP_URL(for example, runmake run-httpin another terminal).
Examples:
make call TOOL=health
make call TOOL=things_capabilities
make call TOOL=things_add_todo ARGS_JSON='{"title":"Inbox review","when":"today"}'
make call TOOL=things_update_project ARGS_JSON='{"id":"PROJECT_ID","deadline":"2026-03-01","auth_token":"TOKEN"}'
make call TOOL=things_update_todo ARGS_JSON='{"id":"TODO_ID","prepend_notes":"[bot] ","add_tags":["automated"],"list_name":"Today","auth_token":"TOKEN"}'
make call TOOL=things_import_json ARGS_JSON='{"data":[{"type":"to-do","attributes":{"title":"Batch item"}}]}'
make call TOOL=things_read_todos ARGS_JSON='{"list_id":"today","limit":20}'
make call TOOL=things_find_todos ARGS_JSON='{"query":"invoice","limit":10}'
make call TOOL=things_read_todos ARGS_JSON='{"list_id":"anytime","status":"open","project_id":"PROJECT_ID","include_notes":true,"limit":50}'
make call TOOL=things_read_projects ARGS_JSON='{"status":"open","area_id":"AREA_ID","limit":20}'
make call TOOL=things_read_areas
make call TOOL=things_read_headings ARGS_JSON='{"project_id":"PROJECT_ID","query":"plan","limit":20}'health: basic service heartbeat.things_add_todo: create a to-do viathings:///add.things_add_project: create a project viathings:///add-project.things_update_todo: update an existing to-do viathings:///update(requires auth token).- includes common automation params:
prepend_notes,append_notes,add_tags, checklist variants, list/heading moves,duplicate.
- includes common automation params:
things_show: navigate/show a list or item viathings:///show.things_search: open Things search with optional query viathings:///search.things_update_project: update an existing project viathings:///update-project(requires auth token).- includes common automation params:
prepend_notes,append_notes,add_tags,duplicate.
- includes common automation params:
things_import_json: create/update Things items in batch viathings:///json(auth token required when JSON includesupdateoperations).things_read_todos: read to-dos by list via AppleScript (osascript), with filters (status,project_id,area_id) and pagination (offset,limit).- supports date filters:
deadline_before,deadline_after,completed_before,completed_after(ISO-8601). - status is normalized to
open,completed, orcanceledand original value is preserved asstatus_raw.
- supports date filters:
things_read_todo: read a single to-do by ID via AppleScript (include_notessupported).things_find_todos: search to-do titles via AppleScript with filters/pagination and date filters.things_read_projects: read projects via AppleScript with status/area/date filters.things_read_areas: read area IDs and titles via AppleScript.things_read_headings: read headings across projects via AppleScript with optionalproject_idand titlequeryfilters.things_auth_set_token: store token in keychain.things_auth_get_status: check token source availability.things_auth_clear_token: clear keychain token.things_version: fetch Things version/build throughthings:///versionwith x-callback.things_validate_token_config: verify update-token configuration state.things_capabilities: list currently implemented capabilities.
See route coverage matrix: docs/things-route-matrix.md Practical command examples: docs/examples.md
things_import_json includes lightweight shape validation before dispatch:
operationmust becreateorupdate.idis required forupdateoperations.attributesmust be an object when present.- nested
to-dos/headings/itemsmust be arrays of objects.
For safe end-to-end CLI smoke checks without modifying real Things data, use make smoke-json. It runs the server with THINGS_MCP_DRY_RUN=1, which skips launching things:/// URLs.
Sensitive URL params are redacted in tool responses (for example auth-token) so returned URLs are safe to log.
Read tools use AppleScript and require macOS Automation permission for the app running Codex/Terminal to control Things. If read calls fail with permission errors:
- Open
System Settings > Privacy & Security > Automation. - Allow the terminal/Codex app to control
Things3. - Re-run
make smoke-read.
Common normalized error codes:
THINGS_AUTH_MISSING: no auth token in arg/env/keychain for update operations.THINGS_AUTH_KEYCHAIN_WRITE_FAILED/THINGS_AUTH_KEYCHAIN_READ_FAILED/THINGS_AUTH_KEYCHAIN_CLEAR_FAILED: keychain operation failed.THINGS_AUTOMATION_DENIED: macOS Automation permission blocked AppleScript access to Things3.THINGS_APP_UNAVAILABLE: Things3 is not running or not reachable via AppleScript.THINGS_SCRIPT_FAILED: generic AppleScript execution failure.THINGS_INVALID_STATUS: unsupportedstatusfilter (must beopen,completed, orcanceled).THINGS_INVALID_DATE_FILTER: invalid date filter format (must be ISO-8601 date/datetime).
app/server.py: FastMCP server entrypoint and tool registration.app/tools.py: tool payload builders and validation.app/things_client.py: URL builder, launcher, and callback listener.scripts/local_fastmcp.sh: local FastMCP CLI helper.tests/test_tools.py: payload and validation tests.tests/test_things_client.py: URL encoding and callback client tests.docs/things-route-matrix.md: route coverage and next implementation targets.docs/examples.md: copy-paste local CLI examples for each major tool.
uv sync
uv run pytest
uv run ruff check .
uv run mypy .