Things Cloud MCP
MCP server that gives AI agents read/write access to your Things3 tasks via the Things API.
README
Things API
RESTful API over Things3 data. Syncs bidirectionally with Things Cloud via the reverse-engineered sync protocol and exposes your tasks, projects, areas, and tags over HTTP/HTTPS.
This repository ships three related products:
things-api— a ready-to-run HTTP/HTTPS servicethings-sdk— a standalone Python SDK for scripts, CLIs, workers, and integrationsthings-cloud-mcp— an MCP server that gives AI agents (Claude, Codex, etc.) read/write access to your tasks
Looking for the Python library instead of the HTTP service? See
packages/things-sdk/README.md. Want to connect your AI agent? Seepackages/things-mcp/README.md.
Which package should I use?
| Use case | What to use |
|---|---|
| You want a hosted/self-hosted HTTP/HTTPS API | things-api |
| You want to build a CLI, script, worker, or integration in Python | things-sdk |
| You want AI agents to read/write your tasks | things-cloud-mcp (requires a running things-api) |
If you just want to run a server and call it over HTTP/HTTPS, continue with the API docs below. If you want to embed the core functionality directly in Python, jump to the SDK README.
Quick Start
cp .env.example .env
# Edit .env with your Things Cloud credentials and a strong API key
docker compose up -d
The API is available at http://localhost:3117. Interactive docs at http://localhost:3117/docs.
Configuration
All settings are configured via environment variables (or a .env file):
| Variable | Required | Default | Description |
|---|---|---|---|
API_KEY |
Yes | — | Primary API key for authentication. Must be at least 32 characters. Passed via X-API-Key header. |
API_KEY_NEXT |
No | — | Optional secondary API key for zero-downtime key rotation. |
THINGS_EMAIL |
Yes | — | Your Things Cloud account email |
THINGS_PASSWORD |
Yes | — | Your Things Cloud account password |
SYNC_INTERVAL_SECONDS |
No | 0 |
Background sync interval in seconds. 0 disables background sync. Recommended: 60. |
ENABLE_SCHEDULER |
No | true |
Enable background scheduler in this process. |
SCHEDULER_LOCK_SECONDS |
No | 30 |
Distributed scheduler leadership lease duration. Only the lock owner runs background sync. |
SCHEDULER_HEARTBEAT_SECONDS |
No | 10 |
Lease renewal interval for scheduler leadership. |
MANUAL_SYNC_LOCK_SECONDS |
No | 120 |
Lease duration for manual sync lock to prevent overlapping POST /api/sync runs. |
SYNC_RETRY_ATTEMPTS |
No | 3 |
Number of retry attempts for transient cloud pull/push failures. |
SYNC_RETRY_BASE_SECONDS |
No | 0.25 |
Exponential backoff base delay for retries. |
SYNC_CIRCUIT_BREAKER_FAILURES |
No | 3 |
Consecutive sync failures required to open the circuit breaker. |
SYNC_CIRCUIT_BREAKER_COOLDOWN_SECONDS |
No | 60 |
Cooldown period while breaker is open before a half-open probe is allowed. |
READINESS_MAX_SYNC_ERRORS |
No | 5 |
Degrade /ready when total sync errors exceed this threshold. |
LOG_FORMAT |
No | text |
Set to json to enable structured JSON logging. |
ENABLE_METRICS |
No | false |
Set to true to expose GET /metrics (Prometheus-compatible counters). |
DATABASE_URL |
No | sqlite+aiosqlite:///./data/things.db |
SQLAlchemy database URL |
API Endpoints
All /api/* endpoints require the X-API-Key header.
Tasks
GET /api/tasks # List all non-trashed tasks
GET /api/tasks/{uuid} # Get a single task
POST /api/tasks # Create a task
PATCH /api/tasks/{uuid} # Update a task
DELETE /api/tasks/{uuid} # Soft-delete (trash) a task
Smart Lists
GET /api/tasks/inbox # Unscheduled tasks
GET /api/tasks/today # Tasks for today or earlier
GET /api/tasks/upcoming # Tasks scheduled for the future
GET /api/tasks/anytime # Tasks available anytime
GET /api/tasks/someday # Low-priority ideas
GET /api/tasks/logbook # Completed tasks (default: last 30 days, ?since=<epoch>)
GET /api/tasks/trash # Trashed tasks
Search
GET /api/tasks/search?q=<text> # Full-text search (title, notes, checklist items)
GET /api/tasks/search/advanced?... # Multi-predicate filter (status, type, schedule, area, project, tag, date ranges, modified/completed since)
Projects
GET /api/projects # List active projects (?include_completed=true to include completed)
POST /api/projects # Create a project
PATCH /api/projects/{uuid} # Update a project
POST /api/projects/{uuid}/complete # Mark a project as completed
DELETE /api/projects/{uuid} # Soft-delete (trash) a project
All smart lists, GET /api/tasks, and GET /api/tasks/by-tag/{tag} accept
optional ?limit=<int>&offset=<int> query params. Pagination is opt-in:
omit both to fetch the complete result set in a single response, which is
the recommended path for agents and scripts that need every task. To page
through a very large list, increment offset by limit and stop when the
returned array is shorter than limit.
Tags
GET /api/tags # List all tags
POST /api/tags # Create a tag
PATCH /api/tags/{uuid} # Update a tag
DELETE /api/tags/{uuid} # Delete a tag
GET /api/tasks/by-tag/{tag} # List tasks by tag UUID or name (?include_descendants=true&limit=&offset=)
Tasks now include a tags field in all responses. Pass tags: ["uuid-or-name", ...] when creating or updating tasks.
Areas
GET /api/areas # List all areas
Sync
GET /api/sync/status # Current sync state (status, head index, last sync time, errors)
POST /api/sync # Manually trigger a full pull + push cycle (rate limited; overlap protected by lock)
Health
GET /health # Liveness check (no auth required)
GET /ready # Readiness check (DB + sync degradation/circuit state)
GET /metrics # Prometheus-compatible counters (disabled by default, set ENABLE_METRICS=true)
Create a task
curl -X POST http://localhost:3117/api/tasks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk", "schedule": 1}'
Update a task
curl -X PATCH http://localhost:3117/api/tasks/{uuid} \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"status": 3}'
Status values: 0 = pending, 2 = cancelled, 3 = completed.
Schedule values: 0 = inbox, 1 = anytime, 2 = someday.
Type values: 0 = task, 1 = project.
How Sync Works
The sync mechanism mirrors how Things3 itself operates:
| Trigger | Behavior |
|---|---|
| API write (create/update/delete) | Task is flagged for push. Next sync cycle sends it to Things Cloud. |
| Background interval | Pulls remote changes, then pushes local changes. Configurable via SYNC_INTERVAL_SECONDS. |
| Manual trigger | POST /api/sync runs an immediate pull + push cycle. |
Things Cloud uses an event-sourced model with a monotonically increasing index. Each sync pulls all changes since the last known index and applies them locally. Conflicts are resolved with remote-wins semantics.
Local Development
The repository uses a uv workspace:
- root package:
things-api - workspace packages:
things-sdk,things-cloud-mcp
# Install both packages in editable mode
uv sync
You can then run the API or import things_sdk directly in local scripts/tests.
Requires Python 3.12+ and uv.
# Install dependencies
uv sync
# Run the dev server
uv run uvicorn things_api.main:app --reload
# Run tests
uv run pytest -v
# Run type checks (current typed foundation)
uv run pyright
# Run migrations
uv run alembic upgrade head
Deploying with Docker
docker compose up -d
The Docker setup uses a named volume (things-data) to persist the SQLite database across container restarts. The docker-compose.yml pulls the published image from ghcr.io/nkootstra/things.
For local development, build from source instead:
docker compose -f docker-compose.dev.yml up -d
For production, put a reverse proxy (Caddy, nginx, Traefik) in front for TLS termination:
┌──────────┐ ┌──────────────┐
HTTPS :443 ───▶ │ Caddy │ ───▶ │ Things API │
│ (TLS) │ │ :8000 │
└──────────┘ └──────────────┘
CI / Release smoke checks
The automation now verifies both build artifacts and published artifacts:
- SDK smoke test: build wheel, install it into a clean virtualenv, import
things_sdk, and verify basic engine creation - Docker smoke test: build image, boot container, and verify
/health,/ready, and authenticatedGET /api/tasks - Post-release verification: after publication, install
things-sdk==<version>from PyPI and pullghcr.io/nkootstra/things:<version>from GHCR, then run the same basic checks against the published artifacts
This means a green release is not just "built" — it is also verified as installable from PyPI and runnable from GHCR.
Releasing a New Version
Releases are fully automated via GitHub Actions. Pushing a version tag triggers the pipeline:
preflight (tests) ─┬─▶ build (Docker image) ─▶ release (GitHub release)
├─▶ publish-sdk (PyPI)
└─▶ verify-published-artifacts
To release:
./scripts/release.sh 0.2.1
The script will:
- update versions in both
pyproject.tomlfiles - run
uv sync --dev - run the full test suite
- commit
release: vX.Y.Z - create tag
vX.Y.Z - push the commit and tag
Useful flags:
./scripts/release.sh 0.2.1 --no-push
./scripts/release.sh 0.2.1 --skip-tests
Manual fallback:
git add pyproject.toml packages/things-sdk/pyproject.toml
git commit -m "release: v0.2.1"
git tag v0.2.1
git push && git push --tags
This will:
- Run all tests (preflight gate)
- Build and push the Docker image to
ghcr.io/nkootstra/thingswith tags0.2.0,0.2, andlatest - Publish
things-sdkto PyPI - Create a GitHub release with auto-generated release notes
Note: PyPI publishing uses trusted publishers. You must configure the GitHub Actions publisher for
things-sdkon PyPI before the first publish.
Project Structure
This project is a monorepo with two packages:
| Package | Path | Description |
|---|---|---|
things-sdk |
packages/things-sdk/ |
Reusable core library — models, cloud client, sync engine, task operations |
things-api |
root | FastAPI HTTP service built on top of the SDK |
things-cloud-mcp |
packages/things-mcp/ |
MCP server for AI agents (Claude, Codex, etc.) |
You can use them together (run the API) or install only the SDK for scripts, CLIs, or other integrations.
SDK standalone usage
from things_sdk import ThingsClient, TaskService, configure_sync, create_engine_and_session, init_db, pull_sync
engine, session_factory = create_engine_and_session("sqlite+aiosqlite:///data/things.db")
await init_db(engine)
configure_sync(my_config)
client = ThingsClient(email="...", password="...")
async with session_factory() as session:
await pull_sync(client, session)
tasks = await TaskService().list_tasks(session)
await client.close()
See packages/things-sdk/README.md for full SDK documentation.
Directory layout
packages/things-sdk/src/things_sdk/ # SDK (reusable core)
├── __init__.py # Public API exports
├── protocols.py # CloudClientProtocol, SyncConfig
├── tasks.py # TaskService (CRUD + smart lists)
├── tags.py # TagService (CRUD + hierarchy resolution)
├── cloud/
│ ├── client.py # ThingsCloudClient
│ ├── handlers.py # Entity handler strategy pattern
│ ├── schema.py # Wire format Pydantic models
│ └── sync.py # Sync engine + circuit breaker
└── db/
├── engine.py # Engine factory
└── models.py # Domain models (Task, Tag, TaskTag, Area, etc.)
src/things_api/ # API (HTTP adapter)
├── main.py # FastAPI app, lifespan, scheduler
├── config.py # pydantic-settings configuration
├── auth.py # API key authentication
├── api/
│ └── routes.py # HTTP endpoints (tasks, smart lists, tags)
├── cloud/
│ └── scheduler.py # Background sync loop
└── services/
├── contracts.py # API-layer service protocols
├── health_service.py # Readiness checks
├── sync_service.py # Manual sync orchestration
├── task_service.py # Re-exports SDK TaskService
├── tag_service.py # Re-exports SDK TagService
├── task_command_mapper.py# Request DTO mapping
├── scheduler_leadership.py # Distributed lock
└── scheduler_runtime.py # Scheduler lifecycle
packages/things-mcp/src/things_mcp/ # MCP server
├── __init__.py # Entry point
├── server.py # FastMCP tools (22 tools)
├── client.py # HTTP client for things-api
└── __main__.py # python -m things_mcp
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.