Announcing CIMD support for MCP Client registration
Learn more

Todoist MCP vs Todoist API for AI Agents (2026)

TL;DR

  • Todoist's hosted MCP server is one of the better-designed official servers: it ships composed tools like get-overview, reschedule-tasks, and manage-assignments rather than thin endpoint wrappers. What it does not expose is the machinery background agents depend on: batched /sync commands, idempotent retries, incremental sync, and webhooks.
  • The hosted MCP server is OAuth-only. The self-hosted build of the same server authenticates with a single TODOIST_API_KEY supplied by the operator, which means every tool call runs as that one Todoist user. That is a shared-credential model, not a multi-user one.
  • Todoist OAuth token exchange returns access_token and token_type. No refresh token, no expires_in. Your problem is not refresh storms; it is a long-lived bearer credential that stays valid until someone explicitly revokes it.
  • Todoist's OAuth scopes stop at the account boundary. data:read_write covers tasks, projects, labels, and filters together, with no per-project or per-workspace narrowing. Least privilege has to be enforced above Todoist.
  • Scalekit's Todoist MCP connector handles the OAuth flow, per-user token storage, and revocation state for either path, so the MCP versus API decision does not change what you build at the credential layer.

Your agent needs to read and write Todoist. Doist ships a hosted MCP server at ai.todoist.net/mcp and a unified REST API at api.todoist.com/api/v1. Both are official, both are free to use, and both are actively maintained. They are not interchangeable: the MCP server gives you composed, LLM-shaped workflow tools, while the API gives you batching, idempotency, and webhooks that the tool surface does not expose. Here is how to pick.

What Todoist MCP and Todoist API actually are

Doist treats agents as first-class users and publishes both surfaces from the same object model: tasks, projects, sections, comments, labels, filters, reminders, and workspaces. The difference is the shape of the interface and the credential it accepts. Both are documented from the Todoist developer hub.

The hosted Todoist MCP server

The hosted server runs at https://ai.todoist.net/mcp over Streamable HTTP, and authenticates by initiating an OAuth flow when a client connects. There is no API key option on the hosted endpoint. Doist documents client setup for Claude, Claude Code, Cursor, and VS Code in the Todoist MCP setup guide. The server also ships MCP Apps support, rendering inline task-list widgets in clients that support them.

The self-hosted build of the same server

The server is open source and publishable to npm, so you can run it yourself over stdio or local HTTP from Doist's todoist-mcp repository. This version reads a single TODOIST_API_KEY from the environment and runs every tool call as that user. Doist's own documentation binds it to 127.0.0.1 by default, validates Host and Origin headers against a trusted-hostname allowlist, and warns against exposing it without adding your own authentication controls.

The Todoist API v1

The API is a unified surface at api.todoist.com/api/v1, described by a published OpenAPI specification, with official Python and TypeScript SDKs. Alongside conventional REST resources it exposes /sync, the batching endpoint that powers Doist's first-party clients, plus webhooks for change events. Authentication is a bearer token: either a personal API token from the user's developer settings, or an OAuth access token. Details are in the Todoist API v1 reference.

Comparing them where it matters for agents

The gap here is not the usual "MCP covers 60 percent of the API" story. Todoist's tool surface is broad and thoughtfully composed. The gap is structural: the MCP server is built for a user-present conversation, and the API is built for clients that maintain state, retry safely, and react to change.

What your agent can actually do

Scalekit's connector catalog currently enumerates 50 tools on the Todoist MCP server, covering tasks, projects, sections, comments, labels, filters, reminders, goals, collaborators, workspaces, and analytics. The absences are specific and they cluster in one place.

Capability
Todoist MCP (hosted)
Todoist API v1
Create, update, complete, reopen tasks
Yes
Yes
Reschedule while preserving recurrence
Yes (reschedule-tasks)
Yes, hand-built
Search across tasks and projects
Yes (search, find-tasks)
Yes
Comments and reading attachments
Yes (view-attachment)
Yes
Bulk assignment with dry run and rollback
Yes (manage-assignments)
No, hand-built
Project health and workspace insights
Yes (get-project-health)
No equivalent endpoint
Batched multi-command writes in one request
No
Yes, /sync commands
Idempotent retry via command uuid
No
Yes
Incremental sync via sync_token
No
Yes
Webhooks for real-time change events
No
Yes
File uploads onto comments
No
Yes
Sharing and workspace member administration
Read-only (list-workspaces)
Yes

Where the MCP server is genuinely better

Three tools in that list have no clean API equivalent, and they are not cosmetic. get-overview returns a Markdown rollup of an account or project with hierarchy and sections intact, which is exactly the context shape an LLM wants and exactly the thing you would otherwise assemble from four paginated calls.

