Things Cloud MCP
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Things Cloud MCPshow my tasks for today"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
|
You want to build a CLI, script, worker, or integration in Python |
|
You want AI agents to read/write your tasks |
|
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 -dThe 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 |
| Yes | — | Primary API key for authentication. Must be at least 32 characters. Passed via |
| No | — | Optional secondary API key for zero-downtime key rotation. |
| Yes | — | Your Things Cloud account email |
| Yes | — | Your Things Cloud account password |
| No |
| Background sync interval in seconds. |
| No |
| Enable background scheduler in this process. |
| No |
| Distributed scheduler leadership lease duration. Only the lock owner runs background sync. |
| No |
| Lease renewal interval for scheduler leadership. |
| No |
| Lease duration for manual sync lock to prevent overlapping |
| No |
| Number of retry attempts for transient cloud pull/push failures. |
| No |
| Exponential backoff base delay for retries. |
| No |
| Consecutive sync failures required to open the circuit breaker. |
| No |
| Cooldown period while breaker is open before a half-open probe is allowed. |
| No |
| Degrade |
| No |
| Set to |
| No |
| Set to |
| No |
| 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 taskSmart 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 tasksSearch
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 projectAll 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 areasSync
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 |
Manual trigger |
|
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-apiworkspace packages:
things-sdk,things-cloud-mcp
# Install both packages in editable mode
uv syncYou 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 headDeploying with Docker
docker compose up -dThe 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 -dFor 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 creationDocker smoke test: build image, boot container, and verify
/health,/ready, and authenticatedGET /api/tasksPost-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-artifactsTo release:
./scripts/release.sh 0.2.1The script will:
update versions in both
pyproject.tomlfilesrun
uv sync --devrun the full test suite
commit
release: vX.Y.Zcreate tag
vX.Y.Zpush the commit and tag
Useful flags:
./scripts/release.sh 0.2.1 --no-push
./scripts/release.sh 0.2.1 --skip-testsManual 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 --tagsThis will:
Run all tests (preflight gate)
Build and push the Docker image to
ghcr.io/nkootstra/thingswith tags0.2.0,0.2, andlatestPublish
things-sdkto PyPICreate 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 |
|
| Reusable core library — models, cloud client, sync engine, task operations |
| root | FastAPI HTTP service built on top of the SDK |
|
| 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_mcpMaintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nkootstra/things'
If you have feedback or need assistance with the MCP directory API, please join our Discord server