todoist-mcp
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:
| Mode | Write tools |
unset / empty | read-only (fail-safe default) | not registered — do not exist in the process |
| read-only | not registered |
| 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) |
|
|
|
|
|
|
|
|
|
|
|
|
| Aggregates |
Write tools (only when TODOIST_READONLY=false)
Tool | Todoist API v1 endpoint(s) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
| — | The Todoist personal API token. Full read+write (see constraint above). |
| — | Alternative to the above: path to a file whose contents are the token. Used if |
| (unset → read-only) |
|
|
| Whole-response output-size cap per read tool. |
|
| Per-field truncation cap for untrusted text. |
|
| 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/tokenRunning
npm install
# read-only (default)
TODOIST_API_KEY=... node src/index.js
# read/write
TODOIST_API_KEY=... TODOIST_READONLY=false node src/index.jsThe 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 smokeThe 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)Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/kevlasher/todoist-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server