reschedule-tasks encodes a semantic guardrail: Todoist's own tool instructions warn that updating a task's due string replaces the whole string and destroys recurrence, so rescheduling gets a separate tool that changes only the date. manage-assignments handles up to 50 tasks with a dry-run mode and atomic rollback. Writing those correctly against the raw API is real work, and Doist already did it.

Where the gap bites

Every absence in the table above points at the same class of agent: the one that runs without a user watching. A nightly digest agent needs sync_token so it reads only what changed. A task-mirroring agent needs webhooks so it reacts to item:completed instead of polling on a timer.

An agent that writes a project plan needs /sync batching, where a project_add and its child item_add commands share one request through temp_id resolution. And any agent that retries needs command idempotency: Todoist will not execute a command carrying a uuid it has already processed, which is the difference between a safe retry and a duplicate task. None of that has a tool on the MCP surface.

The auth path each one puts you on

The hosted MCP server initiates OAuth on connect. That is architecturally correct for an assistant sitting next to a human, and it is a hard stop for a scheduled job with no browser in the loop. The self-hosted build removes the browser but replaces it with something worse for multi-tenant work: one operator-supplied TODOIST_API_KEY, one Todoist identity, every user's request attributed to the same account.

The API accepts either a personal API token or an OAuth access token in the Authorization header. Todoist documents the Authorization Code flow with client_id, client_secret, code, and state; there is no client-credentials grant and no JWT-bearer server-to-server flow. For a headless multi-tenant agent, the pattern is a stored per-user OAuth token, obtained once while the user was present, replayed later while they are not. Understanding who holds the token across agent tool-calling patterns is critical before you choose either path.

The token that never expires

Read Todoist's token exchange response carefully, because it inverts the usual agent-auth problem. A successful exchange returns exactly two fields: access_token and token_type. There is no refresh token and no expires_in.

That removes an entire class of failure. You will not debug a 3am refresh race across twelve tenants, because there is nothing to refresh. It also creates a harder one. A Todoist access token is a long-lived bearer credential that remains valid until it is explicitly revoked, so a token copied into a .env file eighteen months ago still works today, and nothing in the protocol will tell you when a user disconnects your app from their Todoist settings. Your only signal is a 401 on the next call. The implications for secure token management at scale are significant.

Revocation is a write you have to make

Todoist gives you two revocation endpoints: a legacy one taking client_id, client_secret, and access_token as query parameters, and an RFC 7009 compliant endpoint that takes the token in a form-encoded body with HTTP Basic client authentication.

Both require you to already hold the specific token you want to kill. There is no admin endpoint that enumerates the tokens your application holds and invalidates them by user. If your token store is incomplete, your offboarding is incomplete, and no amount of correctness in the Todoist API will cover for that.

Scope granularity stops at the account boundary

Todoist publishes six OAuth scopes: task:add, data:read, data:read_write, data:delete, project:delete, and backups:read. task:add is a genuinely narrow write-only grant and the right choice for a pure capture agent that turns Slack messages into tasks.

Everything above it is coarse. data:read_write covers tasks, projects, labels, and filters together, across the entire account. There is no scope that says "this project only" or "read tasks, never touch filters." An agent that only needs to read today's tasks gets the same grant as one that can restructure the workspace. Least privilege for Todoist agents is not a scope selection problem; it is enforced above Todoist or it is not enforced at all.

What you own in production

On the MCP path, Doist owns hosting, transport, and tool schemas. You own the OAuth session per user, storage of whatever credential your client holds, detection of revocation, and tenant isolation. You also own rate-limit behaviour indirectly: MCP tool calls resolve to Todoist API calls underneath, so they draw down the same per-user budget that Todoist documents at 1,000 requests per 15 minutes per token, with 429 returned on breach.

On the API path you own all of the above plus endpoint selection, pagination through next_cursor, error handling, retry logic, and the /sync command lifecycle. That is more surface area and more control. It is also the only path where you can compress a burst of writes into a single batched request instead of spending one rate-limit unit per task.

Schema drift is measurable here

The versioning asymmetry is easy to check on this connector, which makes it worth stating plainly. The API is a single published v1 surface with an OpenAPI document you can diff. The MCP tool surface has no version header and no deprecation contract.

As of this writing, Doist's open-source server registers 45 tools, while the hosted surface reflected in Scalekit's connector catalog enumerates 50, including a goals family (add-goals, find-goals, update-goals, complete-goals, link-goal-tasks) that is absent from the published repository's registry. Neither number is wrong. They just move independently, and an agent pinned to specific tool names inherits that movement.

When to use MCP, when to use the API

Both paths are legitimate. The question is which failure mode you can absorb.

