todoist-mcp
A local-stdio MCP server for a single personal Todoist account with env-gated read-only mode and prompt-injection mitigations.
README
todoist-mcp
A purpose-built, local-stdio Model Context Protocol server for a single personal Todoist (GTD) account. It exposes a deliberately narrow tool surface, an env-gated read-only mode, and in-server prompt-injection mitigations.
It was built fresh against the Todoist API v1. It is not a fork of any existing server; two proven patterns (SSRF allowlist, token redaction) were reimplemented from MadLlama25/fastmail-mcp as reference, everything else is written clean.
Why this exists (and the one hard limitation)
Off-the-shelf Todoist MCP servers were rejected because none offers a credential-level read-only tier reachable from local stdio, and all expose far more surface than personal GTD needs. This server gives a controlled tool set, in-server injection defenses, an env-gated read-only mode, and full auditability.
Credential constraint — read this first
Todoist's personal API token (TODOIST_API_KEY, from Settings → Integrations → Developer) is always full read + write, account-wide. Todoist has no scope parameter, flag, or token tier that narrows a personal token to read-only. Scoped OAuth exists, but only via a registered OAuth app + browser consent flow, which does not fit the per-subagent local-stdio isolation pattern this connection uses.
Therefore the token can always write. All read/write separation in this server is enforced in the server process, not at the credential layer. On the connection-architecture "enforcement ladder" this is the server layer (strong: holds regardless of harness behavior) — but it is softer than a credential-layer guarantee, because the same token, used by a different process, could still write. This is stated plainly, not hidden.
Env-gated read-only mode
The server reads TODOIST_READONLY once, at process startup, when it registers tools:
TODOIST_READONLY |
Mode | Write tools |
|---|---|---|
| unset / empty | read-only (fail-safe default) | not registered — do not exist in the process |
true / 0 / anything ≠ false |
read-only | not registered |
false (exact string) |
read/write | registered |
When read-only, the write tools are not registered at all — they are absent from the process, not merely hidden or rejected. This is not a runtime toggle; nothing flips it mid-session. Which mode you get is decided entirely by the launching environment (i.e. which subagent frontmatter launched the process).
What read-only mode does and does NOT protect against
- Does: cap the blast radius of a data-borne injection during a read session. If a task or comment contains "delete everything in project X" and the reading process has no write tools registered, the injected instruction has nothing to call.
- Does NOT: protect against a compromised orchestrator that can choose to invoke a write-capable process. Read-only mode is not a defense against orchestrator compromise, and this README does not claim it is.
Tool set
Read tools are registered in all modes. Write tools are registered only when TODOIST_READONLY=false.
Read tools (all modes)
| Tool | Todoist API v1 endpoint(s) |
|---|---|
find-tasks |
GET /tasks — or GET /tasks/filter?query= when a filter query is supplied. Also narrows by project_id / section_id / label / parent_id / ids. |
find-tasks-by-date |
GET /tasks/filter?query=<date filter> — builds the filter from a preset (today / overdue / next7days / nodate / recurring) or a date + comparison (on / before / after). |
find-projects |
GET /projects |
find-sections |
GET /sections (optionally ?project_id=) |
find-labels |
GET /labels |
find-comments |
GET /comments?task_id= or ?project_id= (exactly one required) |
get-overview |
Aggregates GET /projects + GET /sections + GET /labels + GET /tasks into a compact GTD overview (per-project active-task counts, sections, labels, due-today / overdue counts). |
Write tools (only when TODOIST_READONLY=false)
| Tool | Todoist API v1 endpoint(s) |
|---|---|
add-tasks |
POST /tasks (one call per task; batch input) |
update-tasks |
POST /tasks/{id} |
complete-tasks |
POST /tasks/{id}/close |
uncomplete-tasks |
POST /tasks/{id}/reopen |
reschedule-tasks |
POST /tasks/{id} with due_string / due_date / due_datetime |
add-comments |
POST /comments |
add-projects |
POST /projects |
add-sections |
POST /sections |
add-labels |
POST /labels |
Never implemented / never registered (any mode)
delete-object (deletion is a manual, human-only operation), manage-assignments, list-workspaces, find-project-collaborators, get-workspace-insights, analyze-project-health, get-productivity-stats, get-project-activity-stats, project-health, reorder-objects, project-move, add-reminders, add-filters, update-filters.
There is no delete tool of any kind. Removing a task, project, section, or label is done by a human in Todoist directly. Reminders and filters may be added later if adopted; they are deliberately out of scope for now.
In-server injection mitigations
Ingress is the read path: untrusted content enters agent context when a tool reads task/description/comment/project/section/label text. All of the following are built in:
- Content framing — every untrusted field value is wrapped in explicit
‹UNTRUSTED›…‹/UNTRUSTED›delimiters marking it as data, not instructions, with a leading notice on every read result. A field cannot forge a closing fence (any embedded fence marker is neutralized). - HTML/markdown stripping — HTML tags/comments are removed, markdown links/images/emphasis/code markers are defanged, and control characters are stripped, before framing.
- Output-size caps — total text returned by any read tool is bounded (
TODOIST_MAX_OUTPUT_CHARS, default 50000) and each field is capped (TODOIST_MAX_FIELD_CHARS, default 2000); pagination is bounded (TODOIST_MAX_ITEMS, default 200). A huge list cannot flood context. - SSRF allowlist — every outbound request is pinned to
https://api.todoist.com. Any other host, or non-HTTPS scheme, is rejected before a socket opens. - Token redaction — the API token is registered as a secret at startup and scrubbed from all logs and error messages;
Bearer …/Authorization:material is also redacted generically. Logs go to stderr only (stdout is the MCP transport). - Env-gated read-only registration — as described above.
A full prompt-injection threat model for this connection is deliberately deferred (a task list is a narrower, lower-stakes blast radius than a mailbox). The six mitigations above are the agreed baseline.
Configuration
All configuration is environment-based and read once at startup. See .env.example.
| Variable | Default | Meaning |
|---|---|---|
TODOIST_API_KEY |
— | The Todoist personal API token. Full read+write (see constraint above). |
TODOIST_API_KEY_FILE |
— | Alternative to the above: path to a file whose contents are the token. Used if TODOIST_API_KEY is unset. Preferred for deployment (keeps the token out of the agent file and process listing). |
TODOIST_READONLY |
(unset → read-only) | false enables writes; anything else is read-only. |
TODOIST_MAX_OUTPUT_CHARS |
50000 |
Whole-response output-size cap per read tool. |
TODOIST_MAX_FIELD_CHARS |
2000 |
Per-field truncation cap for untrusted text. |
TODOIST_MAX_ITEMS |
200 |
Max items fetched across pagination per read call. |
Credential storage (deployment)
Follow the M365 / Fastmail file precedent on the host (LegioNix):
- Store the token in a file owned by
claudecode, mode0600, inside a directory mode0700— e.g./home/claudecode/.todoist-mcp/token. - Point the subagent at it with
TODOIST_API_KEY_FILE=/home/claudecode/.todoist-mcp/token. - Never commit a real token.
.gitignoreexcludes.env,*.token, andsecrets/.
install -d -m 0700 -o claudecode -g claudecode /home/claudecode/.todoist-mcp
printf '%s' 'YOUR_TODOIST_TOKEN' > /home/claudecode/.todoist-mcp/token
chmod 0600 /home/claudecode/.todoist-mcp/token
Running
npm install
# read-only (default)
TODOIST_API_KEY=... node src/index.js
# read/write
TODOIST_API_KEY=... TODOIST_READONLY=false node src/index.js
The server speaks MCP over stdio. It is intended to be launched by a Claude Code subagent via inline mcpServers frontmatter — see .claude/agents/todoist.md, which follows the m365-briefing isolation pattern.
Tests
npm test # offline suite (registration, config, sanitize, redact, client/SSRF, MCP e2e)
# real spawned-process checks (fake token; tools/list makes no network call)
TODOIST_READONLY=true node scripts/stdio-check.js
TODOIST_READONLY=false node scripts/stdio-check.js
# LIVE write round-trip — needs a real token, hits the real account
TODOIST_API_KEY=... TODOIST_READONLY=false npm run smoke
The offline suite covers: excluded tools absent in every mode; read-only vs. read/write registration split (verified both in-process and against a real spawned stdio process); content framing + markup stripping; forged-fence neutralization; per-field and whole-output size caps; token redaction (including on a deliberately triggered error); and the SSRF allowlist rejecting non-api.todoist.com hosts and non-HTTPS schemes.
The one acceptance test that cannot run offline is the live write round-trip (add + read-back + complete a throwaway task) — run npm run smoke with a real token to exercise it.
Layout
src/
index.js stdio bootstrap; reads config once, connects transport
server.js builds the MCP server; env-gated tool registration
config.js env parsing; read-only default; token resolution (env or file)
client.js Todoist API v1 client; SSRF allowlist; pagination
sanitize.js framing + markup stripping + size caps (read-path ingress)
shape.js raw API object -> compact, sanitized result object
redact.js token/secret redaction
logger.js stderr logging (redacted)
tools/read.js the 7 read tools
tools/write.js the 9 write tools (registered only when writes enabled)
scripts/
stdio-check.js list tools from a real spawned process
live-smoke.js live write round-trip (needs a real token)
test/ offline test suite
.claude/agents/todoist.md the read/write subagent (inline mcpServers frontmatter)
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。