todoist-mcp

todoist-mcp

A local-stdio MCP server for a single personal Todoist account with env-gated read-only mode and prompt-injection mitigations.

Category
访问服务器

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:

  1. 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).
  2. HTML/markdown stripping — HTML tags/comments are removed, markdown links/images/emphasis/code markers are defanged, and control characters are stripped, before framing.
  3. 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.
  4. 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.
  5. 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).
  6. 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, mode 0600, inside a directory mode 0700 — 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. .gitignore excludes .env, *.token, and secrets/.
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

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选