Use Todoist MCP when:

  • Your agent is interactive and the user is present for OAuth: a planning assistant in Claude Code, Cursor, or your own chat surface
  • The workflow maps onto composed tools you would otherwise rebuild, particularly get-overview, reschedule-tasks, and manage-assignments
  • You want project health and workspace insight rollups without writing the aggregation yourself
  • You are validating a Todoist agent concept and want a working tool loop before you write any integration code

Use the Todoist API v1 when:

  • Your agent runs headless on a schedule and needs incremental reads via sync_token rather than full re-reads
  • Your agent must react to changes as they happen, which means webhooks, not polling
  • You write in bursts and need /sync batching with temp_id resolution to stay inside the per-user rate limit
  • Retry safety matters and you need command-level idempotency through uuid
  • You need uploads, project sharing, invitations, or workspace member administration

The credential problem that exists on both paths

Whichever path you choose, a multi-tenant Todoist agent ends up holding one credential per user. The MCP OAuth flow produces a session bound to that user. The API's OAuth flow produces an access token bound to that user. Neither produces a vault, a rotation policy, or a revocation workflow.

The N-credential math

Forty customers means forty Todoist grants, each encrypted at rest, each isolated per tenant, each revocable independently. Because Todoist tokens do not expire on a schedule, staleness is invisible until a call fails, and a token that outlives the employee who authorized it looks identical to a healthy one. This is precisely the challenge of moving from single-tenant to multi-tenant tool-calling auth.

Where Scalekit fits

Scalekit's Todoist MCP connector resolves the per-user credential on every tool call, so the task the agent creates is attributed to the person who asked for it rather than a shared account. The same connected-account model works whether you call tools through Scalekit or point an MCP client at a Scalekit-generated endpoint. Full details are on the Todoist MCP connector page and in the Todoist MCP connector docs.

Building a Todoist agent with Scalekit

The connector is todoistmcp and it uses OAuth 2.1 with Dynamic Client Registration (DCR). Every tool is namespaced with the connector prefix, so find-tasks-by-date on the Todoist server is todoistmcp_find-tasks-by-date when you call it through Scalekit.

Prerequisites and the connection name

Create the connection once in the Scalekit dashboard under AgentKit > Connections, then copy the exact connection name into your code. The string in code must match the dashboard value character for character; a mismatch here is the single most common integration error. Setup steps are in Configure connections.

pip install scalekit-sdk-python langchain-openai # .env SCALEKIT_ENV_URL= SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET=

Authorize the user once

The user grants Todoist access one time. Scalekit stores the resulting credential against your identifier for that user and keeps the connected account's state current, so subsequent runs check status instead of re-prompting.

import os import scalekit.client scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions # "todoistmcp" must match the connection name in AgentKit > Connections exactly response = actions.get_or_create_connected_account( connection_name="todoistmcp", identifier="user_123", ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name="todoistmcp", identifier="user_123", ) print("Authorize Todoist:", link.link) input("Press Enter after authorizing...")

Retrieve the tools this user is authorized to call

Before any code runs, be precise about what happens next. The agent is not loading a Todoist tool catalog. list_scoped_tools returns the tools the current user's connected account is authorized to call, which is a deterministic surface derived from that user's identity rather than a menu of everything the connector could do.

The size of that surface is the lever on tool-calling accuracy. Handing a model all 50 Todoist tools means it selects from a decision space it was never designed to handle at that scale, and it burns roughly 200 tokens per tool definition before the agent does any work. Narrow it to the six tools a daily-planning agent needs and both problems shrink together.

scoped_response, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["todoistmcp"]}, page_size=100, # fetch beyond the default page so no connector tools are missed ) for scoped in scoped_response.tools: print(scoped.tool.definition.name)

Execute the agent loop with LangChain

Scalekit returns native LangChain StructuredTool objects, so there is no schema reshaping between the connector and the model. Credentials are resolved server-side on every call: the Todoist token never enters your agent process and never appears in the model context. For a deeper look at how this fits into broader LangChain tool calling patterns, the approach maps directly.

from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier="user_123", connection_names=["todoistmcp"], page_size=100, ) tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ HumanMessage( "Show me everything due today including overdue items, " "then move anything tagged @waiting to tomorrow." ) ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = tool_map[tc["name"]].invoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

The same pattern in TypeScript

If your stack is Node, the shape is identical: retrieve the authorized surface with listScopedTools, then run the model loop and call executeTool for each tool use block. Scalekit returns definitions with input_schema, which is the format Anthropic's tool use API already expects.

import { ScalekitClient } from '@scalekit-sdk/node'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); // connectionNames must match the connection name in AgentKit > Connections const { tools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['todoistmcp'] }, pageSize: 100, }); const llmTools = tools.map(t => ({ name: t.tool.definition.name, description: t.tool.definition.description, input_schema: t.tool.definition.input_schema, })); const messages: Anthropic.MessageParam[] = [ { role: 'user', content: 'Summarise my overdue tasks by project.' }, ]; while (true) { const response = await anthropic.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 1024, tools: llmTools, messages, }); if (response.stop_reason === 'end_turn') { const text = response.content.find(b => b.type === 'text'); if (text?.type === 'text') console.log(text.text); break; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await scalekit.actions.executeTool({ toolName: block.name, identifier: 'user_123', toolInput: block.input as Record, }); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data), }); } } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); }

Virtual MCP servers for multi-tool, multi-tenant Todoist agents

Todoist's coarse scopes mean the grant your user signs cannot express "this agent may read and reschedule, but never delete." Virtual MCP Servers close that gap at the tool layer instead of the OAuth layer.

One server definition, per-user identity

You define the server once per agent role, declaring which connections and which tools are exposed, and you get a static mcp_server_url. Before each run you mint a short-lived session token bound to a specific user. The endpoint is static; the identity is not. No credential is shared between users and there is no per-user server to deploy or host.

Declaring the surface

A daily-planning agent needs to read and reschedule. It has no business holding todoistmcp_delete-object, and with a Virtual MCP server it simply never sees it.

import os from scalekit import ScalekitClient from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENV_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) vmcp_response = scalekit_client.actions.mcp.create_config( name="todoist-daily-planner", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="todoistmcp", tools=[ "todoistmcp_find-tasks-by-date", "todoistmcp_get-overview", "todoistmcp_reschedule-tasks", "todoistmcp_add-tasks", ], ), ], ) config_id = vmcp_response.config.id mcp_server_url = vmcp_response.config.mcp_server_url

Minting a session token before each run

Because Todoist tokens carry no expiry signal, checking connected-account status before you mint is not ceremony. It is how you find out that a user disconnected your app last Tuesday, before your agent discovers it mid-task.

from datetime import timedelta accounts = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier="user_123", include_auth_link=True, ) for account in accounts.connected_accounts: if account.connected_account_status != "ACTIVE": print(f"{account.connection_name} needs auth: {account.authentication_link}") token_response = scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(minutes=30), ) mcp_server = { "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, }

Why this matters for multi-tool agents

Todoist rarely ships alone. A standup agent reads GitHub and Linear, writes Todoist, and posts to Slack. Composed naively, that agent carries every tool from four connectors into every context window. One Virtual MCP server per agent role, holding the eight tools that role actually needs, is the difference between a scoped surface and an accumulated one. See the engineering standup agent build for a practical example of how this composition works in production. Setup instructions are in Set up and connect a Virtual MCP server.

Observability for downstream Todoist tool calls

There is a specific auditing problem with task systems: a task created by an agent looks exactly like a task created by a person, and Todoist's own activity log records only the Todoist identity that made the change.

What the log has to answer

When a customer asks why forty tasks appeared in their workspace overnight, the Todoist-side record tells you which account created them. It does not tell you which agent run, triggered by which user, under which authorization grant. That correlation lives in your auth layer or nowhere.

What Scalekit records

Scalekit logs each tool call against the connected account that authorized it, so every downstream Todoist write traces back to a specific user and a specific grant rather than a service identity. The connector page documents 90 days of tool-call history tied to the authorizing user, and auth events are visible alongside it in Auth Logs. That is the record a SOC 2 auditor asks for, and it is considerably easier to produce before the incident than after. For a full picture of what agent tool observability requires in production, the underlying framework applies directly to Todoist integrations.

Which one to build against

If your Todoist agent is interactive and the user is present for consent, the hosted MCP server is the faster and better-designed path. Doist has done real work on the tool layer, and the composed tools save you from rebuilding recurrence-safe rescheduling and bulk assignment yourself.

If your agent runs on a schedule, reacts to changes, writes in bursts, or must retry safely, use the API directly. Batched /sync commands, sync_token, webhooks, and command idempotency are not tools Doist forgot to expose; they are a different interface for a different kind of client.

Most production Todoist agents will use both, and the credential layer underneath is identical either way: one long-lived, non-expiring token per user, revocable only by a call you make with a token you still hold. That is the part that needs production-grade infrastructure.

Talk to other Todoist agent builders

If you are working through per-user Todoist auth, scoped tool surfaces, or Virtual MCP setup, join the Scalekit Slack community and ask. For architecture review on a specific multi-tenant deployment, talk to an engineer.

Browse the Scalekit Todoist MCP connector or start with the connector documentation.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
zero upfront cost.

Every feature unlocked. No hidden fees.